在线复诊选择药品、说明主诉

This commit is contained in:
李琦
2026-05-06 16:41:19 +08:00
parent 1693b0e236
commit cd369ddf09
12 changed files with 2351 additions and 877 deletions

View File

@@ -1,131 +1,31 @@
<template>
<view class="consultation-container safe-area-inset-bottom">
<!-- 顶部导航栏 -->
<view class="consultation-header">
<view class="header-back" @click="handleBack">
<text class="iconfont icon-arrow-left"></text>
</view>
<view class="header-title">转诊咨询</view>
<view class="header-placeholder"></view>
</view>
<!-- 提示信息(类似图片中的灰色提示框) -->
<view class="notice-box" v-if="showNotice">
<view class="notice-text">
<text v-for="(part, index) in parsedNoticeParts" :key="index">
<text v-if="part.type === 'text'">{{ part.text }}</text>
<text v-else-if="part.type === 'agreement'" class="notice-link" @click="viewAgreement(part.agreementId)">
《{{ part.text }}》
</text>
</text>
</view>
</view>
<!-- 协议抽屉 -->
<u-popup v-model="showAgreement" :safe-area-inset-bottom="true" mode="bottom" height="88%" border-radius="20"
:closeable="true" z-index="20000">
<view class="agreement-popup">
<view class="agreement-header">
<text class="agreement-title">{{ currentAgreementName || '协议详情' }}</text>
</view>
<view class="agreement-content">
<u-parse :html="agreementContent"></u-parse>
</view>
</view>
</u-popup>
<!-- 聊天消息区域 -->
<scroll-view class="messages-scroll" scroll-y="true" :scroll-into-view="scrollIntoView" scroll-with-animation>
<view class="messages-container">
<!-- 按时间顺序渲染消息 -->
<view v-for="(message, index) in displayedMessages" :key="message.id">
<!-- 欢迎消息 -->
<view v-if="message.type === 'welcome'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>您好! 我是医生助理, 已收到您的用药申请:「{{ prescriptionInfo }}」, 为了帮助医生判断准确, 请按照您的真实情况回答以下问题。</text>
</view>
</view>
</view>
<!-- 问题消息 -->
<view v-else-if="message.type === 'question'" class="message-item other" :id="'question-' + message.questionId">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<!-- 用户回答消息 -->
<view v-else-if="message.type === 'answer'" class="message-item mine">
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="message-bubble">
<text>{{ message.content }}</text>
</view>
</view>
</view>
<!-- 等待转接消息 -->
<view v-else-if="message.type === 'waiting'" class="message-item other">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble">
<text>请稍等, 正在为您转接医生进行复诊开方...</text>
</view>
</view>
</view>
</view>
<!-- "正在回复"提示气泡 -->
<view v-if="isTyping" class="message-item other typing-indicator">
<image class="avatar" :src="assistantAvatar" mode="aspectFill"></image>
<view class="message-content">
<view class="assistant-label">{{ assistantName }}</view>
<view class="message-bubble typing-bubble">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
<!-- 底部锚点 -->
<view id="bottom-anchor" style="height: 1px;"></view>
</view>
</scroll-view>
<!-- 底部操作按钮(根据问题的answers动态生成) -->
<view class="action-footer safe-area-inset-bottom" v-if="currentQuestion && !isSubmitting && !showWaiting">
<button
v-for="(answer, index) in currentQuestionAnswers"
:key="index"
:class="['action-btn', getAnswerButtonClass(answer, index)]"
@click="handleAnswer(answer)"
>
{{ answer }}
</button>
</view>
<!-- 提交中提示 -->
<view class="submitting-overlay" v-if="isSubmitting">
<view class="submitting-content">
<view class="loading-spinner"></view>
<text class="submitting-text">正在提交...</text>
</view>
</view>
</view>
<AssistantConsultationShell
header-title="转诊咨询"
:show-notice="showNotice"
:parsed-notice-parts="parsedNoticeParts"
:show-agreement.sync="showAgreement"
:agreement-content="agreementContent"
:current-agreement-name="currentAgreementName"
:displayed-messages="displayedMessages"
:assistant-avatar="assistantAvatar"
:assistant-name="assistantName"
:user-avatar="userAvatar"
:prescription-info="prescriptionInfo"
:is-typing="isTyping"
:scroll-into-view="scrollIntoView"
:show-question-buttons="true"
:current-question="currentQuestion"
:current-question-answers="currentQuestionAnswers"
:is-submitting="isSubmitting"
:show-waiting="showWaiting"
@back="handleBack"
@answer="handleAnswer"
@view-agreement="viewAgreement"
/>
</template>
<script>
import AssistantConsultationShell from '@/components/AssistantConsultationShell.vue'
import {
getTransferPrescriptionDetailApi,
getTransferQuestionsApi,
@@ -137,6 +37,7 @@ import {
export default {
name: 'TransferConsultation',
components: { AssistantConsultationShell },
data() {
return {
transferId: 0, // 转诊ID
@@ -176,10 +77,10 @@ export default {
if (!this.currentQuestion) {
return ['是', '否'] // 默认答案
}
// 获取问题的answers字段
let answers = this.currentQuestion.answers
// 如果answers是字符串(JSON格式),解析为数组
if (typeof answers === 'string') {
try {
@@ -189,19 +90,19 @@ export default {
answers = ['是', '否']
}
}
// 如果answers不是数组或为空,使用默认值
if (!Array.isArray(answers) || answers.length === 0) {
answers = ['是', '否']
}
// 过滤空值
return answers.filter(answer => answer && answer.trim().length > 0)
},
// 按时间顺序显示的消息列表
displayedMessages() {
const messages = []
// 1. 欢迎消息
if (this.showWelcome) {
messages.push({
@@ -210,7 +111,7 @@ export default {
timestamp: 0
})
}
// 2. 问题和答案交替显示
for (let i = 0; i <= this.currentQuestionIndex && i < this.questions.length; i++) {
const question = this.questions[i]
@@ -222,7 +123,7 @@ export default {
content: question.question,
timestamp: i * 2 + 1
})
// 如果有对应的答案,添加答案
if (i < this.userAnswers.length) {
messages.push({
@@ -233,7 +134,7 @@ export default {
})
}
}
// 3. 等待转接消息
if (this.showWaiting) {
messages.push({
@@ -242,7 +143,7 @@ export default {
timestamp: messages.length
})
}
return messages
}
},
@@ -307,8 +208,8 @@ export default {
this.prescriptionInfo = transferData.drug_name_text
} else if (transferData.content) {
// 如果后端没有返回drug_name_text,前端自己解析
const content = typeof transferData.content === 'string'
? JSON.parse(transferData.content)
const content = typeof transferData.content === 'string'
? JSON.parse(transferData.content)
: transferData.content
this.prescriptionInfo = this.extractDrugName(content, transferData.prescription_type || 1)
}
@@ -321,9 +222,9 @@ export default {
// 从处方内容中提取药品名称(前端备用方法)
extractDrugName(content, prescriptionType) {
if (!content) return '未知药品'
const drugNames = []
if (prescriptionType === 1) {
// 中药处方
if (content.repice && Array.isArray(content.repice)) {
@@ -369,7 +270,7 @@ export default {
})
}
}
if (drugNames.length === 0) return '未知药品'
if (drugNames.length === 1) return drugNames[0]
return drugNames[0] + '等共' + drugNames.length + '味'
@@ -402,7 +303,7 @@ export default {
}
return a.id - b.id
})
if (this.questions.length === 0) {
uni.showToast({
title: '暂无咨询问题',
@@ -455,7 +356,7 @@ export default {
if (this.currentQuestionIndex < this.questions.length - 1) {
// 显示"正在回复"提示
this.isTyping = true
// 延迟后显示下一个问题
setTimeout(() => {
this.isTyping = false
@@ -514,7 +415,7 @@ export default {
const doctorInfo = result.doctor_info || {}
const doctorName = doctorInfo.name || '医生'
const doctorAvatar = doctorInfo.avatar || ''
// 构建跳转URL,包含所有必要参数
let chatUrl = `/subPackages/chat/chat?user_id=${result.doctor_id}&room_id=${result.room_id}&nick_name=${encodeURIComponent(doctorName)}&room_status=0`
if (registerId) {
@@ -523,7 +424,7 @@ export default {
if (doctorAvatar) {
chatUrl += `&avatar=${encodeURIComponent(doctorAvatar)}`
}
uni.navigateTo({
url: chatUrl
})
@@ -545,27 +446,6 @@ export default {
})
}
},
// 获取答案按钮的样式类
getAnswerButtonClass(answer, index) {
const answers = this.currentQuestionAnswers
const totalAnswers = answers.length
// 如果只有两个答案,使用原来的样式(是/否)
if (totalAnswers === 2) {
if (answer === '是') {
return 'yes-btn'
} else if (answer === '否') {
return 'no-btn'
}
}
// 多个答案时,第一个按钮使用主色调,其他使用次要样式
if (index === 0) {
return 'yes-btn' // 第一个按钮使用主色调
} else {
return 'no-btn' // 其他按钮使用次要样式
}
},
// 返回
handleBack() {
uni.showModal({
@@ -594,7 +474,7 @@ export default {
const result = res.data.result || {}
this.noticeText = result.notice_text || ''
this.agreements = result.agreements || []
// 解析文案,提取协议链接
this.parseNoticeText()
}
@@ -630,7 +510,7 @@ export default {
// 查找匹配的协议
const agreementName = match[1]
const agreement = this.agreements.find(ag => ag.name === agreementName || ag.name.includes(agreementName))
if (agreement) {
// 找到匹配的协议,添加为可点击的链接
parts.push({
@@ -699,283 +579,3 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.consultation-container {
width: 100%;
height: 100vh;
background-color: #F8FAFC;
display: flex;
flex-direction: column;
}
.consultation-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
.header-back {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.iconfont {
font-size: 32rpx;
color: #333;
}
}
.header-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.header-placeholder {
width: 60rpx;
}
}
.notice-box {
margin: 20rpx 30rpx;
padding: 24rpx;
background: #F5F5F5;
border-radius: 12rpx;
font-size: 24rpx;
line-height: 1.6;
color: #666;
.notice-text {
display: block;
word-break: break-all;
}
.notice-link {
color: #0A84FF;
text-decoration: underline;
}
}
.messages-scroll {
flex: 1;
overflow: hidden;
}
.messages-container {
padding: 30rpx;
min-height: 100%;
}
.message-item {
display: flex;
align-items: flex-start;
margin-bottom: 30rpx;
.avatar {
width: 72rpx;
height: 72rpx;
border-radius: 10rpx;
flex-shrink: 0;
background: #f2f2f2;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
margin-left: 16rpx;
.assistant-label {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.message-bubble {
padding: 18rpx 24rpx;
font-size: 30rpx;
line-height: 1.5;
border-radius: 4rpx 12rpx 12rpx 12rpx;
background: #fff;
color: #333;
word-break: break-all;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
}
}
&.mine {
flex-direction: row-reverse;
.message-content {
margin-left: 0;
margin-right: 16rpx;
align-items: flex-end;
.message-bubble {
background: #0A84FF;
color: #fff;
border-radius: 12rpx 4rpx 12rpx 12rpx;
}
}
}
&.typing-indicator {
.typing-bubble {
padding: 18rpx 24rpx;
min-width: 80rpx;
}
.typing-dots {
display: flex;
align-items: center;
gap: 8rpx;
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #999;
animation: typing 1.4s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
}
}
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-10rpx);
opacity: 1;
}
}
.action-footer {
padding: 20rpx 30rpx;
background: #fff;
border-top: 1rpx solid #f0f0f0;
display: flex;
gap: 20rpx;
justify-content: space-between;
.action-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
&::after {
border: none;
}
&.yes-btn {
background: #0A84FF;
color: #fff;
}
&.no-btn {
background: #fff;
color: #333;
border: 2rpx solid #ddd;
}
}
}
.submitting-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: 9999;
.submitting-content {
background: #fff;
padding: 60rpx 80rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #0A84FF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.submitting-text {
font-size: 28rpx;
color: #666;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.agreement-popup {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
}
.agreement-header {
padding: 30rpx 50rpx 20rpx;
border-bottom: 1rpx solid #e5e7eb;
background: #fff;
position: sticky;
top: 0;
z-index: 10;
}
.agreement-title {
font-size: 32rpx;
font-weight: 600;
color: #1e293b;
line-height: 1.5;
}
.agreement-content {
flex: 1;
padding: 30rpx 50rpx 40rpx;
overflow-y: auto;
min-height: 0;
}
</style>