feat: 增加删除记录功能

This commit is contained in:
2026-08-25 15:15:40 +08:00
parent e42d0854ec
commit 98d74918a0
11 changed files with 260 additions and 112 deletions

View File

@@ -1,6 +1,11 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { getOverview, saveOverview } from "@/utils/profile";
onLaunch(() => {
if (getOverview().length === 0) {
saveOverview(['weight', 'sleep', 'sport', 'bloodPressure'])
}
console.log("App Launch");
});
onShow(() => {

View File

@@ -0,0 +1,54 @@
<template>
<wd-popup
v-model="profileStore.showOverviewPopup"
position="bottom"
:safe-area-inset-bottom="true"
custom-style="border-radius: 32rpx 32rpx 0 0; padding: 40rpx 32rpx"
>
<view class="mb-4 text-center text-base text-gray-700 font-semibold">
今日概览最多4个
</view>
<wd-checkbox-group v-model="profileStore.overviewList" type="button" :max="4">
<wd-checkbox name="weight">体重</wd-checkbox>
<wd-checkbox name="sleep">睡眠</wd-checkbox>
<wd-checkbox name="sport">运动</wd-checkbox>
<wd-checkbox name="bloodPressure">血压</wd-checkbox>
<wd-checkbox name="bloodSugar">血糖</wd-checkbox>
<wd-checkbox name="temperature">体温</wd-checkbox>
</wd-checkbox-group>
<view class="flex w-full items-center justify-evenly">
<wd-button type="info" @click="cancelClick">
取消
</wd-button>
<wd-button type="primary" @click="saveClick">
保存
</wd-button>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { useProfileStore } from '@/store/profile'
import { countRecordsByMonth, getTotalCount } from "@/utils/record";
import {saveOverview} from "@/utils/profile";
const profileStore = useProfileStore()
function cancelClick() {
profileStore.showOverviewPopup = false
}
function saveClick() {
if (profileStore.overviewList.length < 4) {
wx.showToast({
title: '至少添加 4 项指标',
icon: 'none'
})
return
}
saveOverview(profileStore.overviewList)
profileStore.showOverviewPopup = false
}
</script>

View File

@@ -28,12 +28,6 @@
<text class="shrink-0 text-sm text-gray-400">cm</text>
</view>
</wd-form-item>
<wd-form-item title="体重" prop="weight">
<view class="flex items-center gap-2">
<wd-input v-model.number="profileStore.profileForm.weight" type="number" placeholder="请输入体重" />
<text class="shrink-0 text-sm text-gray-400">Kg</text>
</view>
</wd-form-item>
</wd-form>
<view class="flex w-full items-center justify-evenly">

View File

@@ -1,16 +1,16 @@
<template>
<view class="min-h-screen flex flex-col gap-3 bg-[#f2f4f3]">
<view class="border-b border-gray-100 bg-white px-4 pb-3">
<view class="mt-2 flex items-center justify-between">
<view class="bg-gradient-to-r from-[#7c3aed] to-[#c084fc] p-2 flex items-center justify-between text-white">
<view class="flex flex-col">
<text class="text-lg text-gray-800 font-semibold">
<text class="text-lg text-white font-semibold">
{{ greeting }}
</text>
<text class="mt-0.5 text-xs text-gray-400">
<text class="mt-0.5 text-xs text-white">
{{ today }}
</text>
</view>
</view>
<wd-img :width="40" :height="40" :src="imgURL" />
</view>
<wd-card title="📊 今日概览" custom-class="!mx-2 !mb-0">
@@ -18,22 +18,16 @@
<view
v-for="item in overviewItems"
:key="item.label"
class="rounded-xl bg-[#f2f4f3] p-3 shadow-sm"
class="rounded-xl bg-[#f2f4f3] p-3 shadow-sm flex flex-col gap-1"
>
<view class="flex items-center gap-2">
<view class="h-8 w-8 flex items-center justify-center rounded-full" :class="item.bg">
<text class="text-sm">
{{ item.icon }}
</text>
<text>{{ item.icon }}</text>
<text class="text-gray-400">{{ item.label }}</text>
</view>
<text class="text-xs text-gray-400">
{{ item.label }}
</text>
</view>
<text class="mt-2 block text-xl text-gray-800 font-bold">
<text class="block text-xl text-gray-800 font-bold">
{{ item.value }}
</text>
<text class="mt-1 text-xs text-gray-400">
<text class="text-gray-400">
{{ item.sub }}
</text>
</view>
@@ -83,9 +77,14 @@ import GlucosePopup from "@/components/Home/GlucosePopup.vue";
import TempPopup from "@/components/Home/TempPopup.vue";
import { ref, computed, watch } from 'vue'
import { useRecordStore } from '@/store/record'
import { countRecordsByMonth, getDateStr, getDayRecords } from '@/utils/record'
import {getDateStr, getDayRecords, getTimeStr} from '@/utils/record'
import type { IHealthRecord } from "@/type/record";
import { onShow } from "@dcloudio/uni-app";
import appLogo from '@/static/logo.png'
import {getOverview, getProfile} from "@/utils/profile";
import type {IOverview} from "@/type/profile";
const imgURL = appLogo
defineOptions({
name: 'Home',
@@ -130,43 +129,73 @@ const overviewItems = computed(() => {
// 体重:取最新一条
const weight = list.filter(r => r.type === 'weight').pop()
// 血压:取最新一条
const bp = list.filter(r => r.type === 'bloodPressure').pop()
const bloodPressure = list.filter(r => r.type === 'bloodPressure').pop()
// 睡眠:取最新一条
const sleep = list.filter(r => r.type === 'sleep').pop()
// 运动:全部累加
const sports = list.filter(r => r.type === 'sport')
const sportTotal = sports.reduce((sum, r) => sum + (r.duration || 0), 0)
// 血糖:取最新一条
const bloodSugar = list.filter(r => r.type === 'bloodSugar').pop()
// 体温:取最新一条
const temperature = list.filter(r => r.type === 'temperature').pop()
return [
{
const overviewList = getOverview()
const result = [] as IOverview[]
overviewList.forEach((item) => {
if (item === 'weight') {
const profile = getProfile()
result.push({
icon: '⚖️',
label: '体重',
value: weight ? `${weight.value} kg` : '未记录',
sub: '',
bg: 'bg-[#E89E3A]',
},
{
sub: weight ? profile.height ? `BMI${(weight.value / Math.pow(profile.height / 100, 2)).toFixed(2)} ` : '身高未知' : '',
})
}
else if (item === 'sleep') {
result.push({
icon: '😴',
label: '睡眠',
value: sleep ? `${sleep.duration}h` : '未记录',
sub: '',
bg: 'bg-[#2E5B8C]',
},
{
sub: sleep ? `${getTimeStr(sleep.startTime)} ~ ${getTimeStr(sleep.endTime)}` : '',
})
}
else if (item === 'sport') {
result.push({
icon: '🏃',
label: '运动',
value: sportTotal > 0 ? `${sportTotal}min` : '0min',
sub: sportTotal >= 30 ? '已达标' : '目标 30min',
bg: 'bg-[#3A7C5B]',
},
{
})
}
else if (item === 'bloodPressure') {
result.push({
icon: '❤️',
label: '血压',
value: bp ? `${bp.systolic}/${bp.diastolic}` : '未记录',
sub: '',
bg: 'bg-[#C04A3C]',
},
]
value: bloodPressure ? `${bloodPressure.systolic}/${bloodPressure.diastolic}` : '未记录',
sub: bloodPressure ? `心率:${bloodPressure.heartRate}` : '',
})
}
else if (item === 'bloodSugar') {
result.push({
icon: '🩸',
label: '血糖',
value: bloodSugar ? `${bloodSugar.value}mmol/L` : '未记录',
sub: bloodSugar ? `${bloodSugar.period}` : '',
})
}
else if (item === 'temperature') {
result.push({
icon: '🌡️',
label: '体温',
value: temperature ? `${temperature.value}` : '未记录',
sub: temperature ? `${temperature.location}` : '',
})
}
})
return result
})
// 今日计划

View File

@@ -13,64 +13,57 @@
</wd-button>
</view>
<wd-segmented v-model:value="current" :options="list"/>
<wd-segmented
v-model:value="currentSegment"
:options="segmentList"
@change="handleChangeSegment"
>
<template #label="{ option }">
<view class="name">{{ option.payload }}</view>
</template>
</wd-segmented>
</view>
<!-- 时间轴区域 -->
<view class="px-5 py-2">
<wd-empty v-if="grouped.length === 0" icon="no-content" tip="暂无记录" />
<!-- 外层每个日期分组 -->
<view v-for="group in grouped" :key="group.date" class="mb-6">
<!-- 日期分类标题 -->
<view class="mb-2 flex items-center gap-2 pl-1">
<text class="text-sm text-gray-500 font-medium">{{ group.date }}</text>
<text class="text-xs text-gray-400">{{ group.weekday }}</text>
<text class="text-sm text-gray-400">{{ group.weekday }}</text>
<view class="ml-2 h-[1px] flex-1 bg-gray-200" />
</view>
<!-- 该日期下的时间轴 -->
<view class="relative pl-6">
<view
class="absolute bottom-1 top-1 w-[2px] from-[#07c160] via-[#2E5B8C] to-[#dde3e0] bg-gradient-to-b"
style="left: 0;"
/>
<view
v-for="(item, idx) in group.items"
:key="item.id || idx"
class="relative mb-5 last:mb-0"
class="mb-5 last:mb-0"
>
<view
class="absolute border-[2.5px] border-white rounded-full shadow-sm"
style="width: 14px; height: 14px; left: -30px; top: 0; z-index: 2;"
:class="getRecordMeta(item.type).color"
/>
<!-- 卡片 -->
<view class="rounded-xl bg-white p-3.5 shadow-sm">
<view class="flex items-center justify-between">
<text class="text-xs text-gray-400 font-mono">{{ item.time }}</text>
<text class="text-sm text-gray-400 font-mono">{{ item.time }}</text>
<wd-tag :type="getRecordStatus(item).type" round>
{{ getRecordStatus(item).label }}
</wd-tag>
</view>
<view class="mt-1.5 flex items-center gap-2">
<view
class="h-7 w-7 flex items-center justify-center rounded-lg text-sm"
:class="getRecordMeta(item.type).color"
>
<text>{{ getRecordMeta(item.type).icon }}</text>
</view>
<text class="text-sm text-gray-700 font-medium">{{ getRecordMeta(item.type).label }}</text>
<text class="text-gray-700 font-medium">{{ getRecordMeta(item.type).label }}</text>
</view>
<text class="mt-1.5 block text-xl text-gray-800 font-bold">{{ getRecordValue(item) }}</text>
<view class="flex items-center justify-between">
<text class="mt-1.5 block text-l text-gray-800 font-bold">{{ getRecordValue(item) }}</text>
<wd-button v-if="!item.note" type="danger" icon="delete" size="mini" round
@click="deleteClick(item)"></wd-button>
</view>
<text v-if="item.note" class="mt-0.5 block text-xs text-gray-400 leading-relaxed">
{{ item.note }}
</text>
<view class="flex items-center justify-between">
<text class="mt-0.5 block text-sm text-gray-400 leading-relaxed">{{ item.note }}</text>
<wd-button v-if="item.note" type="danger" icon="delete" size="mini" round
@click="deleteClick(item)"></wd-button>
</view>
</view>
</view>
@@ -91,6 +84,7 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import {
deleteRecordByDate,
getMonthRecords,
getMonthStr,
getRecordMeta,
@@ -103,8 +97,21 @@ import type { IHealthRecord } from "@/type/record";
const currentDate = ref(Date.now())
const showDatePicker = ref(false)
const list = ref<string[]>(['所有', '体重', '睡眠', '运动', '血压', '血糖', '心率'])
const current = ref('所有')
interface SegmentOption {
value: string
payload: string
}
const segmentList = ref<SegmentOption[]>([
{ value: '', payload: '所有' },
{ value: 'weight', payload: '体重' },
{ value: 'sleep', payload: '睡眠' },
{ value: 'sport', payload: '运动' },
{ value: 'bloodPressure', payload: '血压' },
{ value: 'bloodSugar', payload: '血糖' },
{ value: 'temperature', payload: '心率' },
])
const currentSegment = ref('')
const displayDate = computed(() => {
const d = new Date(currentDate.value)
@@ -115,7 +122,7 @@ const records = ref<IHealthRecord[]>([])
const grouped = ref<ReturnType<typeof groupByDate>>([])
function loadRecords() {
const all = getMonthRecords(getMonthStr(currentDate.value))
const all = getMonthRecords(getMonthStr(currentDate.value), currentSegment.value)
records.value = all
grouped.value = groupByDate(all)
}
@@ -144,6 +151,24 @@ function openDatePicker() {
function onDateConfirm() {
loadRecords()
}
function handleChangeSegment() {
loadRecords()
}
function deleteClick(item: IHealthRecord) {
console.log(item)
wx.showModal({
title: '提示',
content: '确定删除这条记录吗?',
success(res) {
if (res.confirm) {
deleteRecordByDate(item.date, item.id)
loadRecords()
}
},
})
}
</script>
<style scoped lang="scss">

View File

@@ -21,20 +21,25 @@
</wd-card>
<wd-cell-group>
<wd-cell title="今日概览" is-link @click="openOverviewPopup"/>
<wd-cell title="今日计划" is-link @click="openStoragePopup"/>
<wd-cell title="存储详情" is-link @click="openStoragePopup"/>
</wd-cell-group>
<profile-popup />
<storage-popup />
<overview-popup />
</view>
</template>
<script lang="ts" setup>
import ProfilePopup from "@/components/Profile/ProfilePopup.vue";
import StoragePopup from "@/components/Profile/StoragePopup.vue";
import OverviewPopup from "@/components/Profile/OverviewPopup.vue";
import { computed, ref, watch } from 'vue'
import { getProfile } from '@/utils/profile'
import { getOverview, getProfile } from '@/utils/profile'
import { useProfileStore } from '@/store/profile'
import type { IProfile } from "@/type/profile";
import { countRecordsByMonth, getTotalCount } from "@/utils/record";
@@ -62,6 +67,11 @@ function openProfile() {
profileStore.showProfilePopup = true
}
function openOverviewPopup() {
profileStore.overviewList = getOverview()
profileStore.showOverviewPopup = true
}
function openStoragePopup() {
profileStore.storageStats = countRecordsByMonth()
profileStore.storageTotal = getTotalCount()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -6,14 +6,15 @@ export const useProfileStore = defineStore('profile', {
state: () => ({
showProfilePopup: false,
showStoragePopup: false,
showOverviewPopup: false,
profileForm: {
gender: '',
height: 0,
name: '',
weight: 0,
} as IProfile,
storageStats: [] as IStats[],
storageTotal: 0,
overviewList: [] as string[],
isRefresh: false,
}),
actions: {

View File

@@ -2,5 +2,11 @@ export interface IProfile {
name: string
gender: 'male' | 'female' | ''
height: number | null
weight: number | null
}
export interface IOverview {
icon: string
label: string
value:string
sub: string
}

View File

@@ -2,12 +2,13 @@ import type { IProfile } from "@/type/profile";
const PROFILE_KEY = 'user_profile'
const OVERVIEW_KEY = 'user_overview'
function getEmptyProfile(): IProfile {
return {
gender: '',
height: null,
name: '',
weight: null,
}
}
@@ -18,3 +19,12 @@ export function getProfile(): IProfile {
export function saveProfile(data: IProfile) {
wx.setStorageSync(PROFILE_KEY, data)
}
export function getOverview(): string[] {
return wx.getStorageSync(OVERVIEW_KEY) || []
}
export function saveOverview(data: string[]) {
wx.setStorageSync(OVERVIEW_KEY, data)
}

View File

@@ -58,6 +58,20 @@ function addRecord(type: string, data: Record<string, any>, timestamp: number) {
wx.setStorageSync(key, all)
}
export function deleteRecordByDate(date: string, id: string) {
const key = `health_${date.slice(0, 7)}`
const all = wx.getStorageSync(key) || {}
const list = all[date]
if (!Array.isArray(list)) return
const idx = list.findIndex((item: any) => item.id === id)
if (idx !== -1) {
list.splice(idx, 1)
if (list.length === 0) delete all[date]
wx.setStorageSync(key, all)
}
}
export function addWeight(value: number, note: string, timestamp: number) {
addRecord('weight', { value, note }, timestamp)
}
@@ -149,7 +163,7 @@ const META_MAP: Record<string, IRecordMeta> = {
weight: { icon: '⚖️', label: '体重', color: 'bg-[#E89E3A]' },
sleep: { icon: '😴', label: '睡眠', color: 'bg-[#2E5B8C]' },
bloodPressure: { icon: '❤️', label: '血压', color: 'bg-[#C04A3C]' },
bloodSugar: { icon: '📈', label: '血糖', color: 'bg-[#9C27B0]' },
bloodSugar: { icon: '🩸', label: '血糖', color: 'bg-[#9C27B0]' },
sport: { icon: '🏃', label: '运动', color: 'bg-[#3A7C5B]' },
temperature: { icon: '🌡️', label: '体温', color: 'bg-[#D32F2F]' },
}
@@ -270,10 +284,10 @@ export function groupByDate(records: IHealthRecord[]): Array<{
map[r.date].push(r)
})
return Object.keys(map)
.sort((a, b) => b.localeCompare(a)) // 日期倒序,最新的在上
.sort((a, b) => b.localeCompare(a))
.map(date => ({
date,
weekday: WEEKDAY[new Date(date).getDay()],
items: map[date].sort((a, b) => a.time.localeCompare(b.time)), // 当天按时间正序
items: map[date].sort((a, b) => b.time.localeCompare(a.time)),
}))
}