first commit
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<block v-else-if="article">
|
||||
<!-- 封面图 -->
|
||||
<image v-if="article.image" :src="article.image" class="cover-image" mode="aspectFill" />
|
||||
|
||||
<!-- 内容区 -->
|
||||
<view class="content-wrap">
|
||||
<text class="article-title">{{ article.title }}</text>
|
||||
|
||||
<view class="meta-row">
|
||||
<text class="meta-text">{{ article.author || '健康科普' }}</text>
|
||||
<text class="meta-dot">·</text>
|
||||
<text class="meta-text">{{ article.create_time }}</text>
|
||||
<text class="meta-dot">·</text>
|
||||
<text class="meta-text">{{ article.click }} 阅读</text>
|
||||
</view>
|
||||
|
||||
<!-- 富文本内容 -->
|
||||
<rich-text :nodes="article.content" class="article-content" />
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 加载失败 -->
|
||||
<view v-else class="empty-wrap">
|
||||
<text class="empty-text">文章不存在或已下架</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const article = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const fetchDetail = async (id) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await proxy.apiUrl({
|
||||
url: '/api/article/detail',
|
||||
method: 'GET',
|
||||
data: { id }
|
||||
})
|
||||
if (response && response.code === 1) {
|
||||
article.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文章详情失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
const id = currentPage?.options?.id
|
||||
if (id) fetchDetail(id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.loading-wrap, .empty-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 200rpx;
|
||||
}
|
||||
|
||||
.loading-text, .empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 420rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
background: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
margin-top: -24rpx;
|
||||
padding: 40rpx 32rpx 60rpx;
|
||||
min-height: calc(100vh - 396rpx);
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.meta-dot {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
margin: 0 12rpx;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,940 @@
|
||||
<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">100{{ 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="qualification-section" v-if="qualificationImages.length > 0">
|
||||
<text class="section-title-plain">医生资质</text>
|
||||
<view class="qualification-grid">
|
||||
<image
|
||||
v-for="(img, idx) in qualificationImages"
|
||||
:key="idx"
|
||||
:src="img"
|
||||
class="qualification-image"
|
||||
mode="aspectFill"
|
||||
@click="previewImage(img)"
|
||||
/>
|
||||
</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">
|
||||
<!-- 底部操作栏 -->
|
||||
<view v-if="doctor.enable_image_consult || doctor.enable_video_consult" class="bottom-bar">
|
||||
<view class="btn-chat" @click="showCardSelector('chat')" v-if="doctor.enable_image_consult">
|
||||
<text class="btn-icon">💬</text>
|
||||
<text class="btn-text">图文问诊</text>
|
||||
</view>
|
||||
<view class="btn-video" @click="showCardSelector('video')" v-if="doctor.enable_video_consult">
|
||||
<text class="btn-icon">📹</text>
|
||||
<text class="btn-text">视频问诊</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 就诊人选择弹窗 -->
|
||||
<view v-if="showCardModal" class="modal-overlay" @click="closeCardModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择就诊人</text>
|
||||
<text class="modal-close" @click="closeCardModal">✕</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-body">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loadingCards" class="modal-loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 就诊卡列表 -->
|
||||
<view v-else-if="cardList.length > 0" class="card-list">
|
||||
<view
|
||||
v-for="card in cardList"
|
||||
:key="card.id"
|
||||
class="card-item"
|
||||
@click="selectCard(card)"
|
||||
>
|
||||
<view class="card-info">
|
||||
<text class="card-name">{{ card.patient_name }}</text>
|
||||
<text class="card-detail">{{ card.gender_desc }} / {{ card.age }}岁</text>
|
||||
</view>
|
||||
<text class="card-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="modal-empty">
|
||||
<text class="empty-text">暂无就诊卡</text>
|
||||
<view class="add-card-btn" @click="createNewCard">新建就诊卡</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { getCurrentInstance } from 'vue'
|
||||
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 showCardModal = ref(false)
|
||||
const cardList = ref([])
|
||||
const loadingCards = ref(false)
|
||||
const consultType = ref('') // 'chat' 或 'video'
|
||||
|
||||
// 医生资质图片
|
||||
const qualificationImages = computed(() => {
|
||||
if (!doctor.value?.qualification_images) return []
|
||||
try {
|
||||
const images = typeof doctor.value.qualification_images === 'string'
|
||||
? JSON.parse(doctor.value.qualification_images)
|
||||
: doctor.value.qualification_images
|
||||
return Array.isArray(images) ? images : []
|
||||
} catch (e) {
|
||||
console.error('解析资质图片失败:', e)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
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 previewImage = (current) => {
|
||||
uni.previewImage({
|
||||
current: current,
|
||||
urls: qualificationImages.value
|
||||
})
|
||||
}
|
||||
|
||||
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 showCardSelector = async (type) => {
|
||||
consultType.value = type
|
||||
showCardModal.value = true
|
||||
await loadCardList()
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeCardModal = () => {
|
||||
showCardModal.value = false
|
||||
}
|
||||
|
||||
// 加载就诊卡列表
|
||||
const loadCardList = async () => {
|
||||
loadingCards.value = true
|
||||
try {
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||
closeCardModal()
|
||||
return
|
||||
}
|
||||
|
||||
const userData = uni.getStorageSync('userData')
|
||||
const userId = userData?.id
|
||||
|
||||
if (!userId) {
|
||||
uni.showToast({ title: '用户信息不存在', icon: 'none' })
|
||||
closeCardModal()
|
||||
return
|
||||
}
|
||||
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/tcm/getCardList',
|
||||
method: 'GET',
|
||||
data: { user_id: userId,patient_id:userId }
|
||||
}, false)
|
||||
|
||||
if (res && res.code === 1) {
|
||||
cardList.value = res.data || []
|
||||
} else {
|
||||
uni.showToast({ title: res?.msg || '获取失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载就诊卡列表失败:', e)
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
loadingCards.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择就诊卡
|
||||
const selectCard = async (card) => {
|
||||
closeCardModal()
|
||||
|
||||
// 更新本地存储的就诊卡信息
|
||||
const userData = uni.getStorageSync('userData') || {}
|
||||
userData.diagnosis = {
|
||||
patient_id: card.patient_id,
|
||||
id: card.id,
|
||||
patient_name: card.patient_name
|
||||
}
|
||||
uni.setStorageSync('userData', userData)
|
||||
|
||||
// 根据类型跳转
|
||||
if (consultType.value === 'chat') {
|
||||
await goChat(card)
|
||||
} else if (consultType.value === 'video') {
|
||||
await goChat(card)
|
||||
}
|
||||
}
|
||||
|
||||
// 新建就诊卡
|
||||
const createNewCard = () => {
|
||||
closeCardModal()
|
||||
uni.navigateTo({
|
||||
url: `/pages/Card/edit_card?add=1&returnUrl=/doctor/pages/doctor/doctor?id=${doctorId.value}`
|
||||
})
|
||||
}
|
||||
|
||||
const goChat = async (card) => {
|
||||
console.log('开始图文问诊', uni.getStorageSync('userData'))
|
||||
const userData = uni.getStorageSync('userData')
|
||||
if (!userData.diagnosis) {
|
||||
uni.showToast({ title: '请先选择就诊人', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
await loginHandler(userData.diagnosis.patient_id)
|
||||
const doctor = 'doctor_' + doctorId.value
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/TUIKit/pages/chat/chat?userID=${encodeURIComponent(userId.value)}&userSig=${encodeURIComponent(userSig.value)}&SDKAppID=${SDKAppID.value}&targetUserID=${doctor}&msg=0`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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: patientUserId,
|
||||
userSig: patientUserSig,
|
||||
sdkAppId: patientSDKAppID,
|
||||
} = res.data
|
||||
console.log('获取签名成功:', patientUserSig)
|
||||
|
||||
// 更新ref值
|
||||
userId.value = patientUserId
|
||||
userSig.value = patientUserSig
|
||||
SDKAppID.value = patientSDKAppID
|
||||
|
||||
getApp().globalData.userID = patientUserId
|
||||
getApp().globalData.userSig = patientUserSig
|
||||
getApp().globalData.SDKAppID = patientSDKAppID
|
||||
getApp().globalData.isIMInitialized = false
|
||||
|
||||
await uni.CallManager.init({
|
||||
sdkAppID: patientSDKAppID,
|
||||
userID: patientUserId,
|
||||
userSig: patientUserSig,
|
||||
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;
|
||||
}
|
||||
|
||||
.qualification-section {
|
||||
margin-bottom: 96rpx;
|
||||
}
|
||||
|
||||
.qualification-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.qualification-image {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
border-radius: 16rpx;
|
||||
background: $surface-high;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 640rpx;
|
||||
max-height: 80vh;
|
||||
background: $surface-lowest;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 48rpx;
|
||||
border-bottom: 2rpx solid $outline-variant;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: $on-surface;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
font-size: 48rpx;
|
||||
color: $on-surface-variant;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-loading {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-list {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 16rpx;
|
||||
background: $surface-low;
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background: $surface-high;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: $on-surface;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.card-detail {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: $on-surface-variant;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
font-size: 48rpx;
|
||||
color: $on-surface-variant;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.modal-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 48rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: $on-surface-variant;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.add-card-btn {
|
||||
padding: 24rpx 64rpx;
|
||||
background: $primary;
|
||||
color: $on-primary;
|
||||
border-radius: 40rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<!-- 顶部标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">名医推荐</text>
|
||||
</view>
|
||||
|
||||
<!-- 医生列表 -->
|
||||
<view class="doctors-list">
|
||||
<view
|
||||
v-for="doctor in doctors"
|
||||
:key="doctor.id"
|
||||
class="doctor-card"
|
||||
@click="selectDoctor(doctor)"
|
||||
>
|
||||
<image :src="doctor.avatar || defaultAvatar" class="doctor-photo" mode="aspectFill"></image>
|
||||
<view class="doctor-info">
|
||||
<view class="doctor-top">
|
||||
<text class="doctor-name">{{ doctor.name }}</text>
|
||||
<text class="doctor-rating">★ {{ doctor.rating || '4.9' }}</text>
|
||||
</view>
|
||||
<text class="doctor-specialty">{{ doctor.specialty || doctor.title || '中医' }}</text>
|
||||
<view class="doctor-price-row">
|
||||
<text class="doctor-price">{{ doctor.price || '' }}</text>
|
||||
<text class="doctor-unit" v-if="doctor.title">{{doctor.title}}</text>
|
||||
</view>
|
||||
<view class="doctor-actions">
|
||||
<view class="appointment-btn" @click.stop="selectDoctor(doctor)">预约</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="hasMore" @click="loadMore">
|
||||
<text class="load-more-text">{{ loading ? '加载中...' : '加载更多' }}</text>
|
||||
</view>
|
||||
<view class="no-more" v-else-if="doctors.length > 0">
|
||||
<text class="no-more-text">没有更多了</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty" v-if="!loading && doctors.length === 0">
|
||||
<text class="empty-text">暂无医生信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const doctors = ref([])
|
||||
const loading = ref(false)
|
||||
const pageNo = ref(1)
|
||||
const pageSize = 10
|
||||
const hasMore = ref(true)
|
||||
const defaultAvatar = '/static/user/user.png'
|
||||
|
||||
const selectDoctor = (doctor) => {
|
||||
uni.navigateTo({
|
||||
url: `/doctor/pages/doctor/doctor?id=${doctor.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const fetchDoctors = async (reset = false) => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await proxy.apiUrl({
|
||||
url: '/api/doctor/lists',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page_no: pageNo.value,
|
||||
page_size: pageSize
|
||||
}
|
||||
})
|
||||
|
||||
if (response && response.code === 1) {
|
||||
const list = response.data?.lists || []
|
||||
if (reset) {
|
||||
doctors.value = list
|
||||
} else {
|
||||
doctors.value.push(...list)
|
||||
}
|
||||
hasMore.value = list.length === pageSize
|
||||
} else {
|
||||
console.warn('获取医生列表失败:', response?.msg)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求异常:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
if (!hasMore.value || loading.value) return
|
||||
pageNo.value++
|
||||
fetchDoctors()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDoctors(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$background: #faf9f5;
|
||||
$primary: #204e2b;
|
||||
$on-primary: #ffffff;
|
||||
$on-primary-container: #afe2b3;
|
||||
$surface-container-lowest: #ffffff;
|
||||
$surface-container-high: #e9e8e4;
|
||||
$surface-container-highest: #e3e2df;
|
||||
$on-surface: #1b1c1a;
|
||||
$on-surface-variant: #414941;
|
||||
$outline: #727970;
|
||||
|
||||
.container {
|
||||
background: $background;
|
||||
min-height: 100vh;
|
||||
padding: 24rpx 32rpx 120rpx;
|
||||
font-family: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: $on-surface;
|
||||
font-family: 'Noto Serif', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif;
|
||||
}
|
||||
|
||||
.doctors-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.doctor-card {
|
||||
background: $surface-container-lowest;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.doctor-photo {
|
||||
width: 160rpx;
|
||||
height: 230rpx;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
background: $surface-container-highest;
|
||||
}
|
||||
|
||||
.doctor-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 192rpx;
|
||||
}
|
||||
|
||||
.doctor-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.doctor-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: $on-surface;
|
||||
font-family: 'Noto Serif', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', serif;
|
||||
}
|
||||
|
||||
.doctor-rating {
|
||||
font-size: 24rpx;
|
||||
color: #805533;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.doctor-specialty {
|
||||
font-size: 28rpx;
|
||||
color: $on-surface-variant;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.doctor-price-row {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.doctor-price {
|
||||
font-size: 28rpx;
|
||||
color: $primary;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.doctor-unit {
|
||||
font-size: 24rpx;
|
||||
font-weight: normal;
|
||||
color: $on-surface-variant;
|
||||
}
|
||||
|
||||
.doctor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.appointment-btn {
|
||||
background: $primary;
|
||||
color: $on-primary;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 9999rpx;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 40rpx 0 20rpx;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
font-size: 28rpx;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 40rpx 0 20rpx;
|
||||
}
|
||||
|
||||
.no-more-text {
|
||||
font-size: 26rpx;
|
||||
color: $outline;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 30rpx;
|
||||
color: $outline;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user