feat: 更新每日记录模块

This commit is contained in:
2026-08-23 21:56:15 +08:00
parent af0f396ed8
commit 0d587ac241
5 changed files with 292 additions and 161 deletions

View File

@@ -39,7 +39,7 @@
<wd-form-item title="时长">
<view class="py-1 text-gray-800">
{{ sleepDuration }}
{{ sleepDuration }} 小时
</view>
</wd-form-item>
@@ -106,22 +106,14 @@ const sleepDuration = computed(() => {
const end = recordStore.sleepForm.endTime
if (!start || !end)
return '请选择开始和结束时间'
return 0
const diffMs = new Date(end).getTime() - new Date(start).getTime()
if (diffMs <= 0)
return '结束时间需晚于开始时间'
return 0
const totalMinutes = Math.floor(diffMs / 60000)
const hours = Math.floor(totalMinutes / 60)
const minutes = totalMinutes % 60
if (hours > 0 && minutes > 0)
return `${hours}小时${minutes}分钟`
if (hours > 0)
return `${hours}小时`
return `${minutes}分钟`
return Math.round((diffMs / 3600000) * 10) / 10
})
function handeClosePopup() {
@@ -131,7 +123,7 @@ function handeClosePopup() {
}
function saveSleep() {
const { startTime, endTime, time, note } = recordStore.sleepForm
const { startTime, endTime, time } = recordStore.sleepForm
if (!startTime) {
uni.showToast({ title: '请选择开始时间', icon: 'none' })
@@ -150,6 +142,7 @@ function saveSleep() {
return
}
recordStore.sleepForm.duration = sleepDuration.value
recordStore.submitSleep()
uni.showToast({ title: '睡眠记录成功', icon: 'success' })

View File

@@ -79,7 +79,8 @@
<script lang="ts" setup>
import { useRecordStore } from '@/store/record'
import { getDateStr, getDayRecords, getMonthRecords } from '@/utils/record'
import { getDateStr, getDayRecords } from '@/utils/record'
import type { IHealthRecord } from '@/types/record'
defineOptions({
name: 'Home',
@@ -94,28 +95,103 @@ definePage({
const recordStore = useRecordStore()
const greeting = '下午好,小明 👋'
const today = '8月19日 周三'
const todayRecords = ref<IHealthRecord[]>([])
// 今日概览数据
const overviewItems = [
{ icon: '⚖️', label: '体重', value: '68.5kg', sub: '较昨日 -0.3kg', color: 'text-[#1F2D28]', bg: 'bg-[#E89E3A]' },
{ icon: '😴', label: '睡眠', value: '7.5h', sub: '深睡 2.1h', color: 'text-[#1F2D28]', bg: 'bg-[#2E5B8C]' },
{ icon: '🏃', label: '运动', value: '30min', sub: '目标 30min', color: 'text-[#1F2D28]', bg: 'bg-[#3A7C5B]' },
{ icon: '❤️ ', label: '血压', value: '120/80', sub: '正常', color: 'text-[#1F2D28]', bg: 'bg-[#C04A3C]' },
]
const now = new Date()
const hour = now.getHours()
// 今日计划
const plans = [
{ icon: '🏃', label: '运动30分钟', done: true },
{ icon: '🛏️', label: '23:00 前入睡', done: false },
]
const greeting = computed(() => {
if (hour < 6)
return '夜深了 🌙'
if (hour < 12)
return '早上好 ☀️'
if (hour < 18)
return '下午好 👋'
return '晚上好 🌆'
})
const today = computed(() => {
const m = now.getMonth() + 1
const d = now.getDate()
const w = ['日', '一', '二', '三', '四', '五', '六'][now.getDay()]
return `${m}${d}日 周${w}`
})
onMounted(() => {
const result = getDayRecords(getDateStr(Date.now()))
console.log(result)
todayRecords.value = getDayRecords(getDateStr(Date.now()))
})
console.log(getMonthRecords('2026-08'))
watch(() => recordStore.isRecord, () => {
todayRecords.value = getDayRecords(getDateStr(Date.now()))
})
// 今日概览
const overviewItems = computed(() => {
const list = todayRecords.value
// 体重:取最新一条
const weight = list.filter(r => r.type === 'weight').pop()
// 血压:取最新一条
const bp = 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)
return [
{
icon: '⚖️',
label: '体重',
value: weight ? `${weight.value} kg` : '未记录',
sub: '',
bg: 'bg-[#E89E3A]',
},
{
icon: '😴',
label: '睡眠',
value: sleep ? `${sleep.duration}h` : '未记录',
sub: '',
bg: 'bg-[#2E5B8C]',
},
{
icon: '🏃',
label: '运动',
value: sportTotal > 0 ? `${sportTotal}min` : '0min',
sub: sportTotal >= 30 ? '已达标' : '目标 30min',
bg: 'bg-[#3A7C5B]',
},
{
icon: '❤️',
label: '血压',
value: bp ? `${bp.systolic}/${bp.diastolic}` : '未记录',
sub: '',
bg: 'bg-[#C04A3C]',
},
]
})
// 今日计划
const plans = computed(() => {
const list = todayRecords.value
const hasWeight = list.some(r => r.type === 'weight')
const sportTotal = list
.filter(r => r.type === 'sport')
.reduce((sum, r) => sum + (r.duration || 0), 0)
const sleepOnTime = list
.filter(r => r.type === 'sleep')
.some((r) => {
const hour = new Date(r.startTime).getHours()
return hour <= 23
})
const hasBp = list.some(r => r.type === 'bloodPressure')
return [
{ icon: '⚖️', label: '记录体重', done: hasWeight },
{ icon: '❤️', label: '测量血压', done: hasBp },
{ icon: '🏃', label: '运动30分钟', done: sportTotal >= 30 },
{ icon: '🛏️', label: '23:00 前入睡', done: sleepOnTime },
]
})
function handelFabClick() {

View File

@@ -18,57 +18,64 @@
<!-- 时间轴区域 -->
<view class="px-5 py-2">
<!-- 时间轴容器 -->
<view v-if="grouped.length === 0" class="py-16 text-center">
<text class="text-4xl">📭</text>
<text class="mt-3 block text-sm text-gray-400">暂无记录</text>
</view>
<!-- 外层每个日期分组 -->
<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>
<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 records"
:key="idx"
v-for="(item, idx) in group.items"
:key="item.id || idx"
class="relative 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="item.color"
: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>
<wd-tag type="primary" round>
{{ item.status }}
{{ getRecordStatus(item) }}
</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="item.color">
<text>{{ item.icon }}</text>
<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">{{ item.type }}</text>
<text class="text-sm text-gray-700 font-medium">{{ getRecordMeta(item.type).label }}</text>
</view>
<!-- 第三行数值 -->
<text class="mt-1.5 block text-xl text-gray-800 font-bold">{{ item.value }}</text>
<text class="mt-1.5 block text-xl text-gray-800 font-bold">{{ getRecordValue(item) }}</text>
<!-- 第四行备注 -->
<text v-if="item.note" class="mt-0.5 block text-xs text-gray-400 leading-relaxed">
{{ item.note }}
</text>
</view>
</view>
<!-- 无数据 -->
<view v-if="records.length === 0" class="py-16 text-center">
<text class="text-4xl">📭</text>
<text class="mt-3 block text-sm text-gray-400">这一天还没有记录</text>
</view>
</view>
</view>
@@ -116,7 +123,8 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { getMonthRecords, getMonthStr } from '@/utils/record'
import { getMonthRecords, getMonthStr, getRecordMeta, getRecordStatus, getRecordValue, groupByDate } from '@/utils/record'
import type { IHealthRecord } from '@/types/record'
definePage({
style: {
@@ -134,10 +142,17 @@ const displayDate = computed(() => {
return `${d.getFullYear()}${d.getMonth() + 1}`
})
const records = ref([])
const records = ref<IHealthRecord[]>([])
const grouped = ref<ReturnType<typeof groupByDate>>([])
function loadRecords() {
const all = getMonthRecords(getMonthStr(currentDate.value))
records.value = all
grouped.value = groupByDate(all)
}
onMounted(() => {
records.value = getMonthRecords(getMonthStr(currentDate.value))
loadRecords()
})
function changeMonth(offset: number) {
@@ -145,12 +160,12 @@ function changeMonth(offset: number) {
d.setMonth(d.getMonth() + offset)
currentDate.value = d.getTime()
records.value = getMonthRecords(getMonthStr(currentDate.value))
loadRecords()
}
function goToday() {
currentDate.value = Date.now()
records.value = getMonthRecords(getMonthStr(currentDate.value))
loadRecords()
}
function openDatePicker() {
@@ -158,67 +173,9 @@ function openDatePicker() {
}
function onDateConfirm({ value }) {
records.value = getMonthRecords(getMonthStr(currentDate.value))
loadRecords()
}
// ===== 时间轴数据 =====
// 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: '今日整体健康状态良好。体重平稳,血压正常,建议增加有氧运动频率,保持当前作息规律。',

View File

@@ -11,7 +11,6 @@ export const useRecordStore = defineStore('record', {
showBpPopup: false,
showGlucosePopup: false,
showTempPopup: false,
weightForm: {
value: 0,
note: '',
@@ -20,14 +19,13 @@ export const useRecordStore = defineStore('record', {
sleepForm: {
startTime: Date.now(),
endTime: Date.now(),
duration: 0,
note: '',
time: Date.now(),
},
sportForm: {
category: '',
duration: 0,
calories: 0,
distance: 0,
note: '',
time: Date.now(),
},
@@ -50,6 +48,7 @@ export const useRecordStore = defineStore('record', {
note: '',
time: Date.now(),
},
isRecord: false,
}),
actions: {
openWeight() {
@@ -79,17 +78,18 @@ export const useRecordStore = defineStore('record', {
submitWeight() {
addWeight(this.weightForm.value, this.weightForm.note, this.weightForm.time)
this.showWeightPopup = false
this.resetWeight()
this.isRecord = !this.isRecord
},
submitSleep() {
addSleep(
this.sleepForm.startTime,
this.sleepForm.endTime,
this.sleepForm.duration,
this.sleepForm.note,
this.sleepForm.time,
)
this.showSleepPopup = false
this.resetSleep()
this.isRecord = !this.isRecord
},
submitSport() {
addSport(
@@ -99,7 +99,7 @@ export const useRecordStore = defineStore('record', {
this.sportForm.time,
)
this.showSportPopup = false
this.resetSport()
this.isRecord = !this.isRecord
},
submitBp() {
addBloodPressure(
@@ -110,7 +110,7 @@ export const useRecordStore = defineStore('record', {
this.bpForm.time,
)
this.showBpPopup = false
this.resetBp()
this.isRecord = !this.isRecord
},
submitGlucose() {
addBloodSugar(
@@ -120,7 +120,7 @@ export const useRecordStore = defineStore('record', {
this.glucoseForm.time,
)
this.showGlucosePopup = false
this.resetGlucose()
this.isRecord = !this.isRecord
},
submitTemp() {
addTemperature(
@@ -130,16 +130,16 @@ export const useRecordStore = defineStore('record', {
this.tempForm.time,
)
this.showTempPopup = false
this.resetTemp()
this.isRecord = !this.isRecord
},
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() }
this.sleepForm = { startTime: Date.now() - 8 * 60 * 60 * 1000, endTime: Date.now(), duration: 8, note: '', time: Date.now() }
},
resetSport() {
this.sportForm = { category: '跑步', duration: 30, calories: 0, distance: 0, note: '', time: Date.now() }
this.sportForm = { category: '跑步', duration: 30, note: '', time: Date.now() }
},
resetBp() {
this.bpForm = { systolic: 120, diastolic: 70, heartRate: 80, note: '', time: Date.now() }

View File

@@ -1,3 +1,5 @@
import type { IHealthRecord, IRecordMeta } from '@/types/record'
const MONTH_KEEP = 12
function getMonthKey(timestamp: number) {
@@ -56,8 +58,8 @@ 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 addSleep(startTime: number, endTime: number, duration: number, note: string, timestamp: number) {
addRecord('sleep', { startTime, endTime, duration, note }, timestamp)
}
export function addSport(category: string, duration: number, note: string, timestamp: number) {
@@ -92,20 +94,20 @@ function getMonthKeyForDate(dateStr: string): string {
return `health_${y}-${m}`
}
export function getDayRecords(date: string): Record<string, any>[] {
export function getDayRecords(date: string): IHealthRecord[] {
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>[] {
export function getMonthRecords(yearMonth: string, type?: string): IHealthRecord[] {
const key = `health_${yearMonth}`
const all = wx.getStorageSync(key) || {}
const result: Record<string, any>[] = []
const result: IHealthRecord[] = []
Object.keys(all).forEach((date) => {
all[date].forEach((r: Record<string, any>) => {
all[date].forEach((r: IHealthRecord) => {
if (!type || r.type === type) {
result.push({ date, ...r })
}
@@ -116,7 +118,7 @@ export function getMonthRecords(yearMonth: string, type?: string): Record<string
}
// 排序:先按 date 正序,再按 time 正序
function sortRecords(records: Record<string, any>[]): Record<string, any>[] {
function sortRecords(records: IHealthRecord[]): IHealthRecord[] {
return records.sort((a, b) => {
const dateCmp = a.date.localeCompare(b.date)
if (dateCmp !== 0)
@@ -124,3 +126,106 @@ function sortRecords(records: Record<string, any>[]): Record<string, any>[] {
return a.time.localeCompare(b.time)
})
}
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]' },
sport: { icon: '🏃', label: '运动', color: 'bg-[#3A7C5B]' },
temperature: { icon: '🌡️', label: '体温', color: 'bg-[#D32F2F]' },
}
export function getRecordMeta(type: string): IRecordMeta {
return META_MAP[type] || { icon: '📋', label: '其他', color: 'bg-gray-400' }
}
export function getRecordValue(record: IHealthRecord): string {
switch (record.type) {
case 'weight':
return `${record.value} kg`
case 'bloodPressure':
return `${record.systolic}/${record.diastolic} mmHg${record.heartRate ? ` · 心率 ${record.heartRate}` : ''}`
case 'bloodSugar':
return `${record.value} mmol/L · ${record.period}`
case 'heartRate':
return `${record.value} bpm`
case 'temperature':
return `${record.value}${record.location ? ` · ${record.location}` : ''}`
case 'sport':
return `${record.category} · ${record.duration}min`
case 'sleep':
return `${new Date(record.startTime).toTimeString().slice(0, 5)} ~ ${new Date(record.endTime).toTimeString().slice(0, 5)}${record.duration}h`
default:
return ''
}
}
export function getRecordStatus(record: IHealthRecord): string {
switch (record.type) {
case 'bloodPressure':
if (record.systolic >= 140 || record.diastolic >= 90)
return '偏高'
if (record.systolic < 90 || record.diastolic < 60)
return '偏低'
return '正常'
case 'bloodSugar':
if (record.period.includes('空腹')) {
return record.value >= 6.1 ? '偏高' : '正常'
}
// 餐后
return record.value >= 7.8 ? '偏高' : '正常'
case 'heartRate':
if (record.value < 60)
return '偏慢'
if (record.value <= 100)
return '正常'
return '偏快'
case 'temperature':
if (record.value < 36.1)
return '偏低'
if (record.value <= 37.2)
return '正常'
return '偏高'
case 'sport':
return record.duration >= 30 ? '达标' : '偏少'
case 'sleep':
if (record.duration >= 7)
return '充足'
if (record.duration >= 6)
return '良好'
return '不足'
default:
return '未知'
}
}
const WEEKDAY = ['日', '一', '二', '三', '四', '五', '六']
export function groupByDate(records: IHealthRecord[]): Array<{
date: string
weekday: string
items: IHealthRecord[]
}> {
const map: Record<string, IHealthRecord[]> = {}
records.forEach((r) => {
if (!r.date)
return
if (!map[r.date])
map[r.date] = []
map[r.date].push(r)
})
return Object.keys(map)
.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)), // 当天按时间正序
}))
}