Browse Source

feat(patient): 添加家属管理功能

- 新增"家人管理"页面,支持查看和解绑家属
- 在患者首页添加"我的家属"入口
- 实现家属列表展示及头像加载
- 支持解除与家属的绑定关系
- 添加空状态提示及默认头像处理
- 调整首页功能模块布局和跳转逻辑
mcbaiyun 1 tháng trước cách đây
mục cha
commit
c913621afc
3 tập tin đã thay đổi với 337 bổ sung28 xóa
  1. 6 0
      src/pages.json
  2. 310 0
      src/pages/patient/index/family.vue
  3. 21 28
      src/pages/patient/index/index.vue

+ 6 - 0
src/pages.json

@@ -203,6 +203,12 @@
 			"style": {
 				"navigationBarTitleText": "申请复诊"
 			}
+		},
+		{
+			"path": "pages/patient/index/family",
+			"style": {
+				"navigationBarTitleText": "家人管理"
+			}
 		}
 	],
 	"globalStyle": {

+ 310 - 0
src/pages/patient/index/family.vue

@@ -0,0 +1,310 @@
+<template>
+  <CustomNav title="家属管理" leftType="back" />
+  <view class="page-container">
+    <view class="family-card" v-for="family in families" :key="family.id">
+      <view class="family-header">
+        <view class="avatar-section">
+          <view class="avatar-frame">
+            <image class="avatar-img" :src="familyAvatar(family)" mode="aspectFill" />
+          </view>
+        </view>
+        <view class="family-info">
+          <text class="family-name">{{ family.boundUserNickname }}</text>
+          <text class="family-phone" v-if="family.boundUserPhone">联系电话: {{ family.boundUserPhone }}</text>
+          <text class="family-phone" v-else>联系电话: 未提供</text>
+        </view>
+      </view>
+      
+      <view class="action-buttons">
+        <button class="action-btn danger" @click="unbindFamily(family)">解除绑定</button>
+      </view>
+    </view>
+    
+    <view class="empty-state" v-if="families.length === 0">
+      <image class="empty-icon" src="/static/icons/remixicon/account-circle-line.svg" />
+      <text class="empty-text">暂无绑定的家属</text>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import CustomNav from '@/components/custom-nav.vue'
+import { listUserBindingsByPatient, deleteUserBinding, type UserBindingResponse, type UserBindingPageResponse } from '@/api/userBinding'
+import { downloadAvatar } from '@/api/user'
+import { avatarCache } from '@/utils/avatarCache'
+
+interface FamilyInfo extends UserBindingResponse {
+  avatar?: string
+}
+
+const families = ref<FamilyInfo[]>([])
+const pageData = ref({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0,
+  pages: 0
+})
+
+const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+
+// 获取家属列表
+const fetchFamilies = async () => {
+  uni.showLoading({ title: '加载中...' })
+  
+  try {
+    const token = uni.getStorageSync('token')
+    if (!token) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '未登录',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 获取当前用户ID(患者ID)
+    const userInfo = uni.getStorageSync('user_info')
+    const patientUserId = userInfo?.id
+    
+    if (!patientUserId) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '获取用户信息失败',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 查询患者绑定的家属列表
+    const response = await listUserBindingsByPatient(
+      patientUserId, 
+      'FAMILY', 
+      {
+        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
+      families.value = pageResult.records as FamilyInfo[]
+      pageData.value.total = pageResult.total
+      pageData.value.pages = pageResult.pages
+      
+      // 为每个家属尝试下载头像
+      for (const family of families.value) {
+        try {
+          if (family.patientUserId) {
+            // 检查是否有缓存的头像
+            if (avatarCache.has(family.patientUserId)) {
+              family.avatar = avatarCache.get(family.patientUserId)
+            } else {
+              const dlRes: any = await downloadAvatar(String(family.patientUserId))
+              if (dlRes && dlRes.statusCode === 200 && dlRes.tempFilePath) {
+                family.avatar = dlRes.tempFilePath
+                // 缓存头像路径
+                avatarCache.set(family.patientUserId, dlRes.tempFilePath)
+              }
+            }
+          }
+        } catch (err) {
+          console.warn('下载家属头像失败:', err)
+        }
+      }
+    } else {
+      uni.showToast({
+        title: '获取家属信息失败',
+        icon: 'none'
+      })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('获取家属信息失败:', error)
+    uni.showToast({
+      title: '获取家属信息失败',
+      icon: 'none'
+    })
+  }
+}
+
+const familyAvatar = (family: FamilyInfo) => {
+  if (family.avatar) {
+    return family.avatar
+  }
+  return defaultAvatar
+}
+
+const unbindFamily = (family: FamilyInfo) => {
+  uni.showModal({
+    title: '确认解除绑定',
+    content: `确定要解除与 ${family.boundUserNickname} 的绑定关系吗?`,
+    success: async (res) => {
+      if (res.confirm) {
+        try {
+          uni.showLoading({ title: '正在解除绑定...' })
+          
+          // 调用解除绑定接口
+          const response = await deleteUserBinding({
+            patientUserId: family.patientUserId,
+            boundUserId: family.boundUserId
+          })
+          
+          uni.hideLoading()
+          
+          const resp = response.data as any
+          if (resp && resp.code === 200) {
+            uni.showToast({
+              title: '解除绑定成功',
+              icon: 'success'
+            })
+            
+            // 重新加载家属列表
+            fetchFamilies()
+          } else {
+            uni.showToast({
+              title: resp?.message || '解除绑定失败',
+              icon: 'none'
+            })
+          }
+        } catch (error) {
+          uni.hideLoading()
+          console.error('解除绑定失败:', error)
+          uni.showToast({
+            title: '解除绑定失败',
+            icon: 'none'
+          })
+        }
+      }
+    }
+  })
+}
+
+onLoad(() => {
+  fetchFamilies()
+})
+
+// 如果在微信小程序端且未登录,自动跳转到登录页
+onShow(() => {
+  const token = uni.getStorageSync('token')
+  if (!token) {
+    uni.reLaunch({ url: '/pages/public/login/index' })
+  }
+})
+</script>
+
+<style scoped>
+.page-container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-top: calc(var(--status-bar-height) + 44px);
+  padding-bottom: 40rpx;
+}
+
+.family-card {
+  background-color: #fff;
+  margin: 20rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.family-header {
+  display: flex;
+  padding: 40rpx;
+  border-bottom: 1rpx solid #eee;
+}
+
+.avatar-section {
+  margin-right: 30rpx;
+}
+
+.avatar-frame {
+  width: 120rpx;
+  height: 120rpx;
+  border-radius: 50%;
+  overflow: hidden;
+  border: 1px solid rgba(128, 128, 128, 0.5);
+}
+
+.avatar-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.family-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.family-name {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.family-phone {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.action-buttons {
+  display: flex;
+  padding: 30rpx 40rpx;
+  gap: 20rpx;
+  flex-wrap: wrap;
+}
+
+.action-btn {
+  flex: 1;
+  border-radius: 10rpx;
+  font-size: 28rpx;
+  line-height: 70rpx;
+  min-width: 40%;
+}
+
+.primary {
+  background-color: #3742fa;
+  color: #fff;
+}
+
+.secondary {
+  background-color: #f0f0f0;
+  color: #333;
+}
+
+.danger {
+  background-color: #ff4757;
+  color: #fff;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 100rpx 40rpx;
+}
+
+.empty-icon {
+  width: 120rpx;
+  height: 120rpx;
+  margin-bottom: 30rpx;
+  opacity: 0.5;
+}
+
+.empty-text {
+  font-size: 32rpx;
+  color: #666;
+  margin-bottom: 40rpx;
+}
+</style>

+ 21 - 28
src/pages/patient/index/index.vue

@@ -30,25 +30,26 @@
               <text class="item-desc">一键预约复诊</text>
             </view>
           </view>
-          <view class="function-item blue" @click="onItemClick('用药管理')">
+          <view class="function-item orange" @click="onItemClick('我的家属')">
             <view class="item-content">
               <view class="title-row">
                 <view class="item-line"></view>
-                <text class="item-title">用药管理</text>
+                <text class="item-title">我的家属</text>
               </view>
-              <text class="item-desc">管理您的用药记录</text>
+              <text class="item-desc">添加或解除绑定您的家属</text>
             </view>
           </view>
 
+
         </view>
         <view class="function-row">
-          <view class="function-item orange" @click="onItemClick('提醒管理')">
+          <view class="function-item blue" @click="onItemClick('用药管理')">
             <view class="item-content">
               <view class="title-row">
                 <view class="item-line"></view>
-                <text class="item-title">提醒管理</text>
+                <text class="item-title">用药管理</text>
               </view>
-              <text class="item-desc">管理您的健康提醒</text>
+              <text class="item-desc">管理您的用药方案</text>
             </view>
           </view>
           <view class="function-item purple" @click="onItemClick('健康档案')">
@@ -70,22 +71,14 @@
           <text class="card-subtitle">{{ currentDate }}</text>
         </view>
         <view class="card-content">
-          <view 
-            class="checkin-item" 
-            v-for="(task, index) in checkinTasks" 
-            :key="index"
-            @click="toggleCheckin(task)"
-          >
+          <view class="checkin-item" v-for="(task, index) in checkinTasks" :key="index" @click="toggleCheckin(task)">
             <view class="checkin-info">
               <text class="checkin-time">{{ task.time }}</text>
               <text class="checkin-type">{{ task.type }}</text>
             </view>
             <view class="checkin-status" :class="{ checked: task.checked }">
-              <uni-icons 
-                :type="task.checked ? 'checkbox-filled' : 'circle'" 
-                :color="task.checked ? '#07C160' : '#ddd'" 
-                size="24" 
-              />
+              <uni-icons :type="task.checked ? 'checkbox-filled' : 'circle'" :color="task.checked ? '#07C160' : '#ddd'"
+                size="24" />
             </view>
           </view>
         </view>
@@ -262,7 +255,7 @@ const generateCheckinTasks = () => {
 
     // 按时间排序
     tasks.sort((a, b) => a.time.localeCompare(b.time))
-    
+
     checkinTasks.value = tasks
   } catch (e) {
     console.error('生成打卡任务失败:', e)
@@ -274,12 +267,12 @@ const toggleCheckin = (task: { time: string; type: string }) => {
   try {
     const today = currentDate.value
     const checkinRecords = uni.getStorageSync('checkinRecords') || {}
-    
+
     // 初始化今天的记录
     if (!checkinRecords[today]) {
       checkinRecords[today] = {}
     }
-    
+
     // 生成任务键名
     let taskKey = ''
     if (task.type === '测量血压') {
@@ -291,13 +284,13 @@ const toggleCheckin = (task: { time: string; type: string }) => {
     } else if (task.type === '用药提醒') {
       taskKey = `medication_${task.time}`
     }
-    
+
     // 切换打卡状态
     checkinRecords[today][taskKey] = !checkinRecords[today][taskKey]
-    
+
     // 保存到本地存储
     uni.setStorageSync('checkinRecords', checkinRecords)
-    
+
     // 更新界面显示
     generateCheckinTasks()
   } catch (e) {
@@ -311,11 +304,11 @@ const cleanExpiredCheckinRecords = () => {
     const checkinRecords = uni.getStorageSync('checkinRecords') || {}
     const today = currentDate.value
     const keys = Object.keys(checkinRecords)
-    
+
     // 只保留最近7天的记录
     const sevenDaysAgo = new Date()
     sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
-    
+
     const cleanedRecords: Record<string, any> = {}
     keys.forEach(key => {
       // 简单的日期比较(格式:YYYY-MM-DD)
@@ -323,7 +316,7 @@ const cleanExpiredCheckinRecords = () => {
         cleanedRecords[key] = checkinRecords[key]
       }
     })
-    
+
     uni.setStorageSync('checkinRecords', cleanedRecords)
   } catch (e) {
     console.error('清理过期打卡记录失败:', e)
@@ -366,8 +359,8 @@ function handleScan(res: any) {
 function onItemClick(type: string) {
   if (type === '健康档案') {
     uni.navigateTo({ url: '/pages/patient/profile/infos/patient-filing' })
-  } else if (type === '提醒管理') {
-    uni.navigateTo({ url: '/pages/patient/health/reminder' })
+  } else if (type === '我的家属') {
+    uni.navigateTo({ url: '/pages/patient/index/family' })
   } else if (type === '我的医生') {
     uni.navigateTo({ url: '/pages/patient/index/my-doctor' })
   } else if (type === '用药管理') {