Переглянути джерело

feat(patient): 实现健康数据概览页面数据加载与展示

- 新增 patientData API 模块,封装最新数据概览接口
- 重构健康数据页面,使用响应式变量展示各项生理指标
- 增加对 request 返回结构的兼容性处理(支持 res.data 和顶层 res 两种格式)
- 添加 loadOverview 方法,在页面显示时加载并解析后端数据
- 更新模板绑定,动态显示身高、体重、BMI、血压、血糖和心率数据
- 引入 onShow 生命周期钩子,确保页面激活时刷新数据
- 增加详细的日志输出,便于调试和问题追踪
- 为数值显示添加单位和空值处理逻辑
- 创建相关文档,记录接口响应解析问题及修复方案
mcbaiyun 3 тижнів тому
батько
коміт
106f8e653b

+ 0 - 0
docs/uniapp-modal-multiline-text-fix.md → docs/uni-app 弹窗多行文本显示问题与修复.md


+ 0 - 0
docs/uniapp-user-binding-params-querystring-fix.md → docs/uni.request params 无效修复记录(user-binding+list-by-patient).md


+ 0 - 0
docs/base-info-avatar-logic.md → docs/完善基本信息页面-头像下载-选择-上传交互逻辑.md


+ 0 - 0
docs/page-entry-source-detection.md → docs/小程序页面入口来源检测方案.md


+ 0 - 0
docs/subscription-status-check.md → docs/微信订阅消息状态检测与同步方案.md


+ 110 - 0
docs/病人生理数据聚合接口(overview)响应解析修复总结.md

@@ -0,0 +1,110 @@
+````markdown
+# 病人生理数据聚合接口(patient-data/overview/latest)前端响应解析问题及修复总结
+
+## 概要 ✅
+在 `patient/health/index.vue` 页面,我们看到网络请求成功命中了 `https://wx.baiyun.work/patient-data/overview/latest` 并返回了完整 JSON(HTTP 200),但页面仍显示预留占位 `--` 而没有更新为后端数据。排查发现是前端解析后端响应的逻辑判断不匹配导致赋值逻辑没有执行;修复后页面正常显示数据。
+
+此问题属于前端与 `request` 封装/接口契约不一致的典型示例,文档将描述重现步骤、定位过程、修复与预防措施,并将本次经历与之前的类似问题(`uni.request` 参数/返回差异)结合说明。
+
+---
+
+## 现象(复现)
+- 在开发者工具的 Network 面板或微信小程序调试工具能看到请求成功:
+  ```json
+  {
+    "code":200,
+    "message":"ok",
+    "data":{ "height":155.00, "weight":76.00, "bmi":31.63, ... }
+  }
+  ```
+- 页面仍然显示占位 `--`。
+- 控制台最初只有 `loadOverview start` 的 log,没有打印响应数据的 log(表示没进入赋值分支)。
+
+---
+
+## 根因分析 🕵️
+- 项目 `request()` 封装(`uni.request` 封装)在不同场景下返回的结构可能存在两种常见形式:
+  1) `res.data` 为后端原始 JSON:`{ code, message, data }`;
+  2) `res`(顶层)就是后端原始 JSON(少见但有时会在封装中做了数据抽取)。
+- 页面原始判断条件写法为 `if (res && res.code === 200 && res.data)`,而项目中大部分 API 返回是 `res.data.code===200`,`payload` 在 `res.data.data`。所以赋值分支未执行。
+- 因此是**响应契约/代码判断不一致**而非后端数据丢失或网络错误。
+
+---
+
+## 定位步骤(我如何发现问题)🔬
+1. 查看 Network 面板,确认 HTTP 返回体包含后端数据(你已经在问题里贴出了返回 JSON)。
+2. 在页面 `loadOverview()` 中添加 debug 日志(`console.log`)并重新触发页面 onShow():
+   - 确认有 `loadOverview start` 日志(函数被调用)。
+   - 未看到 `response data` 日志,说明数据赋值的分支未进入。 
+3. 检查 `loadOverview()` 的 `res` 解析代码:发现只判断 `res.code`,而不判断 `res.data.code` 或 `res.data.data`。
+4. 对比项目里其它页面(例如 `reminder.vue`、`physical.vue`)的写法,它们一致采用 `res && res.data && res.data.code === 200 && res.data.data` 的结构。  
+
+---
+
+## 修复(已提交)🔧
+1. 在 `uniapp-ts/src/pages/patient/health/index.vue` 的 `loadOverview()` 中,改为更兼容和稳健的解析逻辑:
+   - 优先解析 `res.data.code === 200 && res.data.data` 的结构;
+   - 兼容直接返回 `{ code, message, data }` 的写法(顶层 `res.code === 200 && res.data`);
+   - 一旦提取到 `payload`,把后端 `data` 字段赋给页面的响应式变量并渲染。  
+2. 增加日志以便调试:`loadOverview start`、`response data`、`height,weight,bmi`,用于确认解析与赋值执行情况。
+3. 因为该页面使用 `onShow` 钩子加载数据,我也保留了这一逻辑(保证页面激活时刷新)。
+
+示例核心变动:
+```ts
+const res: any = await getLatestOverview()
+let payload: any = null
+if (res && res.data && res.data.code === 200 && res.data.data) {
+  payload = res.data.data
+} else if (res && res.code === 200 && res.data) {
+  payload = res.data
+}
+if (payload) {
+  // 赋值到 height/weight/bmi 等
+}
+```
+
+---
+
+## 经验教训与最佳实践(结合历史问题)📚
+1. 统一 API 返回契约或封装层:
+   - 建议在 `src/api/request.ts` 中统一将 `res` 转换为后端原始 payload(例如:始终返回 `{ code, message, data }`,或者直接返回 `res.data`),从而避免在每个页面做不同的判断。参见项目中 `request.ts` 的封装模式。  
+2. 在页面添加稳健的响应解析逻辑:
+   - 在使用 `request()` 的页面上做双重兼容判断(`res.data.code` 或 `res.code`),降低因封装层改动导致的页面错误。  
+3. 日志与快速验证很关键:
+   - 在排查时 `console.log()` 是最直接的方式:记录请求启动、raw response、以及关键变量赋值后日志,能快速确认代码流程是否走到赋值分支。  
+4. 与 `uni.request` 相关的历史问题对比:
+   - 见 `docs/uniapp-user-binding-params-querystring-fix.md`:`uni.request` 的参数行为可能和其它 HTTP 客户端(axios)不同,开发时应按 `uni-app` 官方文档约定来处理(例如 `params` 不一定会自动拼接)。类似地,返回值的包装也可能因封装不同而变化;建议在封装层统一行为。  
+5. 页面状态更新与反应式变量:
+   - 即使数据正确拿到,也需要检查是否赋值给响应式变量(`ref`/`reactive`),以及模板是否正确引用这些变量(未被覆盖/无 CSS/DOM 层遮挡)。  
+
+---
+
+## 推荐改进(工程层)🔭
+1. 规范 `request` 返回:在 `src/api/request.ts` 中约定并实现:
+   - `request()` 返回一个统一结构 `Promise<{ code: number; message: string; data: any }>`,或
+   - `request()` 返回 `res.data`(即 server payload)直接用于页面逻辑(使页面写法更简单)。
+2. 提供 `api` 层适配器:对每个后端接口编写 `src/api/patientData.ts`,在其中做 payload 解析与类型转换,由 API 层返回统一的 JS 对象,页面无需重复解析。  
+3. 测试与监控:
+   - 为关键 API 添加集成测试(Mock response),确保页面解析与渲染在两种常见返回形态下都能兼容;
+   - 在开发/测试环境启用调试日志以便问题重现和排查。  
+
+---
+
+## 与项目中已有问题的关联
+- 与 `docs/uniapp-user-binding-params-querystring-fix.md` 记录的问题类似,本次问题同样与 `uni-app` SDK 封装细节 / `request` 的差异有关,都建议我们把约定落在封装层以减轻页面修改负担。
+- 与 `docs/从模拟数据到接口化-心率到体格数据迁移详解.md` 的建议一致:(1)后端字段要健壮处理;(2)客户端应在渲染前做兼容判断并处理空字段/类型,在 `physical.vue` 的迁移指南中已有很多类似的注意点。  
+
+---
+
+## 结论 ✅
+本次问题是对“接口/封装契约不一致”一个典型的排查/修复例子:
+- 发现方式:在控制台看到了请求成功但页面没有更新(没有进入赋值分支),可通过日志命中来确认;
+- 修复方式:在页面增加对返回结构的稳健解析并加日志;长远建议是在 `request` 层统一 payload 解析或把解析放在 `api` 层(保持页面逻辑简洁)。
+
+本次问题与项目里过去出现的 `uni.request` 参数/返回差异问题有高度相似性:都是“封装行为不一导致页面/调用方出错”,因此请优先把 contract 定义并固化到 `request()` 层,减少二次改动带来的回归。
+
+---
+
+如果需要:我可以把 `request()` 的返回规范化改动(例如统一返回后端 `data`)和 `getLatestOverview()` 的封装抽象合并为一个 PR,并为 `patientData.ts` 提供统一 adapter,顺便增加小型单元测试覆盖。请告诉我你的偏好(保守兼容 vs. 统一改造),我将继续实现。
+
+````

+ 0 - 0
docs/patient-home-popup-message-page-active-check.md → docs/病人首页弹窗消息活跃性检查与优化.md


+ 0 - 0
docs/switch-state-sync-issue.md → docs/订阅消息开关状态同步问题及解决方案.md


+ 48 - 0
src/api/patientData.ts

@@ -0,0 +1,48 @@
+import request from './request'
+
+export interface PatientLatestOverview {
+  height?: number | null
+  weight?: number | null
+  bmi?: number | null
+  physicalMeasureTime?: string | null
+  systolicPressure?: number | null
+  diastolicPressure?: number | null
+  bloodPressureMeasureTime?: string | null
+  bloodGlucoseType?: string | null
+  bloodGlucose?: number | null
+  bloodGlucoseMeasureTime?: string | null
+  heartRate?: number | null
+  heartRateMeasureTime?: string | null
+}
+
+// 获取当前登录用户的最新数据概览
+export async function getLatestOverview(params?: { pageNum?: number; pageSize?: number; startTime?: string; endTime?: string }) {
+  const res: any = await request({
+    url: 'https://wx.baiyun.work/patient-data/overview/latest',
+    method: 'POST',
+    data: params || {},
+    header: { 'content-type': 'application/json' }
+  })
+  return res as { code: number; message: string; data?: PatientLatestOverview }
+}
+
+// 绑定方(医生/家属)查询患者最新数据概览
+export async function getLatestOverviewByBoundUser(params: {
+  patientUserId: number | string
+  bindingType: string
+  baseQueryRequest?: { pageNum?: number; pageSize?: number; startTime?: string; endTime?: string }
+}) {
+  const { patientUserId, bindingType, baseQueryRequest } = params
+  const res: any = await request({
+    url: `https://wx.baiyun.work/patient-data/overview/latest-by-bound-user?patientUserId=${patientUserId}&bindingType=${bindingType}`,
+    method: 'POST',
+    data: baseQueryRequest || {},
+    header: { 'content-type': 'application/json' }
+  })
+  return res as { code: number; message: string; data?: PatientLatestOverview }
+}
+
+export default {
+  getLatestOverview,
+  getLatestOverviewByBoundUser
+}

+ 53 - 6
src/pages/patient/health/index.vue

@@ -6,35 +6,35 @@
         <view class="menu-item" @click="openDetail('physical','height')">
           <image src="/static/icons/remixicon/height.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">身高</text>
-          <text class="menu-value">--</text>
+          <text class="menu-value">{{ height !== null ? height + 'cm' : '--' }}</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
         <view class="menu-item" @click="openDetail('physical','weight')">
           <image src="/static/icons/remixicon/weight.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">体重</text>
-          <text class="menu-value">--</text>
+          <text class="menu-value">{{ weight !== null ? weight + 'kg' : '--' }}</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
         <view class="menu-item" @click="openDetail('physical','bmi')">
           <image src="/static/icons/remixicon/bmi.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">BMI</text>
-          <text class="menu-value">--</text>
+          <text class="menu-value">{{ bmi !== null ? (Number.isFinite(Number(bmi)) ? Number(bmi).toFixed(1) : bmi) : '--' }}</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
         <view class="menu-item" @click="openDetail('blood-pressure')"><image src="/static/icons/remixicon/scan-line.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">血压</text>
-          <text class="menu-value">--</text>
+          <text class="menu-value">{{ systolic !== null && diastolic !== null ? systolic + '/' + diastolic : '--' }}</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
         <view class="menu-item" @click="openDetail('blood-glucose')"><image src="/static/icons/remixicon/contrast-drop-2-line.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">血糖</text>
-          <text class="menu-value">--</text>
+          <text class="menu-value">{{ bloodGlucose !== null ? (bloodGlucoseType ? bloodGlucoseType + ' ' + bloodGlucose : String(bloodGlucose)) : '--' }}</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
         <view class="menu-item" @click="openDetail('heart-rate')">
           <image src="/static/icons/remixicon/heart-pulse.svg" class="menu-icon" mode="widthFix" />
           <text class="menu-text">心率</text>
-          <text class="menu-value">--</text>
+          <text class="menu-value">{{ heartRate !== null ? heartRate + ' bpm' : '--' }}</text>
           <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
         </view>
       </view>
@@ -53,12 +53,59 @@
 
 <script setup lang="ts">
 import { ref } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { getLatestOverview } from '@/api/patientData'
 
 import CustomNav from '@/components/custom-nav.vue'
 import TabBar from '@/components/tab-bar.vue'
 
 const title = ref('健康数据')
 
+const height = ref<number | null>(null)
+const weight = ref<number | null>(null)
+const bmi = ref<number | null>(null)
+const systolic = ref<number | null>(null)
+const diastolic = ref<number | null>(null)
+const bloodGlucose = ref<number | null>(null)
+const bloodGlucoseType = ref<string | null>(null)
+const heartRate = ref<number | null>(null)
+
+async function loadOverview() {
+  try {
+    console.log('[patient-data] loadOverview start')
+    const res: any = await getLatestOverview()
+    // `request()` 返回的 res 包含 data 字段,data 字段里才是后端返回的 { code, message, data }
+    // 为了兼容其他实现,支持两类响应结构:
+    // 1) res.data === { code, message, data }
+    // 2) res === { code, message, data }
+    let payload: any = null
+    if (res && res.data && res.data.code === 200 && res.data.data) {
+      payload = res.data.data
+    } else if (res && res.code === 200 && res.data) {
+      payload = res.data
+    }
+    if (payload) {
+      const data = payload
+      console.log('[patient-data] response data:', data)
+      height.value = data.height ?? null
+      weight.value = data.weight ?? null
+      bmi.value = data.bmi ?? null
+      systolic.value = data.systolicPressure ?? null
+      diastolic.value = data.diastolicPressure ?? null
+      bloodGlucose.value = data.bloodGlucose ?? null
+      bloodGlucoseType.value = data.bloodGlucoseType ?? null
+      heartRate.value = data.heartRate ?? null
+      console.log('[patient-data] height,weight,bmi:', height.value, weight.value, bmi.value)
+    }
+  } catch (e) {
+    console.error('[patient-data] loadOverview error', e)
+  }
+}
+
+onShow(() => {
+  loadOverview()
+})
+
 const openDetail = (type: string, metric?: string) => {
   const url = metric ? `details/${type}?metric=${metric}` : `details/${type}`
   uni.navigateTo({ url })