commit d9d3833d92ed92603153554a2382e30abda8aef7 Author: ypc <15051963820@163.com> Date: Sat Jun 6 11:46:55 2026 +0800 <新增> 1、提交微信小程序初始开发版本 diff --git a/app.js b/app.js new file mode 100644 index 0000000..672be92 --- /dev/null +++ b/app.js @@ -0,0 +1,59 @@ +App({ + globalData: { + items: ["血红蛋白", "尿素", "肌酐", "尿酸", "血钾", "血钙", "血磷", "β2微球蛋白", "体重"], + records: [] + }, + + onLaunch() { + const defaults = ["血红蛋白", "尿素", "肌酐", "尿酸", "血钾", "血钙", "血磷", "β2微球蛋白", "体重"] + const data = wx.getStorageSync("health_data") + if (data) { + // 合并:保留用户自定义项目,同时补上新增的默认项目 + const storedItems = data.items || [] + const merged = [...defaults] + storedItems.forEach(item => { + if (!merged.includes(item)) merged.push(item) + }) + this.globalData.items = merged + this.globalData.records = data.records || [] + } else { + this.globalData.items = [...defaults] + } + this.saveData() + }, + + saveData() { + wx.setStorageSync("health_data", { + items: this.globalData.items, + records: this.globalData.records + }) + }, + + addRecord(record) { + this.globalData.records.push({ + id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6), + item: record.item, + date: record.date, + value: parseFloat(record.value) + }) + this.saveData() + }, + + deleteRecord(id) { + this.globalData.records = this.globalData.records.filter(r => r.id !== id) + this.saveData() + }, + + addItem(name) { + if (!this.globalData.items.includes(name)) { + this.globalData.items.push(name) + this.saveData() + } + }, + + removeItem(name) { + this.globalData.items = this.globalData.items.filter(i => i !== name) + this.globalData.records = this.globalData.records.filter(r => r.item !== name) + this.saveData() + } +}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..d1093f5 --- /dev/null +++ b/app.json @@ -0,0 +1,18 @@ +{ + "pages": [ + "pages/index/index", + "pages/home/home", + "pages/entry/entry", + "pages/items/items", + "pages/trend/trend", + "pages/details/details" + ], + "window": { + "navigationBarBackgroundColor": "#2c7a7b", + "navigationBarTitleText": "健康数据追踪", + "navigationBarTextStyle": "white", + "backgroundColor": "#f5f7fa" + }, + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents" +} diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..a0c0d6c --- /dev/null +++ b/app.wxss @@ -0,0 +1,165 @@ +page { + --primary: #2c7a7b; + --primary-light: #e6f2f2; + --primary-dark: #1a5c5d; + --danger: #e53e3e; + --danger-light: #fff5f5; + --text: #2d3748; + --text-secondary: #718096; + --text-muted: #a0aec0; + --border: #e2e8f0; + --bg: #f5f7fa; + --card-bg: #ffffff; + --shadow: 0 1px 3px rgba(0,0,0,0.06); + --shadow-md: 0 4px 12px rgba(0,0,0,0.08); + + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 28rpx; + color: var(--text); + background: var(--bg); + box-sizing: border-box; +} + +.container { + padding: 24rpx; + min-height: 100vh; +} + +/* ===== 通用卡片 ===== */ +.card { + background: #fff; + border-radius: 12rpx; + padding: 28rpx; + margin-bottom: 20rpx; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.card-title { + font-size: 30rpx; + font-weight: 600; + color: #2d3748; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx solid #edf2f7; +} + +/* ===== 按钮 ===== */ +.btn-primary { + background: #2c7a7b; + color: #fff; + font-size: 28rpx; + font-weight: 500; + padding: 16rpx 32rpx; + border-radius: 8rpx; + text-align: center; + border: none; + line-height: 1.5; +} + +.btn-primary:active { + background: #1a5c5d; +} + +.btn-danger { + background: #fff; + color: #e53e3e; + border: 1rpx solid #e53e3e; + font-size: 24rpx; + padding: 8rpx 20rpx; + border-radius: 6rpx; + text-align: center; + line-height: 1.5; +} + +.btn-cancel { + flex: 1; + background: #edf2f7; + color: #718096; + font-size: 28rpx; + padding: 16rpx 0; + border-radius: 8rpx; + text-align: center; + border: none; + line-height: 1.5; +} + +.btn-save { + flex: 1; + background: #2c7a7b; + color: #fff; + font-size: 28rpx; + padding: 16rpx 0; + border-radius: 8rpx; + text-align: center; + border: none; + line-height: 1.5; +} + +/* ===== 空状态 ===== */ +.empty-hint { + color: #a0aec0; + font-size: 26rpx; + text-align: center; + padding: 40rpx 0; +} + +/* ===== 弹窗 ===== */ +.modal-wrap { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.modal-box { + width: 85%; + max-height: 80vh; + background: #fff; + padding: 36rpx; + border-radius: 16rpx; + overflow-y: auto; +} + +.modal-title { + font-size: 34rpx; + font-weight: 600; + color: #2d3748; + text-align: center; + margin-bottom: 24rpx; +} + +.btn-row { + display: flex; + gap: 20rpx; + margin-top: 24rpx; +} + +/* ===== 模式切换 ===== */ +.mode-tabs { + display: flex; + gap: 0; + margin-bottom: 20rpx; + border-radius: 8rpx; + overflow: hidden; + border: 1rpx solid #2c7a7b; +} + +.mode-tab { + flex: 1; + text-align: center; + padding: 14rpx 0; + font-size: 26rpx; + color: #2c7a7b; + background: #fff; +} + +.mode-tab--active { + color: #fff; + background: #2c7a7b; +} diff --git a/minitest/test.config.json b/minitest/test.config.json new file mode 100644 index 0000000..41cf3d0 --- /dev/null +++ b/minitest/test.config.json @@ -0,0 +1,3 @@ +{ + "treeData": [] +} \ No newline at end of file diff --git a/pages/details/details.js b/pages/details/details.js new file mode 100644 index 0000000..a48a923 --- /dev/null +++ b/pages/details/details.js @@ -0,0 +1,226 @@ +var app = getApp() + +Page({ + data: { + items: [], + viewMode: "month", + filterMonth: "", + filterYear: "", + customStart: "", + customEnd: "", + tableData: [], + showEditModal: false, + editingDate: "", + editValues: {} + }, + + onLoad: function () { + var today = this.formatDate(new Date(), "day") + var month = today.slice(0, 7) + var year = today.slice(0, 4) + this.setData({ + items: app.globalData.items, + filterMonth: month, + filterYear: year, + customStart: month, + customEnd: month + }) + this.refreshData() + }, + + onShow: function () { + var items = app.globalData.items + if (items.length !== this.data.items.length || + items.some(function (v, i) { return v !== this.data.items[i] }, this)) { + this.setData({ items: items }) + } + this.refreshData() + }, + + getDateRange: function () { + var viewMode = this.data.viewMode + var filterMonth = this.data.filterMonth + var filterYear = this.data.filterYear + var customStart = this.data.customStart + var customEnd = this.data.customEnd + var start, end, parts, y, m, sy, sm, ey, em + + if (viewMode === "month") { + parts = filterMonth.split("-") + y = Number(parts[0]) + m = Number(parts[1]) + start = new Date(y, m - 1, 1) + end = new Date(y, m, 0) + } else if (viewMode === "year") { + y = parseInt(filterYear) + start = new Date(y, 0, 1) + end = new Date(y, 11, 31) + } else { + parts = customStart.split("-") + sy = Number(parts[0]) + sm = Number(parts[1]) + parts = customEnd.split("-") + ey = Number(parts[0]) + em = Number(parts[1]) + start = new Date(sy, sm - 1, 1) + end = new Date(ey, em, 0) + } + return { start: start, end: end } + }, + + refreshData: function () { + var records = app.globalData.records + var items = this.data.items + var range = this.getDateRange() + var start = range.start + var end = range.end + + var inRange = function (r) { + var d = new Date(r.date + "T00:00:00") + return d >= start && d <= end + } + + var filtered = records.filter(inRange) + this._filteredRecords = filtered + + var dateMap = {} + filtered.forEach(function (r) { + if (!dateMap[r.date]) dateMap[r.date] = {} + if (!dateMap[r.date][r.item]) { + dateMap[r.date][r.item] = { sum: 0, count: 0 } + } + dateMap[r.date][r.item].sum += r.value + dateMap[r.date][r.item].count += 1 + }) + + var dates = Object.keys(dateMap).sort() + var tableData = dates.map(function (d) { + var values = items.map(function (item) { + var entry = dateMap[d][item] + if (!entry) return undefined + return Math.round((entry.sum / entry.count) * 100) / 100 + }) + return { rowKey: d, values: values } + }) + + this.setData({ tableData: tableData }) + }, + + onSwitchMode: function (e) { + var that = this + this.setData({ viewMode: e.currentTarget.dataset.mode }, function () { that.refreshData() }) + }, + + onMonthChange: function (e) { + var that = this + this.setData({ filterMonth: e.detail.value }, function () { that.refreshData() }) + }, + + onYearChange: function (e) { + var that = this + this.setData({ filterYear: e.detail.value }, function () { that.refreshData() }) + }, + + onCustomStartChange: function (e) { + var that = this + this.setData({ customStart: e.detail.value }, function () { that.refreshData() }) + }, + + onCustomEndChange: function (e) { + var that = this + this.setData({ customEnd: e.detail.value }, function () { that.refreshData() }) + }, + + onDeleteRow: function (e) { + var dateKey = e.currentTarget.dataset.date + var that = this + wx.showModal({ + title: "确认删除", + content: "删除「" + dateKey + "」的所有记录?", + confirmColor: "#e53e3e", + success: function (res) { + if (!res.confirm) return + app.globalData.records = app.globalData.records.filter(function (r) { return r.date !== dateKey }) + app.saveData() + that.refreshData() + wx.showToast({ title: "已删除", icon: "success", duration: 1000 }) + } + }) + }, + + onEditRow: function (e) { + var dateKey = e.currentTarget.dataset.date + var items = this.data.items + + var editValues = {} + var records = this._filteredRecords || [] + items.forEach(function (item) { + var related = records.filter(function (r) { return r.item === item && r.date === dateKey }) + if (related.length > 0) { + var sum = related.reduce(function (s, r) { return s + r.value }, 0) + var avg = sum / related.length + editValues[item] = String(Math.round(avg * 100) / 100) + } else { + editValues[item] = "" + } + }) + + this.setData({ + showEditModal: true, + editingDate: dateKey, + editValues: editValues + }) + }, + + onEditValueInput: function (e) { + var item = e.currentTarget.dataset.item + var update = {} + update["editValues." + item] = e.detail.value + this.setData(update) + }, + + saveEdit: function () { + var editingDate = this.data.editingDate + var editValues = this.data.editValues + var items = this.data.items + var savedCount = 0 + var that = this + + app.globalData.records = app.globalData.records.filter(function (r) { return r.date !== editingDate }) + + items.forEach(function (item) { + var raw = editValues[item] + if (!raw || raw.trim() === "") return + var v = parseFloat(raw) + if (isNaN(v) || v <= 0) return + app.globalData.records.push({ + id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6), + item: item, + date: editingDate, + value: v + }) + savedCount++ + }) + + app.saveData() + this.setData({ showEditModal: false, editingDate: "", editValues: {} }, function () { + that.refreshData() + wx.showToast({ title: "已保存 " + savedCount + " 项", icon: "success", duration: 1500 }) + }) + }, + + cancelEdit: function () { + this.setData({ showEditModal: false, editingDate: "", editValues: {} }) + }, + + formatDate: function (d, mode) { + var y = d.getFullYear() + var m = String(d.getMonth() + 1) + if (m.length === 1) m = "0" + m + var day = String(d.getDate()) + if (day.length === 1) day = "0" + day + if (mode === "year") return String(y) + if (mode === "month") return y + "-" + m + return y + "-" + m + "-" + day + } +}) diff --git a/pages/details/details.json b/pages/details/details.json new file mode 100644 index 0000000..a3a3550 --- /dev/null +++ b/pages/details/details.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "数据详细信息", + "usingComponents": {} +} diff --git a/pages/details/details.wxml b/pages/details/details.wxml new file mode 100644 index 0000000..65548b7 --- /dev/null +++ b/pages/details/details.wxml @@ -0,0 +1,114 @@ + + + + + + 按月 + 按年 + 自定义 + + + + + + {{filterMonth}} + + + + + + + + + {{filterYear}} + + + + + + + + + + {{customStart}} + + + + + + {{customEnd}} + + + + + + + + 数据明细(共{{tableData.length}}条) + + + + + 日期 + + {{item}} + + 删除 + 修改 + + + + {{item.rowKey}} + + + {{item.values[fidx]}} + - + + + + 删除 + + + 修改 + + + + + + + + 该时间范围内暂无数据 + + + + + + + 修改记录 + 日期:{{editingDate}} + + + + + + {{item}} + + + + + + + + + + + + + diff --git a/pages/details/details.wxss b/pages/details/details.wxss new file mode 100644 index 0000000..cd30c36 --- /dev/null +++ b/pages/details/details.wxss @@ -0,0 +1,163 @@ +/* ===== 数据详细信息 ===== */ + +/* 筛选 */ +.filter-row { + margin-top: 4rpx; +} + +.custom-range { + display: flex; + align-items: center; + gap: 16rpx; +} + +.range-sep { + font-size: 26rpx; + color: #718096; + flex-shrink: 0; +} + +.picker-display { + display: flex; + align-items: center; + justify-content: center; + padding: 16rpx 24rpx; + border: 1rpx solid #e2e8f0; + border-radius: 8rpx; + background: #f7fafc; + font-size: 28rpx; + color: #2d3748; +} + +.picker-display--sm { + flex: 1; + padding: 12rpx 16rpx; + font-size: 26rpx; +} + +.picker-arrow { + font-size: 20rpx; + color: #a0aec0; + margin-left: 10rpx; +} + +.picker-label-sm { + color: #a0aec0; + margin-right: 8rpx; +} + +/* 表格 */ +.table-scroll { + width: 100%; + overflow-x: auto; +} + +.table { + display: flex; + flex-direction: column; +} + +.table-row { + display: flex; + flex-wrap: nowrap; + border-bottom: 1rpx solid #edf2f7; +} + +.table-row--head { + background: #f7fafc; + font-weight: 600; + color: #4a5568; +} + +.table-cell { + flex-shrink: 0; + min-width: 120rpx; + padding: 16rpx 10rpx; + font-size: 24rpx; + text-align: center; + box-sizing: border-box; +} + +.table-cell--date { + min-width: 150rpx; + color: #4a5568; + font-weight: 500; +} + +.table-cell--value { + color: #2c7a7b; +} + +.table-cell--action { + min-width: 70rpx; +} + +.table-empty { + color: #cbd5e0; +} + +.btn-del-text { + color: #e53e3e; + font-size: 22rpx; + padding: 4rpx 8rpx; +} + +.btn-edit-text { + color: #3182ce; + font-size: 22rpx; + padding: 4rpx 8rpx; +} + +/* ===== 修改弹窗 ===== */ +.modal-date { + font-size: 26rpx; + color: #718096; + text-align: center; + margin-bottom: 20rpx; + padding-bottom: 16rpx; + border-bottom: 1rpx solid #edf2f7; +} + +.modal-scroll { + max-height: 480rpx; +} + +.modal-row { + display: flex; + align-items: center; + padding: 14rpx 0; + border-bottom: 1rpx solid #f7fafc; +} + +.modal-row:last-of-type { + border-bottom: none; +} + +.modal-row-label { + width: 160rpx; + flex-shrink: 0; + font-size: 26rpx; + color: #2d3748; + font-weight: 500; +} + +.modal-row-input { + flex: 1; +} + +.modal-input { + width: 100%; + padding: 12rpx 16rpx; + border: 1rpx solid #e2e8f0; + border-radius: 8rpx; + background: #f7fafc; + font-size: 26rpx; + text-align: center; + box-sizing: border-box; + height: 56rpx; +} + +.modal-input:focus { + border-color: #2c7a7b; + background: #e6f2f2; +} diff --git a/pages/entry/entry.js b/pages/entry/entry.js new file mode 100644 index 0000000..8556549 --- /dev/null +++ b/pages/entry/entry.js @@ -0,0 +1,90 @@ +var app = getApp() + +Page({ + data: { + items: [], + inputDate: "", + values: {}, + existingRecords: [] + }, + + onLoad: function () { + var today = this.formatDate(new Date()) + this.setData({ inputDate: today, items: app.globalData.items }) + this.loadExisting(today) + }, + + onShow: function () { + var items = app.globalData.items + if (items.length !== this.data.items.length || + items.some(function (v, i) { return v !== this.data.items[i] }, this)) { + this.setData({ items: items }) + } + this.loadExisting(this.data.inputDate) + }, + + loadExisting: function (date) { + var records = app.globalData.records.filter(function (r) { return r.date === date }) + this.setData({ existingRecords: records }) + }, + + onDateChange: function (e) { + var date = e.detail.value + this.setData({ inputDate: date, values: {} }) + this.loadExisting(date) + }, + + onValueInput: function (e) { + var item = e.currentTarget.dataset.item + var value = e.detail.value + var update = {} + update["values." + item] = value + this.setData(update) + }, + + onSubmit: function () { + var items = this.data.items + var inputDate = this.data.inputDate + var values = this.data.values + var savedCount = 0 + var that = this + + items.forEach(function (item) { + var raw = values[item] + if (!raw || raw.trim() === "") return + var v = parseFloat(raw) + if (isNaN(v) || v <= 0) return + + var existing = app.globalData.records.find(function (r) { return r.item === item && r.date === inputDate }) + if (existing) { + existing.value = v + } else { + app.addRecord({ item: item, date: inputDate, value: v }) + } + savedCount++ + }) + + if (savedCount === 0) { + wx.showToast({ title: "请至少输入一项有效数值", icon: "none" }) + return + } + + app.saveData() + this.setData({ values: {} }) + this.loadExisting(inputDate) + wx.showToast({ title: "已保存 " + savedCount + " 项", icon: "success", duration: 1500 }) + }, + + goItems: function () { + wx.navigateTo({ url: '/pages/items/items' }) + }, + + formatDate: function (d) { + var y = d.getFullYear() + var m = String(d.getMonth() + 1) + if (m.length === 1) m = "0" + m + var day = String(d.getDate()) + if (day.length === 1) day = "0" + day + return y + "-" + m + "-" + day + } +}) diff --git a/pages/entry/entry.json b/pages/entry/entry.json new file mode 100644 index 0000000..a54ba65 --- /dev/null +++ b/pages/entry/entry.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "录入检查数据", + "usingComponents": {} +} diff --git a/pages/entry/entry.wxml b/pages/entry/entry.wxml new file mode 100644 index 0000000..325438f --- /dev/null +++ b/pages/entry/entry.wxml @@ -0,0 +1,61 @@ + + + + + 选择日期 + + + 📅 + {{inputDate}} + + + + + + + + 录入数据 + + + 检查项目 + 数值 + + + + + {{item}} + + + + + + + + + + + + + 暂无检查项目,请先前往「管理检查项目」添加 + + + + + + + 今日已有记录 + + {{item.item}} + {{item.value}} + + 修改上方数值后保存将覆盖原有数据 + + diff --git a/pages/entry/entry.wxss b/pages/entry/entry.wxss new file mode 100644 index 0000000..90e8dcf --- /dev/null +++ b/pages/entry/entry.wxss @@ -0,0 +1,162 @@ +/* ===== 录入检查数据 ===== */ + +/* 日期选择 */ +.card-date { + margin-bottom: 20rpx; +} + +.date-display { + display: flex; + align-items: center; + padding: 20rpx 24rpx; + border: 1rpx solid #e2e8f0; + border-radius: 10rpx; + background: #f7fafc; +} + +.date-icon { + font-size: 32rpx; + margin-right: 12rpx; +} + +.date-text { + flex: 1; + font-size: 30rpx; + color: #2d3748; + font-weight: 500; +} + +.date-arrow { + font-size: 20rpx; + color: #a0aec0; +} + +/* 表头 */ +.entry-header { + display: flex; + background: #f7fafc; + border-radius: 8rpx 8rpx 0 0; + padding: 16rpx 0; + border-bottom: 2rpx solid #e2e8f0; +} + +.entry-header-left, +.entry-header-right { + font-size: 26rpx; + font-weight: 600; + color: #4a5568; + text-align: center; +} + +.entry-header-left { + flex: 1; +} + +.entry-header-right { + width: 180rpx; +} + +/* 数据行 */ +.entry-row { + display: flex; + align-items: center; + border-bottom: 1rpx solid #edf2f7; +} + +.entry-row:last-of-type { + border-bottom: none; +} + +.entry-cell { + padding: 12rpx 8rpx; +} + +.entry-cell--label { + flex: 1; + font-size: 28rpx; + color: #2d3748; + font-weight: 500; + text-align: center; +} + +.entry-cell--input { + width: 180rpx; +} + +.entry-input { + width: 100%; + padding: 12rpx 16rpx; + border: 1rpx solid #e2e8f0; + border-radius: 8rpx; + background: #fff; + font-size: 26rpx; + text-align: center; + box-sizing: border-box; + height: 56rpx; +} + +.entry-input:focus { + border-color: #2c7a7b; + background: #e6f2f2; +} + +/* 提交按钮 */ +.btn-submit { + width: 100%; + margin-top: 28rpx; + background: #2c7a7b; + color: #fff; + font-size: 30rpx; + font-weight: 600; + padding: 20rpx 0; + border-radius: 10rpx; + border: none; + line-height: 1.5; +} + +.btn-submit:active { + background: #1a5c5d; +} + +/* 前往添加按钮 */ +.btn-goto { + margin-top: 20rpx; + background: #2c7a7b; + color: #fff; + font-size: 26rpx; + padding: 14rpx 40rpx; + border-radius: 8rpx; + border: none; +} + +/* 已有记录 */ +.card-existing { + margin-top: 20rpx; +} + +.existing-row { + display: flex; + justify-content: space-between; + padding: 14rpx 16rpx; + background: #f0fff4; + border-radius: 8rpx; + margin-bottom: 8rpx; +} + +.existing-item { + font-size: 26rpx; + color: #4a5568; +} + +.existing-value { + font-size: 26rpx; + font-weight: 600; + color: #38a169; +} + +.existing-note { + margin-top: 12rpx; + font-size: 22rpx; + color: #a0aec0; + text-align: center; +} diff --git a/pages/home/home.js b/pages/home/home.js new file mode 100644 index 0000000..1d181c7 --- /dev/null +++ b/pages/home/home.js @@ -0,0 +1,31 @@ +var app = getApp() + +Page({ + goEntry: function () { + wx.navigateTo({ url: '/pages/entry/entry' }) + }, + goItems: function () { + wx.navigateTo({ url: '/pages/items/items' }) + }, + goTrend: function () { + wx.navigateTo({ url: '/pages/trend/trend' }) + }, + goDetails: function () { + wx.navigateTo({ url: '/pages/details/details' }) + }, + onClearAll: function () { + var that = this + wx.showModal({ + title: "⚠️ 危险操作", + content: "确定要清除所有检查记录吗?\n此操作将删除所有历史数据,但保留检查项目,且不可恢复!", + confirmText: "确认清除", + confirmColor: "#e53e3e", + success: function (res) { + if (!res.confirm) return + app.globalData.records = [] + app.saveData() + wx.showToast({ title: "已清除全部记录", icon: "success", duration: 1500 }) + } + }) + } +}) diff --git a/pages/home/home.json b/pages/home/home.json new file mode 100644 index 0000000..c369489 --- /dev/null +++ b/pages/home/home.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "健康数据追踪", + "usingComponents": {} +} diff --git a/pages/home/home.wxml b/pages/home/home.wxml new file mode 100644 index 0000000..abe6bad --- /dev/null +++ b/pages/home/home.wxml @@ -0,0 +1,53 @@ + + + + 健康数据追踪 + 选择您需要的功能 + + + + + + + 📝 + + 录入检查数据 + 记录每日健康指标 + + + + + + ⚙️ + + 管理检查项目 + 自定义追踪指标 + + + + + + 📈 + + 数据趋势 + 查看变化趋势图 + + + + + + 📋 + + 数据详细信息 + 浏览与管理数据 + + + + + + + 🗑️ + 清除所有历史数据 + + + diff --git a/pages/home/home.wxss b/pages/home/home.wxss new file mode 100644 index 0000000..cee3b92 --- /dev/null +++ b/pages/home/home.wxss @@ -0,0 +1,118 @@ +/* ===== 首页 ===== */ +.home { + min-height: 100vh; + background: linear-gradient(180deg, #e6f2f2 0%, #f5f7fa 30%); +} + +.home-header { + text-align: center; + padding: 60rpx 0 40rpx; +} + +.home-title { + display: block; + font-size: 40rpx; + font-weight: 700; + color: #2c7a7b; + margin-bottom: 12rpx; +} + +.home-desc { + font-size: 26rpx; + color: #718096; +} + +.home-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20rpx; + padding: 0 24rpx; +} + +.home-card { + background: #fff; + border-radius: 16rpx; + padding: 36rpx 24rpx; + text-align: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.home-card:active { + transform: scale(0.97); + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.home-card-icon { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 20rpx; +} + +.home-card-emoji { + font-size: 44rpx; + line-height: 1; +} + +.home-card-icon--entry { + background: #e6f7ff; +} + +.home-card-icon--items { + background: #f0f5ff; +} + +.home-card-icon--trend { + background: #f6ffed; +} + +.home-card-icon--details { + background: #fff7e6; +} + +.home-card-title { + display: block; + font-size: 28rpx; + font-weight: 600; + color: #2d3748; + margin-bottom: 8rpx; +} + +.home-card-desc { + font-size: 22rpx; + color: #a0aec0; +} + +/* 清除数据 */ +.clear-section { + padding: 40rpx 24rpx; + text-align: center; +} + +.clear-btn { + display: inline-flex; + align-items: center; + gap: 12rpx; + padding: 20rpx 40rpx; + border: 1rpx dashed #e53e3e; + border-radius: 12rpx; + background: #fff5f5; +} + +.clear-btn:active { + background: #ffe0e0; +} + +.clear-icon { + font-size: 32rpx; +} + +.clear-text { + font-size: 26rpx; + color: #e53e3e; + font-weight: 500; +} diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..25e9740 --- /dev/null +++ b/pages/index/index.js @@ -0,0 +1,10 @@ +Page({ + onLoad: function () { + var that = this + setTimeout(function () { + wx.redirectTo({ + url: '/pages/home/home' + }) + }, 2000) + } +}) diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..b16ef3e --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,4 @@ +{ + "navigationStyle": "custom", + "usingComponents": {} +} diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..19ca988 --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,14 @@ + + + + + + + 健康数据追踪 + 记录每一次检查,守护您的健康 + + + + 南京市中医院 + + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..44bee81 --- /dev/null +++ b/pages/index/index.wxss @@ -0,0 +1,101 @@ +/* ===== 启动动画页 ===== */ +.splash { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + background: linear-gradient(135deg, #2c7a7b 0%, #1a5c5d 40%, #0f4a4b 100%); +} + +.splash-content { + display: flex; + flex-direction: column; + align-items: center; + animation: fadeInUp 0.8s ease-out; +} + +.splash-icon { + margin-bottom: 40rpx; + animation: pulse 1.2s ease-in-out infinite; +} + +.splash-heart { + width: 120rpx; + height: 120rpx; + background: #fff; + border-radius: 50%; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.splash-heart::before { + content: ""; + font-size: 60rpx; + color: #2c7a7b; +} + +/* 心跳图标 - 十字医疗符号 */ +.splash-heart { + background: rgba(255,255,255,0.95); +} + +.splash-heart::after { + content: "+"; + font-size: 72rpx; + font-weight: 300; + color: #2c7a7b; + line-height: 1; +} + +.splash-title { + font-size: 44rpx; + font-weight: 700; + color: #fff; + margin-bottom: 16rpx; + letter-spacing: 4rpx; +} + +.splash-subtitle { + font-size: 26rpx; + color: rgba(255,255,255,0.75); + letter-spacing: 2rpx; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(60rpx); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } +} + +/* 底部医院名称 */ +.splash-footer { + position: absolute; + bottom: 80rpx; + left: 0; + right: 0; + text-align: center; + animation: fadeInUp 0.8s ease-out 0.4s both; +} + +.splash-hospital { + font-size: 30rpx; + color: rgba(255,255,255,0.7); + letter-spacing: 6rpx; +} diff --git a/pages/items/items.js b/pages/items/items.js new file mode 100644 index 0000000..bd4344e --- /dev/null +++ b/pages/items/items.js @@ -0,0 +1,50 @@ +var app = getApp() + +Page({ + data: { + items: [], + newItemName: "" + }, + + onShow: function () { + this.setData({ items: app.globalData.items }) + }, + + onNameInput: function (e) { + this.setData({ newItemName: e.detail.value }) + }, + + onAddItem: function () { + var name = this.data.newItemName.trim() + if (!name) { + wx.showToast({ title: "请输入项目名称", icon: "none" }) + return + } + if (app.globalData.items.indexOf(name) > -1) { + wx.showToast({ title: "该项目已存在", icon: "none" }) + return + } + app.addItem(name) + this.setData({ + items: app.globalData.items, + newItemName: "" + }) + wx.showToast({ title: "已添加", icon: "success", duration: 1000 }) + }, + + onDeleteItem: function (e) { + var item = e.currentTarget.dataset.item + var that = this + wx.showModal({ + title: "确认删除", + content: "确定删除项目「" + item + "」吗?\n该项目的所有历史数据也将被删除。", + confirmColor: "#e53e3e", + success: function (res) { + if (!res.confirm) return + app.removeItem(item) + that.setData({ items: app.globalData.items }) + wx.showToast({ title: "已删除", icon: "success", duration: 1000 }) + } + }) + } +}) diff --git a/pages/items/items.json b/pages/items/items.json new file mode 100644 index 0000000..256cbe8 --- /dev/null +++ b/pages/items/items.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "管理检查项目", + "usingComponents": {} +} diff --git a/pages/items/items.wxml b/pages/items/items.wxml new file mode 100644 index 0000000..b193379 --- /dev/null +++ b/pages/items/items.wxml @@ -0,0 +1,44 @@ + + + + + 新增检查项目 + + + + + + + + + 已有项目(共{{items.length}}项) + + + 项目名称 + 操作 + + + + + + {{item}} + + + + + + + + + + 暂无检查项目,请在上方添加 + + diff --git a/pages/items/items.wxss b/pages/items/items.wxss new file mode 100644 index 0000000..7d39fac --- /dev/null +++ b/pages/items/items.wxss @@ -0,0 +1,120 @@ +/* ===== 管理检查项目 ===== */ + +/* 新增区域 */ +.add-row { + display: flex; + gap: 16rpx; + align-items: center; +} + +.add-input { + flex: 1; + padding: 16rpx 20rpx; + border: 1rpx solid #e2e8f0; + border-radius: 8rpx; + background: #f7fafc; + font-size: 28rpx; + height: 56rpx; + box-sizing: border-box; +} + +.add-input:focus { + border-color: #2c7a7b; + background: #fff; +} + +.btn-add { + flex-shrink: 0; + width: 130rpx; + height: 56rpx; + line-height: 56rpx; + font-size: 26rpx; + font-weight: 500; + color: #fff; + background: #2c7a7b; + border-radius: 8rpx; + padding: 0; + text-align: center; + border: none; +} + +.btn-add:active { + background: #1a5c5d; +} + +/* 列表表头 */ +.list-header { + display: flex; + background: #f7fafc; + border-radius: 8rpx 8rpx 0 0; + padding: 16rpx 0; + border-bottom: 2rpx solid #e2e8f0; +} + +.list-header-left, +.list-header-right { + font-size: 26rpx; + font-weight: 600; + color: #4a5568; + text-align: center; +} + +.list-header-left { + flex: 1; +} + +.list-header-right { + width: 140rpx; +} + +/* 列表行 */ +.list-row { + display: flex; + align-items: center; + padding: 20rpx 8rpx; + border-bottom: 1rpx solid #edf2f7; +} + +.list-row:last-of-type { + border-bottom: none; +} + +.list-cell--name { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 10rpx; + font-size: 28rpx; + color: #2d3748; + font-weight: 500; +} + +.list-dot { + width: 10rpx; + height: 10rpx; + border-radius: 50%; + background: #2c7a7b; + flex-shrink: 0; +} + +.list-cell--action { + width: 140rpx; + display: flex; + justify-content: center; +} + +.btn-del { + padding: 8rpx 24rpx; + font-size: 24rpx; + color: #e53e3e; + background: #fff; + border: 1rpx solid #e53e3e; + border-radius: 6rpx; + line-height: 1.5; + text-align: center; +} + +.btn-del:active { + background: #fff5f5; +} diff --git a/pages/trend/trend.js b/pages/trend/trend.js new file mode 100644 index 0000000..7404c31 --- /dev/null +++ b/pages/trend/trend.js @@ -0,0 +1,219 @@ +var app = getApp() +var drawChart = require("../../utils/chart").drawChart + +var COLORS = ["#e53e3e", "#3182ce", "#38a169", "#d69e2e", "#805ad5", "#dd6b20", "#319795", "#d53f8c"] + +Page({ + data: { + items: [], + filterItems: [], + viewMode: "month", + filterMonth: "", + filterYear: "", + customStart: "", + customEnd: "", + hasData: false, + colors: COLORS + }, + + onLoad: function () { + var today = this.formatDate(new Date(), "day") + var month = today.slice(0, 7) + var year = today.slice(0, 4) + var items = app.globalData.items + this.setData({ + items: items, + filterItems: items.slice(), + filterMonth: month, + filterYear: year, + customStart: month, + customEnd: month + }) + }, + + onReady: function () { + this._initCanvas() + }, + + onShow: function () { + var items = app.globalData.items + if (items.length !== this.data.items.length || + items.some(function (v, i) { return v !== this.data.items[i] }, this)) { + this.setData({ items: items }) + var newFilter = items.filter(function (i) { return this.data.items.indexOf(i) === -1 }, this) + if (newFilter.length > 0) { + this.setData({ filterItems: this.data.filterItems.concat(newFilter) }) + } + } + if (this._canvasReady) { + this.refreshChart() + } + }, + + _initCanvas: function () { + var that = this + wx.createSelectorQuery().in(this) + .select("#trendCanvas") + .boundingClientRect(function (rect) { + if (!rect || rect.width <= 0) return + var dpr = wx.getWindowInfo().pixelRatio + that._canvasW = rect.width + that._canvasH = rect.height + that.canvasCtx = wx.createCanvasContext('trendCanvas', that) + that.canvasCtx.scale(dpr, dpr) + that._canvasReady = true + that.refreshChart() + }) + .exec() + }, + + getDateRange: function () { + var viewMode = this.data.viewMode + var filterMonth = this.data.filterMonth + var filterYear = this.data.filterYear + var customStart = this.data.customStart + var customEnd = this.data.customEnd + var start, end, parts, y, m, sy, sm, ey, em + + if (viewMode === "month") { + parts = filterMonth.split("-") + y = Number(parts[0]) + m = Number(parts[1]) + start = new Date(y, m - 1, 1) + end = new Date(y, m, 0) + } else if (viewMode === "year") { + y = parseInt(filterYear) + start = new Date(y, 0, 1) + end = new Date(y, 11, 31) + } else { + parts = customStart.split("-") + sy = Number(parts[0]) + sm = Number(parts[1]) + parts = customEnd.split("-") + ey = Number(parts[0]) + em = Number(parts[1]) + start = new Date(sy, sm - 1, 1) + end = new Date(ey, em, 0) + } + return { start: start, end: end } + }, + + refreshChart: function () { + var records = app.globalData.records + var filterItems = this.data.filterItems + var range = this.getDateRange() + var start = range.start + var end = range.end + + var inRange = function (r) { + var d = new Date(r.date + "T00:00:00") + return d >= start && d <= end + } + + var filtered = records.filter(function (r) { + return inRange(r) && filterItems.indexOf(r.item) > -1 + }) + + if (filtered.length === 0) { + this.setData({ hasData: false }) + return + } + + var dateMap = {} + filtered.forEach(function (r) { + if (!dateMap[r.date]) dateMap[r.date] = {} + if (!dateMap[r.date][r.item]) { + dateMap[r.date][r.item] = { sum: 0, count: 0 } + } + dateMap[r.date][r.item].sum += r.value + dateMap[r.date][r.item].count += 1 + }) + + var dates = Object.keys(dateMap).sort() + var xLabels = dates.map(function (d) { return d.slice(5) }) + var series = filterItems.map(function (fi) { + return { + name: fi, + data: dates.map(function (d) { + var entry = dateMap[d][fi] + if (!entry) return null + return Math.round((entry.sum / entry.count) * 100) / 100 + }) + } + }) + + this._chartData = { xLabels: xLabels, series: series } + var that = this + this.setData({ hasData: true }, function () { that.drawChart() }) + }, + + drawChart: function () { + if (!this.canvasCtx || !this._canvasW || !this._canvasH) return + var chartData = this._chartData + if (!chartData || !chartData.xLabels.length) return + var filterItems = this.data.filterItems + var items = this.data.items + var colors = this.data.colors + var that = this + drawChart(this.canvasCtx, this._canvasW, this._canvasH, { + xLabels: chartData.xLabels, + series: chartData.series, + colors: filterItems.map(function (fi) { + return colors[items.indexOf(fi) % colors.length] + }) + }) + }, + + onSwitchMode: function (e) { + var that = this + this.setData({ viewMode: e.currentTarget.dataset.mode }, function () { that.refreshChart() }) + }, + + onMonthChange: function (e) { + var that = this + this.setData({ filterMonth: e.detail.value }, function () { that.refreshChart() }) + }, + + onYearChange: function (e) { + var that = this + this.setData({ filterYear: e.detail.value }, function () { that.refreshChart() }) + }, + + onCustomStartChange: function (e) { + var that = this + this.setData({ customStart: e.detail.value }, function () { that.refreshChart() }) + }, + + onCustomEndChange: function (e) { + var that = this + this.setData({ customEnd: e.detail.value }, function () { that.refreshChart() }) + }, + + onToggleItem: function (e) { + var item = e.currentTarget.dataset.item + var filterItems = this.data.filterItems.slice() + var idx = filterItems.indexOf(item) + var that = this + if (idx > -1) { + if (filterItems.length <= 1) { + wx.showToast({ title: "至少保留一个项目", icon: "none" }) + return + } + filterItems.splice(idx, 1) + } else { + filterItems.push(item) + } + this.setData({ filterItems: filterItems }, function () { that.refreshChart() }) + }, + + formatDate: function (d, mode) { + var y = d.getFullYear() + var m = String(d.getMonth() + 1) + if (m.length === 1) m = "0" + m + var day = String(d.getDate()) + if (day.length === 1) day = "0" + day + if (mode === "year") return String(y) + if (mode === "month") return y + "-" + m + return y + "-" + m + "-" + day + } +}) diff --git a/pages/trend/trend.json b/pages/trend/trend.json new file mode 100644 index 0000000..905d486 --- /dev/null +++ b/pages/trend/trend.json @@ -0,0 +1,4 @@ +{ + "navigationBarTitleText": "数据趋势", + "usingComponents": {} +} diff --git a/pages/trend/trend.wxml b/pages/trend/trend.wxml new file mode 100644 index 0000000..63007e2 --- /dev/null +++ b/pages/trend/trend.wxml @@ -0,0 +1,74 @@ + + + + + + 按月 + 按年 + 自定义 + + + + + + + {{filterMonth}} + + + + + + + + + + {{filterYear}} + + + + + + + + + + + {{customStart}} + + + + + + {{customEnd}} + + + + + + + + 选择展示项目(可多选) + + + + + {{item}} + + + + + + + + 趋势图 + + + 暂无数据 + + + diff --git a/pages/trend/trend.wxss b/pages/trend/trend.wxss new file mode 100644 index 0000000..ecff3ef --- /dev/null +++ b/pages/trend/trend.wxss @@ -0,0 +1,97 @@ +/* ===== 数据趋势 ===== */ + +/* 筛选行 */ +.filter-row { + margin-top: 4rpx; +} + +.custom-range { + display: flex; + align-items: center; + gap: 16rpx; +} + +.range-sep { + font-size: 26rpx; + color: #718096; + flex-shrink: 0; +} + +.picker-display { + display: flex; + align-items: center; + justify-content: center; + padding: 16rpx 24rpx; + border: 1rpx solid #e2e8f0; + border-radius: 8rpx; + background: #f7fafc; + font-size: 28rpx; + color: #2d3748; +} + +.picker-display--sm { + flex: 1; + padding: 12rpx 16rpx; + font-size: 26rpx; +} + +.picker-arrow { + font-size: 20rpx; + color: #a0aec0; + margin-left: 10rpx; +} + +.picker-label-sm { + color: #a0aec0; + margin-right: 8rpx; +} + +/* 项目标签 */ +.items-tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; +} + +.item-tag { + display: flex; + align-items: center; + gap: 8rpx; + padding: 10rpx 20rpx; + border-radius: 20rpx; + font-size: 24rpx; + color: #718096; + background: #edf2f7; + border: 1rpx solid #e2e8f0; +} + +.item-tag--active { + font-weight: 500; +} + +.item-tag-dot { + width: 12rpx; + height: 12rpx; + border-radius: 50%; + flex-shrink: 0; +} + +/* 图表 */ +.card-chart { + position: relative; + overflow: hidden; +} + +.chart-canvas { + width: 100%; + height: 460rpx; +} + +.chart-empty-overlay { + position: absolute; + top: 55%; + left: 0; + right: 0; + text-align: center; + pointer-events: none; +} diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..41a8b4b --- /dev/null +++ b/project.config.json @@ -0,0 +1,26 @@ +{ + "setting": { + "es6": true, + "postcss": true, + "minified": true, + "uglifyFileName": false, + "enhance": true, + "packNpmRelationList": [], + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "useCompilerPlugins": false, + "minifyWXML": true + }, + "compileType": "miniprogram", + "simulatorPluginLibVersion": {}, + "packOptions": { + "ignore": [], + "include": [] + }, + "appid": "wxefee3681fb349e54", + "editorSetting": {}, + "testRoot": "minitest/" +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..2b46f79 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,14 @@ +{ + "libVersion": "3.15.2", + "projectname": "%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F", + "setting": { + "urlCheck": true, + "coverView": true, + "lazyloadPlaceholderEnable": false, + "skylineRenderEnable": false, + "preloadBackgroundData": false, + "autoAudits": false, + "showShadowRootInWxmlPanel": true, + "compileHotReLoad": true + } +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..6b83b54 --- /dev/null +++ b/sitemap.json @@ -0,0 +1 @@ +{"rules":[{"action":"allow","page":"*"}]} diff --git a/utils/chart.js b/utils/chart.js new file mode 100644 index 0000000..757d87e --- /dev/null +++ b/utils/chart.js @@ -0,0 +1,119 @@ +var PADDING = { top: 24, right: 16, bottom: 44, left: 48 } + +function drawChart(ctx, w, h, opts) { + var xLabels = opts.xLabels + var series = opts.series + var colors = opts.colors + if (!xLabels.length || !series.length) return + + ctx.clearRect(0, 0, w, h) + + var plotW = w - PADDING.left - PADDING.right + var plotH = h - PADDING.top - PADDING.bottom + var cx = PADDING.left + var cy = PADDING.top + + var yMin = Infinity, yMax = -Infinity + series.forEach(function (s) { + s.data.forEach(function (v) { + if (v !== null && v !== undefined) { + if (v < yMin) yMin = v + if (v > yMax) yMax = v + } + }) + }) + if (!isFinite(yMin) || !isFinite(yMax)) return + + var yRange = yMax - yMin || 1 + yMin = Math.floor(yMin - yRange * 0.1) + yMax = Math.ceil(yMax + yRange * 0.1) + if (yMin < 0 && yMin + yRange * 0.1 > 0) yMin = 0 + var realRange = yMax - yMin + + var toX = function (i) { return cx + (i / Math.max(xLabels.length - 1, 1)) * plotW } + var toY = function (v) { return cy + plotH - ((v - yMin) / realRange) * plotH } + + var gridCount = Math.min(5, Math.ceil(realRange)) + ctx.strokeStyle = "#edf2f7" + ctx.lineWidth = 0.5 + ctx.setLineDash([4, 4]) + ctx.font = "10px -apple-system, sans-serif" + ctx.fillStyle = "#a0aec0" + ctx.textAlign = "right" + ctx.textBaseline = "middle" + + for (var i = 0; i <= gridCount; i++) { + var val = yMin + (realRange * i) / gridCount + var y = toY(val) + ctx.beginPath() + ctx.moveTo(cx, y) + ctx.lineTo(cx + plotW, y) + ctx.stroke() + ctx.fillText(formatVal(val), cx - 6, y) + } + ctx.setLineDash([]) + + var maxLabels = Math.floor(plotW / 50) + var step = Math.max(1, Math.ceil(xLabels.length / maxLabels)) + ctx.textAlign = "center" + ctx.textBaseline = "top" + ctx.fillStyle = "#718096" + for (var i = 0; i < xLabels.length; i += step) { + ctx.fillText(xLabels[i], toX(i), cy + plotH + 8) + } + + ctx.strokeStyle = "#cbd5e0" + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(cx, cy) + ctx.lineTo(cx, cy + plotH) + ctx.lineTo(cx + plotW, cy + plotH) + ctx.stroke() + + series.forEach(function (s, si) { + var color = colors[si % colors.length] + var points = s.data + .map(function (v, i) { + if (v !== null && v !== undefined) { + return { x: toX(i), y: toY(v), v: v } + } + return null + }) + .filter(function (p) { return p !== null }) + + if (points.length < 2) return + + ctx.strokeStyle = color + ctx.lineWidth = 2 + ctx.lineJoin = "round" + ctx.beginPath() + ctx.moveTo(points[0].x, points[0].y) + for (var i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y) + } + ctx.stroke() + + points.forEach(function (p) { + ctx.fillStyle = "#fff" + ctx.beginPath() + ctx.arc(p.x, p.y, 3.5, 0, Math.PI * 2) + ctx.fill() + ctx.fillStyle = color + ctx.beginPath() + ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2) + ctx.fill() + }) + }) + + ctx.draw() +} + +function formatVal(v) { + if (v === 0) return "0" + var abs = Math.abs(v) + if (abs >= 1000) return (v / 1000).toFixed(1).replace(/\.0$/, "") + "k" + if (abs >= 1) return Number(v.toFixed(1)).toString() + return Number(v.toFixed(2)).toString() +} + +module.exports = { drawChart: drawChart }