|
@@ -1,7 +1,39 @@
|
|
|
<template>
|
|
<template>
|
|
|
<CustomNav title="我的病人" leftType="back" />
|
|
<CustomNav title="我的病人" leftType="back" />
|
|
|
<view class="page-container">
|
|
<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="patient-header">
|
|
|
<view class="avatar-section">
|
|
<view class="avatar-section">
|
|
|
<view class="avatar-frame">
|
|
<view class="avatar-frame">
|
|
@@ -16,15 +48,11 @@
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<view class="action-buttons">
|
|
<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>
|
|
</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>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
@@ -42,6 +70,10 @@ interface PatientInfo extends UserBindingResponse {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const patients = ref<PatientInfo[]>([])
|
|
const patients = ref<PatientInfo[]>([])
|
|
|
|
|
+// 加载/错误状态
|
|
|
|
|
+const isLoading = ref(false)
|
|
|
|
|
+const isError = ref(false)
|
|
|
|
|
+const loadError = ref('')
|
|
|
const pageData = ref({
|
|
const pageData = ref({
|
|
|
pageNum: 1,
|
|
pageNum: 1,
|
|
|
pageSize: 10,
|
|
pageSize: 10,
|
|
@@ -53,57 +85,46 @@ const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQ
|
|
|
|
|
|
|
|
// 获取病人列表
|
|
// 获取病人列表
|
|
|
const fetchPatients = async () => {
|
|
const fetchPatients = async () => {
|
|
|
- uni.showLoading({ title: '加载中...' })
|
|
|
|
|
-
|
|
|
|
|
|
|
+ isLoading.value = true
|
|
|
|
|
+ isError.value = false
|
|
|
|
|
+ loadError.value = ''
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const token = uni.getStorageSync('token')
|
|
const token = uni.getStorageSync('token')
|
|
|
if (!token) {
|
|
if (!token) {
|
|
|
- uni.hideLoading()
|
|
|
|
|
- uni.showToast({
|
|
|
|
|
- title: '未登录',
|
|
|
|
|
- icon: 'none'
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ isLoading.value = false
|
|
|
|
|
+ uni.showToast({ title: '未登录', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 获取当前用户ID(医生ID)
|
|
|
|
|
|
|
+
|
|
|
const userInfo = uni.getStorageSync('user_info')
|
|
const userInfo = uni.getStorageSync('user_info')
|
|
|
const doctorUserId = userInfo?.id
|
|
const doctorUserId = userInfo?.id
|
|
|
-
|
|
|
|
|
if (!doctorUserId) {
|
|
if (!doctorUserId) {
|
|
|
- uni.hideLoading()
|
|
|
|
|
- uni.showToast({
|
|
|
|
|
- title: '获取用户信息失败',
|
|
|
|
|
- icon: 'none'
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ isLoading.value = false
|
|
|
|
|
+ uni.showToast({ title: '获取用户信息失败', icon: 'none' })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // 查询医生绑定的病人列表(使用新的接口)
|
|
|
|
|
|
|
+
|
|
|
const response = await listUserBindingsByBoundUser(
|
|
const response = await listUserBindingsByBoundUser(
|
|
|
- doctorUserId,
|
|
|
|
|
- 'PATIENT',
|
|
|
|
|
|
|
+ doctorUserId,
|
|
|
|
|
+ 'PATIENT',
|
|
|
{
|
|
{
|
|
|
pageNum: pageData.value.pageNum,
|
|
pageNum: pageData.value.pageNum,
|
|
|
pageSize: pageData.value.pageSize
|
|
pageSize: pageData.value.pageSize
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const resp = response.data as any
|
|
const resp = response.data as any
|
|
|
-
|
|
|
|
|
if (resp && resp.code === 200 && resp.data) {
|
|
if (resp && resp.code === 200 && resp.data) {
|
|
|
const pageResult = resp.data as UserBindingPageResponse
|
|
const pageResult = resp.data as UserBindingPageResponse
|
|
|
patients.value = pageResult.records as PatientInfo[]
|
|
patients.value = pageResult.records as PatientInfo[]
|
|
|
pageData.value.total = pageResult.total
|
|
pageData.value.total = pageResult.total
|
|
|
pageData.value.pages = pageResult.pages
|
|
pageData.value.pages = pageResult.pages
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 为每个病人尝试下载头像
|
|
// 为每个病人尝试下载头像
|
|
|
for (const patient of patients.value) {
|
|
for (const patient of patients.value) {
|
|
|
try {
|
|
try {
|
|
|
if (patient.patientUserId) {
|
|
if (patient.patientUserId) {
|
|
|
- // 检查是否有缓存的头像
|
|
|
|
|
const patientIdStr = String(patient.patientUserId)
|
|
const patientIdStr = String(patient.patientUserId)
|
|
|
if (avatarCache.has(patientIdStr)) {
|
|
if (avatarCache.has(patientIdStr)) {
|
|
|
patient.avatar = avatarCache.get(patientIdStr)
|
|
patient.avatar = avatarCache.get(patientIdStr)
|
|
@@ -111,7 +132,6 @@ const fetchPatients = async () => {
|
|
|
const dlRes: any = await downloadAvatar(String(patient.patientUserId))
|
|
const dlRes: any = await downloadAvatar(String(patient.patientUserId))
|
|
|
if (dlRes && dlRes.statusCode === 200 && dlRes.tempFilePath) {
|
|
if (dlRes && dlRes.statusCode === 200 && dlRes.tempFilePath) {
|
|
|
patient.avatar = dlRes.tempFilePath
|
|
patient.avatar = dlRes.tempFilePath
|
|
|
- // 缓存头像路径
|
|
|
|
|
avatarCache.set(patientIdStr, dlRes.tempFilePath)
|
|
avatarCache.set(patientIdStr, dlRes.tempFilePath)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -120,19 +140,23 @@ const fetchPatients = async () => {
|
|
|
console.warn('下载病人头像失败:', err)
|
|
console.warn('下载病人头像失败:', err)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ isError.value = false
|
|
|
|
|
+ loadError.value = ''
|
|
|
} else {
|
|
} 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) {
|
|
} catch (error) {
|
|
|
- uni.hideLoading()
|
|
|
|
|
|
|
+ const msg = formatError(error) || '获取病人信息失败'
|
|
|
|
|
+ isError.value = true
|
|
|
|
|
+ loadError.value = msg
|
|
|
console.error('获取病人信息失败:', error)
|
|
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
|
|
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) => {
|
|
const viewHealthData = (patient: PatientInfo) => {
|
|
|
// 跳转到公共健康数据查看页面,传递患者ID和绑定类型参数
|
|
// 跳转到公共健康数据查看页面,传递患者ID和绑定类型参数
|
|
|
uni.navigateTo({
|
|
uni.navigateTo({
|
|
@@ -222,6 +263,10 @@ onShow(() => {
|
|
|
uni.reLaunch({ url: '/pages/public/login/index' })
|
|
uni.reLaunch({ url: '/pages/public/login/index' })
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+const retryLoad = async () => {
|
|
|
|
|
+ await fetchPatients()
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
@@ -338,4 +383,24 @@ onShow(() => {
|
|
|
color: #666;
|
|
color: #666;
|
|
|
margin-bottom: 40rpx;
|
|
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>
|
|
</style>
|