4 Коммиты 5b5b4259d1 ... e89b4764e4

Автор SHA1 Сообщение Дата
  mcbaiyun e89b4764e4 fix(index.vue): 修改药品信息管理为药品管理 2 недель назад
  mcbaiyun b0bd138881 feat(critical-values): 添加骨架加载视图和数据加载失败提示功能 2 недель назад
  mcbaiyun e2982ea081 feat(patient-activities): 添加患者动态加载失败提示和重试功能文档 2 недель назад
  mcbaiyun a2808a6d0b feat(patient-activities): 添加患者动态加载失败提示和重试功能 2 недель назад

+ 61 - 0
docs/uni-app 患者动态加载失败显示问题与修复.md

@@ -0,0 +1,61 @@
+# uni-app 患者动态加载失败显示问题与修复
+
+## 问题现象
+在医生首页的患者动态模块中,当接口调用失败时,页面显示“暂无患者动态”,这会误导用户认为没有患者动态数据,而不是加载失败。用户无法区分是成功获取但无数据,还是网络错误或其他原因导致的加载失败。
+
+## 原因分析
+代码中 `fetchPatientActivities` 函数在 catch 块中设置 `patientActivities.value = []`,与成功但无数据时的处理相同。模板只检查 `patientActivities.length === 0` 来显示“暂无患者动态”,没有区分失败状态。
+
+## 解决办法
+1. **添加错误状态变量**:
+   - 新增 `patientActivitiesError` ref,用于标记加载是否失败。
+
+2. **更新加载逻辑**:
+   - 在 `fetchPatientActivities` 中,成功时设置 `patientActivitiesError.value = false`。
+   - 失败时设置 `patientActivitiesError.value = true`。
+
+3. **修改模板显示**:
+   - 成功但无数据:`v-if="patientActivities.length === 0 && !patientActivitiesError"` 显示“暂无患者动态”。
+   - 加载失败:`v-if="patientActivitiesError"` 显示“加载失败,请重试”并提供重试按钮。
+
+4. **添加样式**:
+   - 为错误提示和重试按钮定义 CSS 样式。
+
+## 代码示例
+```javascript
+// 添加状态变量
+const patientActivitiesError = ref(false)
+
+// 更新获取函数
+const fetchPatientActivities = async () => {
+  try {
+    patientActivitiesLoading.value = true
+    patientActivitiesError.value = false
+    // ... 接口调用
+  } catch (err) {
+    patientActivitiesError.value = true
+    patientActivities.value = []
+  } finally {
+    patientActivitiesLoading.value = false
+  }
+}
+```
+
+```vue
+<!-- 模板修改 -->
+<view v-if="patientActivities.length === 0 && !patientActivitiesError" class="no-activity">
+  <text>暂无患者动态</text>
+</view>
+<view v-if="patientActivitiesError" class="error-activity">
+  <text class="error-text">加载失败,请重试</text>
+  <view class="retry-button" @click="fetchPatientActivities">
+    <text class="retry-text">重试</text>
+  </view>
+</view>
+```
+
+## 经验总结
+在开发中,UI 状态管理需要仔细区分不同场景(如加载中、成功无数据、失败),避免单一状态导致的用户体验问题。建议:
+- 使用多个状态变量来精确控制显示逻辑。
+- 失败时提供明确的错误提示和恢复操作(如重试按钮)。
+- 参考现有代码模式,确保一致性。

+ 41 - 3
src/pages/doctor/index/index.vue

@@ -59,7 +59,7 @@
               <text class="item-desc">管理慢性病患者</text>
             </view>
           </view>
-          <view class="function-item orange" @click="onItemClick('药品信息管理')">
+          <view class="function-item orange" @click="onItemClick('药品管理')">
             <view class="item-content">
               <view class="title-row">
                 <view class="item-line"></view>
@@ -146,9 +146,15 @@
                 <text class="activity-time">{{ activity.time }}</text>
               </view>
             </view>
-            <view v-if="patientActivities.length === 0" class="no-activity">
+            <view v-if="patientActivities.length === 0 && !patientActivitiesError" class="no-activity">
               <text>暂无患者动态</text>
             </view>
+            <view v-if="patientActivitiesError" class="error-activity">
+              <text class="error-text">加载失败,请重试</text>
+              <view class="retry-button" @click="fetchPatientActivities">
+                <text class="retry-text">重试</text>
+              </view>
+            </view>
           </view>
         </view>
       </view>
@@ -214,6 +220,9 @@ const patientActivities = ref<Array<{
 // 患者动态加载状态
 const patientActivitiesLoading = ref(true)
 
+// 患者动态获取失败状态
+const patientActivitiesError = ref(false)
+
 const loadUser = () => {
   try {
     const u = uni.getStorageSync('user_info')
@@ -311,6 +320,7 @@ const fetchTodayReminders = async () => {
 const fetchPatientActivities = async () => {
   try {
     patientActivitiesLoading.value = true
+    patientActivitiesError.value = false
     const token = uni.getStorageSync('token')
     if (!token) {
       console.log('No token found, skipping fetchPatientActivities')
@@ -350,7 +360,8 @@ const fetchPatientActivities = async () => {
     }
   } catch (err) {
     console.error('Fetch patient activities error:', err)
-    // 如果接口调用失败,显示空数据
+    // 如果接口调用失败,显示错误状态
+    patientActivitiesError.value = true
     patientActivities.value = []
   } finally {
     patientActivitiesLoading.value = false
@@ -912,6 +923,33 @@ const formatActivityDescription = (activity: any) => {
   color: #999;
 }
 
+.error-activity {
+  padding: 40rpx;
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.error-text {
+  color: #ff6b6b;
+  font-size: 28rpx;
+  margin-bottom: 20rpx;
+}
+
+.retry-button {
+  background-color: #007aff;
+  color: white;
+  padding: 16rpx 32rpx;
+  border-radius: 8rpx;
+  display: inline-block;
+}
+
+.retry-text {
+  font-size: 28rpx;
+  color: white;
+}
+
 /* 骨架屏样式 */
 .skeleton-container {
   padding: 20rpx;

+ 111 - 56
src/pages/doctor/manage/critical-values.vue

@@ -1,6 +1,21 @@
 <template>
   <CustomNav title="危急值管理" leftType="back" />
   <view class="page-container">
+    <!-- 骨架加载视图 -->
+    <view v-if="isLoading" class="skeleton-container">
+      <view class="skeleton-card" />
+      <view class="skeleton-card" />
+      <view class="skeleton-card" />
+      <view class="skeleton-card" />
+    </view>
+
+    <!-- 数据加载失败时的内联提示 -->
+    <view v-if="isError" class="load-error">
+      <text class="error-text">加载数据失败:{{ loadError || '网络或服务器异常' }}</text>
+      <button class="btn-secondary" @click="retryLoad">重试加载</button>
+    </view>
+
+    <view v-if="!isLoading" class="content-root">
     <view class="card">
       <view class="card-header">体格(身高 / 体重 / BMI)</view>
       <view class="card-body">
@@ -118,12 +133,13 @@
         <!-- 按钮已移至页面底部全局操作区 -->
       </view>
     </view>
-  </view>
-  
+    </view>
+    </view>
+
     <!-- 全局操作区:仅保留保存 -->
     <view class="global-actions" style="padding: 20rpx 20rpx 40rpx; background: transparent;">
       <view class="actions">
-        <button class="btn-primary" @click="save">保存设置</button>
+        <button class="btn-primary" @click="save" :disabled="isLoading || isError">保存设置</button>
       </view>
     </view>
 </template>
@@ -163,9 +179,7 @@ interface CriticalValues {
   heartRate: { min: number | null; max: number | null }
 }
 
-// 新的保存键,包含多模块
-const STORAGE_KEY = 'criticalValues_v1'
-const LEGACY_PHYSICAL_KEY = 'criticalValues_physical'
+// 危急值配置页面
 
 const form = ref<CriticalValues>({
   physical: { heightMin: null, heightMax: null, weightMin: null, weightMax: null },
@@ -175,11 +189,19 @@ const form = ref<CriticalValues>({
   heartRate: { min: null, max: null }
 })
 
+// 加载状态
+const isLoading = ref(true)
+const isError = ref(false)
+const loadError = ref('')
+
 onMounted(async () => {
-  // 尝试从后端加载整表配置,若失败再回退到本地缓存
+  // 尝试从后端加载整表配置(显示骨架直到完成)
+  isLoading.value = true
+  isError.value = false
+  loadError.value = ''
   try {
     const res: any = await getCriticalValuesForm()
-    if (res.statusCode === 401) {
+    if (res && res.statusCode === 401) {
       uni.removeStorageSync('token')
       uni.removeStorageSync('role')
       uni.reLaunch({ url: '/pages/public/login/index' })
@@ -189,45 +211,49 @@ onMounted(async () => {
       const payload = (res.data as any).data
       if (payload) {
         form.value = { ...form.value, ...payload }
-        // 更新本地缓存以便离线使用
-        try { uni.setStorageSync(STORAGE_KEY, JSON.stringify(form.value)) } catch (e) { /* ignore */ }
-        return
       }
+    } else {
+      // 后端返回非200的错误信息
+      const msg = (res && res.data && (res.data as any).message) || '服务器返回异常'
+      isError.value = true
+      loadError.value = msg
+      uni.showToast({ title: `加载失败:${msg}`, icon: 'none' })
     }
-  } catch (e) {
-    // 网络或其他异常,回退到本地缓存
-  }
-
-  // 回退:优先读取新键
-  const savedNew: any = uni.getStorageSync(STORAGE_KEY)
-  if (savedNew) {
-    try {
-      const parsed = typeof savedNew === 'string' ? JSON.parse(savedNew) : savedNew
-      form.value = { ...form.value, ...parsed }
-    } catch (e) {
-      // ignore
-    }
-    return
+  } catch (e: any) {
+    // 网络或其他异常:展示错误并允许重试
+    isError.value = true
+    loadError.value = e && e.message ? e.message : String(e)
+    uni.showToast({ title: '加载失败,点击重试', icon: 'none' })
+  } finally {
+    isLoading.value = false
   }
+})
 
-  // 兼容老的 physical 键,如果存在则迁移
-  const savedLegacy: any = uni.getStorageSync(LEGACY_PHYSICAL_KEY)
-  if (savedLegacy) {
-    try {
-      const parsed = typeof savedLegacy === 'string' ? JSON.parse(savedLegacy) : savedLegacy
-      form.value.physical = {
-        heightMin: parsed.heightMin ?? null,
-        heightMax: parsed.heightMax ?? null,
-        weightMin: parsed.weightMin ?? null,
-        weightMax: parsed.weightMax ?? null
-      }
-      // 保存到新键以完成迁移
-      uni.setStorageSync(STORAGE_KEY, JSON.stringify(form.value))
-    } catch (e) {
-      // ignore
+const retryLoad = async () => {
+  isLoading.value = true
+  isError.value = false
+  loadError.value = ''
+  try {
+    const res: any = await getCriticalValuesForm()
+    if ((res.data as any) && (res.data as any).code === 200) {
+      const payload = (res.data as any).data
+      if (payload) form.value = { ...form.value, ...payload }
+      isError.value = false
+      uni.showToast({ title: '加载成功', icon: 'success' })
+    } else {
+      const msg = (res && res.data && (res.data as any).message) || '服务器返回异常'
+      isError.value = true
+      loadError.value = msg
+      uni.showToast({ title: `加载失败:${msg}`, icon: 'none' })
     }
+  } catch (e: any) {
+    isError.value = true
+    loadError.value = e && e.message ? e.message : String(e)
+    uni.showToast({ title: '加载失败,点击重试', icon: 'none' })
+  } finally {
+    isLoading.value = false
   }
-})
+}
 
 const validatePairs = (min: any, max: any, label: string) => {
   const isEmpty = (v: any) => v === null || v === undefined || v === ''
@@ -308,20 +334,13 @@ const save = async () => {
     // 先尝试调用后端接口保存(使用规范化后的 payload)
     const res: any = await saveCriticalValuesForm(payload)
     if ((res.data as any) && (res.data as any).code === 200) {
-      // 更新本地缓存
-      try { uni.setStorageSync(STORAGE_KEY, JSON.stringify(form.value)) } catch (e) { /* ignore */ }
       uni.showToast({ title: '保存成功', icon: 'success' })
       return
     }
     uni.showToast({ title: '保存失败', icon: 'none' })
   } catch (e) {
-    // 网络或其它错误,回退到本地缓存保存
-    try {
-      uni.setStorageSync(STORAGE_KEY, JSON.stringify(form.value))
-      uni.showToast({ title: '已保存到本地(离线)', icon: 'success' })
-    } catch (err) {
-      uni.showToast({ title: '保存失败', icon: 'none' })
-    }
+    // 网络或其它错误
+    uni.showToast({ title: '保存失败', icon: 'none' })
   }
 }
 
@@ -336,7 +355,6 @@ const resetForm = async () => {
         bloodGlucose: { fastingMin: null, fastingMax: null, randomMin: null, randomMax: null },
         heartRate: { min: null, max: null }
       }
-      try { uni.removeStorageSync(STORAGE_KEY) } catch (e) { /* ignore */ }
       uni.showToast({ title: '已重置', icon: 'success' })
       return
     }
@@ -352,11 +370,7 @@ const resetForm = async () => {
     bloodGlucose: { fastingMin: null, fastingMax: null, randomMin: null, randomMax: null },
     heartRate: { min: null, max: null }
   }
-  try {
-    uni.removeStorageSync(STORAGE_KEY)
-  } catch (e) {
-    // ignore
-  }
+  // 本地清理完成
   uni.showToast({ title: '已重置', icon: 'success' })
 }
 </script>
@@ -437,4 +451,45 @@ const resetForm = async () => {
   border-radius: 10rpx;
   font-size: 28rpx;
 }
+
+/* Skeleton styles */
+.skeleton-container {
+  padding: 20rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.skeleton-card {
+  height: 240rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(90deg, #f0f0f0 25%, #e6e6e6 37%, #f0f0f0 63%);
+  background-size: 400% 100%;
+  animation: shimmer 1.4s ease infinite;
+}
+@keyframes shimmer {
+  0% { background-position: 100% 0 }
+  100% { background-position: -100% 0 }
+}
+
+.load-error {
+  padding: 20rpx;
+  margin: 20rpx;
+  background: #fff7f7;
+  border: 1rpx solid #ffd2d2;
+  border-radius: 10rpx;
+  display:flex;
+  align-items:center;
+  gap: 20rpx;
+}
+.error-text {
+  color: #d93025;
+  font-size: 28rpx;
+  flex:1;
+}
+
+/* Save 按钮禁用样式 */
+.btn-primary[disabled], .btn-primary.disabled {
+  background-color: #9ecbff;
+  opacity: 0.7;
+}
 </style>