概述
medicine.vue(药品管理列表页)中遇到的“加载失败时页面显示为‘暂无数据’且错误信息显示为 [object Object]”的问题,记录了定位、根因、完整修复(含骨架加载、错误提示与重试交互、样式优化)以及相关经验教训。背景(问题现象)
加载失败:[object Object],对用户无帮助。根因分析
v-if="isLoading" -> v-else-if="filteredMedicines.length === 0" -> v-else-if="isError"。当请求失败且列表为空,空列表分支先命中,错误分支被隐藏。String(err) 或错误对象未格式化,导致显示 [object Object]。解决方案(概述)
状态管理
isLoading、isError、loadError。isLoading=true; isError=false; loadError=''。medicines,isError=false; loadError=''。isError=true; loadError=格式化后的错误字符串。模板调整(错误优先)
v-if="isLoading"(骨架)v-else-if="isError"(错误提示 + 重试)v-else-if="!isError && filteredMedicines.length === 0"(空列表)v-else(正常列表)错误格式化
formatError(e) 辅助函数,优先提取常见字段:e.message、e.data.message、e.response.data.message,否则尝试 JSON.stringify(e) 或 String(e),保证展示可读文本而非 [object Object]。重试交互
retryLoad(),内部直接调用 loadMedicineList()。骨架与样式优化
.skeleton-list、.skeleton-line、.skeleton-btn,并添加 shimmer 动画(@keyframes shimmer)。.load-error 容器包裹图标、错误文本和操作,使用轻微红色背景/边框以提示严重性。.retry-btn 采用扁平且紧凑的尺寸(示例:高度 48rpx,min-width 96rpx,icon size 20),保证行高不会被压缩。:disabled 或 .fab.disabled)。代码位置(已修改文件)
src/pages/doctor/manage/medicine.vue — 主实现文件:状态、模板分支、formatError、retryLoad、样式、骨架样式。src/pages/doctor/manage/critical-values.vue — 同样应用了骨架与加载/错误处理(此处也已做类似改动)。关键代码片段
错误优先渲染分支(示例):
<view v-if="isLoading" class="skeleton-list">...</view>
<view v-else-if="isError" class="load-error">...<button @click="retryLoad">重试</button></view>
<view v-else-if="!isError && filteredMedicines.length === 0">暂无药品</view>
<view v-else class="medicine-list">列表</view>
错误格式化函数(示例):
const formatError = (e) => {
if (!e) return ''
if (typeof e === 'string') return e
if (e.message) return e.message
if (e.data && e.data.message) return e.data.message
if (e.response && e.response.data && e.response.data.message) return e.response.data.message
try { const s = JSON.stringify(e); if (s && s !== '{}' && s !== 'null') return s } catch(_){}
return String(e)
}
和 loadMedicineList 中的使用:
try {
const res = await getMedicineList(params)
if (res?.data?.code === 200) { ... }
else { loadError.value = formatError(res && res.data ? res.data : res) }
} catch (err) { loadError.value = formatError(err) }
样式与完整代码清单(便于复制粘贴)
1) 模板片段(错误区 + 骨架 + 空态顺序)
<!-- 骨架 -->
<view v-if="isLoading" class="skeleton-list">
<!-- repeat skeleton-card -->
</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 && filteredMedicines.length === 0" class="empty-state">
<text class="empty-text">暂无药品</text>
</view>
<!-- 正常列表 -->
<view v-else class="medicine-list"> ... </view>
2) 关键样式(CSS)
/* 骨架 shimmer */
.skeleton-list { padding: 18rpx 18rpx; }
.skeleton-card { background: #fff; display: flex; gap: 18rpx; padding: 16rpx; border-radius: 12rpx; margin-bottom: 16rpx; align-items: center; justify-content: space-between; }
.skeleton-meta { flex: 1; display: flex; flex-direction: column; gap: 10rpx; }
.skeleton-line { background: linear-gradient(90deg, #eee 25%, #f6f6f6 50%, #eee 75%); border-radius: 6rpx; background-size: 200% 100%; animation: shimmer 1.2s linear infinite; }
.skeleton-line.title { width: 40%; height: 28rpx; }
.skeleton-line.summary { width: 60%; height: 18rpx; }
.skeleton-actions { display:flex; gap: 12rpx; align-items: center; }
.skeleton-btn { width: 100rpx; height: 40rpx; background: linear-gradient(90deg, #eee 25%, #f6f6f6 50%, #eee 75%); border-radius: 20rpx; background-size: 200% 100%; animation: shimmer 1.2s linear infinite; }
@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; }
/* disabled */
.fab.disabled { opacity: 0.6; pointer-events: none; }
button[disabled] { opacity: 0.6; pointer-events: none; }
3) 错误格式化函数(完整)
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 (_) {}
return String(e)
}
说明:以上样式与函数可直接拷贝到页面中,或将 formatError 提取为共享 util 并在多个列表页使用。
与已有文档的关联
docs/uni-app 页面加载提示对比分析(showLoading vs 骨架动画).md(见 docs),讨论了何时使用全局 uni.showLoading() 与何时使用骨架屏。本次修复在实践上采用了骨架屏(适用于列表类、加载时间较长或希望提升感知速度的场景),与该文档结论一致。经验教训与建议
formatError)抽成工具函数复用,减少重复实现并统一错误文案策略。后续改进(可选)
formatError 提取到 src/utils/error.ts 并在其他页面复用。结束语
medicine.vue(和 critical-values.vue 的类似改动),建议将 formatError 提取为共享 util 并在更多列表页统一使用。