Explorar o código

feat(patient): 新增我的医生页面及医生信息展示功能

- 新增医生信息接口类型定义和请求方法
- 新增用户绑定关系查询接口
- 新增"我的医生"页面,展示医生基本信息
- 在患者首页添加"我的医生"入口
- 实现医生头像、科室、电话等信息展示
- 添加预约复诊和联系医生按钮(待实现)
- 添加未绑定医生时的空状态提示
mcbaiyun hai 1 mes
pai
achega
c9ed9fe006

+ 39 - 0
src/api/doctor.ts

@@ -0,0 +1,39 @@
+export interface DoctorInfo {
+  id: string
+  name: string
+  title?: string
+  hospital?: string
+  department?: string
+  phone?: string
+  specialty?: string
+  introduction?: string
+  avatar?: string
+}
+
+// 获取医生详细信息
+export async function getDoctorInfo(doctorId: number) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: `https://wx.baiyun.work/doctor/${doctorId}`,
+    method: 'GET',
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}
+
+// 获取当前登录医生的信息
+export async function getCurrentDoctorInfo() {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/doctor/current',
+    method: 'GET',
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}

+ 13 - 0
src/api/user.ts

@@ -0,0 +1,13 @@
+// 获取用户详细信息
+export async function getUserInfo(userId: number) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: `https://wx.baiyun.work/user/${userId}`,
+    method: 'GET',
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}

+ 53 - 0
src/api/userBinding.ts

@@ -0,0 +1,53 @@
+export interface BaseQueryRequest {
+  pageNum?: number
+  pageSize?: number
+  startTime?: string
+  endTime?: string
+}
+
+export interface UserBindingResponse {
+  id: string
+  patientUserId: number
+  boundUserId: number
+  bindingType: string
+  status: number
+  createTime: string
+  boundUserNickname?: string
+  boundUserPhone?: string
+}
+
+export interface UserBindingPageResponse {
+  records: UserBindingResponse[]
+  total: number
+  size: number
+  current: number
+  orders: Array<{ column: string; asc: boolean }>
+  optimizeCountSql: boolean
+  searchCount: boolean
+  optimizeJoinOfCountSql: boolean
+  maxLimit: number
+  countId: string
+  pages: number
+}
+
+export async function listUserBindingsByPatient(
+  patientUserId: number,
+  bindingType: string,
+  query: BaseQueryRequest
+) {
+  const token = uni.getStorageSync('token')
+  const res: any = await uni.request({
+    url: 'https://wx.baiyun.work/user-binding/list-by-patient',
+    method: 'POST',
+    data: query,
+    params: {
+      patientUserId,
+      bindingType
+    },
+    header: {
+      'content-type': 'application/json',
+      Authorization: `Bearer ${token}`
+    }
+  })
+  return res
+}

+ 6 - 0
src/pages.json

@@ -78,6 +78,12 @@
 				"navigationBarTitleText": "患者建档信息"
 			}
 		},
+		{
+			"path": "pages/patient/profile/infos/my-doctor",
+			"style": {
+				"navigationBarTitleText": "我的医生"
+			}
+		},
 		{
 			"path": "pages/doctor/index/index",
 			"style": {

+ 4 - 4
src/pages/patient/index/index.vue

@@ -229,11 +229,11 @@ function handleScan(res: any) {
 function onItemClick(type: string) {
   if (type === '个人中心') {
     uni.switchTab({ url: '/pages/patient/profile/index' })
-  }
-  if (type === '提醒管理') {
+  } else if (type === '提醒管理') {
     uni.navigateTo({ url: '/pages/patient/health/reminder' })
-  }
-  else {
+  } else if (type === '我的医生') {
+    uni.navigateTo({ url: '/pages/patient/profile/infos/my-doctor' })
+  } else {
     uni.showToast({ title: '功能正在开发中', icon: 'none' })
   }
 }

+ 378 - 0
src/pages/patient/profile/infos/my-doctor.vue

@@ -0,0 +1,378 @@
+<template>
+  <CustomNav title="我的医生" leftType="back" />
+  <view class="page-container">
+    <view class="doctor-card" v-if="doctorInfo">
+      <view class="doctor-header">
+        <view class="avatar-section">
+          <view class="avatar-frame">
+            <image class="avatar-img" :src="doctorAvatar" mode="aspectFill" />
+          </view>
+        </view>
+        <view class="doctor-info">
+          <text class="doctor-name">{{ doctorInfo.name }}</text>
+          <text class="doctor-title" v-if="doctorInfo.title">{{ doctorInfo.title }}</text>
+          <text class="doctor-hospital" v-if="doctorInfo.hospital">{{ doctorInfo.hospital }}</text>
+        </view>
+      </view>
+      
+      <view class="doctor-details">
+        <view class="detail-item">
+          <text class="label">科室:</text>
+          <text class="value">{{ doctorInfo.department || '未填写' }}</text>
+        </view>
+        <view class="detail-item">
+          <text class="label">联系电话:</text>
+          <text class="value">{{ doctorInfo.phone || '未填写' }}</text>
+        </view>
+        <view class="detail-item">
+          <text class="label">擅长领域:</text>
+          <text class="value">{{ doctorInfo.specialty || '未填写' }}</text>
+        </view>
+        <view class="detail-item">
+          <text class="label">简介:</text>
+          <text class="value intro">{{ doctorInfo.introduction || '暂无介绍' }}</text>
+        </view>
+      </view>
+      
+      <view class="action-buttons">
+        <button class="action-btn primary" @click="makeAppointment">预约复诊</button>
+        <button class="action-btn secondary" @click="contactDoctor">联系医生</button>
+      </view>
+    </view>
+    
+    <view class="empty-state" v-else>
+      <image class="empty-icon" src="/static/icons/remixicon/account-circle-line.svg" />
+      <text class="empty-text">暂无绑定的医生</text>
+      <button class="bind-btn" @click="bindDoctor">绑定医生</button>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import CustomNav from '@/components/custom-nav.vue'
+import { listUserBindingsByPatient, type UserBindingResponse, type UserBindingPageResponse } from '@/api/userBinding'
+import { getUserInfo } from '@/api/user'
+
+// 为避免与导入的类型冲突,重命名本地接口
+interface LocalDoctorInfo {
+  id: string
+  name: string
+  title?: string
+  hospital?: string
+  department?: string
+  phone?: string
+  specialty?: string
+  introduction?: string
+  avatar?: string
+}
+
+const doctorInfo = ref<LocalDoctorInfo | null>(null)
+const userBindings = ref<UserBindingResponse[]>([])
+const pageData = ref({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0,
+  pages: 0
+})
+
+const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
+
+const doctorAvatar = computed(() => {
+  if (doctorInfo.value?.avatar) {
+    return doctorInfo.value.avatar
+  }
+  return defaultAvatar
+})
+
+// 获取医生信息
+const fetchDoctorInfo = async () => {
+  uni.showLoading({ title: '加载中...' })
+  
+  try {
+    const token = uni.getStorageSync('token')
+    if (!token) {
+      uni.hideLoading()
+      uni.showToast({
+        title: '未登录',
+        icon: 'none'
+      })
+      return
+    }
+    
+    // 获取当前用户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, 
+      'DOCTOR', 
+      {
+        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
+      userBindings.value = pageResult.records
+      pageData.value.total = pageResult.total
+      pageData.value.pages = pageResult.pages
+      
+      // 如果有绑定的医生,获取第一个医生的详细信息
+      // 这里需要另一个接口获取医生详细信息,暂时使用模拟数据
+      if (pageResult.records && pageResult.records.length > 0) {
+        const boundDoctor = pageResult.records[0]
+        
+        // 通过用户接口获取医生详细信息
+        try {
+          const userInfoResponse = await getUserInfo(boundDoctor.boundUserId)
+          const userInfo = userInfoResponse.data?.data
+          
+          if (userInfo) {
+            doctorInfo.value = {
+              id: boundDoctor.id,
+              name: userInfo.nickname || boundDoctor.boundUserNickname || '未知医生',
+              title: userInfo.title || '医生',
+              hospital: userInfo.hospital || '未知医院',
+              department: userInfo.department || '未知科室',
+              phone: boundDoctor.boundUserPhone || userInfo.phone || '未提供',
+              specialty: userInfo.specialty || '未知',
+              introduction: userInfo.introduction || '暂无介绍',
+              avatar: userInfo.avatar
+            }
+          } else {
+            // 如果获取详细信息失败,使用绑定接口返回的基础信息
+            doctorInfo.value = {
+              id: boundDoctor.id,
+              name: boundDoctor.boundUserNickname || '未知医生',
+              title: '医生',
+              hospital: '未知医院',
+              department: '未知科室',
+              phone: boundDoctor.boundUserPhone || '未提供',
+              specialty: '未知',
+              introduction: '暂无介绍'
+            }
+          }
+        } catch (error) {
+          console.error('获取医生详细信息失败:', error)
+          // 使用绑定接口返回的基础信息
+          doctorInfo.value = {
+            id: boundDoctor.id,
+            name: boundDoctor.boundUserNickname || '未知医生',
+            title: '医生',
+            hospital: '未知医院',
+            department: '未知科室',
+            phone: boundDoctor.boundUserPhone || '未提供',
+            specialty: '未知',
+            introduction: '暂无介绍'
+          }
+        }
+      } else {
+        doctorInfo.value = null
+      }
+    } else {
+      uni.showToast({
+        title: '获取医生信息失败',
+        icon: 'none'
+      })
+    }
+  } catch (error) {
+    uni.hideLoading()
+    console.error('获取医生信息失败:', error)
+    uni.showToast({
+      title: '获取医生信息失败',
+      icon: 'none'
+    })
+  }
+}
+
+const makeAppointment = () => {
+  uni.showToast({
+    title: '预约功能开发中',
+    icon: 'none'
+  })
+}
+
+const contactDoctor = () => {
+  uni.showToast({
+    title: '联系医生功能开发中',
+    icon: 'none'
+  })
+}
+
+const bindDoctor = () => {
+  uni.showToast({
+    title: '绑定医生功能开发中',
+    icon: 'none'
+  })
+}
+
+onLoad(() => {
+  fetchDoctorInfo()
+})
+</script>
+
+<style scoped>
+.page-container {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  padding-top: calc(var(--status-bar-height) + 44px);
+  padding-bottom: 40rpx;
+}
+
+.doctor-card {
+  background-color: #fff;
+  margin: 20rpx;
+  border-radius: 20rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+}
+
+.doctor-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;
+}
+
+.doctor-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.doctor-name {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.doctor-title {
+  font-size: 28rpx;
+  color: #666;
+  margin-bottom: 10rpx;
+}
+
+.doctor-hospital {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.doctor-details {
+  padding: 30rpx 40rpx;
+}
+
+.detail-item {
+  display: flex;
+  margin-bottom: 20rpx;
+}
+
+.detail-item:last-child {
+  margin-bottom: 0;
+}
+
+.label {
+  width: 160rpx;
+  font-size: 28rpx;
+  color: #666;
+}
+
+.value {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.5;
+}
+
+.value.intro {
+  white-space: pre-wrap;
+}
+
+.action-buttons {
+  display: flex;
+  padding: 30rpx 40rpx;
+  gap: 20rpx;
+}
+
+.action-btn {
+  flex: 1;
+  border-radius: 10rpx;
+  font-size: 32rpx;
+  line-height: 80rpx;
+}
+
+.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;
+  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;
+}
+
+.bind-btn {
+  background-color: #3742fa;
+  color: #fff;
+  border-radius: 10rpx;
+  font-size: 32rpx;
+  line-height: 80rpx;
+  width: 80%;
+}
+</style>