This commit is contained in:
Your Name
2026-03-16 15:08:28 +08:00
parent 294fee4d88
commit cda63b5ae2
26 changed files with 1695 additions and 492 deletions
@@ -0,0 +1,649 @@
<template>
<view class="page">
<!-- 加载中 -->
<view v-if="loading" class="loading-wrap">
<text class="loading-text">加载中...</text>
</view>
<!-- 医生详情 -->
<view v-else-if="doctor" class="main">
<!-- 医生头部 -->
<section class="hero-section">
<view class="hero-content">
<view class="avatar-wrap">
<image :src="doctor.avatar || defaultAvatar" class="avatar" mode="aspectFill"></image>
<view class="badge">{{ doctor.title || '主任医师' }}</view>
</view>
<view class="hero-info">
<text class="doctor-name">{{ doctor.name }}</text>
<text class="hospital">{{ doctor.hospital || '甄养堂医院' }}</text>
<view class="experience-row">
<text class="verified"></text>
<text class="experience">{{ doctor.experience || '24年临床经验' }}</text>
</view>
<view class="stats-row">
<view class="stat-item">
<text class="stat-value">{{ doctor.rating || '4.9' }}</text>
<text class="stat-label">评分</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-value">{{ formatPatientCount(doctor.patient_count) }}</text>
<text class="stat-label">患者数</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item" v-if="doctor.license_no">
<text class="stat-value">执业证书编号</text>
<text class="stat-label">{{doctor.license_no}}</text>
</view>
</view>
</view>
</view>
</section>
<!-- 擅长领域 -->
<section class="about-section">
<view class="section-title-bar">
<text class="section-title">擅长领域</text>
</view>
<view class="about-content">
<text class="about-text">{{ doctor.about || '擅长运用传统中医理论与现代诊断相结合...' }}</text>
</view>
</section>
<!-- 专业领域 -->
<section class="expertise-section">
<text class="section-title-plain">专业领域</text>
<view class="expertise-grid">
<view
v-for="(item, idx) in (doctor.expertise || [])"
:key="idx"
class="expertise-card"
:class="{ 'expertise-wide': item.wide }"
>
<view class="expertise-icon" :class="'icon-' + (item.icon || 'default')">
<text class="icon-emoji">{{ getExpertiseIcon(item.icon) }}</text>
</view>
<view class="expertise-text">
<text class="expertise-title">{{ item.title }}</text>
<text class="expertise-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</section>
<!-- 患者评价 -->
<section class="reviews-section">
<view class="reviews-header">
<text class="section-title-plain">患者评价</text>
<!-- <text class="view-all" @click="viewAllReviews">查看全部</text> -->
</view>
<view class="reviews-list">
<view
v-for="review in reviews"
:key="review.id"
class="review-card"
>
<view class="review-header">
<view class="review-avatar">{{ review.patient_initials }}</view>
<view class="review-meta">
<text class="review-name">{{ review.patient_name }}</text>
<view class="review-stars">
<text v-for="i in 5" :key="i" class="star"></text>
</view>
</view>
<text class="review-time">{{ review.create_time }}</text>
</view>
<text class="review-content">"{{ review.content }}"</text>
</view>
</view>
</section>
</view>
<!-- 错误 -->
<view v-else-if="!loading && error" class="error-wrap">
<text class="error-text">{{ error }}</text>
<view class="retry-btn" @click="loadDetail">重试</view>
</view>
<!-- 底部操作栏 -->
<view v-if="doctor.enable_image_consult || doctor.enable_video_consult" class="bottom-bar">
<view class="btn-chat" @click="goChat" v-if="doctor.enable_image_consult">
<text class="btn-icon">💬</text>
<text class="btn-text">图文问诊</text>
</view>
<view class="btn-video" @click="goVideo" v-if="doctor.enable_video_consult">
<text class="btn-icon">📹</text>
<text class="btn-text">视频问诊</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { getCurrentInstance } from 'vue'
import * as GenerateTestUserSig from "@/debug/GenerateTestUserSig-es.js";
import { CallManager } from "@/TUICallKit/src/TUICallService/serve/callManager";
const { proxy } = getCurrentInstance()
let userId = ref("");
let userSig= ref("");
let SDKAppID=ref("");
uni.CallManager = new CallManager();
const doctorId = ref('')
const doctor = ref(null)
const reviews = ref([])
const loading = ref(true)
const error = ref('')
const defaultAvatar = '/static/user/user.png'
const formatPatientCount = (count) => {
if (count === undefined || count === null) return '1.2k+'
if (count >= 1000) return (count / 1000).toFixed(1) + 'k+'
return count + '+'
}
const getExpertiseIcon = (icon) => {
const map = { psychiatry: '🌿', eco: '🌱', vital_signs: '💓' }
return map[icon] || '📋'
}
const loadDetail = async () => {
if (!doctorId.value) return
loading.value = true
error.value = ''
try {
const res = await proxy.apiUrl({
url: '/api/doctor/detail',
method: 'GET',
data: { id: doctorId.value }
}, false)
if (res && res.code === 1) {
doctor.value = res.data
} else {
error.value = res?.msg || '加载失败'
}
} catch (e) {
error.value = '网络异常,请重试'
console.error(e)
} finally {
loading.value = false
}
}
const loadReviews = async () => {
if (!doctorId.value) return
try {
const res = await proxy.apiUrl({
url: '/api/doctor/reviews',
method: 'GET',
data: { doctor_id: doctorId.value, page_no: 1, page_size: 10 }
}, false)
if (res && res.code === 1) {
reviews.value = res.data?.lists || []
}
} catch (e) {
console.error(e)
}
}
const viewAllReviews = () => {
uni.showToast({ title: '查看全部评价', icon: 'none' })
}
const goChat = async () => {
// uni.navigateTo({
// url: `/TUIKit/pages/chat/chat?doctorId=${doctorId.value}&doctorName=${encodeURIComponent(doctor.value?.name || '')}`
// })
console.log('dfafafa',uni.getStorageSync('userData'))
if(!uni.getStorageSync('userData').diagnosis){
uni.navigateTo({
url: `/pages/Card/edit_card?add=1`
});
}else{
await loginHandler(uni.getStorageSync('userData').diagnosis.patient_id)
const doctor='doctor_'+doctorId.value
// 跳转到聊天页面
uni.navigateTo({
url: `/TUIKit/pages/chat/chat?userID=${encodeURIComponent(userId)}&userSig=${encodeURIComponent(userSig)}&SDKAppID=${SDKAppID}&targetUserID=${doctor}`
});
}
}
const goVideo = () => {
if(!uni.getStorageSync('userData').diagnosis){
uni.navigateTo({
url: `/pages/Card/edit_card?add=1`
});
}else{
uni.navigateTo({
url: `/pages/login/login?doctorId=${doctorId.value}&doctorName=${encodeURIComponent(doctor.value?.name || '')}`
})
}
}
const loginHandler = async (patient_id) => {
console.log('获取签名开始:',patient_id);
const res = await proxy.apiUrl({
url: '/api/tcm/getPatientSignature',
method: 'GET',
data: { patient_id: patient_id },
}, false)
const {userId } = res.data;
const { userSig, SDKAppID } = GenerateTestUserSig.genTestUserSig({
userID:userId,
});
console.log('获取签名成功:',userSig);
getApp().globalData.userID = userId;
getApp().globalData.userSig = userSig;
getApp().globalData.SDKAppID = SDKAppID;
getApp().globalData.isIMInitialized = false; // 标记IM未初始化
// 初始化视频通话
await uni.CallManager.init({
sdkAppID: SDKAppID,
userID: userId,
userSig: userSig,
globalCallPagePath: "TUICallKit/src/Components/TUICallKit",
});
console.log('初始化完成,跳转到聊天页面');
};
onMounted(() => {
const pages = getCurrentPages()
const current = pages[pages.length - 1]
const options = current.options || current.$page?.options || {}
doctorId.value = options.id || options.doctorId || ''
if (doctorId.value) {
loadDetail().then(() => loadReviews())
} else {
error.value = '缺少医生ID'
loading.value = false
}
})
</script>
<style lang="scss" scoped>
$bg: #faf9f5;
$primary: #204e2b;
$primary-container: #386641;
$on-primary: #ffffff;
$secondary-container: #fdc39a;
$on-secondary-container: #794e2e;
$surface-lowest: #ffffff;
$surface-low: #f4f4f0;
$surface-high: #e9e8e4;
$surface-container: #efeeea;
$on-surface: #1b1c1a;
$on-surface-variant: #414941;
$tertiary-container: #4b6500;
$outline-variant: #c1c9be;
.page {
background: $bg;
min-height: 100vh;
padding-bottom: 180rpx;
}
.loading-wrap, .error-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
}
.loading-text, .error-text {
font-size: 28rpx;
color: $on-surface-variant;
}
.retry-btn {
margin-top: 24rpx;
padding: 16rpx 48rpx;
background: $primary;
color: $on-primary;
border-radius: 40rpx;
font-size: 28rpx;
}
.main {
padding: 32rpx 48rpx 40rpx;
}
.hero-section {
margin-bottom: 80rpx;
}
.hero-content {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.avatar-wrap {
position: relative;
width: 320rpx;
height: 320rpx;
flex-shrink: 0;
}
.avatar {
width: 100%;
height: 100%;
border-radius: 24rpx;
background: $surface-high;
}
.badge {
position: absolute;
bottom: -16rpx;
right: -16rpx;
background: $tertiary-container;
color: $on-primary;
font-size: 22rpx;
padding: 12rpx 24rpx;
border-radius: 9999rpx;
font-weight: bold;
}
.hero-info {
flex: 1;
}
.doctor-name {
display: block;
font-size: 64rpx;
font-weight: bold;
color: $on-surface;
margin-bottom: 16rpx;
font-family: 'Noto Serif SC', 'STSong', 'SimSun', serif;
}
.hospital {
display: block;
font-size: 36rpx;
color: $primary;
font-weight: 500;
margin-bottom: 16rpx;
}
.experience-row {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
}
.verified {
font-size: 32rpx;
color: $primary;
}
.experience {
font-size: 28rpx;
color: $on-surface-variant;
}
.stats-row {
display: flex;
align-items: center;
gap: 32rpx;
}
.stat-item {
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 48rpx;
font-weight: bold;
color: $on-surface;
font-family: 'Noto Serif SC', serif;
}
.stat-label {
font-size: 20rpx;
text-transform: uppercase;
letter-spacing: 0.1em;
color: $on-surface-variant;
font-weight: bold;
margin-top: 4rpx;
}
.stat-divider {
width: 2rpx;
height: 64rpx;
background: $outline-variant;
opacity: 0.3;
}
.about-section {
margin-bottom: 96rpx;
}
.section-title-bar {
padding-left: 32rpx;
border-left: 8rpx solid $secondary-container;
margin-bottom: 32rpx;
}
.section-title {
font-size: 48rpx;
font-weight: bold;
color: $on-surface;
font-family: 'Noto Serif SC', serif;
}
.about-content {
background: $surface-low;
padding: 48rpx;
border-radius: 24rpx;
}
.about-text {
font-size: 28rpx;
color: $on-surface-variant;
line-height: 1.8;
}
.expertise-section {
margin-bottom: 96rpx;
}
.section-title-plain {
display: block;
font-size: 48rpx;
font-weight: bold;
color: $on-surface;
margin-bottom: 48rpx;
font-family: 'Noto Serif SC', serif;
}
.expertise-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32rpx;
}
.expertise-card {
background: $surface-lowest;
padding: 40rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.expertise-card.expertise-wide {
grid-column: span 2;
flex-direction: row;
align-items: center;
gap: 48rpx;
}
.expertise-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #ffdcc5;
display: flex;
align-items: center;
justify-content: center;
}
.expertise-icon.icon-eco {
background: #ccf078;
}
.expertise-icon.icon-vital_signs {
width: 96rpx;
height: 96rpx;
background: #bcefc0;
}
.icon-emoji {
font-size: 40rpx;
}
.expertise-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: $on-surface;
margin-bottom: 8rpx;
}
.expertise-desc {
font-size: 24rpx;
color: $on-surface-variant;
line-height: 1.5;
}
.reviews-section {
margin-bottom: 48rpx;
}
.reviews-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 48rpx;
}
.view-all {
font-size: 28rpx;
color: $primary;
font-weight: bold;
}
.reviews-list {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.review-card {
background: $surface-lowest;
padding: 48rpx;
border-radius: 24rpx;
}
.review-header {
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 24rpx;
}
.review-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: $surface-container;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: bold;
color: $on-surface-variant;
}
.review-meta {
flex: 1;
}
.review-name {
display: block;
font-size: 28rpx;
font-weight: bold;
color: $on-surface;
margin-bottom: 8rpx;
}
.review-stars {
color: #805533;
font-size: 24rpx;
}
.review-time {
font-size: 24rpx;
color: $on-surface-variant;
}
.review-content {
font-size: 28rpx;
color: $on-surface-variant;
line-height: 1.6;
font-style: italic;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 48rpx;
display: flex;
gap: 32rpx;
background: rgba(250, 249, 245, 0.95);
z-index: 50;
}
.btn-chat {
flex: 1;
height: 112rpx;
background: $secondary-container;
color: $on-secondary-container;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
font-weight: bold;
font-size: 30rpx;
}
.btn-video {
flex: 1;
height: 112rpx;
background: linear-gradient(135deg, $primary 0%, $primary-container 100%);
color: $on-primary;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 24rpx;
font-weight: bold;
font-size: 30rpx;
}
.btn-icon {
font-size: 40rpx;
}
</style>