2 Incheckningar a96f9a4737 ... 5e4619fec2

Upphovsman SHA1 Meddelande Datum
  mcbaiyun 5e4619fec2 feat(my-patients.vue): 添加骨架加载视图和错误提示功能,优化病人列表加载体验 2 veckor sedan
  mcbaiyun 37a9f5a6ea feat(docs): 添加文档总览,概述各文档内容与适用场景 2 veckor sedan
2 ändrade filer med 214 tillägg och 43 borttagningar
  1. 106 0
      docs/README.md
  2. 108 43
      src/pages/doctor/index/my-patients.vue

+ 106 - 0
docs/README.md

@@ -0,0 +1,106 @@
+# 文档总览 — docs/
+
+此 README 列出 `docs/` 目录下所有文档的简要说明与适用场景,方便快速定位需要阅读的知识点或复盘记录。
+
+- `uni-app 页面加载提示对比分析(showLoading vs 骨架动画).md`
+  - 内容:对比了两种常见的加载反馈实现:全局 `uni.showLoading()` 与页面骨架屏(skeleton)的优缺点与适用场景。
+  - 何时用:设计或评估页面加载体验时;决定采用哪种加载反馈机制(短请求用 `showLoading`,列表/详情类长请求首选骨架)。
+
+- `uni-app 列表页面加载失败显示与骨架重试处理总结.md`
+  - 内容:针对列表页(以 `medicine.vue` 为例)出现“加载失败却显示空列表”和错误信息为 `[object Object]` 的问题的根因、修复步骤、样式与完整代码片段。
+  - 何时用:当遇到列表类页面加载后显示空态但实际上是请求失败,或需要为列表页添加骨架/重试/格式化错误时参考。
+
+- `uni-app 弹窗多行文本显示问题与修复.md`
+  - 内容:记录小程序/uni-app 弹窗中多行文本显示被截断或换行异常的定位与修复方法(样式/组件使用层面的调整)。
+  - 何时用:弹窗(modal/uni.showModal/自定义弹框)出现文本显示问题时查阅。
+
+- `uni-app 患者动态加载失败显示问题与修复.md`
+  - 内容:复盘患者动态或时间线类页面在数据加载/刷新时的异常显示与解决办法。
+  - 何时用:患者动态、消息流、时间线类页面异常显示或刷新逻辑出错时参考。
+
+- `uni-app 页面参数获取问题与修复.md`
+  - 内容:记录页面间传参、路由参数或 onLoad/onShow 中参数获取失败的常见场景与修复策略。
+  - 何时用:页面拿不到预期参数、跳转参数丢失或生命周期导致参数未及时可用时查看。
+
+- `uni.request params 无效修复记录(user-binding+list-by-patient).md`
+  - 内容:记录在使用 `uni.request` 时 params/请求体无效或拼接错误的定位与修复,包含具体案例。
+  - 何时用:遇到接口请求参数不生效、后端收不到参数或调试请求 payload 问题时参考。
+
+- `从模拟数据到接口化-心率到体格数据迁移详解.md`
+  - 内容:把前端从使用本地模拟数据迁移到正式接口的具体步骤、数据结构调整与兼容策略。
+  - 何时用:将页面从 mock 切换到真实接口、或需要对接后端新接口时使用。
+
+- `复诊管理设计.md`
+  - 内容:复诊管理功能的交互与页面设计草案(workflow、入口、关键按钮、权限点)。
+  - 何时用:开始开发复诊功能或评审产品设计时参考。
+
+- `复诊管理涉及的页面和按钮.md`
+  - 内容:列出复诊管理涉及到的页面、按钮与跳转关系,便于前后端对齐。
+  - 何时用:开发或验收复诊相关页面时核对界面元素与跳转。
+
+- `复诊管理接口适配方案.md`
+  - 内容:接口字段与前端适配方案,包含字段映射、缺省值和容错处理建议。
+  - 何时用:对接后端接口或调整前端数据模型时参考。
+
+- `复诊管理功能限制与适配问题.md`
+  - 内容:记录在实现复诊功能过程中遇到的限制(权限、后端规则)及对应的前端适配措施。
+  - 何时用:复诊功能上线前的兼容与降级方案参考。
+
+- `完善基本信息页面-头像下载-选择-上传交互逻辑.md`
+  - 内容:用户头像获取、缓存、选择与上传的完整交互流程和实现要点(包括 loading/遮罩策略)。
+  - 何时用:实现或修复头像相关功能(下载失败、上传、展示等)时参考。
+
+- `小程序页面入口来源检测方案.md`
+  - 内容:如何检测小程序/页面的来源(share、推送、tab、扫描等)及据此调整页面行为的方案。
+  - 何时用:需要根据入口来源作差异化逻辑(例如统计、埋点或跳转回流)时使用。
+
+- `微信订阅消息状态检测与同步方案.md`
+  - 内容:订阅消息授权状态在前端的检测、同步与用户提示策略。
+  - 何时用:实现微信订阅消息或需要提示/恢复订阅状态时参考。
+
+- `患者头像ID精度丢失修复总结.md`
+  - 内容:复盘由于 ID 精度问题导致头像 URL 错误的定位与修复策略(数值精度、序列化问题)。
+  - 何时用:遇到用户 ID 精度丢失或因数值精度导致资源请求失败时查看。
+
+- `病人生理数据聚合接口(overview)响应解析修复总结.md`
+  - 内容:对聚合接口响应格式解析问题的修复记录与兼容实现。
+  - 何时用:对接或解析复杂聚合接口时参考,包含数据校验与容错。
+
+- `病人首页弹窗消息活跃性检查与优化.md`
+  - 内容:弹窗消息展示时机、延迟与活跃度检测策略,避免页面加载争抢资源或影响渲染。
+  - 何时用:需要展示弹窗消息但要避免干扰首屏加载或引起卡顿时使用。
+
+- `订阅消息开关状态同步问题及解决方案.md`
+  - 内容:记录订阅消息开关在前端/后端不同步导致的问题与同步策略。
+  - 何时用:处理订阅状态出现不一致或需要周期性同步时参考。
+
+文档设计目的与内容规范
+- 目的:让团队和自动化工具(包括 AI 助手)能快速定位、理解并复用项目中出现的问题与解决方案,降低重复修复成本,加快新功能开发和故障排查。
+- 适用范围:本目录记录的是“与前端实现/交互/接口适配相关的实战复盘、修复记录与设计方案”,不用于记录通用库 API 或外部依赖文档。
+- 文件命名与标题规范:
+  - 文件名:使用中文,能概况问题与修复,后缀 `.md`,例如 `uni-app 列表页面加载失败显示与骨架重试处理总结.md`。尽量包含关键主体(组件/页面名)和问题类型。
+  - 标题(文件内第一级标题):必须为中文,简洁概括问题(1 行)。
+- 内容结构建议(必备项):
+  1. 概述:一句话说明问题与影响范围;
+  2. 复现步骤/场景:如何触发问题;
+ 1. 根因分析:定位结果、原因链;
+ 2. 解决方案:代码/配置/样式修改要点;
+ 3. 关键代码片段或补丁(可复制粘贴);
+ 4. 验证步骤:如何复测;
+ 5. 关联文档:指向其他相关 md;
+ 6. 经验教训与后续改进建议。
+- 格式与样式规范:
+  - 用小标题分段,代码片段用三反引号包裹并标注语言(如 ` ```vue `、` ```css `);
+  - 引用文件或符号时使用反引号(`src/pages/...`);
+  - 若包含补丁或大段代码,优先提供可直接复制的最小可运行示例。
+- 何时更新本文档:
+  - 当修复方案上线后(PR 合并或 release)应更新文档并记录版本/PR 链接;
+  - 如果发现新的边界条件或更稳妥的做法,应在原文档追加“补充/更新”小节并注明日期与作者。
+
+AI 与自动化使用/更新指引
+- 输入要求给 AI 的上下文:
+  - 提供相关源码路径、出错日志、复现步骤、以及期望行为;
+- AI 更新文档时应遵循:
+  1. 先在文档中添加“变更记录(Change log)”小节,注明修改人/时间/PR;
+  2. 若涉及代码修改,AI 应同时生成简要补丁和说明如何手动验证;
+  3. 保持文档语言简洁、可操作,避免冗长论述。

+ 108 - 43
src/pages/doctor/index/my-patients.vue

@@ -1,7 +1,39 @@
 <template>
   <CustomNav title="我的病人" leftType="back" />
   <view class="page-container">
-    <view class="patient-card" v-for="patient in patients" :key="patient.id">
+    <!-- 骨架加载 -->
+    <view v-if="isLoading" class="skeleton-list">
+      <view v-for="i in 3" :key="i" class="skeleton-patient-card">
+        <view class="skeleton-avatar" />
+        <view class="skeleton-meta">
+          <view class="skeleton-line name" />
+          <view class="skeleton-line phone" />
+        </view>
+      </view>
+    </view>
+
+    <!-- 错误优先展示 -->
+    <view v-else-if="isError" class="load-error compact">
+      <view class="error-left">
+        <uni-icons type="warn" size="34" color="#d93025" />
+        <text class="error-text">加载病人列表失败:{{ loadError || '网络或服务器异常' }}</text>
+      </view>
+      <view class="error-actions">
+        <button class="retry-btn" @click="retryLoad">
+          <uni-icons type="refresh" size="20" color="#fff" />
+          <text class="retry-text">重试</text>
+        </button>
+      </view>
+    </view>
+
+    <!-- 空态(仅当无错误时展示) -->
+    <view v-else-if="!isError && patients.length === 0" class="empty-state">
+      <image class="empty-icon" src="/static/icons/remixicon/account-circle-line.svg" />
+      <text class="empty-text">暂无绑定的病人</text>
+    </view>
+
+    <view v-else class="patient-list">
+      <view class="patient-card" v-for="patient in patients" :key="patient.id">
       <view class="patient-header">
         <view class="avatar-section">
           <view class="avatar-frame">
@@ -16,15 +48,11 @@
       </view>
       
       <view class="action-buttons">
-        <button class="action-btn primary" @click="viewHealthData(patient)">健康数据</button>
-        <button class="action-btn info" @click="viewMessageHistory(patient)">消息管理</button>
-        <button class="action-btn danger" @click="unbindPatient(patient)">解除绑定</button>
+        <button class="action-btn primary" @click="viewHealthData(patient)" :disabled="isLoading || isError">健康数据</button>
+        <button class="action-btn info" @click="viewMessageHistory(patient)" :disabled="isLoading || isError">消息管理</button>
+        <button class="action-btn danger" @click="unbindPatient(patient)" :disabled="isLoading || isError">解除绑定</button>
       </view>
     </view>
-    
-    <view class="empty-state" v-if="patients.length === 0">
-      <image class="empty-icon" src="/static/icons/remixicon/account-circle-line.svg" />
-      <text class="empty-text">暂无绑定的病人</text>
     </view>
   </view>
 </template>
@@ -42,6 +70,10 @@ interface PatientInfo extends UserBindingResponse {
 }
 
 const patients = ref<PatientInfo[]>([])
+// 加载/错误状态
+const isLoading = ref(false)
+const isError = ref(false)
+const loadError = ref('')
 const pageData = ref({
   pageNum: 1,
   pageSize: 10,
@@ -53,57 +85,46 @@ const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQ
 
 // 获取病人列表
 const fetchPatients = async () => {
-  uni.showLoading({ title: '加载中...' })
-  
+  isLoading.value = true
+  isError.value = false
+  loadError.value = ''
+
   try {
     const token = uni.getStorageSync('token')
     if (!token) {
-      uni.hideLoading()
-      uni.showToast({
-        title: '未登录',
-        icon: 'none'
-      })
+      isLoading.value = false
+      uni.showToast({ title: '未登录', icon: 'none' })
       return
     }
-    
-    // 获取当前用户ID(医生ID)
+
     const userInfo = uni.getStorageSync('user_info')
     const doctorUserId = userInfo?.id
-    
     if (!doctorUserId) {
-      uni.hideLoading()
-      uni.showToast({
-        title: '获取用户信息失败',
-        icon: 'none'
-      })
+      isLoading.value = false
+      uni.showToast({ title: '获取用户信息失败', icon: 'none' })
       return
     }
-    
-    // 查询医生绑定的病人列表(使用新的接口)
+
     const response = await listUserBindingsByBoundUser(
-      doctorUserId, 
-      'PATIENT', 
+      doctorUserId,
+      'PATIENT',
       {
         pageNum: pageData.value.pageNum,
         pageSize: pageData.value.pageSize
       }
     )
-    
-    uni.hideLoading()
-    
+
     const resp = response.data as any
-    
     if (resp && resp.code === 200 && resp.data) {
       const pageResult = resp.data as UserBindingPageResponse
       patients.value = pageResult.records as PatientInfo[]
       pageData.value.total = pageResult.total
       pageData.value.pages = pageResult.pages
-      
+
       // 为每个病人尝试下载头像
       for (const patient of patients.value) {
         try {
           if (patient.patientUserId) {
-            // 检查是否有缓存的头像
             const patientIdStr = String(patient.patientUserId)
             if (avatarCache.has(patientIdStr)) {
               patient.avatar = avatarCache.get(patientIdStr)
@@ -111,7 +132,6 @@ const fetchPatients = async () => {
               const dlRes: any = await downloadAvatar(String(patient.patientUserId))
               if (dlRes && dlRes.statusCode === 200 && dlRes.tempFilePath) {
                 patient.avatar = dlRes.tempFilePath
-                // 缓存头像路径
                 avatarCache.set(patientIdStr, dlRes.tempFilePath)
               }
             }
@@ -120,19 +140,23 @@ const fetchPatients = async () => {
           console.warn('下载病人头像失败:', err)
         }
       }
+
+      isError.value = false
+      loadError.value = ''
     } else {
-      uni.showToast({
-        title: '获取病人信息失败',
-        icon: 'none'
-      })
+      const msg = formatError(resp && resp.data ? resp.data : resp) || '获取病人信息失败'
+      isError.value = true
+      loadError.value = msg
+      uni.showToast({ title: msg, icon: 'none' })
     }
   } catch (error) {
-    uni.hideLoading()
+    const msg = formatError(error) || '获取病人信息失败'
+    isError.value = true
+    loadError.value = msg
     console.error('获取病人信息失败:', error)
-    uni.showToast({
-      title: '获取病人信息失败',
-      icon: 'none'
-    })
+    uni.showToast({ title: msg, icon: 'none' })
+  } finally {
+    isLoading.value = false
   }
 }
 
@@ -143,6 +167,23 @@ const patientAvatar = (patient: PatientInfo) => {
   return defaultAvatar
 }
 
+// 把各种可能的错误对象格式化为用户友好的字符串
+const formatError = (e: any): string => {
+  if (!e) return ''
+  if (typeof e === 'string') return e
+  if (typeof e === 'number' || typeof e === 'boolean') return String(e)
+  if (e.message && typeof e.message === 'string') return e.message
+  if (e.data && typeof e.data.message === 'string') return e.data.message
+  if (e.response && e.response.data && typeof e.response.data.message === 'string') return e.response.data.message
+  try {
+    const s = JSON.stringify(e)
+    if (s && s !== '{}' && s !== 'null') return s
+  } catch (_) {
+    // ignore
+  }
+  return String(e)
+}
+
 const viewHealthData = (patient: PatientInfo) => {
   // 跳转到公共健康数据查看页面,传递患者ID和绑定类型参数
   uni.navigateTo({
@@ -222,6 +263,10 @@ onShow(() => {
     uni.reLaunch({ url: '/pages/public/login/index' })
   }
 })
+
+const retryLoad = async () => {
+  await fetchPatients()
+}
 </script>
 
 <style scoped>
@@ -338,4 +383,24 @@ onShow(() => {
   color: #666;
   margin-bottom: 40rpx;
 }
+
+/* Skeleton & error styles */
+.skeleton-list { padding: 18rpx 20rpx; display:flex; flex-direction:column; gap: 18rpx }
+.skeleton-patient-card { display:flex; gap:18rpx; align-items:center; background:#fff; padding:24rpx; border-radius:16rpx }
+.skeleton-avatar { width:120rpx; height:120rpx; border-radius:50%; background: linear-gradient(90deg,#eee 25%,#f6f6f6 50%,#eee 75%); background-size:200% 100%; animation: shimmer 1.2s linear infinite }
+.skeleton-meta { flex:1; display:flex; flex-direction:column; gap:12rpx }
+.skeleton-line { height:20rpx; border-radius:8rpx; background: linear-gradient(90deg,#eee 25%,#f6f6f6 50%,#eee 75%); background-size:200% 100%; animation: shimmer 1.2s linear infinite }
+.skeleton-line.name { width: 50%; height:28rpx }
+.skeleton-line.phone { width: 40%; height:18rpx }
+
+@keyframes shimmer { 0% { background-position:200% 0 } 100% { background-position:-200% 0 } }
+
+.load-error { display:flex; gap: 20rpx; align-items:center; justify-content: space-between; padding: 30rpx; margin: 18rpx 20rpx; background: #fff7f7; border: 1rpx solid #ffd2d2; border-radius: 12rpx }
+.load-error.compact { padding: 20rpx }
+.load-error .error-left { display:flex; gap:12rpx; align-items:center; flex:1 }
+.load-error .error-actions { display:flex; align-items:center }
+.retry-btn { display:flex; align-items:center; justify-content:center; gap:8rpx; background:#d93025; color:#fff; height:48rpx; min-width:96rpx; padding:0 12rpx; border-radius:12rpx; font-size:24rpx; border:none }
+.retry-text { color:#fff; font-size:24rpx; line-height:48rpx }
+
+.patient-list { }
 </style>