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