feat: 增加存储详情功能
This commit is contained in:
@@ -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
12
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
54
src/components/Profile/StoragePopup.vue
Normal file
54
src/components/Profile/StoragePopup.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
4
src/type/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IStats {
|
||||
name: string;
|
||||
value: number
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface IProfile {
|
||||
name: string
|
||||
gender: 'male' | 'female' | ''
|
||||
height: number | null
|
||||
weight: number | null
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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]' },
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user