Kaynağa Gözat

feat(doctor): 重构我的病人页面并新增病人列表接口

- 新增 listUserBindingsByBoundUser 接口用于查询医生绑定的病人
- 重构 my-patients.vue 页面 UI,使用卡片式布局展示病人信息
- 实现病人头像下载与显示功能
- 添加病人相关操作按钮(健康数据、健康动态、发送提醒、邀请复诊)
- 优化页面空状态显示
- 移除旧版菜单项点击逻辑
- 更新导航栏返回类型为 back
- 强制转换 Snowflake ID 为字符串避免精度丢失
mcbaiyun 1 ay önce
ebeveyn
işleme
8fbc36d67d
2 değiştirilmiş dosya ile 269 ekleme ve 66 silme
  1. 30 0
      src/api/userBinding.ts
  2. 239 66
      src/pages/doctor/index/my-patients.vue

+ 30 - 0
src/api/userBinding.ts

@@ -63,6 +63,36 @@ export async function listUserBindingsByPatient(
   return res
 }
 
+export async function listUserBindingsByBoundUser(
+  boundUserId: string | number,
+  bindingType: string,
+  query: BaseQueryRequest
+) {
+  // token will be injected by request wrapper
+  const qs = `?boundUserId=${encodeURIComponent(String(boundUserId))}&bindingType=${encodeURIComponent(bindingType)}`
+  const res: any = await request({
+    url: 'https://wx.baiyun.work/user-binding/list-by-bound-user' + qs,
+    method: 'POST',
+    data: query,
+    header: { 'content-type': 'application/json' }
+  })
+  // 强制把 Snowflake ID 字段转换为字符串,避免后续使用 Number 导致精度丢失
+  try {
+    const parsed = res?.data as any
+    if (parsed && parsed.code === 200 && parsed.data && Array.isArray(parsed.data.records)) {
+      parsed.data.records = parsed.data.records.map((r: any) => ({
+        ...r,
+        patientUserId: String(r.patientUserId),
+        boundUserId: String(r.boundUserId)
+      }))
+      res.data = parsed
+    }
+  } catch (e) {
+    // ignore
+  }
+  return res
+}
+
 // 创建用户绑定关系 POST /user-binding/create
 export async function createUserBinding(payload: any) {
   const res: any = await request({

+ 239 - 66
src/pages/doctor/index/my-patients.vue

@@ -1,38 +1,171 @@
 <template>
-  <CustomNav title="我的病人" leftType="home" />
-  <view class="content">
-    <view class="menu-card">
-      <view class="menu-list">
-        <view class="menu-item" @click="onItemClick('我的档案')">
-          <image src="/static/icons/remixicon/profile-line.svg" class="menu-icon" mode="widthFix" />
-          <text class="menu-text">我的档案</text>
-          <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
+  <CustomNav title="我的病人" leftType="back" />
+  <view class="page-container">
+    <view class="patient-card" v-for="patient in patients" :key="patient.id">
+      <view class="patient-header">
+        <view class="avatar-section">
+          <view class="avatar-frame">
+            <image class="avatar-img" :src="patientAvatar(patient)" mode="aspectFill" />
+          </view>
         </view>
-        <view class="menu-item" @click="onItemClick('健康数据')">
-          <image src="/static/icons/remixicon/heart-pulse-line.svg" class="menu-icon" mode="widthFix" />
-          <text class="menu-text">健康数据</text>
-          <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
-        </view>
-        <view class="menu-item" @click="onItemClick('用药记录')">
-          <image src="/static/icons/remixicon/capsule-fill.svg" class="menu-icon" mode="widthFix" />
-          <text class="menu-text">用药记录</text>
-          <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
-        </view>
-        <view class="menu-item" @click="onItemClick('就诊记录')">
-          <image src="/static/icons/remixicon/calendar-check-line.svg" class="menu-icon" mode="widthFix" />
-          <text class="menu-text">就诊记录</text>
-          <uni-icons class="menu-arrow" type="arrowright" size="20" color="#c0c0c0" />
+        <view class="patient-info">
+          <text class="patient-name">{{ patient.boundUserNickname }}</text>
+          <text class="patient-phone" v-if="patient.boundUserPhone">联系电话: {{ patient.boundUserPhone }}</text>
+          <text class="patient-phone" v-else>联系电话: 未提供</text>
         </view>
       </view>
+      
+      <view class="action-buttons">
+        <button class="action-btn primary" @click="viewHealthData(patient)">健康数据</button>
+        <button class="action-btn secondary" @click="viewHealthNews(patient)">健康动态</button>
+        <button class="action-btn secondary" @click="sendReminder(patient)">发送提醒</button>
+        <button class="action-btn secondary" @click="inviteRevisit(patient)">邀请复诊</button>
+      </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>
-  <TabBar />
 </template>
 
 <script setup lang="ts">
-import { onShow } from '@dcloudio/uni-app'
+import { ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
 import CustomNav from '@/components/custom-nav.vue'
-import TabBar from '@/components/tab-bar.vue'
+import { listUserBindingsByBoundUser, type UserBindingResponse, type UserBindingPageResponse } from '@/api/userBinding'
+import { downloadAvatar } from '@/api/user'
+
+interface PatientInfo extends UserBindingResponse {
+  avatar?: string
+}
+
+const patients = ref<PatientInfo[]>([])
+const pageData = ref({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0,
+  pages: 0
+})
+
+const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+
+// 获取病人列表
+const fetchPatients = 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 doctorUserId = userInfo?.id
+    
+    if (!doctorUserId) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '获取用户信息失败',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 查询医生绑定的病人列表(使用新的接口)
+    const response = await listUserBindingsByBoundUser(
+      doctorUserId, 
+      'PATIENT', 
+      {
+        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
+      patients.value = pageResult.records as PatientInfo[]
+      pageData.value.total = pageResult.total
+      pageData.value.pages = pageResult.pages
+      
+      // 为每个病人尝试下载头像
+      for (const patient of patients.value) {
+        try {
+          if (patient.patientUserId) {
+            const dlRes: any = await downloadAvatar(String(patient.patientUserId))
+            if (dlRes && dlRes.statusCode === 200 && dlRes.tempFilePath) {
+              patient.avatar = 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 patientAvatar = (patient: PatientInfo) => {
+  if (patient.avatar) {
+    return patient.avatar
+  }
+  return defaultAvatar
+}
+
+const viewHealthData = (patient: PatientInfo) => {
+  uni.showToast({
+    title: '健康数据功能开发中',
+    icon: 'none'
+  })
+}
+
+const viewHealthNews = (patient: PatientInfo) => {
+  uni.showToast({
+    title: '健康动态功能开发中',
+    icon: 'none'
+  })
+}
+
+const sendReminder = (patient: PatientInfo) => {
+  uni.showToast({
+    title: '发送提醒功能开发中',
+    icon: 'none'
+  })
+}
+
+const inviteRevisit = (patient: PatientInfo) => {
+  uni.showToast({
+    title: '邀请复诊功能开发中',
+    icon: 'none'
+  })
+}
+
+onLoad(() => {
+  fetchPatients()
+})
 
 // 如果在微信小程序端且未登录,自动跳转到登录页
 onShow(() => {
@@ -41,70 +174,110 @@ onShow(() => {
     uni.reLaunch({ url: '/pages/public/login/index' })
   }
 })
-
-function onItemClick(type: string) {
-  switch (type) {
-    case '我的档案':
-      uni.navigateTo({ url: '/pages/patient/profile/infos/base-info' });
-      break;
-    case '健康数据':
-      uni.navigateTo({ url: '/pages/patient/health/index' });
-      break;
-    default:
-      uni.showToast({ title: `${type} 功能正在开发中`, icon: 'none' });
-  }
-}
 </script>
 
 <style scoped>
-.content {
-  padding-top: calc(var(--status-bar-height) + 44px);
+.page-container {
   min-height: 100vh;
   background-color: #f5f5f5;
-  box-sizing: border-box;
-}
-
-.menu-card {
-  padding: 50rpx 0rpx;
+  padding-top: calc(var(--status-bar-height) + 44px);
+  padding-bottom: 40rpx;
 }
 
-.menu-list {
+.patient-card {
   background-color: #fff;
-  border-radius: 12rpx;
+  margin: 20rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
   overflow: hidden;
 }
 
-.menu-item {
+.patient-header {
   display: flex;
-  justify-content: flex-start;
-  align-items: center;
-  padding: 30rpx 40rpx;
+  padding: 40rpx;
   border-bottom: 1rpx solid #eee;
 }
 
-.menu-item:last-child {
-  border-bottom: none;
+.avatar-section {
+  margin-right: 30rpx;
 }
 
-.menu-text {
-  font-size: 32rpx;
-  color: #000000;
+.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;
+}
+
+.patient-info {
   flex: 1;
-  letter-spacing: 1rpx;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.patient-name {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.patient-phone {
+  font-size: 28rpx;
+  color: #666;
 }
 
-.menu-arrow {
-  display: inline-flex;
+.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;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
   align-items: center;
   justify-content: center;
-  width: 44rpx;
-  height: 44rpx;
+  padding: 100rpx 40rpx;
 }
 
-.menu-icon {
-  width: 40rpx;
-  height: 40rpx;
-  margin-right: 30rpx;
-  display: inline-block;
+.empty-icon {
+  width: 120rpx;
+  height: 120rpx;
+  margin-bottom: 30rpx;
+  opacity: 0.5;
+}
+
+.empty-text {
+  font-size: 32rpx;
+  color: #666;
+  margin-bottom: 40rpx;
 }
 </style>