Ver Fonte

feat(my-patients.vue): 添加骨架加载视图和错误提示功能,优化病人列表加载体验

mcbaiyun há 2 semanas atrás
pai
commit
5e4619fec2
1 ficheiros alterados com 108 adições e 43 exclusões
  1. 108 43
      src/pages/doctor/index/my-patients.vue

+ 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>