فهرست منبع

feat(health): 实现血糖、血压、心率数据的前后端交互功能

- 新增 bloodGlucose.ts、bloodPressure.ts、heartRate.ts API 文件
- 实现血糖、血压、心率的列表查询、添加、删除接口调用
- 替换 mock 数据生成逻辑为真实 API 数据获取
- 在页面加载和操作后重新拉取并渲染图表数据
- 添加请求认证处理与登录态检查
- 实现新增和删除记录的后端同步操作
- 优化数据加载提示与错误处理机制
mcbaiyun 1 ماه پیش
والد
کامیت
a7f2357480

+ 41 - 0
src/api/bloodGlucose.ts

@@ -0,0 +1,41 @@
+export async function listBloodGlucose(params: { pageNum?: number; pageSize?: number; startTime?: string; endTime?: string }) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/blood-glucose/list',
+    method: 'POST',
+    data: params,
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}
+
+export async function addBloodGlucose(data: { type: string; value: number; measureTime: string }) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/blood-glucose/add',
+    method: 'POST',
+    data,
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}
+
+export async function deleteBloodGlucose(id: string) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/blood-glucose/delete',
+    method: 'POST',
+    data: { id },
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}

+ 41 - 0
src/api/bloodPressure.ts

@@ -0,0 +1,41 @@
+export async function listBloodPressure(params: { pageNum?: number; pageSize?: number; startTime?: string; endTime?: string }) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/blood-pressure/list',
+    method: 'POST',
+    data: params,
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}
+
+export async function addBloodPressure(data: { systolicPressure: number; diastolicPressure: number; measureTime: string }) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/blood-pressure/add',
+    method: 'POST',
+    data,
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}
+
+export async function deleteBloodPressure(id: string) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/blood-pressure/delete',
+    method: 'POST',
+    data: { id },
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}

+ 41 - 0
src/api/heartRate.ts

@@ -0,0 +1,41 @@
+export async function listHeartRate(params: { pageNum?: number; pageSize?: number; startTime?: string; endTime?: string }) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/heart-rate/list',
+    method: 'POST',
+    data: params,
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}
+
+export async function addHeartRate(data: { heartRate: number; measureTime: string }) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/heart-rate/add',
+    method: 'POST',
+    data,
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}
+
+export async function deleteHeartRate(id: string) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/heart-rate/delete',
+    method: 'POST',
+    data: { id },
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}

+ 86 - 56
src/pages/patient/health/details/blood-glucose.vue

@@ -90,6 +90,8 @@
 
 <script setup lang="ts">
 import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount, getCurrentInstance } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { listBloodGlucose, addBloodGlucose, deleteBloodGlucose } from '@/api/bloodGlucose'
 import CustomNav from '@/components/custom-nav.vue'
 
 import ScaleRuler from '@/components/scale-ruler.vue'
@@ -142,39 +144,53 @@ const displayPeriod = computed(() => {
   }
 })
 
-const records = ref<RecordItem[]>(generateMockRecords(current.value))
-
-function generateMockRecords(d: Date): RecordItem[] {
-  const arr: RecordItem[] = []
-  const typesLocal = ['空腹', '随机']
+const records = ref<RecordItem[]>([])
 
+async function fetchRecords() {
+  let startTime = ''
+  let endTime = ''
   if (viewMode.value === 'month') {
-    const y = d.getFullYear()
-    const m = d.getMonth()
-    const days = daysInMonth(y, m)
-    const n = Math.floor(Math.random() * Math.min(days, 7))
-    for (let i = 0; i < n; i++) {
-      const day = Math.max(1, Math.floor(Math.random() * days) + 1)
-      const date = new Date(y, m, day)
-      const val = Number((3 + Math.random() * 10).toFixed(1))
-      const type = typesLocal[Math.random() > 0.5 ? 0 : 1]
-      arr.push({ id: `${date.getTime()}${i}${Date.now()}`, date: formatDisplayDate(date), value: val, type })
-    }
+    const y = current.value.getFullYear()
+    const m = current.value.getMonth()
+    startTime = new Date(y, m, 1).toISOString()
+    const endDate = new Date(y, m + 1, 0)
+    endDate.setHours(23, 59, 59, 999)
+    endTime = endDate.toISOString()
   } else {
-    // 周视图:生成本周内的随机记录
-    const weekStart = getWeekStart(d)
-    const n = Math.floor(Math.random() * 7)
-    for (let i = 0; i < n; i++) {
-      const dayOffset = Math.floor(Math.random() * 7)
-      const date = new Date(weekStart)
-      date.setDate(weekStart.getDate() + dayOffset)
-      const val = Number((3 + Math.random() * 10).toFixed(1))
-      const type = typesLocal[Math.random() > 0.5 ? 0 : 1]
-      arr.push({ id: `${date.getTime()}${i}${Date.now()}`, date: formatDisplayDate(date), value: val, type })
+    const weekStart = getWeekStart(current.value)
+    const weekEnd = getWeekEnd(current.value)
+    startTime = weekStart.toISOString()
+    try {
+      const we = new Date(weekEnd)
+      we.setHours(23, 59, 59, 999)
+      endTime = we.toISOString()
+    } catch (e) {
+      endTime = weekEnd.toISOString()
     }
   }
 
-  return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
+  try { if (typeof uni !== 'undefined' && uni.showLoading) uni.showLoading({ title: '加载中...' }) } catch (e) {}
+
+  try {
+    const res: any = await listBloodGlucose({ pageNum: 1, pageSize: 100, startTime, endTime })
+    if (res.statusCode === 401) {
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('role')
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
+    }
+    if ((res.data as any) && (res.data as any).code === 200) {
+      const apiRecords = (res.data as any).data?.records || []
+      records.value = apiRecords.map((item: any) => ({ id: String(item.id), date: formatDisplayDate(new Date(item.measureTime)), value: Number(item.value ?? 0), type: item.type || '随机' }))
+      try { await drawChart() } catch (e) { console.warn('bgChart draw failed', e) }
+    } else {
+      console.error('Fetch blood-glucose records failed', res.data)
+    }
+  } catch (e) {
+    console.error('Fetch blood-glucose error', e)
+  } finally {
+    try { if (typeof uni !== 'undefined' && uni.hideLoading) uni.hideLoading() } catch (e) {}
+  }
 }
 
 // 获取指定日期所在周的开始日期(星期一)
@@ -257,6 +273,8 @@ onMounted(() => {
     } catch (e) {
       console.warn('getCanvasSize failed on mounted', e)
     }
+    // 拉取数据并绘制
+    await fetchRecords()
     await drawChart()
   }, 500)
 })
@@ -300,7 +318,7 @@ async function prevPeriod() {
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
+  await fetchRecords()
   await rebuildChart()
 }
 
@@ -321,14 +339,14 @@ async function nextPeriod() {
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
+  await fetchRecords()
   await rebuildChart()
 }
 
 async function setViewMode(mode: 'month' | 'week') {
   if (viewMode.value !== mode) {
     viewMode.value = mode
-    records.value = generateMockRecords(current.value)
+    await fetchRecords()
     await rebuildChart()
   }
 }
@@ -349,7 +367,7 @@ async function onPickerChange(e: any) {
         pickerValue.value = [Number(val[0]), Number(val[1])]
       }
       current.value = d
-      records.value = generateMockRecords(d)
+      await fetchRecords()
       await rebuildChart()
   }
 }
@@ -404,13 +422,7 @@ async function confirmAdd() {
       confirmText: '知道了'
     })
   }
-  const id = `user-${Date.now()}`
-  const item: RecordItem = {
-    id,
-    date: addDateLabel.value,
-    value: Number(Number(addGlucose.value).toFixed(1)),
-    type: types[typeIndex.value]
-  }
+  // 校验日期
   const parts = addDate.value.split('-')
   const addY = parseInt(parts[0], 10)
   const addM = parseInt(parts[1], 10) - 1
@@ -420,25 +432,26 @@ async function confirmAdd() {
     uni.showToast && uni.showToast({ title: '不能添加未来日期的数据', icon: 'none' })
     return
   }
-  let isInCurrentPeriod = false
-  if (viewMode.value === 'month') {
-    isInCurrentPeriod = addY === current.value.getFullYear() && addM === current.value.getMonth()
-  } else {
-    const weekStart = getWeekStart(current.value)
-    const recordWeekStart = getWeekStart(addDateObj)
-    isInCurrentPeriod = weekStart.getTime() === recordWeekStart.getTime()
-  }
 
-  if (isInCurrentPeriod) {
-    records.value = [item, ...records.value]
-  }
-  uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
-  closeAdd()
-  // 新增记录后彻底重建图表,确保像退出再进入一样刷新
   try {
-    await rebuildChart()
+    const res: any = await addBloodGlucose({ type: types[typeIndex.value], value: Number(Number(addGlucose.value).toFixed(1)), measureTime: new Date(addDate.value).toISOString() })
+    if (res.statusCode === 401) {
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('role')
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
+    }
+    if ((res.data as any) && (res.data as any).code === 200) {
+      uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
+      closeAdd()
+      await fetchRecords()
+      try { await rebuildChart() } catch (e) { console.warn('rebuildChart after add failed', e) }
+    } else {
+      uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
+    }
   } catch (e) {
-    console.warn('rebuildChart after add failed', e)
+    console.error('Add blood-glucose error', e)
+    uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
   }
 }
 
@@ -449,8 +462,25 @@ async function confirmDeleteRecord(id: string) {
       content: '确认删除该条记录吗?',
       success: async (res: any) => {
         if (res.confirm) {
-          records.value = records.value.filter(r => r.id !== id)
-          try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
+          try {
+            const delRes: any = await deleteBloodGlucose(id)
+            if (delRes.statusCode === 401) {
+              uni.removeStorageSync('token')
+              uni.removeStorageSync('role')
+              uni.reLaunch({ url: '/pages/public/login/index' })
+              return
+            }
+            if ((delRes.data as any) && (delRes.data as any).code === 200) {
+              records.value = records.value.filter(r => r.id !== id)
+              uni.showToast && uni.showToast({ title: '已删除', icon: 'success' })
+              try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
+            } else {
+              uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
+            }
+          } catch (e) {
+            console.error('Delete blood-glucose error', e)
+            uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
+          }
         }
       }
     })

+ 96 - 53
src/pages/patient/health/details/blood-pressure.vue

@@ -92,6 +92,8 @@
 
 <script setup lang="ts">
 import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount, getCurrentInstance } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { listBloodPressure, addBloodPressure, deleteBloodPressure } from '@/api/bloodPressure'
 import { createUChart } from '@/composables/useUChart'
 import CustomNav from '@/components/custom-nav.vue'
 
@@ -144,34 +146,53 @@ const displayPeriod = computed(() => {
   }
 })
 
-const records = ref<RecordItem[]>(generateMockRecords(current.value))
+const records = ref<RecordItem[]>([])
 
-function generateMockRecords(d: Date): RecordItem[] {
-  const arr: RecordItem[] = []
+async function fetchRecords() {
+  let startTime = ''
+  let endTime = ''
   if (viewMode.value === 'month') {
-    const y = d.getFullYear()
-    const m = d.getMonth()
-    const n = Math.floor(Math.random() * Math.min(daysInMonth(y, m), 7))
-    for (let i = 0; i < n; i++) {
-      const day = Math.max(1, Math.floor(Math.random() * daysInMonth(y, m)) + 1)
-      const date = new Date(y, m, day)
-      const s = 100 + Math.floor(Math.random() * 40)
-      const dval = 60 + Math.floor(Math.random() * 30)
-      arr.push({ id: `${date.getTime()}-${i}`, date: formatDisplayDate(date), s, d: dval })
-    }
+    const y = current.value.getFullYear()
+    const m = current.value.getMonth()
+    startTime = new Date(y, m, 1).toISOString()
+    const endDate = new Date(y, m + 1, 0)
+    endDate.setHours(23, 59, 59, 999)
+    endTime = endDate.toISOString()
   } else {
-    const weekStart = getWeekStart(d)
-    const n = Math.floor(Math.random() * 7)
-    for (let i = 0; i < n; i++) {
-      const dayOffset = Math.floor(Math.random() * 7)
-      const date = new Date(weekStart)
-      date.setDate(weekStart.getDate() + dayOffset)
-      const s = 100 + Math.floor(Math.random() * 40)
-      const dval = 60 + Math.floor(Math.random() * 30)
-      arr.push({ id: `${date.getTime()}-${i}`, date: formatDisplayDate(date), s, d: dval })
+    const weekStart = getWeekStart(current.value)
+    const weekEnd = getWeekEnd(current.value)
+    startTime = weekStart.toISOString()
+    try {
+      const we = new Date(weekEnd)
+      we.setHours(23, 59, 59, 999)
+      endTime = we.toISOString()
+    } catch (e) {
+      endTime = weekEnd.toISOString()
+    }
+  }
+
+  try { if (typeof uni !== 'undefined' && uni.showLoading) uni.showLoading({ title: '加载中...' }) } catch (e) {}
+
+  try {
+    const res: any = await listBloodPressure({ pageNum: 1, pageSize: 100, startTime, endTime })
+    if (res.statusCode === 401) {
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('role')
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
     }
+    if ((res.data as any) && (res.data as any).code === 200) {
+      const apiRecords = (res.data as any).data?.records || []
+      records.value = apiRecords.map((item: any) => ({ id: String(item.id), date: formatDisplayDate(new Date(item.measureTime)), s: Number(item.systolicPressure || 0), d: Number(item.diastolicPressure || 0) }))
+      try { await bpChart.draw(records, current, viewMode) } catch (e) { console.warn('bpChart draw failed', e) }
+    } else {
+      console.error('Fetch blood-pressure records failed', res.data)
+    }
+  } catch (e) {
+    console.error('Fetch blood-pressure error', e)
+  } finally {
+    try { if (typeof uni !== 'undefined' && uni.hideLoading) uni.hideLoading() } catch (e) {}
   }
-  return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
 }
 
 // 将 records 聚合为每天一个点(取最新记录)
@@ -239,10 +260,20 @@ onMounted(() => {
     } catch (e) {
       console.warn('getCanvasSize failed on mounted', e)
     }
-    await bpChart.draw(records, current, viewMode)
+    // 拉取数据并绘制
+    await fetchRecords()
+    try { await bpChart.draw(records, current, viewMode) } catch (e) { console.warn('bpChart draw failed', e) }
   }, 500)
 })
 
+// 页面显示时检查登录态
+onShow(() => {
+  const token = uni.getStorageSync('token')
+  if (!token) {
+    uni.reLaunch({ url: '/pages/public/login/index' })
+  }
+})
+
 // 监听并更新图表(轻微去抖)
 watch([() => current.value], async () => {
   setTimeout(async () => {
@@ -277,7 +308,7 @@ async function prevPeriod() {
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
+  await fetchRecords()
   await rebuildChart()
 }
 
@@ -298,14 +329,14 @@ async function nextPeriod() {
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
+  await fetchRecords()
   await rebuildChart()
 }
 
 async function setViewMode(mode: 'month' | 'week') {
   if (viewMode.value !== mode) {
     viewMode.value = mode
-    records.value = generateMockRecords(current.value)
+    await fetchRecords()
     await rebuildChart()
   }
 }
@@ -326,7 +357,7 @@ async function onPickerChange(e: any) {
       pickerValue.value = [val[0], val[1]]
     }
     current.value = d
-    records.value = generateMockRecords(d)
+    await fetchRecords()
     await rebuildChart()
   }
 }
@@ -388,13 +419,7 @@ async function confirmAdd() {
       confirmText: '知道了'
     })
   }
-  const id = `user-${Date.now()}`
-  const item: RecordItem = { 
-    id, 
-    date: addDateLabel.value, 
-    s: Math.round(addSystolic.value), 
-    d: Math.round(addDiastolic.value) 
-  }
+  // 校验日期
   const parts = addDate.value.split('-')
   const addY = parseInt(parts[0], 10)
   const addM = parseInt(parts[1], 10) - 1
@@ -404,25 +429,26 @@ async function confirmAdd() {
     uni.showToast && uni.showToast({ title: '不能添加未来日期的数据', icon: 'none' })
     return
   }
-  if (viewMode.value === 'month') {
-    if (addY === current.value.getFullYear() && addM === current.value.getMonth()) {
-      records.value = [item, ...records.value]
+
+  try {
+    const res: any = await addBloodPressure({ systolicPressure: Math.round(addSystolic.value), diastolicPressure: Math.round(addDiastolic.value), measureTime: new Date(addDate.value).toISOString() })
+    if (res.statusCode === 401) {
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('role')
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
     }
-  } else {
-    // addDateObj already computed above
-    const addWeekStart = getWeekStart(addDateObj)
-    const curWeekStart = getWeekStart(current.value)
-    if (addWeekStart.getTime() === curWeekStart.getTime()) {
-      records.value = [item, ...records.value]
+    if ((res.data as any) && (res.data as any).code === 200) {
+      uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
+      closeAdd()
+      await fetchRecords()
+      try { await rebuildChart() } catch (e) { console.warn('rebuildChart after add failed', e) }
+    } else {
+      uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
     }
-  }
-  uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
-  closeAdd()
-  // 新增记录后彻底重建图表,确保像退出再进入一样刷新
-  try {
-    await rebuildChart()
   } catch (e) {
-    console.warn('rebuildChart after add failed', e)
+    console.error('Add blood-pressure error', e)
+    uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
   }
 }
 
@@ -433,8 +459,25 @@ async function confirmDeleteRecord(id: string) {
       content: '确认删除该条记录吗?', 
       success: async (res: any) => { 
         if (res.confirm) {
-          records.value = records.value.filter(r => r.id !== id)
-          try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
+          try {
+            const delRes: any = await deleteBloodPressure(id)
+            if (delRes.statusCode === 401) {
+              uni.removeStorageSync('token')
+              uni.removeStorageSync('role')
+              uni.reLaunch({ url: '/pages/public/login/index' })
+              return
+            }
+            if ((delRes.data as any) && (delRes.data as any).code === 200) {
+              records.value = records.value.filter(r => r.id !== id)
+              uni.showToast && uni.showToast({ title: '已删除', icon: 'success' })
+              try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
+            } else {
+              uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
+            }
+          } catch (e) {
+            console.error('Delete blood-pressure error', e)
+            uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
+          }
         }
       } 
     }) 

+ 107 - 43
src/pages/patient/health/details/heart-rate.vue

@@ -82,6 +82,8 @@
 
 <script setup lang="ts">
 import { ref, computed, onMounted, watch, nextTick, onBeforeUnmount, getCurrentInstance } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { listHeartRate, addHeartRate, deleteHeartRate } from '@/api/heartRate'
 import { createUChart } from '@/composables/useUChart'
 import CustomNav from '@/components/custom-nav.vue'
 
@@ -133,31 +135,63 @@ const displayPeriod = computed(() => {
   }
 })
 
-const records = ref<RecordItem[]>(generateMockRecords(current.value))
+const records = ref<RecordItem[]>([])
 
-function generateMockRecords(d: Date): RecordItem[] {
-  const arr: RecordItem[] = []
+// 从后端拉取心率记录
+async function fetchRecords() {
+  let startTime = ''
+  let endTime = ''
   if (viewMode.value === 'month') {
-    const y = d.getFullYear()
-    const m = d.getMonth()
-    const days = daysInMonth(y, m)
-    const n = Math.floor(Math.random() * Math.min(days, 7))
-    for (let i = 0; i < n; i++) {
-      const day = Math.max(1, Math.floor(Math.random() * days) + 1)
-      const date = new Date(y, m, day)
-      arr.push({ id: `${date.getTime()}${i}${Date.now()}`, date: formatDisplayDate(date), hr: Math.floor(50 + Math.random() * 70) })
-    }
+    const y = current.value.getFullYear()
+    const m = current.value.getMonth()
+    startTime = new Date(y, m, 1).toISOString()
+    const endDate = new Date(y, m + 1, 0)
+    endDate.setHours(23, 59, 59, 999)
+    endTime = endDate.toISOString()
   } else {
-    const weekStart = getWeekStart(d)
-    const n = Math.floor(Math.random() * 7)
-    for (let i = 0; i < n; i++) {
-      const dayOffset = Math.floor(Math.random() * 7)
-      const date = new Date(weekStart)
-      date.setDate(weekStart.getDate() + dayOffset)
-      arr.push({ id: `${date.getTime()}${i}${Date.now()}`, date: formatDisplayDate(date), hr: Math.floor(50 + Math.random() * 70) })
+    const weekStart = getWeekStart(current.value)
+    const weekEnd = getWeekEnd(current.value)
+    startTime = weekStart.toISOString()
+    try {
+      const we = new Date(weekEnd)
+      we.setHours(23, 59, 59, 999)
+      endTime = we.toISOString()
+    } catch (e) {
+      endTime = weekEnd.toISOString()
     }
   }
-  return arr.sort((a, b) => (a.date < b.date ? 1 : -1))
+
+  try {
+    if (typeof uni !== 'undefined' && uni.showLoading) uni.showLoading({ title: '加载中...' })
+  } catch (e) {}
+
+  try {
+    const res: any = await listHeartRate({ pageNum: 1, pageSize: 100, startTime, endTime })
+    if (res.statusCode === 401) {
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('role')
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
+    }
+    if ((res.data as any) && (res.data as any).code === 200) {
+      const apiRecords = (res.data as any).data?.records || []
+      records.value = apiRecords.map((item: any) => {
+        const hr = item.heartRate == null ? 0 : Number(item.heartRate)
+        return {
+          id: String(item.id),
+          date: formatDisplayDate(new Date(item.measureTime)),
+          hr: Number.isNaN(hr) ? 0 : hr
+        }
+      })
+      try { await hrChart.draw(records, current, viewMode) } catch (e) { console.warn('hrChart draw failed', e) }
+    } else {
+      console.error('Fetch heart-rate records failed', res.data)
+    }
+  } catch (e) {
+    console.error('Fetch heart-rate error', e)
+  } finally {
+    try { if (typeof uni !== 'undefined' && uni.hideLoading) uni.hideLoading() } catch (e) {}
+  }
 }
 
 // 获取指定日期所在周的开始日期(星期一),并规范化到本地午夜
@@ -214,10 +248,20 @@ onMounted(() => {
     } catch (e) {
       console.warn('getCanvasSize failed on mounted', e)
     }
-    await hrChart.draw(records, current, viewMode)
+    // 拉取数据并绘制
+    await fetchRecords()
+    try { await hrChart.draw(records, current, viewMode) } catch (e) { console.warn('hrChart draw failed', e) }
   }, 500)
 })
 
+// 页面显示时检查登录态(与 physical.vue 保持一致)
+onShow(() => {
+  const token = uni.getStorageSync('token')
+  if (!token) {
+    uni.reLaunch({ url: '/pages/public/login/index' })
+  }
+})
+
 // 监听并更新图表(轻微去抖)
 watch([() => current.value], async () => {
   setTimeout(async () => {
@@ -249,7 +293,7 @@ async function prevPeriod() {
   else d.setDate(d.getDate() - 7)
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
+  await fetchRecords()
   await rebuildChart()
 }
 
@@ -270,14 +314,14 @@ async function nextPeriod() {
   }
   current.value = d
   pickerValue.value = [d.getFullYear() - 2000, d.getMonth()]
-  records.value = generateMockRecords(d)
+  await fetchRecords()
   await rebuildChart()
 }
 
 async function setViewMode(mode: 'month' | 'week') {
   if (viewMode.value !== mode) {
     viewMode.value = mode
-    records.value = generateMockRecords(current.value)
+    await fetchRecords()
     await rebuildChart()
   }
 }
@@ -297,7 +341,7 @@ async function onPickerChange(e: any) {
       pickerValue.value = [Number(val[0]), Number(val[1])]
     }
     current.value = d
-    records.value = generateMockRecords(d)
+    await fetchRecords()
     await rebuildChart()
   }
 }
@@ -349,8 +393,7 @@ async function confirmAdd() {
     uni.showToast && uni.showToast({ title: '请输入心率', icon: 'none' })
     return
   }
-  const id = `user-${Date.now()}`
-  const item: RecordItem = { id, date: addDateLabel.value, hr: addHR.value }
+  // 校验日期
   const parts = addDate.value.split('-')
   const addY = parseInt(parts[0], 10)
   const addM = parseInt(parts[1], 10) - 1
@@ -360,22 +403,26 @@ async function confirmAdd() {
     uni.showToast && uni.showToast({ title: '不能添加未来日期的数据', icon: 'none' })
     return
   }
-  let isInCurrentPeriod = false
-  if (viewMode.value === 'month') {
-    isInCurrentPeriod = addY === current.value.getFullYear() && addM === current.value.getMonth()
-  } else {
-    const weekStart = getWeekStart(current.value)
-    const recordWeekStart = getWeekStart(addDateObj)
-    isInCurrentPeriod = weekStart.getTime() === recordWeekStart.getTime()
-  }
-  if (isInCurrentPeriod) records.value = [item, ...records.value]
-  uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
-  closeAdd()
-  // 新增记录后彻底重建图表,确保像退出再进入一样刷新
+
   try {
-    await rebuildChart()
+    const res: any = await addHeartRate({ heartRate: addHR.value as number, measureTime: new Date(addDate.value).toISOString() })
+    if (res.statusCode === 401) {
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('role')
+      uni.reLaunch({ url: '/pages/public/login/index' })
+      return
+    }
+    if ((res.data as any) && (res.data as any).code === 200) {
+      uni.showToast && uni.showToast({ title: '已添加', icon: 'success' })
+      closeAdd()
+      await fetchRecords()
+      try { await rebuildChart() } catch (e) { console.warn('rebuildChart after add failed', e) }
+    } else {
+      uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
+    }
   } catch (e) {
-    console.warn('rebuildChart after add failed', e)
+    console.error('Add heart-rate error', e)
+    uni.showToast && uni.showToast({ title: '添加失败', icon: 'none' })
   }
 }
 
@@ -386,8 +433,25 @@ async function confirmDeleteRecord(id: string) {
       content: '确认删除该条记录吗?', 
       success: async (res: any) => { 
         if (res.confirm) {
-          records.value = records.value.filter(r => r.id !== id)
-          try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
+          try {
+            const delRes: any = await deleteHeartRate(id)
+            if (delRes.statusCode === 401) {
+              uni.removeStorageSync('token')
+              uni.removeStorageSync('role')
+              uni.reLaunch({ url: '/pages/public/login/index' })
+              return
+            }
+            if ((delRes.data as any) && (delRes.data as any).code === 200) {
+              records.value = records.value.filter(r => r.id !== id)
+              uni.showToast && uni.showToast({ title: '已删除', icon: 'success' })
+              try { await rebuildChart() } catch (e) { console.warn('rebuildChart after delete failed', e) }
+            } else {
+              uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
+            }
+          } catch (e) {
+            console.error('Delete heart-rate error', e)
+            uni.showToast && uni.showToast({ title: '删除失败', icon: 'none' })
+          }
         }
       }
     })