1136 lines
29 KiB
Vue
1136 lines
29 KiB
Vue
<template>
|
||
<view class="chat-container">
|
||
<u-navbar class="chat-navbar" :is-back="true" :title="patientName || '转接方患者'" title-color="#000"></u-navbar>
|
||
|
||
<!-- 患者信息头部 -->
|
||
<view class="chat-header">
|
||
<view class="header-main">
|
||
<view class="patient-info-group">
|
||
<text class="patient-name">{{ patientName || '患者' }}</text>
|
||
<view class="patient-badges">
|
||
<view class="status-dot"></view>
|
||
<text class="sub-text">问诊中</text>
|
||
</view>
|
||
</view>
|
||
<view class="header-action" @click="goToPrescription">
|
||
<text class="expand-tip">开方</text>
|
||
<u-icon name="arrow-right" size="16" color="#6ACDBB"></u-icon>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 消息列表 -->
|
||
<scroll-view
|
||
class="chat-messages"
|
||
:scroll-top="scrollTop"
|
||
:scroll-into-view="scrollIntoView"
|
||
scroll-y
|
||
@scroll="onScroll"
|
||
@scrolltolower="loadHistory"
|
||
>
|
||
<view class="message-list-container">
|
||
<!-- 加载历史消息 -->
|
||
<view class="loading-spinner" v-if="loading && page > 1">
|
||
<view class="spinner"></view>
|
||
<text class="spinner-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 消息列表 -->
|
||
<view
|
||
v-for="(msg, index) in messages"
|
||
:key="msg.id ? msg.id : `msg_${index}`"
|
||
:id="`msg-${index}`"
|
||
>
|
||
<!-- 时间分隔符 -->
|
||
<view class="time-divider" v-if="shouldShowTime(msg, index)">
|
||
<text class="time-text">{{ formatTime(msg.created_at || msg.timestamp) }}</text>
|
||
</view>
|
||
|
||
<!-- 消息气泡 -->
|
||
<view class="bubble-container" :class="{ 'mine': isMsgMine(msg), 'other': !isMsgMine(msg) }">
|
||
<image class="avatar" :src="getMsgAvatar(msg)" mode="aspectFill"></image>
|
||
|
||
<view class="content-wrapper">
|
||
<view class="message-bubble" :class="['type-' + msg.message_type]">
|
||
<!-- 文本消息 -->
|
||
<text v-if="msg.message_type === 0" class="text-content" user-select>{{ getParsedContent(msg) }}</text>
|
||
|
||
<!-- 图片消息 -->
|
||
<image
|
||
v-else-if="msg.message_type === 1"
|
||
:src="getParsedContent(msg)"
|
||
mode="widthFix"
|
||
class="media-content image"
|
||
@click="previewImage(getParsedContent(msg))">
|
||
</image>
|
||
|
||
<!-- 语音消息 -->
|
||
<view
|
||
v-else-if="msg.message_type === 2"
|
||
class="audio-content"
|
||
:style="{ width: getAudioWidth(msg.duration) }"
|
||
@click="playAudio(msg)">
|
||
<block v-if="isMsgMine(msg)">
|
||
<text class="duration">{{ msg.duration }}''</text>
|
||
<view class="voice-icon-wrap mine" :class="{ 'playing': isPlaying(msg) }">
|
||
<u-icon name="volume-fill" size="20" color="#fff"></u-icon>
|
||
</view>
|
||
</block>
|
||
<block v-else>
|
||
<view class="voice-icon-wrap other" :class="{ 'playing': isPlaying(msg) }">
|
||
<u-icon name="volume-fill" size="20" color="#333"></u-icon>
|
||
</view>
|
||
<text class="duration">{{ msg.duration }}''</text>
|
||
</block>
|
||
</view>
|
||
|
||
<!-- 处方卡片 -->
|
||
<view
|
||
v-else-if="msg.message_type === 4"
|
||
class="prescription-card"
|
||
@click="viewPrescription(getParsedContent(msg))">
|
||
<view class="card-header">
|
||
<u-icon name="file-text" size="20" color="#6ACDBB"></u-icon>
|
||
<text class="card-title">处方单</text>
|
||
</view>
|
||
<view class="card-content">
|
||
<text class="card-text">点击查看处方详情</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view class="empty-state" v-if="messages.length === 0 && !loading">
|
||
<text class="empty-text">暂无消息</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部输入栏 -->
|
||
<view class="chat-footer" :style="{ bottom: keyboardHeight + 'px' }">
|
||
<view class="consultation-ended-notice" v-if="isConsultationEnded">
|
||
<u-icon name="info-circle" size="16" color="#999"></u-icon>
|
||
<text>问诊已结束</text>
|
||
</view>
|
||
|
||
<view class="input-bar" v-else>
|
||
<!-- 语音/文字切换 -->
|
||
<view class="icon-btn" @click="toggleVoiceInput">
|
||
<u-icon :name="voiceInputActive ? 'keyboard' : 'mic'" size="24" color="#666"></u-icon>
|
||
</view>
|
||
|
||
<!-- 输入框 -->
|
||
<view class="input-field-wrapper">
|
||
<input
|
||
v-if="!voiceInputActive"
|
||
v-model="newMessage"
|
||
class="main-input"
|
||
placeholder="输入消息..."
|
||
@confirm="sendTextMessage"
|
||
@focus="onInputFocus"
|
||
@blur="onInputBlur"
|
||
/>
|
||
<view
|
||
v-else
|
||
class="voice-btn"
|
||
:class="{ 'recording': recording }"
|
||
@touchstart="startVoiceRecording"
|
||
@touchend="stopVoiceRecording"
|
||
@touchcancel="stopVoiceRecording"
|
||
>
|
||
{{ recording ? '录音中...' : '按住说话' }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右侧操作 -->
|
||
<view class="right-action">
|
||
<view class="icon-btn" @click="chooseImage" v-if="!newMessage.trim()">
|
||
<u-icon name="photo" size="24" color="#666"></u-icon>
|
||
</view>
|
||
<view class="send-btn" @click="sendTextMessage" v-else>发送</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 录音遮罩 -->
|
||
<view class="recording-overlay" v-if="showRecordingModal">
|
||
<view class="recording-box">
|
||
<view class="wave-animation"></view>
|
||
<text class="timer">{{ formatRecordingTime(recordingTime) }}</text>
|
||
<text class="tip">松开结束录音</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { chatRoomManager } from '@/utils/chat/chatRoomManager.js';
|
||
import { getPatientItem } from '@/api/reception.js';
|
||
import { req } from '@/common/js/index.js';
|
||
import { chatConfig } from '@/config/chat.js';
|
||
|
||
// 根据环境获取上传URL
|
||
function checkDev(key = '') {
|
||
switch (key) {
|
||
case 'dev':
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
const isDev = checkDev('dev');
|
||
const uploadBaseUrl = isDev ? 'http://127.0.0.1:18001/api/mobile' : 'https://api.xiaokang88.com/api/mobile';
|
||
|
||
// 辅助函数
|
||
function getParsedContent(messageType, messageContent) {
|
||
if ([4, 9, 10, 11, 12, 13].includes(messageType)) {
|
||
try {
|
||
return typeof messageContent === 'object' ? messageContent : JSON.parse(messageContent);
|
||
} catch (e) {
|
||
return messageContent;
|
||
}
|
||
}
|
||
return messageContent;
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
registerId: '',
|
||
roomId: '',
|
||
patientId: '',
|
||
patientName: '',
|
||
patientInfo: null,
|
||
messages: [],
|
||
newMessage: '',
|
||
voiceInputActive: false,
|
||
recording: false,
|
||
recordingTime: 0,
|
||
recordingTimer: null,
|
||
showRecordingModal: false,
|
||
currentPlayingAudio: null,
|
||
innerAudioContext: null,
|
||
scrollTop: 0,
|
||
scrollIntoView: '',
|
||
page: 1,
|
||
pageSize: 20,
|
||
hasMore: true,
|
||
loading: false,
|
||
isConsultationEnded: false,
|
||
keyboardHeight: 0,
|
||
currentUserId: '',
|
||
patientUserId: ''
|
||
};
|
||
},
|
||
|
||
onLoad(options) {
|
||
this.registerId = options.register_id || '';
|
||
this.roomId = options.room_id || '';
|
||
this.patientName = decodeURIComponent(options.patient_name || '患者');
|
||
this.patientId = options.patient_id || '';
|
||
|
||
// 设置当前用户ID(医生)
|
||
this.currentUserId = `doctor-${uni.getStorageSync('doctor_id') || uni.getStorageSync('user_id')}`;
|
||
|
||
// 进入聊天室
|
||
if (this.roomId) {
|
||
chatRoomManager.enterRoom(this.roomId);
|
||
}
|
||
|
||
// 注册消息处理器
|
||
chatRoomManager.onMessage(this.handleChatMessage);
|
||
|
||
// 获取患者信息
|
||
if (this.patientId) {
|
||
this.getPatientInfo();
|
||
}
|
||
|
||
// 加载历史消息
|
||
this.loadHistory();
|
||
|
||
// 监听键盘高度变化
|
||
uni.onKeyboardHeightChange(res => {
|
||
this.keyboardHeight = res.height;
|
||
if (res.height > 0) {
|
||
this.scrollToBottom();
|
||
}
|
||
});
|
||
},
|
||
|
||
onShow() {
|
||
// 如果房间ID存在,确保已进入房间
|
||
if (this.roomId) {
|
||
chatRoomManager.enterRoom(this.roomId);
|
||
}
|
||
},
|
||
|
||
onHide() {
|
||
this.stopAudio();
|
||
},
|
||
|
||
onUnload() {
|
||
if (this.innerAudioContext) {
|
||
this.innerAudioContext.destroy();
|
||
}
|
||
// 离开聊天室
|
||
chatRoomManager.leaveRoom();
|
||
// 移除消息处理器
|
||
chatRoomManager.offMessage(this.handleChatMessage);
|
||
},
|
||
|
||
mounted() {
|
||
this.recorderManager = uni.getRecorderManager();
|
||
this.initRecorder();
|
||
},
|
||
|
||
methods: {
|
||
/**
|
||
* 处理聊天室消息
|
||
* @param {Object} message - 消息对象
|
||
* @param {string} roomId - 房间ID
|
||
* 职责:处理收到的聊天消息,更新消息列表
|
||
*/
|
||
handleChatMessage(message, roomId) {
|
||
// 只处理当前房间的消息
|
||
if (roomId === this.roomId) {
|
||
this.addMessage(message);
|
||
this.scrollToBottom();
|
||
}
|
||
},
|
||
|
||
// 获取患者信息
|
||
async getPatientInfo() {
|
||
try {
|
||
const res = await getPatientItem(this.patientId);
|
||
this.patientInfo = res;
|
||
if (res && res.user_patient) {
|
||
this.patientUserId = `user-${res.user_patient.id}`;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取患者信息失败:', error);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载历史消息
|
||
* 职责:使用聊天室管理器加载历史消息
|
||
*/
|
||
async loadHistory() {
|
||
if (!this.hasMore || this.loading || !this.roomId) return;
|
||
|
||
this.loading = true;
|
||
try {
|
||
// 使用聊天室管理器加载消息
|
||
const lastMessageId = this.page === 1 ? 0 : chatRoomManager.lastMessageId;
|
||
const messages = await chatRoomManager.loadRoomMessages(this.roomId, lastMessageId);
|
||
|
||
if (messages && messages.length > 0) {
|
||
// 如果是首次加载,直接设置消息列表
|
||
if (this.page === 1) {
|
||
this.messages = messages;
|
||
} else {
|
||
// 否则追加到前面
|
||
this.messages = [...messages, ...this.messages];
|
||
}
|
||
this.page++;
|
||
} else {
|
||
this.hasMore = false;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载历史消息失败:', error);
|
||
this.hasMore = false;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 添加消息
|
||
* @param {Object} message - 消息对象
|
||
* 职责:添加消息到消息列表,处理消息类型转换
|
||
*/
|
||
addMessage(message) {
|
||
if (!message.message_content && message.content) {
|
||
message.message_content = message.content;
|
||
}
|
||
|
||
// 使用配置文件中的消息类型映射
|
||
if (message.message_type === undefined && message.type !== undefined) {
|
||
message.message_type = chatConfig.getMessageType(message.type);
|
||
}
|
||
|
||
// 检查消息是否已存在(避免重复)
|
||
const existingIndex = this.messages.findIndex(m => m.id === message.id);
|
||
if (existingIndex !== -1) {
|
||
this.messages[existingIndex] = message;
|
||
return;
|
||
}
|
||
|
||
this.messages.push(message);
|
||
|
||
// 如果是结束问诊消息,设置标志
|
||
if (message.message_type === chatConfig.messageTypes['end-consultation']) {
|
||
this.isConsultationEnded = true;
|
||
}
|
||
},
|
||
|
||
// 判断消息是否是自己发的
|
||
isMsgMine(msg) {
|
||
const senderId = msg.sender_user_id || '';
|
||
return senderId === this.currentUserId || senderId.replace(/^(user-|doctor-)/, '') === this.currentUserId.replace(/^(user-|doctor-)/, '');
|
||
},
|
||
|
||
// 获取消息头像
|
||
getMsgAvatar(msg) {
|
||
if (this.isMsgMine(msg)) {
|
||
// 医生头像
|
||
return uni.getStorageSync('doctor_avatar') || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg';
|
||
} else {
|
||
// 患者头像
|
||
return this.patientInfo?.user_patient?.avatar || 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg';
|
||
}
|
||
},
|
||
|
||
// 解析消息内容
|
||
getParsedContent(msg) {
|
||
return getParsedContent(msg.message_type, msg.message_content);
|
||
},
|
||
|
||
// 判断是否显示时间
|
||
shouldShowTime(msg, index) {
|
||
if (index === 0) return true;
|
||
const prevMsg = this.messages[index - 1];
|
||
const currTime = msg.created_at || msg.timestamp || 0;
|
||
const prevTime = prevMsg.created_at || prevMsg.timestamp || 0;
|
||
return currTime - prevTime > 300000; // 5分钟
|
||
},
|
||
|
||
// 格式化时间
|
||
formatTime(timestamp) {
|
||
if (!timestamp) return '';
|
||
const date = new Date(timestamp);
|
||
const hours = date.getHours().toString().padStart(2, '0');
|
||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||
return `${hours}:${minutes}`;
|
||
},
|
||
|
||
// 格式化录音时间
|
||
formatRecordingTime(seconds) {
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = seconds % 60;
|
||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||
},
|
||
|
||
// 获取语音消息宽度
|
||
getAudioWidth(duration) {
|
||
const minWidth = 120;
|
||
const maxWidth = 300;
|
||
const width = minWidth + (duration / 60) * (maxWidth - minWidth);
|
||
return `${Math.min(width, maxWidth)}rpx`;
|
||
},
|
||
|
||
// 滚动到底部
|
||
scrollToBottom() {
|
||
this.$nextTick(() => {
|
||
if (this.messages.length > 0) {
|
||
const lastIndex = this.messages.length - 1;
|
||
this.scrollIntoView = `msg-${lastIndex}`;
|
||
}
|
||
});
|
||
},
|
||
|
||
// 滚动事件
|
||
onScroll(e) {
|
||
// 可以在这里处理滚动逻辑
|
||
},
|
||
|
||
// 输入框聚焦
|
||
onInputFocus() {
|
||
this.scrollToBottom();
|
||
},
|
||
|
||
// 输入框失焦
|
||
onInputBlur() {
|
||
// 可以在这里处理失焦逻辑
|
||
},
|
||
|
||
// 切换语音输入
|
||
toggleVoiceInput() {
|
||
this.voiceInputActive = !this.voiceInputActive;
|
||
},
|
||
|
||
// 发送文本消息
|
||
sendTextMessage() {
|
||
if (!this.newMessage.trim()) return;
|
||
|
||
this.sendMessage({
|
||
type: 'text',
|
||
content: this.newMessage
|
||
});
|
||
this.newMessage = '';
|
||
},
|
||
|
||
/**
|
||
* 发送消息
|
||
* @param {Object} message - 消息对象
|
||
* 职责:发送消息到聊天室,先显示临时消息,然后通过WebSocket发送
|
||
*/
|
||
sendMessage(message) {
|
||
const now = new Date();
|
||
const timeStr = this.formatTime(now);
|
||
|
||
// 使用配置文件中的消息类型映射
|
||
const messageType = chatConfig.getMessageType(message.type || 'text');
|
||
|
||
// 创建临时消息(用于立即显示)
|
||
const fullMessage = {
|
||
...message,
|
||
id: Date.now().toString(),
|
||
sender_user_id: this.currentUserId,
|
||
receiver_user_id: this.patientUserId,
|
||
room_id: this.roomId,
|
||
created_at_text: timeStr,
|
||
created_at: Math.floor(now.getTime() / 1000), // 转换为秒级时间戳
|
||
timestamp: Math.floor(now.getTime() / 1000),
|
||
message_content: message.message_content || message.content || '',
|
||
message_type: messageType,
|
||
isTemporary: true
|
||
};
|
||
|
||
// 先添加到消息列表(临时显示)
|
||
this.addMessage(fullMessage);
|
||
this.scrollToBottom();
|
||
|
||
// 使用聊天室管理器发送消息
|
||
const success = chatRoomManager.sendMessage({
|
||
room_id: this.roomId,
|
||
sender_user_id: this.currentUserId,
|
||
receiver_user_id: this.patientUserId,
|
||
message_type: messageType,
|
||
message_content: message.message_content || message.content || '',
|
||
duration: message.duration || 0
|
||
});
|
||
|
||
if (!success) {
|
||
console.error('消息发送失败');
|
||
// 可以在这里显示错误提示
|
||
}
|
||
},
|
||
|
||
// 选择图片
|
||
chooseImage() {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
success: (res) => {
|
||
this.uploadImage(res.tempFilePaths[0]);
|
||
}
|
||
});
|
||
},
|
||
|
||
// 上传图片
|
||
uploadImage(filePath) {
|
||
uni.showLoading({ title: '上传中...', mask: true });
|
||
uni.uploadFile({
|
||
url: `${uploadBaseUrl}/chat-friends/upload-chat-file`,
|
||
filePath: filePath,
|
||
name: 'file',
|
||
header: {
|
||
"authorization": uni.getStorageSync('token')
|
||
},
|
||
success: (uploadRes) => {
|
||
try {
|
||
const data = JSON.parse(uploadRes.data);
|
||
if (data.result && data.result.url) {
|
||
this.sendMessage({
|
||
type: 'image',
|
||
content: data.result.url
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('解析上传结果失败:', e);
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('上传失败:', error);
|
||
uni.showToast({ title: '上传失败', icon: 'none' });
|
||
},
|
||
complete: () => {
|
||
uni.hideLoading();
|
||
}
|
||
});
|
||
},
|
||
|
||
// 初始化录音器
|
||
initRecorder() {
|
||
this.recorderManager.onStop((res) => {
|
||
if (res.duration < 1000) {
|
||
uni.showToast({ title: '录音时间太短', icon: 'none' });
|
||
this.recording = false;
|
||
this.showRecordingModal = false;
|
||
clearInterval(this.recordingTimer);
|
||
return;
|
||
}
|
||
this.uploadAudioFile(res.tempFilePath, Math.floor(res.duration / 1000));
|
||
this.recording = false;
|
||
this.showRecordingModal = false;
|
||
clearInterval(this.recordingTimer);
|
||
});
|
||
|
||
this.recorderManager.onError((error) => {
|
||
console.error('录音错误:', error);
|
||
this.recording = false;
|
||
this.showRecordingModal = false;
|
||
clearInterval(this.recordingTimer);
|
||
});
|
||
},
|
||
|
||
// 开始录音
|
||
startVoiceRecording() {
|
||
if (this.recording) return;
|
||
|
||
uni.getSetting({
|
||
success: (res) => {
|
||
if (res.authSetting['scope.record']) {
|
||
this.startRecording();
|
||
} else {
|
||
uni.authorize({
|
||
scope: 'scope.record',
|
||
success: () => {
|
||
this.startRecording();
|
||
},
|
||
fail: () => {
|
||
uni.showToast({ title: '需要录音权限', icon: 'none' });
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
},
|
||
|
||
// 开始录音
|
||
startRecording() {
|
||
this.recording = true;
|
||
this.showRecordingModal = true;
|
||
this.recordingTime = 0;
|
||
this.recordingTimer = setInterval(() => {
|
||
this.recordingTime++;
|
||
}, 1000);
|
||
|
||
this.recorderManager.start({
|
||
duration: 60000,
|
||
sampleRate: 16000,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 96000,
|
||
format: 'mp3'
|
||
});
|
||
},
|
||
|
||
// 停止录音
|
||
stopVoiceRecording() {
|
||
if (!this.recording) return;
|
||
this.recorderManager.stop();
|
||
},
|
||
|
||
// 上传音频文件
|
||
uploadAudioFile(tempFilePath, duration) {
|
||
uni.showLoading({ title: '上传中...', mask: true });
|
||
uni.uploadFile({
|
||
url: `${uploadBaseUrl}/chat-friends/upload-chat-file`,
|
||
filePath: tempFilePath,
|
||
name: 'file',
|
||
header: {
|
||
"authorization": uni.getStorageSync('token')
|
||
},
|
||
success: (uploadRes) => {
|
||
try {
|
||
const data = JSON.parse(uploadRes.data);
|
||
if (data.result && data.result.url) {
|
||
this.sendMessage({
|
||
type: 'audio',
|
||
content: data.result.url,
|
||
duration: duration
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('解析上传结果失败:', e);
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
console.error('上传失败:', error);
|
||
uni.showToast({ title: '上传失败', icon: 'none' });
|
||
},
|
||
complete: () => {
|
||
uni.hideLoading();
|
||
}
|
||
});
|
||
},
|
||
|
||
// 播放音频
|
||
playAudio(msg) {
|
||
const audioUrl = this.getParsedContent(msg);
|
||
|
||
if (this.currentPlayingAudio === msg.id) {
|
||
this.stopAudio();
|
||
return;
|
||
}
|
||
|
||
this.stopAudio();
|
||
|
||
if (!audioUrl) {
|
||
uni.showToast({ title: '无效的音频地址', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
if (!this.innerAudioContext) {
|
||
this.innerAudioContext = uni.createInnerAudioContext();
|
||
this.innerAudioContext.onEnded(() => {
|
||
this.currentPlayingAudio = null;
|
||
});
|
||
this.innerAudioContext.onError((res) => {
|
||
console.error('音频播放错误', res);
|
||
this.currentPlayingAudio = null;
|
||
uni.showToast({ title: '播放失败', icon: 'none' });
|
||
});
|
||
}
|
||
|
||
this.innerAudioContext.src = audioUrl;
|
||
this.innerAudioContext.play();
|
||
this.currentPlayingAudio = msg.id;
|
||
},
|
||
|
||
// 停止音频
|
||
stopAudio() {
|
||
if (this.innerAudioContext) {
|
||
this.innerAudioContext.stop();
|
||
}
|
||
this.currentPlayingAudio = null;
|
||
},
|
||
|
||
// 是否正在播放
|
||
isPlaying(msg) {
|
||
return this.currentPlayingAudio === msg.id;
|
||
},
|
||
|
||
// 预览图片
|
||
previewImage(url) {
|
||
uni.previewImage({
|
||
urls: [url],
|
||
current: url
|
||
});
|
||
},
|
||
|
||
// 查看处方
|
||
viewPrescription(content) {
|
||
if (content && content.id) {
|
||
uni.navigateTo({
|
||
url: `/subPackages/sub_reception/prescription_detail?id=${content.id}`
|
||
});
|
||
}
|
||
},
|
||
|
||
// 跳转到开方页面
|
||
goToPrescription() {
|
||
uni.navigateTo({
|
||
url: `/subPackages/sub_reception/reception_prescription?register_id=${this.registerId}&patient_id=${this.patientId}`
|
||
});
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
$primary-color: #6ACDBB;
|
||
$bg-page: #F4F6F9;
|
||
$bg-white: #ffffff;
|
||
|
||
.chat-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: $bg-page;
|
||
}
|
||
|
||
.chat-header {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
z-index: 100;
|
||
box-shadow: 0 1rpx 2rpx rgba(0,0,0,0.05);
|
||
|
||
.header-main {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 24rpx 32rpx;
|
||
|
||
.patient-info-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.patient-name {
|
||
font-size: 34rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.patient-badges {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.status-dot {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
background-color: #52c41a;
|
||
border-radius: 50%;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.sub-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.header-action {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10rpx 20rpx;
|
||
background: #F0F5FF;
|
||
border-radius: 32rpx;
|
||
|
||
.expand-tip {
|
||
font-size: 24rpx;
|
||
color: $primary-color;
|
||
margin-right: 6rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.chat-messages {
|
||
flex: 1;
|
||
background-color: $bg-page;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.message-list-container {
|
||
padding: 30rpx 24rpx;
|
||
}
|
||
|
||
.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); }
|
||
}
|
||
|
||
.time-divider {
|
||
text-align: center;
|
||
margin: 30rpx 0;
|
||
|
||
.time-text {
|
||
font-size: 22rpx;
|
||
color: #aaa;
|
||
background: rgba(0,0,0,0.03);
|
||
padding: 4rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
}
|
||
|
||
.bubble-container {
|
||
display: flex;
|
||
margin-bottom: 30rpx;
|
||
|
||
&.mine {
|
||
flex-direction: row-reverse;
|
||
|
||
.content-wrapper {
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.message-bubble {
|
||
background-color: $primary-color;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
&.other {
|
||
.content-wrapper {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.message-bubble {
|
||
background-color: #fff;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.avatar {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
margin: 0 20rpx;
|
||
}
|
||
|
||
.content-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-width: 60%;
|
||
|
||
.message-bubble {
|
||
padding: 20rpx;
|
||
border-radius: 16rpx;
|
||
word-wrap: break-word;
|
||
|
||
.text-content {
|
||
font-size: 28rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.media-content {
|
||
max-width: 400rpx;
|
||
border-radius: 8rpx;
|
||
|
||
&.image {
|
||
max-height: 600rpx;
|
||
}
|
||
}
|
||
|
||
.audio-content {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 20rpx;
|
||
min-width: 120rpx;
|
||
|
||
.duration {
|
||
font-size: 24rpx;
|
||
margin: 0 10rpx;
|
||
}
|
||
|
||
.voice-icon-wrap {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
&.mine {
|
||
background-color: rgba(255,255,255,0.3);
|
||
}
|
||
|
||
&.other {
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
&.playing {
|
||
animation: pulse 1s infinite;
|
||
}
|
||
}
|
||
}
|
||
|
||
.prescription-card {
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
padding: 20rpx;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10rpx;
|
||
|
||
.card-title {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
margin-left: 10rpx;
|
||
}
|
||
}
|
||
|
||
.card-content {
|
||
.card-text {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40rpx 0;
|
||
|
||
.empty-text {
|
||
font-size: 24rpx;
|
||
color: #ccc;
|
||
}
|
||
}
|
||
|
||
.chat-footer {
|
||
background-color: #F7F7F7;
|
||
border-top: 1rpx solid rgba(0,0,0,0.05);
|
||
padding: 16rpx 24rpx 40rpx 24rpx;
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 99;
|
||
transition: bottom 0.2s ease-out;
|
||
|
||
.consultation-ended-notice {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 30rpx;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.input-bar {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 16rpx;
|
||
|
||
.icon-btn {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
|
||
&:active {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
.input-field-wrapper {
|
||
flex: 1;
|
||
min-height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.main-input {
|
||
width: 100%;
|
||
height: 72rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.voice-btn {
|
||
width: 100%;
|
||
height: 72rpx;
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
font-weight: 500;
|
||
border: 1rpx solid rgba(0,0,0,0.05);
|
||
|
||
&.recording {
|
||
background: #f0f0f0;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
.right-action {
|
||
height: 72rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.send-btn {
|
||
background: $primary-color;
|
||
color: #fff;
|
||
font-size: 26rpx;
|
||
padding: 0 24rpx;
|
||
height: 60rpx;
|
||
line-height: 60rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.recording-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.6);
|
||
z-index: 999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.recording-box {
|
||
width: 300rpx;
|
||
height: 300rpx;
|
||
background: rgba(0,0,0,0.8);
|
||
border-radius: 32rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
gap: 20rpx;
|
||
|
||
.wave-animation {
|
||
width: 100rpx;
|
||
height: 4rpx;
|
||
background: $primary-color;
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.timer {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tip {
|
||
font-size: 24rpx;
|
||
color: #ccc;
|
||
}
|
||
}
|
||
}
|
||
</style>
|