2 Revize a02107b254 ... 55369b92d7

Autor SHA1 Zpráva Datum
  mcbaiyun 55369b92d7 feat(news): 添加新闻标题展示和优化卡片样式 před 2 týdny
  mcbaiyun ba714c4290 feat(news): 添加加载提示和优化新闻列表展示逻辑 před 2 týdny

+ 193 - 0
docs/uni-app 页面加载提示对比分析.md

@@ -0,0 +1,193 @@
+# uni-app 页面加载提示对比分析
+
+## 概述
+本文档对比分析了 uni-app 项目中两种常见的页面加载提示实现方式:全局加载提示和骨架屏加载动画。通过对比 `my-patients.vue` 和 `message-detail.vue` 两个页面的具体实现,总结各自的特点、适用场景和优缺点。
+
+## 实现方式对比
+
+### 1. 全局加载提示(my-patients.vue)
+
+#### 实现原理
+- 使用 uni-app 提供的 `uni.showLoading()` 和 `uni.hideLoading()` API
+- 在数据请求开始时显示全局模态框,请求完成后隐藏
+
+#### 代码示例
+```typescript
+const fetchPatients = async () => {
+  uni.showLoading({ title: '加载中...' })
+  
+  try {
+    // 数据请求逻辑
+    const response = await listUserBindingsByBoundUser(...)
+    // 处理响应
+  } catch (error) {
+    // 错误处理
+  } finally {
+    uni.hideLoading()
+  }
+}
+```
+
+#### 特点
+- **优点**:
+  - 实现简单,只需几行代码
+  - 用户体验一致,uni-app 原生支持
+  - 阻塞用户交互,防止重复操作
+- **缺点**:
+  - 全局模态框会遮挡整个页面
+  - 无法显示内容预览或进度信息
+  - 在小程序中可能有样式限制
+
+#### 适用场景
+- 数据加载时间较短(< 3秒)
+- 页面内容不需要预览
+- 希望阻止用户在加载期间进行其他操作
+
+### 2. 骨架屏加载动画(message-detail.vue)
+
+#### 实现原理
+- 使用 Vue 的条件渲染 `v-if="loading"`
+- 在模板中创建模拟真实内容的占位符结构
+- 通过 CSS 动画实现闪烁效果
+
+#### 代码示例
+```vue
+<template>
+  <!-- 骨架屏 -->
+  <view v-if="loading" class="skeleton-container">
+    <view class="skeleton-card" v-for="i in 3" :key="i">
+      <view class="skeleton-header">
+        <view class="skeleton-line skeleton-title"></view>
+        <view class="skeleton-line skeleton-small"></view>
+      </view>
+      <view class="skeleton-content">
+        <view class="skeleton-line skeleton-text"></view>
+        <view class="skeleton-line skeleton-text"></view>
+        <view class="skeleton-line skeleton-text skeleton-short"></view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 实际内容 -->
+  <view v-else class="message-list">
+    <!-- 真实消息列表 -->
+  </view>
+</template>
+
+<script setup>
+const loading = ref(true)
+
+const fetchMessages = async () => {
+  loading.value = true
+  try {
+    // 数据请求逻辑
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+```
+
+#### CSS 样式示例
+```css
+.skeleton-line {
+  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+  background-size: 200% 100%;
+  animation: skeleton-loading 1.5s infinite;
+}
+
+@keyframes skeleton-loading {
+  0% { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
+```
+
+#### 特点
+- **优点**:
+  - 视觉体验更好,能预览页面布局
+  - 不阻塞用户交互,可以浏览其他内容
+  - 自定义程度高,可设计独特的加载动画
+  - 提升用户感知的加载速度
+- **缺点**:
+  - 实现复杂度较高,需要设计骨架结构
+  - 需要维护两套布局(骨架和真实内容)
+  - CSS 动画可能影响性能
+
+#### 适用场景
+- 数据加载时间较长(> 2秒)
+- 页面布局复杂,需要内容预览
+- 希望保持用户交互流畅性
+- 追求更好的视觉体验
+
+## 对比总结
+
+| 特性 | 全局加载提示 | 骨架屏加载动画 |
+|------|-------------|---------------|
+| 实现复杂度 | 低 | 中等 |
+| 用户体验 | 一般 | 优秀 |
+| 视觉效果 | 简单 | 丰富 |
+| 交互阻塞 | 是 | 否 |
+| 自定义程度 | 低 | 高 |
+| 性能影响 | 低 | 中等 |
+| 适用数据量 | 小数据 | 大数据/复杂页面 |
+
+## 选择建议
+
+1. **简单页面/快速加载**:推荐使用全局加载提示
+2. **复杂页面/慢速加载**:推荐使用骨架屏
+3. **混合使用**:可以结合两者,先显示骨架屏,再叠加全局提示
+
+## 最佳实践
+
+1. 根据页面复杂度选择合适的加载方式
+2. 设置合理的超时时间,避免无限加载
+3. 在错误情况下也要隐藏加载状态
+4. 考虑不同平台的兼容性
+5. 测试加载状态下的用户交互
+
+## 常见问题与解决方案
+
+### 骨架屏与实际内容同时显示问题
+
+#### 问题现象
+在使用骨架屏加载动画时,从其他页面(如编辑页面)返回时,骨架屏下方残留有之前的实际列表内容,导致加载状态与旧数据同时显示,影响用户体验。
+
+#### 问题原因
+模板中骨架屏使用 `v-if="loading"` 条件显示,但实际内容列表没有相应的条件隐藏。在数据重新加载期间,`loading` 为 true 时骨架屏显示,但旧的列表数据仍然可见,因为没有 `v-if="!loading"` 限制。
+
+#### 解决办法
+将实际内容区域包装在 `v-if="!loading"` 条件中,确保加载时只显示骨架屏,加载完成后显示实际内容。
+
+**错误示例:**
+```vue
+<view v-if="loading" class="skeleton-list">
+  <!-- 骨架屏 -->
+</view>
+<view class="news-card" v-for="item in list" :key="item.id">
+  <!-- 实际列表,无条件隐藏 -->
+</view>
+```
+
+**正确示例:**
+```vue
+<view v-if="loading" class="skeleton-list">
+  <!-- 骨架屏 -->
+</view>
+<view v-if="!loading">
+  <view class="news-card" v-for="item in list" :key="item.id">
+    <!-- 实际列表 -->
+  </view>
+  <!-- 其他内容如空状态、加载更多等 -->
+</view>
+```
+
+#### 经验教训
+- 骨架屏和实际内容应使用互斥的条件渲染,避免同时显示
+- 在页面重新加载(如 `onShow` 触发)时,确保旧数据被正确隐藏
+- 参考 `message-detail.vue` 的实现模式,确保加载状态管理的完整性
+
+## 相关文件
+- `src/pages/doctor/index/my-patients.vue` - 全局加载提示示例
+- `src/pages/public/message-detail.vue` - 骨架屏加载动画示例
+- `src/pages/doctor/manage/news.vue` - 骨架屏逻辑修复示例</content>
+<parameter name="filePath">d:\慢病APP\Dev\uniapp-ts\docs\uni-app 页面加载提示对比分析.md

+ 3 - 0
src/pages/doctor/manage/news-edit.vue

@@ -231,6 +231,7 @@ onMounted(async () => {
   const id = query.value.id || ''
   if (id) {
     console.log('Detected id:', id)
+    uni.showLoading({ title: '加载中...' })
     // 编辑:使用接口拉取真实数据
     try {
       const resp: any = await getNewsById(id)
@@ -252,6 +253,8 @@ onMounted(async () => {
     } catch (err) {
       console.error('get news failed', err)
       uni.showToast({ title: '获取资讯失败', icon: 'none' })
+    } finally {
+      uni.hideLoading()
     }
   }
   // 初始调整编辑框高度以适配可能存在的内容

+ 43 - 30
src/pages/doctor/manage/news.vue

@@ -21,38 +21,44 @@
         </view>
       </view>
     </view>
-    <view class="news-card" v-for="item in filteredList" :key="item.id">
-      <view class="news-content">
-        <view v-if="item.coverImage" class="news-image-container">
-          <image class="news-image" :src="(isProtectedUrl(resolveImageUrl(item.coverImage)) ? (downloadedCovers[resolveImageUrl(item.coverImage)] || defaultCover) : (resolveImageUrl(item.coverImage) || defaultCover))" mode="aspectFill" @load="onCoverImageLoad" @error="onCoverImageError(item.coverImage, $event)" />
-        </view>
-        <view v-else class="news-placeholder">
-              <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
-            </view>
-        <view class="meta">
+    
+    <view v-if="!isLoading">
+      <view class="news-card" v-for="item in filteredList" :key="item.id">
+        <view class="news-header">
           <text class="title">{{ item.title }}</text>
-          <text class="summary">{{ item.summary }}</text>
-          <text class="author">作者: {{ item.authorName }}</text>
-          <text class="time">发布时间: {{ item.publishTime }}</text>
         </view>
-      </view>
+        <view class="news-content">
+          <view v-if="item.coverImage" class="news-image-container">
+            <image class="news-image" :src="(isProtectedUrl(resolveImageUrl(item.coverImage)) ? (downloadedCovers[resolveImageUrl(item.coverImage)] || defaultCover) : (resolveImageUrl(item.coverImage) || defaultCover))" mode="aspectFill" @load="onCoverImageLoad" @error="onCoverImageError(item.coverImage, $event)" />
+          </view>
+          <view v-else class="news-placeholder">
+                <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
+              </view>
+          <view class="meta">
+            <text class="summary">{{ item.summary }}</text>
+            <text class="author">作者: {{ item.authorName }}</text>
+            <text class="time">发布时间: {{ item.publishTime }}</text>
+          </view>
+        </view>
 
-      <view class="action-buttons">
-        <button class="action-btn primary" @click="viewNews(item)">查看</button>
-        <button class="action-btn secondary" @click="editNews(item)">编辑</button>
-        <button class="action-btn danger" @click="removeNews(item)">删除</button>
+        <view class="action-buttons">
+          <button class="action-btn primary" @click="viewNews(item)">查看</button>
+          <button class="action-btn secondary" @click="editNews(item)">编辑</button>
+          <button class="action-btn danger" @click="removeNews(item)">删除</button>
+        </view>
       </view>
-    </view>
 
-    <view class="empty-state" v-if="!isLoading && filteredList.length === 0">
-      <view class="news-placeholder">
-          <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
-        </view>
-      <text class="empty-text">暂无资讯</text>
-      <button class="create-cta" @click="onCreateNews">现在创建</button>
-    </view>
-    <view class="loadmore" v-if="hasMore">
-      <button class="load-btn" @click="loadMore">加载更多</button>
+      <view class="empty-state" v-if="filteredList.length === 0">
+        <view class="news-placeholder">
+            <image class="placeholder-icon" src="/static/icons/remixicon/image-line.svg" />
+          </view>
+        <text class="empty-text">暂无资讯</text>
+        <button class="create-cta" @click="onCreateNews">现在创建</button>
+      </view>
+      
+      <view class="loadmore" v-if="hasMore">
+        <button class="load-btn" @click="loadMore">加载更多</button>
+      </view>
     </view>
   </view>
   <!-- 悬浮新建按钮(样式参考 physical.vue) -->
@@ -326,18 +332,24 @@ onPullDownRefresh(() => {
   margin: 18rpx 20rpx;
   border-radius: 12rpx;
   overflow: hidden;
-  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
   transition: transform 0.12s ease, box-shadow 0.12s ease;
 }
 .news-card:hover {
   transform: translateY(-4rpx);
-  box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.12);
+  box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.2);
+}
+.news-header {
+  padding: 20rpx;
+  background-color: #f8f9fa;
+  border-bottom: 1rpx solid #e6e6e6;
 }
 .news-content {
   display: flex;
   padding: 20rpx;
   gap: 20rpx;
   align-items: center;
+  background-color: #fff;
 }
 .news-image-container {
   width: 220rpx;
@@ -404,9 +416,10 @@ onPullDownRefresh(() => {
   display: flex;
   gap: 12rpx;
   padding: 12rpx 18rpx;
-  border-top: 1rpx solid #f5f5f5;
+  border-top: 1rpx solid #e6e6e6;
   justify-content: flex-end;
   flex-wrap: wrap;
+  background-color: #f8f9fa;
 }
 .action-btn {
   padding: 10rpx 22rpx;