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 }