<新增> 1、提交微信小程序初始开发版本

This commit is contained in:
ypc 2026-06-06 11:46:55 +08:00
commit d9d3833d92
32 changed files with 2176 additions and 0 deletions

59
app.js Normal file
View File

@ -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()
}
})

18
app.json Normal file
View File

@ -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"
}

165
app.wxss Normal file
View File

@ -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;
}

View File

@ -0,0 +1,3 @@
{
"treeData": []
}

226
pages/details/details.js Normal file
View File

@ -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
}
})

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "数据详细信息",
"usingComponents": {}
}

114
pages/details/details.wxml Normal file
View File

@ -0,0 +1,114 @@
<!-- 数据详细信息 -->
<view class="container">
<!-- 时间范围选择 -->
<view class="card card-filter">
<view class="mode-tabs">
<view class="mode-tab {{viewMode === 'month' ? 'mode-tab--active' : ''}}" bindtap="onSwitchMode" data-mode="month">按月</view>
<view class="mode-tab {{viewMode === 'year' ? 'mode-tab--active' : ''}}" bindtap="onSwitchMode" data-mode="year">按年</view>
<view class="mode-tab {{viewMode === 'custom' ? 'mode-tab--active' : ''}}" bindtap="onSwitchMode" data-mode="custom">自定义</view>
</view>
<view wx:if="{{viewMode === 'month'}}" class="filter-row">
<picker mode="date" fields="month" value="{{filterMonth}}" bindchange="onMonthChange">
<view class="picker-display">
<text>{{filterMonth}}</text>
<text class="picker-arrow">&#x25BC;</text>
</view>
</picker>
</view>
<view wx:if="{{viewMode === 'year'}}" class="filter-row">
<picker mode="date" fields="year" value="{{filterYear}}" bindchange="onYearChange">
<view class="picker-display">
<text>{{filterYear}}</text>
<text class="picker-arrow">&#x25BC;</text>
</view>
</picker>
</view>
<view wx:if="{{viewMode === 'custom'}}" class="filter-row custom-range">
<picker mode="date" fields="month" value="{{customStart}}" bindchange="onCustomStartChange">
<view class="picker-display picker-display--sm">
<text class="picker-label-sm">从</text>
<text>{{customStart}}</text>
</view>
</picker>
<text class="range-sep">至</text>
<picker mode="date" fields="month" value="{{customEnd}}" bindchange="onCustomEndChange">
<view class="picker-display picker-display--sm">
<text>{{customEnd}}</text>
</view>
</picker>
</view>
</view>
<!-- 数据表格 -->
<view class="card card-table" wx:if="{{tableData.length > 0}}">
<view class="card-title">数据明细(共{{tableData.length}}条)</view>
<scroll-view scroll-x="{{true}}" enable-flex class="table-scroll">
<view class="table" style="min-width: {{items.length * 130 + 260}}rpx;">
<!-- 表头 -->
<view class="table-row table-row--head">
<view class="table-cell table-cell--date">日期</view>
<block wx:for="{{items}}" wx:key="*this">
<view class="table-cell">{{item}}</view>
</block>
<view class="table-cell table-cell--action">删除</view>
<view class="table-cell table-cell--action">修改</view>
</view>
<!-- 数据行 -->
<view class="table-row" wx:for="{{tableData}}" wx:key="rowKey">
<view class="table-cell table-cell--date">{{item.rowKey}}</view>
<block wx:for="{{items}}" wx:key="*this" wx:for-item="fi" wx:for-index="fidx">
<view class="table-cell table-cell--value">
<text wx:if="{{item.values[fidx] !== undefined}}">{{item.values[fidx]}}</text>
<text wx:else class="table-empty">-</text>
</view>
</block>
<view class="table-cell table-cell--action">
<text class="btn-del-text" bindtap="onDeleteRow" data-date="{{item.rowKey}}">删除</text>
</view>
<view class="table-cell table-cell--action">
<text class="btn-edit-text" bindtap="onEditRow" data-date="{{item.rowKey}}">修改</text>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="card" wx:else>
<view class="empty-hint">该时间范围内暂无数据</view>
</view>
</view>
<!-- ==== 修改弹窗 ==== -->
<view wx:if="{{showEditModal}}" class="modal-wrap" catchtap="cancelEdit">
<view class="modal-box" catchtap="">
<view class="modal-title">修改记录</view>
<view class="modal-date">日期:{{editingDate}}</view>
<!-- 各项目数据 -->
<scroll-view scroll-y="{{true}}" class="modal-scroll">
<view class="modal-row" wx:for="{{items}}" wx:key="*this">
<view class="modal-row-label">
<text>{{item}}</text>
</view>
<view class="modal-row-input">
<input
class="modal-input"
type="digit"
placeholder="无数据"
value="{{editValues[item] || ''}}"
data-item="{{item}}"
bindinput="onEditValueInput"
/>
</view>
</view>
</scroll-view>
<view class="btn-row">
<button class="btn-cancel" bindtap="cancelEdit">取消</button>
<button class="btn-save" bindtap="saveEdit">保存</button>
</view>
</view>
</view>

163
pages/details/details.wxss Normal file
View File

@ -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;
}

90
pages/entry/entry.js Normal file
View File

@ -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
}
})

4
pages/entry/entry.json Normal file
View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "录入检查数据",
"usingComponents": {}
}

61
pages/entry/entry.wxml Normal file
View File

@ -0,0 +1,61 @@
<!-- 录入检查数据 -->
<view class="container">
<!-- 日期选择 -->
<view class="card card-date">
<view class="card-title">选择日期</view>
<picker mode="date" value="{{inputDate}}" bindchange="onDateChange">
<view class="date-display">
<text class="date-icon">📅</text>
<text class="date-text">{{inputDate}}</text>
<text class="date-arrow">&#x25BC;</text>
</view>
</picker>
</view>
<!-- 数据录入两列表格 -->
<view class="card card-entry-form" wx:if="{{items.length > 0}}">
<view class="card-title">录入数据</view>
<!-- 表头 -->
<view class="entry-header">
<text class="entry-header-left">检查项目</text>
<text class="entry-header-right">数值</text>
</view>
<!-- 数据行 -->
<view class="entry-row" wx:for="{{items}}" wx:key="*this">
<view class="entry-cell entry-cell--label">
<text>{{item}}</text>
</view>
<view class="entry-cell entry-cell--input">
<input
class="entry-input"
type="digit"
placeholder="请输入"
value="{{values[item] || ''}}"
data-item="{{item}}"
bindinput="onValueInput"
confirm-type="next"
/>
</view>
</view>
<!-- 提交按钮 -->
<button class="btn-submit" bindtap="onSubmit">保存全部数据</button>
</view>
<!-- 无项目提示 -->
<view class="card" wx:else>
<view class="empty-hint">
<text>暂无检查项目,请先前往「管理检查项目」添加</text>
<button class="btn-goto" bindtap="goItems">前往添加</button>
</view>
</view>
<!-- 提示:已有数据 -->
<view class="card card-existing" wx:if="{{existingRecords.length > 0}}">
<view class="card-title">今日已有记录</view>
<view class="existing-row" wx:for="{{existingRecords}}" wx:key="item">
<text class="existing-item">{{item.item}}</text>
<text class="existing-value">{{item.value}}</text>
</view>
<view class="existing-note">修改上方数值后保存将覆盖原有数据</view>
</view>
</view>

162
pages/entry/entry.wxss Normal file
View File

@ -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;
}

31
pages/home/home.js Normal file
View File

@ -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 })
}
})
}
})

4
pages/home/home.json Normal file
View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "健康数据追踪",
"usingComponents": {}
}

53
pages/home/home.wxml Normal file
View File

@ -0,0 +1,53 @@
<!-- 首页 - 功能入口 -->
<view class="container home">
<view class="home-header">
<text class="home-title">健康数据追踪</text>
<text class="home-desc">选择您需要的功能</text>
</view>
<view class="home-grid">
<!-- 录入检查数据 -->
<view class="home-card" bindtap="goEntry">
<view class="home-card-icon home-card-icon--entry">
<text class="home-card-emoji">📝</text>
</view>
<text class="home-card-title">录入检查数据</text>
<text class="home-card-desc">记录每日健康指标</text>
</view>
<!-- 管理检查项目 -->
<view class="home-card" bindtap="goItems">
<view class="home-card-icon home-card-icon--items">
<text class="home-card-emoji">⚙️</text>
</view>
<text class="home-card-title">管理检查项目</text>
<text class="home-card-desc">自定义追踪指标</text>
</view>
<!-- 数据趋势 -->
<view class="home-card" bindtap="goTrend">
<view class="home-card-icon home-card-icon--trend">
<text class="home-card-emoji">📈</text>
</view>
<text class="home-card-title">数据趋势</text>
<text class="home-card-desc">查看变化趋势图</text>
</view>
<!-- 数据详细信息 -->
<view class="home-card" bindtap="goDetails">
<view class="home-card-icon home-card-icon--details">
<text class="home-card-emoji">📋</text>
</view>
<text class="home-card-title">数据详细信息</text>
<text class="home-card-desc">浏览与管理数据</text>
</view>
</view>
<!-- 清除所有数据 -->
<view class="clear-section">
<view class="clear-btn" bindtap="onClearAll">
<text class="clear-icon">🗑️</text>
<text class="clear-text">清除所有历史数据</text>
</view>
</view>
</view>

118
pages/home/home.wxss Normal file
View File

@ -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;
}

10
pages/index/index.js Normal file
View File

@ -0,0 +1,10 @@
Page({
onLoad: function () {
var that = this
setTimeout(function () {
wx.redirectTo({
url: '/pages/home/home'
})
}, 2000)
}
})

4
pages/index/index.json Normal file
View File

@ -0,0 +1,4 @@
{
"navigationStyle": "custom",
"usingComponents": {}
}

14
pages/index/index.wxml Normal file
View File

@ -0,0 +1,14 @@
<!-- 启动动画页 -->
<view class="splash">
<view class="splash-content">
<view class="splash-icon">
<view class="splash-heart"></view>
</view>
<text class="splash-title">健康数据追踪</text>
<text class="splash-subtitle">记录每一次检查,守护您的健康</text>
</view>
<view class="splash-footer">
<text class="splash-hospital">南京市中医院</text>
</view>
</view>

101
pages/index/index.wxss Normal file
View File

@ -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;
}

50
pages/items/items.js Normal file
View File

@ -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 })
}
})
}
})

4
pages/items/items.json Normal file
View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "管理检查项目",
"usingComponents": {}
}

44
pages/items/items.wxml Normal file
View File

@ -0,0 +1,44 @@
<!-- 管理检查项目 -->
<view class="container">
<!-- 新增项目 -->
<view class="card card-add">
<view class="card-title">新增检查项目</view>
<view class="add-row">
<input
class="add-input"
placeholder="输入项目名称,如:血糖"
value="{{newItemName}}"
bindinput="onNameInput"
bindconfirm="onAddItem"
confirm-type="done"
maxlength="20"
/>
<button class="btn-add" bindtap="onAddItem">新增</button>
</view>
</view>
<!-- 项目列表 -->
<view class="card card-list" wx:if="{{items.length > 0}}">
<view class="card-title">已有项目(共{{items.length}}项)</view>
<!-- 表头 -->
<view class="list-header">
<text class="list-header-left">项目名称</text>
<text class="list-header-right">操作</text>
</view>
<!-- 列表行 -->
<view class="list-row" wx:for="{{items}}" wx:key="*this">
<view class="list-cell list-cell--name">
<view class="list-dot"></view>
<text>{{item}}</text>
</view>
<view class="list-cell list-cell--action">
<button class="btn-del" bindtap="onDeleteItem" data-item="{{item}}">删除</button>
</view>
</view>
</view>
<!-- 无项目 -->
<view class="card" wx:else>
<view class="empty-hint">暂无检查项目,请在上方添加</view>
</view>
</view>

120
pages/items/items.wxss Normal file
View File

@ -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;
}

219
pages/trend/trend.js Normal file
View File

@ -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
}
})

4
pages/trend/trend.json Normal file
View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "数据趋势",
"usingComponents": {}
}

74
pages/trend/trend.wxml Normal file
View File

@ -0,0 +1,74 @@
<!-- 数据趋势 -->
<view class="container">
<!-- 时间范围选择 -->
<view class="card card-filter">
<view class="mode-tabs">
<view class="mode-tab {{viewMode === 'month' ? 'mode-tab--active' : ''}}" bindtap="onSwitchMode" data-mode="month">按月</view>
<view class="mode-tab {{viewMode === 'year' ? 'mode-tab--active' : ''}}" bindtap="onSwitchMode" data-mode="year">按年</view>
<view class="mode-tab {{viewMode === 'custom' ? 'mode-tab--active' : ''}}" bindtap="onSwitchMode" data-mode="custom">自定义</view>
</view>
<!-- 按月选择 -->
<view wx:if="{{viewMode === 'month'}}" class="filter-row">
<picker mode="date" fields="month" value="{{filterMonth}}" bindchange="onMonthChange">
<view class="picker-display">
<text>{{filterMonth}}</text>
<text class="picker-arrow">&#x25BC;</text>
</view>
</picker>
</view>
<!-- 按年选择 -->
<view wx:if="{{viewMode === 'year'}}" class="filter-row">
<picker mode="date" fields="year" value="{{filterYear}}" bindchange="onYearChange">
<view class="picker-display">
<text>{{filterYear}}</text>
<text class="picker-arrow">&#x25BC;</text>
</view>
</picker>
</view>
<!-- 自定义时间范围 -->
<view wx:if="{{viewMode === 'custom'}}" class="filter-row custom-range">
<picker mode="date" fields="month" value="{{customStart}}" bindchange="onCustomStartChange">
<view class="picker-display picker-display--sm">
<text class="picker-label-sm">从</text>
<text>{{customStart}}</text>
</view>
</picker>
<text class="range-sep">至</text>
<picker mode="date" fields="month" value="{{customEnd}}" bindchange="onCustomEndChange">
<view class="picker-display picker-display--sm">
<text>{{customEnd}}</text>
</view>
</picker>
</view>
</view>
<!-- 项目筛选 -->
<view class="card card-items">
<view class="card-title">选择展示项目(可多选)</view>
<view class="items-tags">
<block wx:for="{{items}}" wx:key="*this" wx:for-index="si">
<view
class="item-tag {{filterItems.indexOf(item) > -1 ? 'item-tag--active' : ''}}"
style="{{filterItems.indexOf(item) > -1 ? 'border-color:' + colors[si % colors.length] + '; color:' + colors[si % colors.length] + '; background:' + colors[si % colors.length] + '18;' : ''}}"
bindtap="onToggleItem"
data-item="{{item}}"
>
<view class="item-tag-dot" style="background-color:{{colors[si % colors.length]}};"></view>
<text>{{item}}</text>
</view>
</block>
</view>
</view>
<!-- 趋势图 -->
<view class="card card-chart">
<view class="card-title">趋势图</view>
<canvas canvas-id="trendCanvas" id="trendCanvas" class="chart-canvas"></canvas>
<view class="chart-empty-overlay" wx:if="{{!hasData}}">
<text class="empty-hint">暂无数据</text>
</view>
</view>
</view>

97
pages/trend/trend.wxss Normal file
View File

@ -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;
}

26
project.config.json Normal file
View File

@ -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/"
}

View File

@ -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
}
}

1
sitemap.json Normal file
View File

@ -0,0 +1 @@
{"rules":[{"action":"allow","page":"*"}]}

119
utils/chart.js Normal file
View File

@ -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 }