feat: 增加存储详情功能

This commit is contained in:
2026-08-25 10:42:42 +08:00
parent 8ff7d4548e
commit e42d0854ec
13 changed files with 176 additions and 62 deletions

View File

@@ -54,6 +54,7 @@
"@dcloudio/uni-mp-xhs": "3.0.0-5020420260813003",
"@dcloudio/uni-quickapp-webview": "3.0.0-5020420260813003",
"@wot-ui/ui": "^2.3.2",
"miniprogram-api-typings": "^5.2.3",
"pinia": "^4.0.3",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"

12
pnpm-lock.yaml generated
View File

@@ -59,6 +59,9 @@ importers:
'@wot-ui/ui':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.41(typescript@4.9.5))
miniprogram-api-typings:
specifier: ^5.2.3
version: 5.2.3
pinia:
specifier: ^4.0.3
version: 4.0.3(@vue/devtools-api@6.6.4)(typescript@4.9.5)(vue@3.5.41(typescript@4.9.5))
@@ -3179,6 +3182,9 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
miniprogram-api-typings@5.2.3:
resolution: {integrity: sha512-KVcNSfR2VmRtz35rxkjLxE92HlsBnbT6NsKyTcDX3ebbTKAsAiTOFO1mRM82Xbe848dnEYqNGzaD48LMEvGytw==}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
@@ -6715,7 +6721,7 @@ snapshots:
browserslist: 4.28.8
browserslist-to-esbuild: 2.1.1(browserslist@4.28.8)
core-js: 3.50.0
magic-string: 0.30.11
magic-string: 0.30.21
regenerator-runtime: 0.14.1
systemjs: 6.15.1
terser: 5.50.0
@@ -6814,7 +6820,7 @@ snapshots:
'@vue/compiler-ssr': 3.4.21
'@vue/shared': 3.4.21
estree-walker: 2.0.2
magic-string: 0.30.11
magic-string: 0.30.21
postcss: 8.5.26
source-map-js: 1.2.1
@@ -8343,6 +8349,8 @@ snapshots:
minimist@1.2.8: {}
miniprogram-api-typings@5.2.3: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8

View File

@@ -0,0 +1,54 @@
<template>
<wd-popup
v-model="profileStore.showStoragePopup"
position="bottom"
:safe-area-inset-bottom="true"
custom-style="border-radius: 32rpx 32rpx 0 0; padding: 40rpx 32rpx;"
>
<view class="mb-5 text-center text-base text-gray-700 font-semibold">
存储详情
</view>
<view class="flex flex-col items-center">
<wd-text :text="`${profileStore.storageTotal} / 1000`"></wd-text>
<wd-progress
:percentage="Number((profileStore.storageTotal / 1000 * 100).toFixed(2))"
:color="['#00c740', '#ffb300', '#e2231a']"/>
</view>
<view class="max-h-[40vh] overflow-y-auto">
<wd-cell-group>
<wd-cell v-for="item in profileStore.storageStats"
:key="item.month"
:title="`${item.name}${item.value}条)`"
center>
<wd-button size="small" type="danger" plain @click="clearMonth(item.name)">清除</wd-button>
</wd-cell>
</wd-cell-group>
</view>
<wd-empty v-if="profileStore.storageStats.length === 0" icon="no-content" tip="暂无记录" />
</wd-popup>
</template>
<script lang="ts" setup>
import { useProfileStore } from '@/store/profile'
import { countRecordsByMonth, getTotalCount } from "@/utils/record";
const profileStore = useProfileStore()
function clearMonth(month: string) {
wx.showModal({
title: '提示',
content: `确定清除 ${month} 的所有记录吗?`,
success(res) {
if (res.confirm) {
wx.removeStorageSync(`health_${month}`)
profileStore.storageStats = countRecordsByMonth()
profileStore.storageTotal = getTotalCount()
wx.showToast({ title: '已清除', icon: 'success' })
}
},
})
}
</script>

View File

@@ -81,10 +81,11 @@ import SportPopup from "@/components/Home/SportPopup.vue";
import BpPopup from "@/components/Home/BpPopup.vue";
import GlucosePopup from "@/components/Home/GlucosePopup.vue";
import TempPopup from "@/components/Home/TempPopup.vue";
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, watch } from 'vue'
import { useRecordStore } from '@/store/record'
import type { IHealthRecord } from '@/utils/record'
import { getDateStr, getDayRecords } from '@/utils/record'
import { countRecordsByMonth, getDateStr, getDayRecords } from '@/utils/record'
import type { IHealthRecord } from "@/type/record";
import {onShow} from "@dcloudio/uni-app";
defineOptions({
name: 'Home',
@@ -114,7 +115,7 @@ const today = computed(() => {
return `${m}${d}日 周${w}`
})
onMounted(() => {
onShow(() => {
todayRecords.value = getDayRecords(getDateStr(Date.now()))
})
@@ -196,3 +197,9 @@ function handelFabClick() {
recordStore.showFab = false
}
</script>
<style scoped lang="scss">
:deep(.wd-card:not(:last-child)) {
margin-bottom: 0;
}
</style>

View File

@@ -13,12 +13,12 @@
</wd-button>
</view>
<wd-segmented v-model:value="current" :options="list" style="overflow: auto;" />
<wd-segmented v-model:value="current" :options="list"/>
</view>
<!-- 时间轴区域 -->
<view class="px-5 py-2">
<wd-empty v-if="grouped.length === 0" icon="no-content" tip="暂无内容" />
<wd-empty v-if="grouped.length === 0" icon="no-content" tip="暂无记录" />
<!-- 外层每个日期分组 -->
<view v-for="group in grouped" :key="group.date" class="mb-6">
@@ -90,7 +90,6 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import type { IHealthRecord } from '@/utils/record'
import {
getMonthRecords,
getMonthStr,
@@ -100,6 +99,7 @@ import {
groupByDate,
} from '@/utils/record'
import { onShow } from "@dcloudio/uni-app";
import type { IHealthRecord } from "@/type/record";
const currentDate = ref(Date.now())
const showDatePicker = ref(false)
@@ -141,12 +141,16 @@ function openDatePicker() {
showDatePicker.value = true
}
function onDateConfirm({ value }) {
function onDateConfirm() {
loadRecords()
}
</script>
<style scoped lang="scss">
:deep(.wd-segmented) {
overflow: auto;
}
:deep(.wd-segmented__item) {
min-width: 60px;
flex-shrink: 0;

View File

@@ -20,16 +20,24 @@
</view>
</wd-card>
<wd-cell-group>
<wd-cell title="存储详情" is-link @click="openStoragePopup"/>
</wd-cell-group>
<profile-popup />
<storage-popup />
</view>
</template>
<script lang="ts" setup>
import ProfilePopup from "@/components/Profile/ProfilePopup.vue";
import StoragePopup from "@/components/Profile/StoragePopup.vue";
import { computed, ref, watch } from 'vue'
import type { IProfile } from '@/utils/profile'
import { getProfile } from '@/utils/profile'
import { useProfileStore } from '@/store/profile'
import type { IProfile } from "@/type/profile";
import {countRecordsByMonth, getTotalCount} from "@/utils/record";
const profileStore = useProfileStore()
@@ -53,4 +61,10 @@ function openProfile() {
profileStore.showProfilePopup = true
}
function openStoragePopup() {
profileStore.storageStats = countRecordsByMonth()
profileStore.storageTotal = getTotalCount()
profileStore.showStoragePopup = true
}
</script>

View File

@@ -1,15 +1,19 @@
import { defineStore } from 'pinia'
import type { IProfile } from '@/utils/profile'
import type { IProfile } from "@/type/profile";
import type {IStats} from "@/type";
export const useProfileStore = defineStore('profile', {
state: () => ({
showProfilePopup: false,
showStoragePopup: false,
profileForm: {
gender: '',
height: 0,
name: '',
weight: 0,
} as IProfile,
storageStats: [] as IStats[],
storageTotal: 0,
isRefresh: false,
}),
actions: {

4
src/type/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface IStats {
name: string;
value: number
}

View File

@@ -0,0 +1,6 @@
export interface IProfile {
name: string
gender: 'male' | 'female' | ''
height: number | null
weight: number | null
}

View File

@@ -0,0 +1,18 @@
export interface IHealthRecord {
id: string
type: string
time: string
date?: string
[key: string]: any
}
export interface IRecordMeta {
icon: string
label: string
color: string
}
export interface IRecordStats {
label: string
type: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'volcano' | 'lightblue' | 'cyan' | 'pink' | 'purple'
}

View File

@@ -1,9 +1,4 @@
export interface IProfile {
name: string
gender: 'male' | 'female' | ''
height: number | null
weight: number | null
}
import type { IProfile } from "@/type/profile";
const PROFILE_KEY = 'user_profile'

View File

@@ -1,29 +1,12 @@
import { getProfile } from '@/utils/profile'
export interface IHealthRecord {
id: string
type: string
time: string
date?: string
[key: string]: any
}
export interface IRecordMeta {
icon: string
label: string
color: string
}
export interface IRecordStats {
label: string
type: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'volcano' | 'lightblue' | 'cyan' | 'pink' | 'purple'
}
import type {IHealthRecord, IRecordMeta, IRecordStats} from "@/type/record";
import type {IStats} from "@/type";
const MONTH_KEEP = 12
const RECORD_PREFIX = 'health_'
function getMonthKey(timestamp: number) {
const d = new Date(timestamp)
return `health_${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
return `${RECORD_PREFIX}${getMonthStr(timestamp)}`
}
export function getDateStr(timestamp: number) {
@@ -46,16 +29,17 @@ export function getTimeStr(timestamp: number) {
function cleanOldMonths() {
const cutoff = new Date()
cutoff.setMonth(cutoff.getMonth() - MONTH_KEEP)
const cutoffKey = `health_${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, '0')}`
const cutoffKey = `${RECORD_PREFIX}${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, '0')}`
const keys = wx.getStorageInfoSync().keys
keys.forEach((key) => {
if (key.startsWith('health_') && key < cutoffKey) {
keys.forEach((key: string) => {
if (key.startsWith(RECORD_PREFIX) && key < cutoffKey) {
wx.removeStorageSync(key)
}
})
}
function addRecord(type: string, data: Record<string, any>, timestamp: number) {
// 键为月份 例如 health_2026-08
const key = getMonthKey(timestamp)
const all = wx.getStorageSync(key) || {}
const date = getDateStr(timestamp)
@@ -63,6 +47,7 @@ function addRecord(type: string, data: Record<string, any>, timestamp: number) {
if (!all[date])
all[date] = []
// 在按天存储
all[date].push({
id: Date.now() + Math.random().toString(36).slice(2, 6),
time: getTimeStr(timestamp),
@@ -97,31 +82,25 @@ export function addTemperature(value: number, location: string, note: string, ti
addRecord('temperature', { value, location, note }, timestamp)
}
function getAllMonthKeys(): string[] {
return wx.getStorageInfoSync().keys.filter(k => k.startsWith('health_')).sort()
}
// ─── 根据时间戳反推它属于哪个月的 key ───
function getMonthKeyForTimestamp(timestamp: number): string {
const d = new Date(timestamp)
return `health_${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}
// ─── 根据 "YYYY-MM-DD" 反推月份 key ───
function getMonthKeyForDate(dateStr: string): string {
/**
* 获取某天的记录
* @param dateStr 日期 YYYY-MM-DD
*/
export function getDayRecords(dateStr: string): IHealthRecord[] {
const [y, m] = dateStr.split('-')
return `health_${y}-${m}`
}
export function getDayRecords(date: string): IHealthRecord[] {
const key = getMonthKeyForDate(date)
const key = `${RECORD_PREFIX}${y}-${m}`
const all = wx.getStorageSync(key) || {}
const dayRecords = all[date] || []
const dayRecords = all[dateStr] || []
return [...dayRecords].sort((a, b) => a.time.localeCompare(b.time))
}
/**
* 获取某月的记录
* @param yearMonth 月份 YYYY-MM
* @param type 类型
*/
export function getMonthRecords(yearMonth: string, type?: string): IHealthRecord[] {
const key = `health_${yearMonth}`
const key = `${RECORD_PREFIX}${yearMonth}`
const all = wx.getStorageSync(key) || {}
const result: IHealthRecord[] = []
@@ -146,6 +125,26 @@ function sortRecords(records: IHealthRecord[]): IHealthRecord[] {
})
}
export function countRecordsByMonth(): IStats[] {
const keys = wx.getStorageInfoSync().keys
return keys
.filter(k => k.startsWith(RECORD_PREFIX))
.map(key => {
const all: IHealthRecord = wx.getStorageSync(key) || {}
const value: number = Object.values(all).reduce((sum: number, list: IHealthRecord[]) => sum + list.length, 0)
const name = key.replace(RECORD_PREFIX, '')
return { name, value }
})
.sort((a, b) => a.name.localeCompare(b.name))
}
export function getTotalCount(): number {
return wx.getStorageInfoSync().keys
.filter(k => k.startsWith('health_'))
.reduce((sum, k) => sum + Object.values(wx.getStorageSync(k) || {}).flat().length, 0)
}
const META_MAP: Record<string, IRecordMeta> = {
weight: { icon: '⚖️', label: '体重', color: 'bg-[#E89E3A]' },
sleep: { icon: '😴', label: '睡眠', color: 'bg-[#2E5B8C]' },

View File

@@ -7,7 +7,7 @@
"@/*": ["./src/*"]
},
"lib": ["esnext", "dom"],
"types": ["@dcloudio/types"]
"types": ["@dcloudio/types", "miniprogram-api-typings"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}