feat: 增加数据统计页面

This commit is contained in:
2026-08-28 13:43:37 +08:00
parent f5c4cdf93f
commit 7021d9d525
13 changed files with 324 additions and 52 deletions

View File

@@ -33,7 +33,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useProfileStore } from '@/store/profile' import { useProfileStore } from '@/store/profile'
import { countRecordsByMonth, getTotalCount } from "@/utils/record"; import { countRecordsByMonth, deleteRecordByMonth, getTotalCount } from "@/utils/record";
const profileStore = useProfileStore() const profileStore = useProfileStore()
@@ -43,7 +43,7 @@ function clearMonth(month: string) {
content: `确定清除 ${month} 的所有记录吗?`, content: `确定清除 ${month} 的所有记录吗?`,
success(res) { success(res) {
if (res.confirm) { if (res.confirm) {
wx.removeStorageSync(`health_${month}`) deleteRecordByMonth(month)
profileStore.storageStats = countRecordsByMonth() profileStore.storageStats = countRecordsByMonth()
profileStore.storageTotal = getTotalCount() profileStore.storageTotal = getTotalCount()
wx.showToast({ title: '已清除', icon: 'success' }) wx.showToast({ title: '已清除', icon: 'success' })

View File

@@ -38,8 +38,8 @@
{ {
"pagePath": "pages/stats/index", "pagePath": "pages/stats/index",
"text": "统计", "text": "统计",
"iconPath": "static/tabs/record.png", "iconPath": "static/tabs/stats.png",
"selectedIconPath": "static/tabs/record-active.png" "selectedIconPath": "static/tabs/stats-active.png"
}, },
{ {
"pagePath": "pages/user/index", "pagePath": "pages/user/index",

View File

@@ -140,6 +140,15 @@ function loadListRecords() {
function loadCalendarRecords() { function loadCalendarRecords() {
grouped.value = [] grouped.value = []
const all = getMonthRecords(selectMonth.value) const all = getMonthRecords(selectMonth.value)
if (all.length === 0) {
calendarSelect.value = [{
date: getDateStr(Date.now()),
badge: false
}]
return
}
calendarSelect.value = Array.from(new Set(all.map(r => r.date).filter(Boolean))).map(date => ({ calendarSelect.value = Array.from(new Set(all.map(r => r.date).filter(Boolean))).map(date => ({
date: date!, date: date!,
badge: true, badge: true,

View File

@@ -1,62 +1,224 @@
<template> <template>
<view class="charts-box"> <view class="min-h-screen flex flex-col bg-gradient-to-b from-[#a78bfa] to-[#f3e8ff]">
<view class="self-center py-3 flex items-center gap-2 text-gray-800">
<view class="flex min-w-0 flex-col">
<text class="truncate text-lg text-white font-semibold">
最近7次趋势
</text>
</view>
</view>
<wd-card title="体重">
<qiun-data-charts <qiun-data-charts
v-if="weightChartData.categories?.length"
type="line" type="line"
:opts="opts" :opts="chartOpts"
:chartData="chartData" :chartData="weightChartData"
/> />
<wd-empty v-else icon="no-content" tip="暂无记录" />
</wd-card>
<wd-card title="睡眠">
<qiun-data-charts
v-if="sleepChartData.categories?.length"
type="line"
:opts="chartOpts"
:chartData="sleepChartData"
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
</wd-card>
<wd-card title="运动">
<qiun-data-charts
v-if="sportChartData.categories?.length"
type="line"
:opts="chartOpts"
:chartData="sportChartData"
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
</wd-card>
<wd-card title="血压">
<qiun-data-charts
v-if="bloodPressureChartData.categories?.length"
type="line"
:opts="chartOpts"
:chartData="bloodPressureChartData"
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
</wd-card>
<wd-card title="血糖">
<qiun-data-charts
v-if="bloodSugarChartData.categories?.length"
type="line"
:opts="chartOpts"
:chartData="bloodSugarChartData"
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
</wd-card>
<wd-card title="体温">
<qiun-data-charts
v-if="temperatureChartData.categories?.length"
type="line"
:opts="chartOpts"
:chartData="temperatureChartData"
/>
<wd-empty v-else icon="no-content" tip="暂无记录" />
</wd-card>
</view> </view>
</template> </template>
<script setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import { getRecentRecords } from '@/utils/record'
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
const chartData = ref({}) const weightChartData = ref({
categories: []
})
const sleepChartData = ref({
categories: []
})
const sportChartData = ref({
categories: []
})
const bloodPressureChartData = ref({
categories: []
})
const bloodSugarChartData = ref({
categories: []
})
const temperatureChartData = ref({
categories: []
})
const opts = { // 图表配置
color: ["#1890FF","#91CB74","#FAC858","#EE6666","#73C0DE","#3CA272","#FC8452","#9A60B4","#ea7ccc"], const chartOpts = {
color: ['#1890FF', '#EE6666', '#91CB74', '#FAC858', '#73C0DE'],
padding: [15, 10, 0, 15], padding: [15, 10, 0, 15],
enableScroll: false, enableScroll: false,
legend: {}, legend: { show: true },
xAxis: { xAxis: { disableGrid: true },
disableGrid: true yAxis: { gridType: 'dash', dashLength: 2 },
},
yAxis: {
gridType: "dash",
dashLength: 2
},
extra: { extra: {
line: { line: { type: 'straight', width: 2, activeType: 'hollow'},
type: "straight", },
width: 2, }
activeType: "hollow"
function getWeightChartData() {
const list = getRecentRecords('weight', 7)
if (!list.length) return { categories: [], series: [] }
const categories = list.map((_, index) => String(index + 1))
weightChartData.value = {
categories,
series: [
{
name: '体重Kg',
data: list.map((r) => r.value),
},
],
} }
}
function getSleepChartData() {
const list = getRecentRecords('sleep', 7)
if (!list.length) return { categories: [], series: [] }
const categories = list.map((_, index) => String(index + 1))
sleepChartData.value = {
categories,
series: [
{
name: '睡眠(小时)',
data: list.map((r) => r.duration),
},
],
}
}
function getSportChartData() {
const list = getRecentRecords('sport', 7)
if (!list.length) return { categories: [], series: [] }
const categories = list.map((_, index) => String(index + 1))
sportChartData.value = {
categories,
series: [
{
name: '运动(分钟)',
data: list.map((r) => r.duration),
},
],
}
}
function getBloodPressureChartData() {
const list = getRecentRecords('bloodPressure', 7)
if (!list.length) return { categories: [], series: [] }
const categories = list.map((_, index) => String(index + 1))
bloodPressureChartData.value = {
categories,
series: [
{ name: '收缩压mmHg', data: list.map((r) => r.systolic) },
{ name: '舒张压mmHg', data: list.map((r) => r.diastolic) },
],
}
}
function getBloodSugarChartData() {
const list = getRecentRecords('bloodSugar', 7)
if (!list.length) return { categories: [], series: [] }
const categories = list.map((_, index) => String(index + 1))
bloodSugarChartData.value = {
categories,
series: [
{
name: '血糖mmol/L',
data: list.map((r) => r.value),
},
],
}
}
function getTemperatureChartData() {
const list = getRecentRecords('temperature', 7)
if (!list.length) return { categories: [], series: [] }
const categories = list.map((_, index) => String(index + 1))
temperatureChartData.value = {
categories,
series: [
{
name: '体温(℃)',
data: list.map((r) => r.value),
},
],
} }
} }
onShow(() => { onShow(() => {
getServerData() getWeightChartData()
getSleepChartData()
getSportChartData()
getBloodPressureChartData()
getBloodSugarChartData()
getTemperatureChartData()
}) })
function getServerData() {
setTimeout(() => {
const res = {
categories: ["2018","2019","2020","2021","2022","2023"],
series: [
{ name: "成交量A", data: [35, 8, 25, 37, 4, 20] },
{ name: "成交量B", data: [70, 40, 65, 100, 44, 68] },
{ name: "成交量C", data: [100, 80, 95, 150, 112, 132] }
]
}
chartData.value = res
}, 500)
}
</script> </script>
<style scoped>
.charts-box {
width: 100%;
height: 300px;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
src/static/tabs/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -38,6 +38,12 @@ function cleanOldMonths() {
}) })
} }
/**
* 新增记录
* @param type 类型
* @param data 数据
* @param timestamp 时间戳
*/
function addRecord(type: string, data: Record<string, any>, timestamp: number) { function addRecord(type: string, data: Record<string, any>, timestamp: number) {
// 键为月份 例如 health_2026-08 // 键为月份 例如 health_2026-08
const key = getMonthKey(timestamp) const key = getMonthKey(timestamp)
@@ -47,17 +53,74 @@ function addRecord(type: string, data: Record<string, any>, timestamp: number) {
if (!all[date]) if (!all[date])
all[date] = [] all[date] = []
// 在按天存储 const record = {
all[date].push({
id: Date.now() + Math.random().toString(36).slice(2, 6), id: Date.now() + Math.random().toString(36).slice(2, 6),
time: getTimeStr(timestamp), time: getTimeStr(timestamp),
timestamp,
type, type,
...data, ...data,
}) }
// 在按天存储
all[date].push(record)
wx.setStorageSync(key, all) wx.setStorageSync(key, all)
appendRecentRecord(type, record)
} }
/**
* 添加最近记录
* @param type 类型
* @param record 记录
* @param limit
*/
function appendRecentRecord(type: string, record: any, limit = 7) {
const key = `recent_${type}`
const recent = wx.getStorageSync(key) || []
// 正序timestamp 小的在前
let i = 0
while (i < recent.length && recent[i].timestamp < record.timestamp) {
i++
}
recent.splice(i, 0, record)
// 截断:砍掉前面的(最旧的)
if (recent.length > limit) {
recent.splice(0, recent.length - limit)
}
wx.setStorageSync(key, recent)
}
/**
* 删除最近记录的数据
* @param type 类型
* @param id id
*/
function removeFromRecentIndex(type: string, id: string) {
const key = `recent_${type}`
const recent = wx.getStorageSync(key) || []
const idx = recent.findIndex((item: any) => item.id === id)
if (idx !== -1) {
recent.splice(idx, 1)
wx.setStorageSync(key, recent)
}
}
export function getRecentRecords(type: string, limit = 7): IHealthRecord[] {
const key = `recent_${type}`
const recent = wx.getStorageSync(key) || []
return limit ? recent.slice(0, limit) : recent
}
/**
* 删除单条记录
* @param date 日期
* @param id 记录id
*/
export function deleteRecordByDate(date: string, id: string) { export function deleteRecordByDate(date: string, id: string) {
const key = `health_${date.slice(0, 7)}` const key = `health_${date.slice(0, 7)}`
const all = wx.getStorageSync(key) || {} const all = wx.getStorageSync(key) || {}
@@ -68,6 +131,9 @@ export function deleteRecordByDate(date: string, id: string) {
const idx = list.findIndex((item: any) => item.id === id) const idx = list.findIndex((item: any) => item.id === id)
if (idx === -1) return if (idx === -1) return
const record = list[idx]
const type = record.type
list.splice(idx, 1) list.splice(idx, 1)
if (list.length === 0) { if (list.length === 0) {
@@ -79,6 +145,41 @@ export function deleteRecordByDate(date: string, id: string) {
} else { } else {
wx.setStorageSync(key, all) wx.setStorageSync(key, all)
} }
removeFromRecentIndex(type, id)
}
/**
* 删除月份记录
* @param month
*/
export function deleteRecordByMonth(month: string) {
const key = `health_${month}`
const all = wx.getStorageSync(key) || {}
// 1. 先收集这个月所有记录的 id 和 type 的对应关系
const idsByType: Record<string, string[]> = {}
Object.values(all).forEach((list: any) => {
if (Array.isArray(list)) {
list.forEach((item: any) => {
if (!item.type) return
if (!idsByType[item.type]) idsByType[item.type] = []
idsByType[item.type].push(item.id)
})
}
})
// 2. 删主存储
wx.removeStorageSync(key)
// 3. 按 type 逐个清理索引里对应的 id
Object.entries(idsByType).forEach(([type, ids]) => {
const recentKey = `recent_${type}`
const recent = wx.getStorageSync(recentKey) || []
const idSet = new Set(ids)
const filtered = recent.filter((item: any) => !idSet.has(item.id))
wx.setStorageSync(recentKey, filtered)
})
} }
export function addWeight(value: number, note: string, timestamp: number) { export function addWeight(value: number, note: string, timestamp: number) {