diff --git a/pages.json b/pages.json index ae1c6aa..7e4898b 100644 --- a/pages.json +++ b/pages.json @@ -466,6 +466,7 @@ "path": "chat/chat", "style": { "navigationBarTitleText": "在线问诊", + "navigationStyle": "custom", "enablePullDownRefresh": false } }, diff --git a/request/api/transferPrescription.js b/request/api/transferPrescription.js index 4d6890c..350589e 100644 --- a/request/api/transferPrescription.js +++ b/request/api/transferPrescription.js @@ -58,4 +58,21 @@ export async function submitTransferQuestionsApi(params) { */ export async function getTransferAssistantConfigApi() { return await get('/transfer-assistant-config/get-config', {}, 3) +} + +/** + * 获取转诊咨询协议信息(固定文案和协议列表) + * @returns {Promise<*>} + */ +export async function getTransferConsultationAgreementInfoApi() { + return await get('/transfer-consultation/get-agreement-info', {}, 3) +} + +/** + * 获取协议详情(根据协议ID) + * @param {number} agreementId 协议ID + * @returns {Promise<*>} + */ +export async function getAgreementDetailApi(agreementId) { + return await get(`/agreement/get-detail?id=${agreementId}`, {}, 3) } \ No newline at end of file diff --git a/subPackages/chat/chat.vue b/subPackages/chat/chat.vue index f228aa6..dbf93aa 100644 --- a/subPackages/chat/chat.vue +++ b/subPackages/chat/chat.vue @@ -2,6 +2,16 @@ + + + @@ -49,6 +59,18 @@ > + + + + + {{ part.text }} + + 《{{ part.text }}》 + + + + + @@ -92,6 +114,19 @@ + + + + + {{ currentAgreementName || '协议详情' }} + + + + + + + @@ -161,7 +196,7 @@ import MessageBubble from './components/MessageBubble.vue'; // 引入组件 import { webSocketManager } from '@/utils/ws/websocket'; import { ChatManager } from '@/store/chat/chat.js'; import {getChatRegisterInfoApi, sendToUserApi, upLoadChatFileApi, getDoctorInfoApi, getRoomStatusApi} from "@/request/api/im"; -import {getTransferAssistantConfigApi} from "@/request/api/transferPrescription"; +import {getTransferAssistantConfigApi, getTransferConsultationAgreementInfoApi, getAgreementDetailApi} from "@/request/api/transferPrescription"; import { checkDev } from '@/utils/utils'; // 根据环境获取上传URL @@ -218,6 +253,14 @@ export default { isLoadHistory: false, isConsultationEnded: false, keyboardHeight: 0, + showNotice: true, // 显示协议提示 + noticeText: '', // 提示文案 + agreements: [], // 协议列表 + parsedNoticeParts: [], // 解析后的文案部分(文本和协议链接) + showAgreement: false, // 控制协议抽屉显示 + agreementContent: '', // 协议内容 + currentAgreementId: 0, // 当前查看的协议ID + currentAgreementName: '', // 当前查看的协议名称 }; }, @@ -230,7 +273,9 @@ export default { icon: 'none' }); setTimeout(() => { - uni.navigateBack(); + uni.reLaunch({ + url: '/pages/index/index' + }); }, 1500); return; } @@ -258,8 +303,9 @@ export default { // 无论是否有nickName,都需要获取医生详情信息(职称、医院、擅长) this.fetchDoctorInfo(doctorId); - // 加载助理配置信息 + // 加载助理配置信息和协议信息 this.loadAssistantConfig(); + this.loadAgreementInfo(); const nickName = this.doctorUserInfo.nick_name; if (nickName && nickName !== 'undefined' && nickName !== 'null') { @@ -278,6 +324,12 @@ export default { }); }, + onBackPress() { + // 拦截物理返回键,跳转到首页 + this.handleBack(); + return true; // 阻止默认返回行为 + }, + onUnload() { if (this.innerAudioContext) { this.innerAudioContext.destroy(); @@ -369,6 +421,117 @@ export default { } }, + // 加载协议信息 + async loadAgreementInfo() { + try { + const res = await getTransferConsultationAgreementInfoApi(); + if (res.data && res.data.code === 0) { + const result = res.data.result || {}; + this.noticeText = result.notice_text || ''; + this.agreements = result.agreements || []; + + // 解析文案,提取协议链接 + this.parseNoticeText(); + } + } catch (error) { + console.error('加载协议信息失败:', error); + // 失败时使用默认文案 + this.noticeText = '根据国家互联网医院管理办法要求,平台仅为复诊患者提供服务。为了保障您的用药安全,购买该药物需要经过医生问诊,请根据真实情况回答,并请仔细阅读《互联网医疗风险告知及知情同意书》,继续咨询即表示您已知悉相关规则与风险并同意相关条款。'; + this.parseNoticeText(); + } + }, + // 解析提示文案,将《协议名称》替换为可点击的链接 + parseNoticeText() { + if (!this.noticeText) { + this.parsedNoticeParts = []; + return; + } + + const parts = []; + // 使用正则表达式匹配《》中的协议名称 + const regex = /《([^》]+)》/g; + let lastIndex = 0; + let match; + + while ((match = regex.exec(this.noticeText)) !== null) { + // 添加匹配前的文本 + if (match.index > lastIndex) { + parts.push({ + type: 'text', + text: this.noticeText.substring(lastIndex, match.index) + }); + } + + // 查找匹配的协议 + const agreementName = match[1]; + const agreement = this.agreements.find(ag => ag.name === agreementName || ag.name.includes(agreementName)); + + if (agreement) { + // 找到匹配的协议,添加为可点击的链接 + parts.push({ + type: 'agreement', + text: agreementName, + agreementId: agreement.id + }); + } else { + // 没找到匹配的协议,作为普通文本显示 + parts.push({ + type: 'text', + text: match[0] // 《协议名称》 + }); + } + + lastIndex = regex.lastIndex; + } + + // 添加剩余的文本 + if (lastIndex < this.noticeText.length) { + parts.push({ + type: 'text', + text: this.noticeText.substring(lastIndex) + }); + } + + this.parsedNoticeParts = parts; + }, + // 查看协议 + async viewAgreement(agreementId) { + if (!agreementId) { + uni.showToast({ + title: '协议ID不能为空', + icon: 'none' + }); + return; + } + + // 查找协议名称 + const agreement = this.agreements.find(ag => ag.id === agreementId); + this.currentAgreementId = agreementId; + this.currentAgreementName = agreement ? agreement.name : '协议详情'; + this.showAgreement = true; + + try { + const res = await getAgreementDetailApi(agreementId); + if (res.data && res.data.code === 0) { + const result = res.data.result || {}; + this.agreementContent = result.content || ''; + // 如果API返回了协议名称,使用API返回的名称 + if (result.name) { + this.currentAgreementName = result.name; + } + } else { + throw new Error(res.data.message || '获取协议详情失败'); + } + } catch (error) { + console.error('获取协议详情失败:', error); + uni.showToast({ + title: error.message || '获取协议详情失败', + icon: 'none' + }); + this.showAgreement = false; + } + }, + // === 音频播放逻辑 === playAudio(msg) { const audioUrl = getParsedContent(msg.message_type, msg.message_content); @@ -475,13 +638,22 @@ export default { const systemInfo = uni.getSystemInfoSync(); const windowHeight = systemInfo.windowHeight; const query = uni.createSelectorQuery().in(this); - query.select('.chat-header').boundingClientRect((headerRect) => { - const headerHeight = headerRect ? headerRect.height : 0; - const query2 = uni.createSelectorQuery().in(this); - query2.select('.chat-footer').boundingClientRect((footerRect) => { - const footerHeight = footerRect ? footerRect.height : 0; - const scrollHeight = windowHeight - headerHeight - footerHeight; - this.scrollViewStyle = `height: ${scrollHeight}px;`; + + // 获取u-navbar导航栏高度(uView会自动处理状态栏高度) + query.select('.chat-navbar').boundingClientRect((navbarRect) => { + const navbarHeight = navbarRect ? navbarRect.height : 0; + + // 获取聊天头部高度 + query.select('.chat-header').boundingClientRect((headerRect) => { + const headerHeight = headerRect ? headerRect.height : 0; + + // 获取底部输入栏高度 + const query2 = uni.createSelectorQuery().in(this); + query2.select('.chat-footer').boundingClientRect((footerRect) => { + const footerHeight = footerRect ? footerRect.height : 0; + const scrollHeight = windowHeight - navbarHeight - headerHeight - footerHeight; + this.scrollViewStyle = `height: ${scrollHeight}px;`; + }).exec(); }).exec(); }).exec(); }); @@ -840,7 +1012,27 @@ export default { viewPrescription(id, orderNo) { uni.navigateTo({ url: "/subPackages/my/myrecord-detail?id=" + id + '&no=' + orderNo }); }, goToOrderMedicine(id, orderNo) { uni.navigateTo({ url: "/subPackages/my/my-drug/drug-info?id=" + id + '&order_type=prescription' }); }, viewRegister(id, orderNo) { uni.navigateTo({ url: "/subPackages/register/register-detail?id=" + id + '&no=' + orderNo }); }, - viewOrder(orderId, orderNo) { uni.navigateTo({ url: "/subPackages/my/my-drug/drug-info?id=" + orderId }); } + viewOrder(orderId, orderNo) { uni.navigateTo({ url: "/subPackages/my/my-drug/drug-info?id=" + orderId }); }, + + // 处理返回操作(自定义返回逻辑) + handleBack() { + // 跳转到首页(tabBar页面需要使用switchTab) + uni.switchTab({ + url: '/pages/index/index', + success: () => { + console.log('跳转到首页成功'); + }, + fail: (err) => { + console.error('跳转到首页失败:', err); + // 如果switchTab失败,尝试使用reLaunch + uni.reLaunch({ + url: '/pages/index/index' + }); + } + }); + // 返回 false 阻止 u-navbar 的默认返回行为(重要!) + return false; + } } }; @@ -866,6 +1058,7 @@ $bg-white: #ffffff; z-index: 100; position: relative; box-shadow: 0 1rpx 2rpx rgba(0,0,0,0.05); + // u-navbar会自动处理状态栏和导航栏高度,这里不需要额外设置margin-top .header-main { display: flex; @@ -934,6 +1127,26 @@ $bg-white: #ffffff; padding: 30rpx 24rpx; } +.notice-box { + margin-bottom: 20rpx; + 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; + } +} + .empty-state { text-align: center; padding: 40rpx 0; .empty-text { font-size: 24rpx; color: #ccc; } } .loading-spinner { display: flex; justify-content: center; align-items: center; padding: 20rpx; .spinner { width: 30rpx; height: 30rpx; border: 2rpx solid #ddd; border-top-color: #999; border-radius: 50%; animation: spin 0.8s linear infinite; } .spinner-text { font-size: 24rpx; color: #999; margin-left: 10rpx;} } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @@ -1007,4 +1220,34 @@ $bg-white: #ffffff; .tip { font-size: 24rpx; color: #ccc; } } } + +.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; +} \ No newline at end of file diff --git a/subPackages/chat/components/TransferMessageBubble.vue b/subPackages/chat/components/TransferMessageBubble.vue index 0e52486..01c3a05 100644 --- a/subPackages/chat/components/TransferMessageBubble.vue +++ b/subPackages/chat/components/TransferMessageBubble.vue @@ -39,8 +39,8 @@ {{ index + 1 }}. {{ qa.question }} - - {{ qa.answer === '是' ? '✓' : '✗' }} + + {{ qa.answer === '是' ? '✓' : (qa.answer === '否' ? '✗' : '?') }} {{ qa.answer }} @@ -78,6 +78,26 @@ export default { }, handleReject() { this.$emit('reject', this.messageData); + }, + // 获取答案的样式类:是=绿色,否=红色,其他=黄色 + getAnswerClass(answer) { + if (answer === '是') { + return 'answer-yes'; + } else if (answer === '否') { + return 'answer-no'; + } else { + return 'answer-other'; + } + }, + // 获取答案的图标 + getAnswerIcon(answer) { + if (answer === '是') { + return '✓'; + } else if (answer === '否') { + return '✗'; + } else { + return '?'; + } } } } @@ -273,6 +293,15 @@ export default { color: #dc2626; } +.answer-other { + background: #fef3c7; + color: #d97706; +} + +.answer-other .answer-icon { + color: #d97706; +} + .answer-icon { font-size: 24rpx; font-weight: bold; diff --git a/subPackages/transfer/transfer-consultation.vue b/subPackages/transfer/transfer-consultation.vue index 6444749..fea2fad 100644 --- a/subPackages/transfer/transfer-consultation.vue +++ b/subPackages/transfer/transfer-consultation.vue @@ -11,17 +11,34 @@ - - 根据国家互联网医院管理办法要求,平台仅为复诊患者提供服务。为了保障您的用药安全,购买该药物需要经过医生问诊,请根据真实情况回答,并请仔细阅读《互联网医疗风险告知及知情同意书》,继续咨询即表示您已知悉相关规则与风险并同意相关条款。 - - 《互联网医疗风险告知及知情同意书》 + + + {{ part.text }} + + 《{{ part.text }}》 + + + + + + + + {{ currentAgreementName || '协议详情' }} + + + + + + + - + @@ -86,10 +103,16 @@ - + - - + @@ -107,7 +130,9 @@ import { getTransferPrescriptionDetailApi, getTransferQuestionsApi, submitTransferQuestionsApi, - getTransferAssistantConfigApi + getTransferAssistantConfigApi, + getTransferConsultationAgreementInfoApi, + getAgreementDetailApi } from '@/request/api/transferPrescription.js' export default { @@ -128,7 +153,14 @@ export default { userAvatar: '/static/images/user-avatar.png', // 用户头像(需要替换为实际路径) prescriptionInfo: '炒黄芪等共2味', // 处方信息(可以从转诊详情中获取) replyDelay: 1.00, // 回复延迟时间(秒),从配置中获取 - isTyping: false // 是否显示"正在回复"提示 + isTyping: false, // 是否显示"正在回复"提示 + noticeText: '', // 提示文案 + agreements: [], // 协议列表 + parsedNoticeParts: [], // 解析后的文案部分(文本和协议链接) + showAgreement: false, // 控制协议抽屉显示 + agreementContent: '', // 协议内容 + currentAgreementId: 0, // 当前查看的协议ID + currentAgreementName: '' // 当前查看的协议名称 } }, computed: { @@ -139,6 +171,33 @@ export default { } return null }, + // 当前问题的答案选项 + currentQuestionAnswers() { + if (!this.currentQuestion) { + return ['是', '否'] // 默认答案 + } + + // 获取问题的answers字段 + let answers = this.currentQuestion.answers + + // 如果answers是字符串(JSON格式),解析为数组 + if (typeof answers === 'string') { + try { + answers = JSON.parse(answers) + } catch (e) { + console.error('解析答案选项失败:', e) + answers = ['是', '否'] + } + } + + // 如果answers不是数组或为空,使用默认值 + if (!Array.isArray(answers) || answers.length === 0) { + answers = ['是', '否'] + } + + // 过滤空值 + return answers.filter(answer => answer && answer.trim().length > 0) + }, // 按时间顺序显示的消息列表 displayedMessages() { const messages = [] @@ -187,6 +246,35 @@ export default { return messages } }, + watch: { + // 监听当前问题索引变化,自动滚动到底部 + currentQuestionIndex(newVal, oldVal) { + if (newVal !== oldVal && newVal >= 0) { + this.$nextTick(() => { + this.scrollToBottom() + }) + } + }, + // 监听用户答案变化,自动滚动到底部 + userAnswers: { + handler(newVal, oldVal) { + if (newVal.length > (oldVal?.length || 0)) { + this.$nextTick(() => { + this.scrollToBottom() + }) + } + }, + deep: true + }, + // 监听显示等待消息,自动滚动到底部 + showWaiting(newVal) { + if (newVal) { + this.$nextTick(() => { + this.scrollToBottom() + }) + } + } + }, onLoad(options) { // 获取转诊ID this.transferId = parseInt(options.transfer_id || 0) @@ -201,10 +289,11 @@ export default { return } - // 加载转诊详情、问题列表和助理配置 + // 加载转诊详情、问题列表、助理配置和协议信息 this.loadTransferDetail() this.loadQuestions() this.loadAssistantConfig() + this.loadAgreementInfo() }, methods: { // 加载转诊详情(获取药品信息) @@ -330,7 +419,7 @@ export default { // 显示第一个问题 this.currentQuestionIndex = 0 this.$nextTick(() => { - this.scrollIntoView = 'bottom-anchor' + this.scrollToBottom() }) }, this.replyDelay * 1000) // 延迟时间(毫秒) } @@ -357,30 +446,24 @@ export default { // 保存答案 this.userAnswers.push(answer) + // 立即滚动到底部,显示答案 + this.$nextTick(() => { + this.scrollToBottom() + }) + // 如果还有下一个问题,延迟显示下一个问题(模拟真人回复) if (this.currentQuestionIndex < this.questions.length - 1) { // 显示"正在回复"提示 this.isTyping = true - // 滚动到底部 - this.$nextTick(() => { - this.scrollIntoView = 'bottom-anchor' - }) // 延迟后显示下一个问题 setTimeout(() => { this.isTyping = false this.currentQuestionIndex++ - // 滚动到底部,显示新问题 - this.$nextTick(() => { - this.scrollIntoView = 'bottom-anchor' - }) + // watch会自动触发滚动 }, this.replyDelay * 1000) // 延迟时间(毫秒) } else { // 所有问题已回答,提交答案 - // 先滚动到底部 - this.$nextTick(() => { - this.scrollIntoView = 'bottom-anchor' - }) // 延迟提交,让用户看到最后的问题和答案 setTimeout(() => { this.submitAnswers() @@ -396,10 +479,7 @@ export default { this.isSubmitting = true this.showWaiting = true - // 滚动到底部 - this.$nextTick(() => { - this.scrollIntoView = 'bottom-anchor' - }) + // 滚动到底部(watch会自动触发) try { // 构建答案数组(按照问题的顺序,确保与后端保存顺序一致) @@ -465,6 +545,27 @@ 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({ @@ -477,13 +578,123 @@ export default { } }) }, + // 滚动到底部 + scrollToBottom() { + this.scrollIntoView = 'bottom-anchor' + // 延迟一下确保滚动生效 + setTimeout(() => { + this.scrollIntoView = '' + }, 100) + }, + // 加载协议信息 + async loadAgreementInfo() { + try { + const res = await getTransferConsultationAgreementInfoApi() + if (res.data && res.data.code === 0) { + const result = res.data.result || {} + this.noticeText = result.notice_text || '' + this.agreements = result.agreements || [] + + // 解析文案,提取协议链接 + this.parseNoticeText() + } + } catch (error) { + console.error('加载协议信息失败:', error) + // 失败时使用默认文案 + this.noticeText = '根据国家互联网医院管理办法要求,平台仅为复诊患者提供服务。为了保障您的用药安全,购买该药物需要经过医生问诊,请根据真实情况回答,并请仔细阅读《互联网医疗风险告知及知情同意书》,继续咨询即表示您已知悉相关规则与风险并同意相关条款。' + this.parseNoticeText() + } + }, + // 解析提示文案,将《协议名称》替换为可点击的链接 + parseNoticeText() { + if (!this.noticeText) { + this.parsedNoticeParts = [] + return + } + + const parts = [] + // 使用正则表达式匹配《》中的协议名称 + const regex = /《([^》]+)》/g + let lastIndex = 0 + let match + + while ((match = regex.exec(this.noticeText)) !== null) { + // 添加匹配前的文本 + if (match.index > lastIndex) { + parts.push({ + type: 'text', + text: this.noticeText.substring(lastIndex, match.index) + }) + } + + // 查找匹配的协议 + const agreementName = match[1] + const agreement = this.agreements.find(ag => ag.name === agreementName || ag.name.includes(agreementName)) + + if (agreement) { + // 找到匹配的协议,添加为可点击的链接 + parts.push({ + type: 'agreement', + text: agreementName, + agreementId: agreement.id + }) + } else { + // 没找到匹配的协议,作为普通文本显示 + parts.push({ + type: 'text', + text: match[0] // 《协议名称》 + }) + } + + lastIndex = regex.lastIndex + } + + // 添加剩余的文本 + if (lastIndex < this.noticeText.length) { + parts.push({ + type: 'text', + text: this.noticeText.substring(lastIndex) + }) + } + + this.parsedNoticeParts = parts + }, // 查看协议 - viewAgreement() { - // TODO: 跳转到协议页面 - uni.showToast({ - title: '协议页面开发中', - icon: 'none' - }) + async viewAgreement(agreementId) { + if (!agreementId) { + uni.showToast({ + title: '协议ID不能为空', + icon: 'none' + }) + return + } + + // 查找协议名称 + const agreement = this.agreements.find(ag => ag.id === agreementId) + this.currentAgreementId = agreementId + this.currentAgreementName = agreement ? agreement.name : '协议详情' + this.showAgreement = true + + try { + const res = await getAgreementDetailApi(agreementId) + if (res.data && res.data.code === 0) { + const result = res.data.result || {} + this.agreementContent = result.content || '' + // 如果API返回了协议名称,使用API返回的名称 + if (result.name) { + this.currentAgreementName = result.name + } + } else { + throw new Error(res.data.message || '获取协议详情失败') + } + } catch (error) { + console.error('获取协议详情失败:', error) + uni.showToast({ + title: error.message || '获取协议详情失败', + icon: 'none' + }) + this.showAgreement = false + } } } } @@ -545,7 +756,7 @@ export default { .notice-text { display: block; - margin-bottom: 12rpx; + word-break: break-all; } .notice-link { @@ -737,4 +948,34 @@ export default { 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; +}