feat: 增加数据统计页面
@@ -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' })
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]">
|
||||||
<qiun-data-charts
|
<view class="self-center py-3 flex items-center gap-2 text-gray-800">
|
||||||
type="line"
|
<view class="flex min-w-0 flex-col">
|
||||||
:opts="opts"
|
<text class="truncate text-lg text-white font-semibold">
|
||||||
:chartData="chartData"
|
最近7次趋势
|
||||||
/>
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<wd-card title="体重">
|
||||||
|
<qiun-data-charts
|
||||||
|
v-if="weightChartData.categories?.length"
|
||||||
|
type="line"
|
||||||
|
:opts="chartOpts"
|
||||||
|
: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>
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 681 B After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.6 KiB |
BIN
src/static/tabs/stats-active.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/static/tabs/stats.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.0 KiB |
@@ -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) {
|
||||||
|
|||||||