<新增> 1、提交微信小程序初始开发版本
This commit is contained in:
commit
d9d3833d92
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"treeData": []
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "数据详细信息",
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
|
|
@ -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">▼</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">▼</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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "录入检查数据",
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
|
|
@ -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">▼</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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "健康数据追踪",
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
Page({
|
||||||
|
onLoad: function () {
|
||||||
|
var that = this
|
||||||
|
setTimeout(function () {
|
||||||
|
wx.redirectTo({
|
||||||
|
url: '/pages/home/home'
|
||||||
|
})
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "管理检查项目",
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "数据趋势",
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
|
|
@ -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">▼</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">▼</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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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/"
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"rules":[{"action":"allow","page":"*"}]}
|
||||||
|
|
@ -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 }
|
||||||
Loading…
Reference in New Issue