feat: 增加存储功能

This commit is contained in:
2026-08-22 21:38:14 +08:00
parent 2bb55368cd
commit af0f396ed8
12 changed files with 369 additions and 208 deletions

View File

@@ -65,7 +65,7 @@
title="时间"
prop="time"
is-link
:value="formatTime(recordStore.bpForm.time)"
:value="formatDateTime(recordStore.bpForm.time)"
placeholder="请选择记录时间"
@click="showTimePicker = true"
/>
@@ -89,13 +89,14 @@
<wd-datetime-picker
v-model="recordStore.bpForm.time"
v-model:visible="showTimePicker"
:max-date="Date.now()"
placeholder="请选择记录时间"
/>
</template>
<script setup lang="ts">
import { useRecordStore } from '@/store/record'
import { formatTime } from '@/utils'
import { formatDateTime } from '@/utils'
const showTimePicker = ref(false)
@@ -127,11 +128,8 @@ function saveBp() {
return
}
// TODO: 接接口保存
console.log('保存血压:', recordStore.bpForm)
uni.showToast({ title: '血压记录已保存 ✅', icon: 'success' })
recordStore.showBpPopup = false
recordStore.submitBp()
uni.showToast({ title: '血压记录成功', icon: 'success' })
handeClosePopup()
}

View File

@@ -51,7 +51,7 @@
title="时间"
prop="time"
is-link
:value="formatTime(recordStore.glucoseForm.time)"
:value="formatDateTime(recordStore.glucoseForm.time)"
placeholder="请选择记录时间"
@click="showTimePicker = true"
/>
@@ -75,13 +75,14 @@
<wd-datetime-picker
v-model="recordStore.glucoseForm.time"
v-model:visible="showTimePicker"
:max-date="Date.now()"
placeholder="请选择记录时间"
/>
</template>
<script setup lang="ts">
import { useRecordStore } from '@/store/record'
import { formatTime } from '@/utils'
import { formatDateTime } from '@/utils'
const showTimePicker = ref(false)
@@ -122,11 +123,8 @@ function saveGlucose() {
return
}
// TODO: 接接口保存
console.log('保存血糖:', recordStore.glucoseForm)
uni.showToast({ title: '血糖记录已保存 ✅', icon: 'success' })
recordStore.showGlucosePopup = false
recordStore.submitGlucose()
uni.showToast({ title: '血糖记录成功', icon: 'success' })
handeClosePopup()
}

View File

@@ -47,58 +47,22 @@ function onSelect(item: { key: string, label: string }) {
recordStore.showRecordPopup = false
if (item.key === 'weight') {
recordStore.weightForm.value = 70
recordStore.weightForm.time = Date.now()
recordStore.weightForm.note = ''
setTimeout(() => {
recordStore.showWeightPopup = true
}, 300)
recordStore.openWeight()
}
else if (item.key === 'sleep') {
recordStore.sleepForm.startTime = Date.now() - 8 * 60 * 60 * 1000
recordStore.sleepForm.endTime = Date.now()
recordStore.sleepForm.time = Date.now()
recordStore.sleepForm.note = ''
setTimeout(() => {
recordStore.showSleepPopup = true
}, 300)
recordStore.openSleep()
}
else if (item.key === 'sport') {
recordStore.sportForm.type = '跑步'
recordStore.sportForm.duration = 30
recordStore.sportForm.time = Date.now()
recordStore.sportForm.note = ''
setTimeout(() => {
recordStore.showSportPopup = true
}, 300)
recordStore.openSport()
}
else if (item.key === 'bp') {
recordStore.bpForm.systolic = 120
recordStore.bpForm.diastolic = 80
recordStore.bpForm.heartRate = 80
recordStore.bpForm.time = Date.now()
recordStore.bpForm.note = ''
setTimeout(() => {
recordStore.showBpPopup = true
}, 300)
recordStore.openBp()
}
else if (item.key === 'glucose') {
recordStore.glucoseForm.value = 5.4
recordStore.glucoseForm.period = '空腹'
recordStore.glucoseForm.time = Date.now()
recordStore.glucoseForm.note = ''
setTimeout(() => {
recordStore.showGlucosePopup = true
}, 300)
recordStore.openGlucose()
}
else if (item.key === 'temp') {
recordStore.tempForm.value = 36.5
recordStore.tempForm.location = '腋下'
recordStore.tempForm.time = Date.now()
recordStore.tempForm.note = ''
setTimeout(() => {
recordStore.showTempPopup = true
}, 300)
recordStore.openTemp()
}
else {
// 其他类型暂时提示

View File

@@ -23,7 +23,7 @@
title="开始"
prop="startTime"
is-link
:value="formatTime(recordStore.sleepForm.startTime)"
:value="formatDateTime(recordStore.sleepForm.startTime)"
placeholder="请选择开始时间"
@click="showStartPicker = true"
/>
@@ -32,7 +32,7 @@
title="结束"
prop="endTime"
is-link
:value="formatTime(recordStore.sleepForm.endTime)"
:value="formatDateTime(recordStore.sleepForm.endTime)"
placeholder="请选择结束时间"
@click="showEndPicker = true"
/>
@@ -48,7 +48,7 @@
title="时间"
prop="time"
is-link
:value="formatTime(recordStore.sleepForm.time)"
:value="formatDateTime(recordStore.sleepForm.time)"
placeholder="请选择记录时间"
@click="showTimePicker = true"
/>
@@ -85,13 +85,14 @@
<wd-datetime-picker
v-model="recordStore.sleepForm.time"
v-model:visible="showTimePicker"
:max-date="Date.now()"
placeholder="请选择记录时间"
/>
</template>
<script setup lang="ts">
import { useRecordStore } from '@/store/record'
import { formatTime } from '@/utils'
import { formatDateTime } from '@/utils'
import { computed, ref } from 'vue'
const recordStore = useRecordStore()
@@ -149,14 +150,8 @@ function saveSleep() {
return
}
// TODO: 接接口保存
console.log('保存睡眠:', {
...recordStore.sleepForm,
duration: sleepDuration.value,
})
uni.showToast({ title: '睡眠记录已保存 ✅', icon: 'success' })
recordStore.showSleepPopup = false
recordStore.submitSleep()
uni.showToast({ title: '睡眠记录成功', icon: 'success' })
handeClosePopup()
}

View File

@@ -20,8 +20,8 @@
border
>
<!-- 运动类型 -->
<wd-form-item title="类型" prop="type">
<wd-radio-group v-model="recordStore.sportForm.type" type="button">
<wd-form-item title="类型" prop="category">
<wd-radio-group v-model="recordStore.sportForm.category" type="button">
<wd-radio
v-for="item in sportTypeColumns"
:key="item.value"
@@ -51,7 +51,7 @@
title="时间"
prop="time"
is-link
:value="formatTime(recordStore.sportForm.time)"
:value="formatDateTime(recordStore.sportForm.time)"
placeholder="请选择记录时间"
@click="showTimePicker = true"
/>
@@ -74,13 +74,14 @@
<wd-datetime-picker
v-model="recordStore.sportForm.time"
v-model:visible="showTimePicker"
:max-date="Date.now()"
placeholder="请选择记录时间"
/>
</template>
<script setup lang="ts">
import { useRecordStore } from '@/store/record'
import { formatTime } from '@/utils'
import { formatDateTime } from '@/utils'
import { ref } from 'vue'
const showTimePicker = ref(false)
@@ -103,9 +104,9 @@ function handeClosePopup() {
}
function saveSport() {
const { type, duration, time } = recordStore.sportForm
const { category, duration, time } = recordStore.sportForm
if (!type) {
if (!category) {
uni.showToast({ title: '请选择运动类型', icon: 'none' })
return
}
@@ -118,11 +119,8 @@ function saveSport() {
return
}
// TODO: 接接口保存
console.log('保存运动:', recordStore.sportForm)
uni.showToast({ title: '运动记录已保存 ✅', icon: 'success' })
recordStore.showSportPopup = false
recordStore.submitSport()
uni.showToast({ title: '运动记录成功', icon: 'success' })
handeClosePopup()
}

View File

@@ -51,7 +51,7 @@
title="时间"
prop="time"
is-link
:value="formatTime(recordStore.tempForm.time)"
:value="formatDateTime(recordStore.tempForm.time)"
placeholder="请选择记录时间"
@click="showTimePicker = true"
/>
@@ -75,13 +75,14 @@
<wd-datetime-picker
v-model="recordStore.tempForm.time"
v-model:visible="showTimePicker"
:max-date="Date.now()"
placeholder="请选择记录时间"
/>
</template>
<script setup lang="ts">
import { useRecordStore } from '@/store/record'
import { formatTime } from '@/utils'
import { formatDateTime } from '@/utils'
import { ref } from 'vue'
const showTimePicker = ref(false)
@@ -121,11 +122,8 @@ function saveTemp() {
return
}
// TODO: 接接口保存
console.log('保存体温:', recordStore.tempForm)
uni.showToast({ title: '体温记录已保存 ✅', icon: 'success' })
recordStore.showTempPopup = false
recordStore.submitTemp()
uni.showToast({ title: '体温记录成功', icon: 'success' })
handeClosePopup()
}

View File

@@ -24,7 +24,7 @@
title="时间"
prop="time"
is-link
:value="formatTime(recordStore.weightForm.time)"
:value="formatDateTime(recordStore.weightForm.time)"
placeholder="请选择记录时间"
@click="showTimePicker = true"
/>
@@ -46,13 +46,14 @@
<wd-datetime-picker
v-model="recordStore.weightForm.time"
v-model:visible="showTimePicker"
:max-date="Date.now()"
placeholder="请选择记录时间"
/>
</template>
<script setup lang="ts">
import { useRecordStore } from '@/store/record'
import { formatTime } from '@/utils'
import { formatDateTime } from '@/utils'
const showTimePicker = ref(false)
@@ -69,14 +70,8 @@ function saveWeight() {
uni.showToast({ title: '请输入体重', icon: 'none' })
}
else {
// TODO: 这里接接口保存
console.log('保存体重:', recordStore.weightForm)
uni.showToast({ title: '体重已保存 ✅', icon: 'success' })
recordStore.showWeightPopup = false
// 保存后刷新首页概览TODO: 接真实数据后打开)
// overviewItems[0].value = weightForm.value + 'kg'
recordStore.submitWeight()
uni.showToast({ title: '体重记录成功', icon: 'success' })
}
handeClosePopup()

View File

@@ -79,6 +79,7 @@
<script lang="ts" setup>
import { useRecordStore } from '@/store/record'
import { getDateStr, getDayRecords, getMonthRecords } from '@/utils/record'
defineOptions({
name: 'Home',
@@ -110,6 +111,13 @@ const plans = [
{ icon: '🛏️', label: '23:00 前入睡', done: false },
]
onMounted(() => {
const result = getDayRecords(getDateStr(Date.now()))
console.log(result)
console.log(getMonthRecords('2026-08'))
})
function handelFabClick() {
recordStore.showRecordPopup = true
recordStore.showFab = false

View File

@@ -41,7 +41,7 @@
<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.datetime }}</text>
<text class="text-xs text-gray-400 font-mono">{{ item.time }}</text>
<wd-tag type="primary" round>
{{ item.status }}
</wd-tag>
@@ -52,7 +52,7 @@
<view class="h-7 w-7 flex items-center justify-center rounded-lg text-sm" :class="item.color">
<text>{{ item.icon }}</text>
</view>
<text class="text-sm text-gray-700 font-medium">{{ item.label }}</text>
<text class="text-sm text-gray-700 font-medium">{{ item.type }}</text>
</view>
<!-- 第三行数值 -->
@@ -116,6 +116,7 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { getMonthRecords, getMonthStr } from '@/utils/record'
definePage({
style: {
@@ -133,91 +134,96 @@ const displayDate = computed(() => {
return `${d.getFullYear()}${d.getMonth() + 1}`
})
const records = ref([])
onMounted(() => {
records.value = getMonthRecords(getMonthStr(currentDate.value))
})
function changeMonth(offset: number) {
const d = new Date(currentDate.value)
d.setMonth(d.getMonth() + offset)
currentDate.value = d.getTime()
records.value = getMonthRecords(getMonthStr(currentDate.value))
}
function goToday() {
currentDate.value = Date.now()
records.value = getMonthRecords(getMonthStr(currentDate.value))
}
function openDatePicker() {
showDatePicker.value = true
}
function onDateConfirm() {
// picker 确认后 currentDate 已自动更新
function onDateConfirm({ value }) {
records.value = getMonthRecords(getMonthStr(currentDate.value))
}
// ===== 时间轴数据 =====
const records = ref([
{
icon: '😴',
label: '睡眠',
value: '7.5 h',
datetime: '2026-08-20 07:30',
note: '质量良好深睡2.1h',
color: 'bg-[#2E5B8C]',
status: '良好',
},
{
icon: '⚖️',
label: '体重',
value: '68.5 kg',
datetime: '2026-08-20 08:30',
note: '早起空腹,较昨日 -0.3kg',
color: 'bg-[#E89E3A]',
status: '正常',
},
{
icon: '❤️',
label: '血压',
value: '118/76 mmHg',
datetime: '2026-08-20 09:00',
note: '服药后测量静坐5分钟',
color: 'bg-[#C04A3C]',
status: '正常',
},
{
icon: '📈',
label: '血糖',
value: '5.2 mmol/L',
datetime: '2026-08-20 10:30',
note: '早餐后2小时',
color: 'bg-[#9C27B0]',
status: '正常',
},
{
icon: '🏃',
label: '运动',
value: '30 min',
datetime: '2026-08-20 14:00',
note: '快走,公园环道',
color: 'bg-[#3A7C5B]',
status: '达标',
},
{
icon: '💓',
label: '心率',
value: '68 bpm',
datetime: '2026-08-20 16:00',
note: '静息心率',
color: 'bg-[#D32F2F]',
status: '正常',
},
])
// const records = ref([
// {
// icon: '😴',
// label: '睡眠',
// value: '7.5 h',
// datetime: '2026-08-20 07:30',
// note: '质量良好深睡2.1h',
// color: 'bg-[#2E5B8C]',
// status: '良好',
// },
// {
// icon: '⚖️',
// label: '体重',
// value: '68.5 kg',
// datetime: '2026-08-20 08:30',
// note: '早起空腹,较昨日 -0.3kg',
// color: 'bg-[#E89E3A]',
// status: '正常',
// },
// {
// icon: '❤️',
// label: '血压',
// value: '118/76 mmHg',
// datetime: '2026-08-20 09:00',
// note: '服药后测量静坐5分钟',
// color: 'bg-[#C04A3C]',
// status: '正常',
// },
// {
// icon: '📈',
// label: '血糖',
// value: '5.2 mmol/L',
// datetime: '2026-08-20 10:30',
// note: '早餐后2小时',
// color: 'bg-[#9C27B0]',
// status: '正常',
// },
// {
// icon: '🏃',
// label: '运动',
// value: '30 min',
// datetime: '2026-08-20 14:00',
// note: '快走,公园环道',
// color: 'bg-[#3A7C5B]',
// status: '达标',
// },
// {
// icon: '💓',
// label: '心率',
// value: '68 bpm',
// datetime: '2026-08-20 16:00',
// note: '静息心率',
// color: 'bg-[#D32F2F]',
// status: '正常',
// },
// ])
const aiSummary = ref({
dateRange: '基于今日数据',
text: '今日整体健康状态良好。体重平稳,血压正常,建议增加有氧运动频率,保持当前作息规律。',
tags: ['🟢 体重稳定', '❤️ 血压正常', '🏃 建议多运动', '😴 睡眠良好'],
})
onMounted(() => {
// 这里可以接接口拉取当天的记录数据
})
</script>
<style scoped lang="scss">

View File

@@ -1,79 +1,154 @@
import { defineStore } from 'pinia'
import { addBloodPressure, addBloodSugar, addSleep, addSport, addTemperature, addWeight } from '@/utils/record'
export const useRecordStore = defineStore('record', {
state: () => ({
list: [] as any[],
loading: false,
showFab: true,
showRecordPopup: false,
showRecordPopup: false, // 指标选择弹窗
showWeightPopup: false,
weightForm: {
value: 1,
note: '',
time: '' as number | string,
},
showSleepPopup: false,
sleepForm: {
startTime: '' as number | string,
endTime: '' as number | string,
note: '',
time: '' as number | string,
},
showSportPopup: false,
sportForm: {
type: '',
duration: 0,
note: '',
time: '' as number | string,
},
showBpPopup: false,
showGlucosePopup: false,
showTempPopup: false,
weightForm: {
value: 0,
note: '',
time: Date.now(),
},
sleepForm: {
startTime: Date.now(),
endTime: Date.now(),
note: '',
time: Date.now(),
},
sportForm: {
category: '',
duration: 0,
calories: 0,
distance: 0,
note: '',
time: Date.now(),
},
bpForm: {
systolic: 0,
diastolic: 0,
heartRate: 0,
time: Date.now(),
note: '',
time: Date.now(),
},
showGlucosePopup: false,
glucoseForm: {
value: 0,
period: '' as string,
time: Date.now(),
period: '',
note: '',
time: Date.now(),
},
showTempPopup: false,
tempForm: {
value: 36.5,
location: '' as string,
time: Date.now(),
location: '',
note: '',
time: Date.now(),
},
}),
actions: {
setLoading(val: boolean) {
this.loading = val
openWeight() {
this.resetWeight()
this.showWeightPopup = true
},
async fetchList() {
this.loading = true
try {
const res = await fetch('/api/record/list')
this.list = await res.json()
}
finally {
this.loading = false
}
openSleep() {
this.resetSleep()
this.showSleepPopup = true
},
addRecord(item: any) {
this.list.push(item)
openSport() {
this.resetSport()
this.showSportPopup = true
},
removeRecord(id: string) {
this.list = this.list.filter(item => item.id !== id)
openBp() {
this.resetBp()
this.showBpPopup = true
},
reset() {
openGlucose() {
this.resetGlucose()
this.showGlucosePopup = true
},
openTemp() {
this.resetTemp()
this.showTempPopup = true
},
submitWeight() {
addWeight(this.weightForm.value, this.weightForm.note, this.weightForm.time)
this.showWeightPopup = false
this.resetWeight()
},
submitSleep() {
addSleep(
this.sleepForm.startTime,
this.sleepForm.endTime,
this.sleepForm.note,
this.sleepForm.time,
)
this.showSleepPopup = false
this.resetSleep()
},
submitSport() {
addSport(
this.sportForm.category,
this.sportForm.duration,
this.sportForm.note,
this.sportForm.time,
)
this.showSportPopup = false
this.resetSport()
},
submitBp() {
addBloodPressure(
this.bpForm.systolic,
this.bpForm.diastolic,
this.bpForm.heartRate || undefined,
this.bpForm.note,
this.bpForm.time,
)
this.showBpPopup = false
this.resetBp()
},
submitGlucose() {
addBloodSugar(
this.glucoseForm.value,
this.glucoseForm.period,
this.glucoseForm.note,
this.glucoseForm.time,
)
this.showGlucosePopup = false
this.resetGlucose()
},
submitTemp() {
addTemperature(
this.tempForm.value,
this.tempForm.location,
this.tempForm.note,
this.tempForm.time,
)
this.showTempPopup = false
this.resetTemp()
},
resetWeight() {
this.weightForm = { value: 70, note: '', time: Date.now() }
},
resetSleep() {
this.sleepForm = { startTime: Date.now() - 8 * 60 * 60 * 1000, endTime: Date.now(), note: '', time: Date.now() }
},
resetSport() {
this.sportForm = { category: '跑步', duration: 30, calories: 0, distance: 0, note: '', time: Date.now() }
},
resetBp() {
this.bpForm = { systolic: 120, diastolic: 70, heartRate: 80, note: '', time: Date.now() }
},
resetGlucose() {
this.glucoseForm = { value: 5.4, period: '空腹', note: '', time: Date.now() }
},
resetTemp() {
this.tempForm = { value: 36.5, location: '腋下', note: '', time: Date.now() }
},
},
})

View File

@@ -150,7 +150,7 @@ export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
*/
export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}`
export function formatTime(ts: string | number) {
export function formatDateTime(ts: string | number) {
if (!ts)
return ''
const d = new Date(ts)

126
src/utils/record.ts Normal file
View File

@@ -0,0 +1,126 @@
const MONTH_KEEP = 12
function getMonthKey(timestamp: number) {
const d = new Date(timestamp)
return `health_${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
}
export function getDateStr(timestamp: number) {
const d = new Date(timestamp)
return d.toISOString().slice(0, 10)
}
export function getMonthStr(timestamp: number): string {
const d = new Date(timestamp)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
return `${y}-${m}`
}
export function getTimeStr(timestamp: number) {
const d = new Date(timestamp)
return d.toTimeString().slice(0, 5)
}
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 keys = wx.getStorageInfoSync().keys
keys.forEach((key) => {
if (key.startsWith('health_') && key < cutoffKey) {
wx.removeStorageSync(key)
}
})
}
function addRecord(type: string, data: Record<string, any>, timestamp: number) {
const key = getMonthKey(timestamp)
const all = wx.getStorageSync(key) || {}
const date = getDateStr(timestamp)
if (!all[date])
all[date] = []
all[date].push({
id: Date.now() + Math.random().toString(36).slice(2, 6),
time: getTimeStr(timestamp),
type,
...data,
})
wx.setStorageSync(key, all)
}
export function addWeight(value: number, note: string, timestamp: number) {
addRecord('weight', { value, note }, timestamp)
}
export function addSleep(startTime: number, endTime: number, note: string, timestamp: number) {
addRecord('sleep', { startTime, endTime, note }, timestamp)
}
export function addSport(category: string, duration: number, note: string, timestamp: number) {
addRecord('sport', { category, duration, note }, timestamp)
}
export function addBloodPressure(systolic: number, diastolic: number, heartRate: number, note: string, timestamp: number) {
addRecord('bloodPressure', { systolic, diastolic, heartRate, note }, timestamp)
}
export function addBloodSugar(value: number, period: string, note: string, timestamp: number) {
addRecord('bloodSugar', { value, period, note }, timestamp)
}
export function addTemperature(value: number, location: string, note: string, timestamp: number) {
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 {
const [y, m] = dateStr.split('-')
return `health_${y}-${m}`
}
export function getDayRecords(date: string): Record<string, any>[] {
const key = getMonthKeyForDate(date)
const all = wx.getStorageSync(key) || {}
const dayRecords = all[date] || []
return [...dayRecords].sort((a, b) => a.time.localeCompare(b.time))
}
export function getMonthRecords(yearMonth: string, type?: string): Record<string, any>[] {
const key = `health_${yearMonth}`
const all = wx.getStorageSync(key) || {}
const result: Record<string, any>[] = []
Object.keys(all).forEach((date) => {
all[date].forEach((r: Record<string, any>) => {
if (!type || r.type === type) {
result.push({ date, ...r })
}
})
})
return sortRecords(result)
}
// 排序:先按 date 正序,再按 time 正序
function sortRecords(records: Record<string, any>[]): Record<string, any>[] {
return records.sort((a, b) => {
const dateCmp = a.date.localeCompare(b.date)
if (dateCmp !== 0)
return dateCmp
return a.time.localeCompare(b.time)
})
}