Files
xk-client-wx/subPackages/chat/chat.vue

1559 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="chat-container">
<!-- 顶部导航栏 -->
<view class="chat-header" @click="toggleDoctorDetail">
<view class="header-center">
<view class="doctor-info">
<text class="doctor-name">{{ doctorUserInfo.nick_name }}</text>
<text class="doctor-status">在线 · 心血管内科</text>
</view>
<text class="arrow-icon">{{ showDoctorDetail ? '▲' : '▼' }}</text>
</view>
<view class="header-right">
<u-icon class="icon-btn" name="phone" size="22" @click.stop="startCall('audio')"></u-icon>
<u-icon class="icon-btn" name="video" size="22" @click.stop="startCall('video')"></u-icon>
</view>
<!-- 医生详情信息 -->
<view v-if="showDoctorDetail" class="doctor-detail-panel">
<view class="detail-item">
<text class="detail-label">职称</text>
<text>主任医师</text>
</view>
<view class="detail-item">
<text class="detail-label">医院</text>
<text>北京协和医院</text>
</view>
<view class="detail-item">
<text class="detail-label">擅长</text>
<text>冠心病高血压心力衰竭</text>
</view>
</view>
</view>
<!-- 聊天区域 -->
<scroll-view
class="chat-messages"
scroll-y="true"
:scroll-top="scrollTop"
scroll-with-animation
@scrolltoupper="loadHistory"
>
<view class="message-list">
<view v-if="isEmpty" class="empty-state">
没有更多消息了~
</view>
<view v-for="(msg, index) in messages" :key="index" class="message-item">
<!-- 对方消息 -->
<view v-if="msg.sender !== currentUserId" class="other-message">
<image class="avatar" :src="doctorAvatar"></image>
<view class="message-content-wrapper">
<view class="message-content" :class="msg.type">
<image
v-if="msg.type === 'image'"
:src="msg.content"
mode="widthFix"
@click="previewImage(msg.content)"
></image>
<video
v-else-if="msg.type === 'video'"
:src="msg.content"
controls
class="video-player"
></video>
<view v-else-if="msg.type === 'audio'" class="audio-message" @click="playAudio(msg)">
<u-icon name="music" size="16" color="#298dff"></u-icon>
<text class="duration">{{ msg.duration }}''</text>
<view class="wave-bars">
<view
v-for="(bar, i) in 5"
:key="i"
class="wave-bar"
:class="{'wave-bar-active': isPlaying(msg)}"
></view>
</view>
</view>
<!-- 挂号卡片消息 -->
<view v-else-if="msg.type === 'register'" class="register-card">
<view class="register-header">
<u-icon name="calendar" size="20" color="#059669"></u-icon>
<text class="register-title">挂号信息</text>
<view class="register-status" :class="'status-' + msg.content.status">
{{ getRegisterStatusText(msg.content.status) }}
</view>
</view>
<view class="register-content">
<view class="info-item">
<u-icon name="list-dot" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.order_no }}</text>
</view>
<view class="info-item">
<u-icon name="order" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.order_number }}</text>
</view>
</view>
<!-- 患者信息 -->
<view class="patient-info">
<view class="patient-header">
<text class="patient-title">患者信息</text>
<view class="pay-status" :class="'pay-status-' + msg.content.is_pay">
{{ getPayStatusText(msg.content.is_pay) }}
</view>
</view>
<view class="patient-details">
<view class="patient-item">
<u-icon name="account" size="16" color="#666"></u-icon>
<text>{{ msg.content.user_patient && msg.content.user_patient.name || '未知' }}</text>
</view>
<view class="patient-item">
<u-icon name="integral" size="16" color="#666"></u-icon>
<text>{{ msg.content.user_patient && msg.content.user_patient.age || 0 }}</text>
</view>
<view class="patient-item">
<u-icon name="man" size="16" color="#666"></u-icon>
<text>{{ getSexText(msg.content.user_patient && msg.content.user_patient.sex) }}</text>
</view>
</view>
</view>
<view class="register-footer">
<text class="price">挂号费: ¥{{ msg.content.price }}</text>
<text class="view-btn" @click="viewRegister(msg.content.id, msg.content.order_no)">查看详情</text>
</view>
<view class="register-time">
<text>创建时间: {{ msg.content.created_at }}</text>
</view>
</view>
<!-- 处方卡片消息 -->
<view v-else-if="msg.type === 'prescription'" class="prescription-card">
<view class="prescription-header">
<u-icon name="edit-pen" size="20" color="#2563eb"></u-icon>
<text class="prescription-title">电子处方</text>
<view class="prescription-status" :class="'status-' + msg.content.status">
{{ getStatusText(msg.content.status) }}
</view>
</view>
<view class="prescription-content">
<view class="info-item">
<u-icon name="list-dot" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.order_no }}</text>
</view>
<view class="info-item">
<u-icon name="truck" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.express_name || '待发货' }}</text>
</view>
</view>
<view class="prescription-footer">
<text class="price">支付金额: ¥{{ msg.content.total_pay_price }}</text>
<text class="view-btn" @click="viewPrescription(msg.content.id, msg.content.order_no)">查看详情</text>
</view>
</view>
<text v-else>{{ msg.content }}</text>
</view>
<text class="message-time">{{ msg.time }}</text>
</view>
</view>
<!-- 我的消息 -->
<view v-else class="my-message">
<image class="avatar" :src="userAvatar"></image>
<view class="message-content-wrapper">
<view class="message-content" :class="msg.type">
<image
v-if="msg.type === 'image'"
:src="msg.content"
mode="widthFix"
@click="previewImage(msg.content)"
></image>
<video
v-else-if="msg.type === 'video'"
:src="msg.content"
controls
class="video-player"
></video>
<view v-else-if="msg.type === 'audio'" class="audio-message" @click="playAudio(msg)">
<view class="wave-bars">
<view
v-for="(bar, i) in 5"
:key="i"
class="wave-bar"
:class="{'wave-bar-active': isPlaying(msg)}"
></view>
</view>
<text class="duration">{{ msg.duration }}''</text>
<u-icon name="music" size="16" color="#298dff"></u-icon>
</view>
<!-- 挂号卡片消息 -->
<view v-else-if="msg.type === 'register'" class="register-card">
<view v-if='msg.content != undefined' class="register-header">
<u-icon name="calendar" size="20" color="#059669"></u-icon>
<text class="register-title">挂号信息<!--{{ getChatRegisterInfo(msg, index) }}--></text>
<view class="register-status" :class="'status-' + msg.content.status">
{{ getRegisterStatusText(msg.content.status) }}
</view>
</view>
<view class="register-content">
<view class="info-item">
<u-icon name="list-dot" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.order_no }}</text>
</view>
<view class="info-item">
<u-icon name="order" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.order_number }}</text>
</view>
</view>
<!-- 患者信息 -->
<view class="patient-info">
<view class="patient-header">
<text class="patient-title">患者信息</text>
<view class="pay-status" :class="'pay-status-' + msg.content.is_pay">
{{ getPayStatusText(msg.content.is_pay) }}
</view>
</view>
<view class="patient-details">
<view class="patient-item">
<u-icon name="account" size="16" color="#666"></u-icon>
<text>{{ msg.content.user_patient && msg.content.user_patient.name || '未知' }}</text>
</view>
<view class="patient-item">
<u-icon name="integral" size="16" color="#666"></u-icon>
<text>{{ msg.content.user_patient && msg.content.user_patient.age || 0 }}</text>
</view>
<view class="patient-item">
<u-icon name="man" size="16" color="#666"></u-icon>
<text>{{ getSexText(msg.content.user_patient && msg.content.user_patient.sex) }}</text>
</view>
</view>
</view>
<view class="register-footer">
<text class="price">挂号费: ¥{{ msg.content.price }}</text>
<text class="view-btn" @click="viewRegister(msg.content.id, msg.content.order_no)">查看详情</text>
</view>
<view class="register-time">
<text>创建时间: {{ msg.content.created_at }}</text>
</view>
</view>
<!-- 处方卡片消息 -->
<view v-else-if="msg.type === 'prescription'" class="prescription-card">
<view class="prescription-header">
<u-icon name="edit-pen" size="20" color="#2563eb"></u-icon>
<text class="prescription-title">电子处方</text>
<view class="prescription-status" :class="'status-' + msg.content.status">
{{ getStatusText(msg.content.status) }}
</view>
</view>
<view class="prescription-content">
<view class="info-item">
<u-icon name="list-dot" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.order_no }}</text>
</view>
<view class="info-item">
<u-icon name="truck" size="16" color="#888"></u-icon>
<text class="info-text">{{ msg.content.express_name || '待发货' }}</text>
</view>
</view>
<view class="prescription-footer">
<text class="price">支付金额: ¥{{ msg.content.total_pay_price }}</text>
<text class="view-btn" @click="viewPrescription(msg.content.id, msg.content.order_no)">查看详情</text>
</view>
</view>
<text v-else>{{ msg.content }}</text>
</view>
<text class="message-time">{{ msg.time }}</text>
</view>
</view>
</view>
</view>
<!-- 加载指示器 -->
<view v-if="loading" class="loading-indicator">
<text>加载中...</text>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input">
<!-- 工具按钮行 -->
<view class="toolbar-row">
<view class="tool-btn" @click="openMediaPicker('image')">
<u-icon name="photo" size="18" color="#298dff"></u-icon>
<text>图片</text>
</view>
<view class="tool-btn" @click="openCamera">
<u-icon name="camera" size="18" color="#298dff"></u-icon>
<text>拍照</text>
</view>
<!-- <view class="tool-btn" @click="sendFile">-->
<!-- <u-icon name="file-text" size="18" color="#298dff"></u-icon>-->
<!-- <text>文件</text>-->
<!-- </view>-->
<!-- <view class="tool-btn" @click="sendPrescription">-->
<!-- <u-icon name="edit-pen" size="18" color="#298dff"></u-icon>-->
<!-- <text>处方</text>-->
<!-- </view>-->
<!-- <view class="tool-btn" @click="sendRegister">-->
<!-- <u-icon name="calendar" size="18" color="#298dff"></u-icon>-->
<!-- <text>挂号</text>-->
<!-- </view>-->
</view>
<!-- 输入框行 -->
<view class="input-row">
<view class="action-btn" @click="toggleVoiceInput">
<u-icon :name="voiceInputActive ? 'mic-off' : 'mic'" size="18" color="#298dff"></u-icon>
</view>
<!-- 文本输入 -->
<input
v-if="!voiceInputActive"
v-model="newMessage"
class="message-input"
placeholder="输入消息..."
@confirm="sendTextMessage"
:adjust-position="false"
placeholder-style="color: #aaa;"
/>
<!-- 语音输入 -->
<view v-else class="voice-input"
@touchstart="startVoiceRecording"
@touchend="stopVoiceRecording"
@touchcancel="stopVoiceRecording">
<u-icon name="mic" size="18" color="#fff"></u-icon>
<text>{{ recording ? '松开结束' : '按住说话' }}</text>
<view v-if="recording" class="recording-indicator">
<view class="pulse"></view>
<text>{{ recordingTime }}s</text>
</view>
</view>
<!-- 发送按钮 -->
<button
v-if="newMessage.trim() && !voiceInputActive"
class="send-btn"
@click="sendTextMessage"
>
<u-icon name="paper-plane" size="16" color="#fff"></u-icon>
</button>
</view>
</view>
<!-- 录音提示 -->
<view v-if="showRecordingModal" class="recording-modal">
<view class="recording-content">
<view class="recording-icon">
<u-icon name="mic" size="60" color="#fff"></u-icon>
<view class="pulse-animation"></view>
</view>
<text class="recording-text">录音中... {{ recordingTime }}</text>
<text class="recording-tip">上划取消发送</text>
</view>
</view>
</view>
</template>
<script>
import { webSocketManager } from '@/utils/ws/websocket';
import { ChatManager } from '@/store/chat/chat.js';
import {getChatRegisterInfoApi, sendToUserApi, upLoadChatFileApi} from "@/request/api/im";
export default {
data() {
return {
doctorUserInfo: {
id: 0,
avatar: '',
nick_name: '医生',
room_id: ''
},
showDoctorDetail: false,
currentUserId: '',
doctorId: 'doctor-1',
doctorAvatar: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg',
userAvatar: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk_upload_20250108/29bdb9560340373bb3096394a845afa5.jpg',
messages: [],
newMessage: '',
voiceInputActive: false,
showMoreActions: false,
recording: false,
recordingTime: 0,
recordingTimer: null,
showRecordingModal: false,
currentPlayingAudio: null,
scrollTop: 0,
scrollViewHeight: 0,
oldScrollViewHeight: 0,
page: 1,
pageSize: 20,
hasMore: true,
loading: false,
isEmpty: false,
hasRecordPermission: false,
// 处方数据
prescriptionData: {
id: 159,
order_no: "XY9478601753943762",
status: 0,
express_name: "李测试",
total_pay_price: "11.94"
},
// 挂号数据
registerData: {}
};
},
onLoad: function (option) {
this.doctorUserInfo = option;
if (option.avatar) {
this.doctorAvatar = option.avatar;
}
this.currentUserId = 'user-' + uni.getStorageSync('user_id') || 'user-2512';
this.doctorId = option.user_id;
if (typeof ChatManager !== 'undefined') {
ChatManager.enterRoom(option.room_id);
ChatManager.resetUnreadCount(option.room_id);
this.messages = ChatManager.getRoomMessages(option.room_id);
}
uni.setNavigationBarTitle({
title: `${this.doctorUserInfo.nick_name}医生的对话`,
success: () => {
console.log('修改标题成功');
}
});
this.$nextTick(() => {
this.scrollToBottom();
});
},
onUnload() {
if (typeof ChatManager !== 'undefined') {
ChatManager.leaveRoom();
}
},
onShow() {
uni.$on('new-room-message', this.handleNewRoomMessage);
uni.$on('load-room-message', this.handLoadMessageList);
uni.$on('no-more-history', this.handNoMessageList);
},
onHide() {
uni.$off('new-room-message', this.handleNewRoomMessage);
uni.$off('load-room-message', this.handLoadMessageList);
uni.$off('no-more-history', this.handNoMessageList);
},
mounted() {
this.recorderManager = uni.getRecorderManager();
this.initRecorder();
if (typeof webSocketManager !== 'undefined') {
webSocketManager.addMessageHandler(this.handleSocketMessage);
}
},
beforeDestroy() {
if (typeof webSocketManager !== 'undefined') {
webSocketManager.removeMessageHandler(this.handleSocketMessage);
}
},
methods: {
getChatRegisterInfo(item, index) {
let that = this;
getChatRegisterInfoApi({
id: item.content.id
}).then((res) => {
that.messages[index].content = res.data.result
console.log('getChatRegisterInfo', that.messages[index]);
})
return item;
},
handNoMessageList() {
this.isEmpty = true;
this.hasMore = false;
},
handLoadMessageList(type = 0) {
this.loading = false;
if (typeof ChatManager !== 'undefined') {
this.messages = ChatManager.roomMessages[this.doctorUserInfo.room_id] || [];
}
if (type === 1) {
this.scrollToBottom();
} else {
this.scrollToOldLastMessage();
}
},
getScrollHeight() {
const query = uni.createSelectorQuery().in(this);
query.select('.message-list').boundingClientRect(res => {
if (res) {
this.oldScrollViewHeight = this.scrollViewHeight;
this.scrollViewHeight = res.height;
}
}).exec();
},
handleSocketMessage(data) {
if (typeof ChatManager !== 'undefined') {
ChatManager.handleIncomingMessage(data);
}
},
handleNewRoomMessage(message) {
console.log('handle', message);
this.addMessage(message);
this.scrollToBottom();
},
addMessage(message) {
if (!this.messages.some(m => m.id === message.id)) {
if (message.type === "prescription" || message.type === "register") {
try {
message.content = JSON.parse(message.content);
} catch (e) {
console.error('解析消息内容失败:', e);
}
}
console.log('push', message);
this.messages.push(message);
this.scrollToBottom();
}
},
toggleDoctorDetail() {
this.showDoctorDetail = !this.showDoctorDetail;
},
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);
uni.showToast({ title: '录音失败,请重试', icon: 'none' });
this.recording = false;
this.showRecordingModal = false;
clearInterval(this.recordingTimer);
});
},
async uploadAudioFile(tempFilePath, duration) {
uni.showLoading({ title: '上传中...', mask: true });
try {
uni.uploadFile({
url: 'http://127.0.0.1:18001/api/mobile/chat-friends/upload-chat-file',
filePath: tempFilePath,
name: 'file',
header: {
"authorization": uni.getStorageSync('token')
},
formData: tempFilePath,
success: (uploadFileRes) => {
this.sendMessage({
type: 'audio',
content: JSON.parse(uploadFileRes.data).result.url,
duration,
});
},
fail: (res) => {
console.log(res, '上传失败');
}
});
} catch (error) {
console.error('音频上传失败:', error);
uni.showToast({ title: '音频上传失败', icon: 'none' });
} finally {
uni.hideLoading();
}
},
sendMessage(message) {
const now = new Date();
const timeStr = this.formatTime(now);
const fullMessage = {
...message,
id: Date.now().toString(),
sender: this.currentUserId,
receiver: this.doctorId,
room_id: this.doctorUserInfo.room_id,
time: timeStr,
timestamp: now.getTime()
};
if (typeof ChatManager !== 'undefined') {
ChatManager.addMessageToRoom(this.doctorUserInfo.room_id, fullMessage);
}
const messageTypeMap = {
text: 0,
image: 1,
audio: 2,
video: 3,
prescription: 4,
file: 5,
register: 10
};
const requestData = {
room_id: this.doctorUserInfo.room_id,
sender_user_id: this.currentUserId,
receiver_user_id: this.doctorId,
message_type: messageTypeMap[message.type] || 0,
message_content: message.content || '',
isFromHttp: true,
duration: message.duration || 0
};
if (typeof sendToUserApi !== 'undefined') {
sendToUserApi(requestData).then(res => {
this.scrollToBottom();
}).catch(error => {
console.error('发送失败:', error);
});
}
},
formatTime(date) {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
},
sendTextMessage() {
if (!this.newMessage.trim()) return;
this.sendMessage({
type: 'text',
content: this.newMessage
});
this.newMessage = '';
},
startVoiceRecording() {
if (this.recording) return;
uni.getSetting({
success: (res) => {
if (res.authSetting['scope.record']) {
this.hasRecordPermission = true;
this.startRecording();
} else {
uni.authorize({
scope: 'scope.record',
success: () => {
this.hasRecordPermission = true;
this.startRecording();
},
fail: () => {
this.hasRecordPermission = false;
this.voiceInputActive = false;
// uni.showModal({
// title: '录音权限',
// content: '需要录音权限才能发送语音消息,请在设置中开启',
// showCancel: true,
// cancelText: '取消',
// confirmText: '去设置',
// success: (modalRes) => {
// if (modalRes.confirm) {
// uni.openSetting();
// }
// }
// });
}
});
}
}
});
},
startRecording() {
this.recording = true;
this.showRecordingModal = true;
this.recordingTime = 0;
this.recorderManager.start({
format: 'mp3',
sampleRate: 44100,
numberOfChannels: 1,
encodeBitRate: 192000
});
this.recordingTimer = setInterval(() => {
this.recordingTime++;
if (this.recordingTime >= 60) {
this.stopVoiceRecording();
}
}, 1000);
},
stopVoiceRecording() {
if (!this.recording) return;
this.recorderManager.stop();
},
playAudio(msg) {
console.log('播放音频:', msg.content);
},
isPlaying(msg) {
return this.currentPlayingAudio === msg.id;
},
openMediaPicker(type) {
this.showMoreActions = false;
if (type === 'image') {
uni.chooseImage({
count: 9,
sourceType: ['album', 'camera'],
success: res => {
res.tempFilePaths.forEach(file => {
uni.uploadFile({
url: 'http://127.0.0.1:18001/api/mobile/chat-friends/upload-chat-file',
filePath: file,
name: 'file',
header: {
"authorization": uni.getStorageSync('token')
},
formData: file,
success: (uploadFileRes) => {
this.sendMessage({
type: 'image',
content: JSON.parse(uploadFileRes.data).result.url
});
},
fail: (res) => {
console.log(res, '上传失败');
}
});
});
},
fail: (e) => {
console.log('错误', e);
},
});
}
},
openCamera() {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: res => {
this.sendMessage({
type: 'image',
content: res.tempFilePaths[0]
});
}
});
},
sendFile() {
uni.showToast({
title: '文件发送功能',
icon: 'none'
});
},
sendPrescription() {
this.sendMessage({
type: 'prescription',
content: JSON.stringify(this.prescriptionData)
});
},
sendRegister() {
this.sendMessage({
type: 'register',
content: JSON.stringify(this.registerData)
});
},
previewImage(url) {
uni.previewImage({
urls: [url],
current: url
});
},
startCall(type) {
uni.showToast({
title: `发起${type === 'audio' ? '语音' : '视频'}通话`,
icon: 'none'
});
},
toggleVoiceInput() {
if (!this.voiceInputActive) {
uni.getSetting({
success: (res) => {
if (res.authSetting['scope.record']) {
this.hasRecordPermission = true;
this.voiceInputActive = true;
} else {
this.hasRecordPermission = false;
uni.showModal({
title: '录音权限',
content: '需要录音权限才能使用语音输入',
showCancel: true,
cancelText: '取消',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting();
}
}
});
}
}
});
} else {
this.voiceInputActive = false;
}
},
scrollToBottom() {
this.$nextTick(() => {
this.scrollTop = Date.now();
});
},
scrollToOldLastMessage() {
this.$nextTick(() => {
this.getScrollHeight();
const newHeight = this.scrollViewHeight;
this.scrollTop = newHeight - this.oldScrollViewHeight;
});
},
async loadHistory() {
if (!this.hasMore || this.loading) return;
this.loading = true;
try {
if (typeof ChatManager !== 'undefined') {
ChatManager.loadHistory(this.doctorUserInfo.room_id);
}
} finally {
this.loading = false;
}
},
getStatusText(status) {
const statusMap = {
0: '待审核',
1: '已审核',
2: '已发货',
3: '已完成',
4: '已取消'
};
return statusMap[status] || '未知状态';
},
getRegisterStatusText(status) {
const statusMap = {
0: '待就诊',
1: '已缴费',
2: '已就诊',
3: '已取消',
4: '已退费'
};
return statusMap[status] || '未知状态';
},
getPayStatusText(isPay) {
return isPay === 1 ? '已支付' : '未支付';
},
getSexText(sex) {
return sex === 1 ? '男' : '女';
},
viewPrescription(id, orderNo) {
uni.navigateTo({
url: "/subPackages/my/myrecord-detail?id=" + id + '&no=' + orderNo
});
},
viewRegister(id, orderNo) {
uni.navigateTo({
url: "/subPackages/register/register-detail?id=" + id + '&no=' + orderNo
});
}
}
};
</script>
<style lang="scss">
$primary-color: #298dff;
$doctor-bg: #e6f2ff;
$patient-bg: #e6f9ff;
$light-bg: #f5f9ff;
.empty-state {
text-align: center;
color: #999;
padding: 40rpx 0;
font-size: 28rpx;
background-color: $light-bg;
}
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: $light-bg;
overflow: hidden;
}
.chat-header {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 32rpx;
background-color: #fff;
border-bottom: 2rpx solid #eaeef5;
box-shadow: 0 4rpx 20rpx rgba(41, 141, 255, 0.1);
z-index: 20;
.header-center {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
padding: 0 80rpx;
.doctor-info {
display: flex;
flex-direction: column;
align-items: center;
}
.doctor-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.doctor-status {
font-size: 22rpx;
color: $primary-color;
}
.arrow-icon {
font-size: 22rpx;
color: $primary-color;
}
}
.header-right {
display: flex;
justify-content: flex-end;
gap: 32rpx;
.icon-btn {
padding: 8rpx;
color: $primary-color;
}
}
.doctor-detail-panel {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: #fff;
padding: 32rpx;
border-bottom-left-radius: 32rpx;
border-bottom-right-radius: 32rpx;
box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.1);
z-index: 10;
.detail-item {
padding: 16rpx 0;
font-size: 24rpx;
display: flex;
.detail-label {
font-weight: bold;
color: #666;
width: 120rpx;
}
}
}
}
.chat-messages {
flex: 1;
padding: 32rpx;
padding-bottom: 240rpx;
overflow: hidden;
background-color: #f5f9ff;
}
.message-list {
display: flex;
flex-direction: column;
}
.message-item {
margin-bottom: 32rpx;
}
.other-message, .my-message {
display: flex;
align-items: flex-start;
margin-bottom: 32rpx;
margin-right: 100rpx;
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
flex-shrink: 0;
border: 2rpx solid #eaeef5;
background-color: #fff;
}
.message-content-wrapper {
max-width: 75%;
margin: 0 20rpx;
}
.message-content {
padding: 24rpx 32rpx;
border-radius: 32rpx;
position: relative;
font-size: 28rpx;
line-height: 1.5;
word-break: break-word;
box-shadow: 0 4rpx 16rpx rgba(41, 141, 255, 0.1);
&.text {
background-color: #fff;
border: 2rpx solid #eaeef5;
}
&.image, &.video {
padding: 0;
background: transparent;
border-radius: 16rpx;
overflow: hidden;
border: 2rpx solid #eaeef5;
}
&.audio {
padding: 20rpx 28rpx;
background-color: #f0f7ff;
border: 2rpx solid #d0e6ff;
}
&.prescription, &.register {
padding: 0;
background: transparent;
border: none;
box-shadow: none;
}
}
.message-time {
font-size: 22rpx;
color: #888;
margin-top: 8rpx;
font-weight: 300;
}
}
.other-message {
.message-content {
&.text {
background-color: $doctor-bg;
border-top-left-radius: 8rpx;
border: 2rpx solid #d0e6ff;
}
}
}
.my-message {
flex-direction: row-reverse;
.message-content {
&.text {
background-color: $patient-bg;
border-top-right-radius: 8rpx;
border: 2rpx solid #cce6ff;
}
}
}
.audio-message {
display: flex;
align-items: center;
.wave-bars {
display: flex;
align-items: center;
height: 48rpx;
margin: 0 16rpx;
.wave-bar {
width: 6rpx;
background-color: #a0c8ff;
margin: 0 3rpx;
border-radius: 3rpx;
height: 16rpx;
&.wave-bar-active {
height: 40rpx;
background-color: $primary-color;
}
}
}
.duration {
font-size: 24rpx;
color: #298dff;
font-weight: 500;
}
}
.video-player {
width: 480rpx;
height: 280rpx;
border-radius: 16rpx;
overflow: hidden;
}
/* 挂号卡片样式 - 移动端优化 */
.register-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
width: 480rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid #eaeef5;
margin: 0;
}
.register-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #e8f5e8;
.register-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
flex: 1;
margin-left: 10rpx;
}
.register-status {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 32rpx;
font-weight: 500;
&.status-0 { background: #fef3c7; color: #d97706; }
&.status-1 { background: #dbeafe; color: #2563eb; }
&.status-2 { background: #d1fae5; color: #059669; }
&.status-3 { background: #fee2e2; color: #dc2626; }
&.status-4 { background: #e5e7eb; color: #4b5563; }
}
}
.register-content {
margin-bottom: 20rpx;
.info-item {
display: flex;
align-items: center;
margin-bottom: 12rpx;
font-size: 24rpx;
.info-text {
color: #555;
margin-left: 10rpx;
}
}
}
.patient-info {
background: #f8fdf8;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
.patient-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
.patient-title {
font-size: 24rpx;
font-weight: 500;
color: #333;
}
.pay-status {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
font-weight: 500;
&.pay-status-0 { background: #fee2e2; color: #dc2626; }
&.pay-status-1 { background: #d1fae5; color: #059669; }
}
}
.patient-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12rpx;
.patient-item {
display: flex;
align-items: center;
font-size: 22rpx;
color: #666;
text {
margin-left: 6rpx;
}
}
}
}
.register-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 1rpx solid #e8f5e8;
.price {
font-size: 24rpx;
font-weight: 500;
color: #059669;
}
.view-btn {
font-size: 24rpx;
color: #298dff;
padding: 8rpx 20rpx;
border-radius: 24rpx;
background: #f0f7ff;
font-weight: 500;
}
}
.register-time {
margin-top: 12rpx;
font-size: 20rpx;
color: #888;
text-align: center;
}
/* 处方卡片样式 - 移动端优化 */
.prescription-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
width: 460rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid #eaeef5;
margin: 0;
}
.prescription-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f7ff;
.prescription-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
flex: 1;
margin-left: 10rpx;
}
.prescription-status {
font-size: 20rpx;
padding: 6rpx 16rpx;
border-radius: 32rpx;
font-weight: 500;
&.status-0 { background: #fef3c7; color: #d97706; }
&.status-1 { background: #d1fae5; color: #059669; }
&.status-2 { background: #dbeafe; color: #2563eb; }
&.status-3 { background: #e5e7eb; color: #4b5563; }
&.status-4 { background: #fee2e2; color: #dc2626; }
}
}
.prescription-content {
margin-bottom: 20rpx;
.info-item {
display: flex;
align-items: center;
margin-bottom: 12rpx;
font-size: 24rpx;
.info-text {
color: #555;
margin-left: 10rpx;
}
}
}
.prescription-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20rpx;
border-top: 1rpx solid #f0f7ff;
.price {
font-size: 24rpx;
font-weight: 500;
color: #dc2626;
}
.view-btn {
font-size: 24rpx;
color: #298dff;
padding: 8rpx 20rpx;
border-radius: 24rpx;
background: #f0f7ff;
font-weight: 500;
}
}
.chat-input {
background-color: #fff;
border-top: 2rpx solid #eaeef5;
padding: 16rpx 28rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 -8rpx 24rpx rgba(41, 141, 255, 0.1);
}
.toolbar-row {
display: flex;
justify-content: space-around;
padding: 12rpx 0;
margin-bottom: 12rpx;
border-bottom: 1rpx solid #f0f7ff;
.tool-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 6rpx 12rpx;
border-radius: 12rpx;
&:active {
background-color: #f0f7ff;
}
text:last-child {
font-size: 20rpx;
color: #555;
font-weight: 500;
margin-top: 6rpx;
}
}
}
.input-row {
display: flex;
align-items: center;
gap: 16rpx;
.action-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f7ff;
&:active {
background-color: #d0e6ff;
}
}
.message-input {
flex: 1;
height: 80rpx;
padding: 0 28rpx;
background-color: #f0f7ff;
border-radius: 40rpx;
font-size: 28rpx;
color: #333;
border: 1rpx solid #d0e6ff;
&:focus {
border-color: $primary-color;
}
}
.voice-input {
flex: 1;
height: 80rpx;
background: linear-gradient(135deg, #298dff, #3a9eff);
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 28rpx;
position: relative;
font-weight: 500;
text {
margin-left: 10rpx;
}
.recording-indicator {
position: absolute;
right: 24rpx;
display: flex;
align-items: center;
.pulse {
width: 16rpx;
height: 16rpx;
background-color: #ff4d4f;
border-radius: 50%;
margin-right: 8rpx;
}
text {
font-size: 24rpx;
color: #fff;
}
}
}
.send-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(135deg, #298dff, #3a9eff);
display: flex;
align-items: center;
justify-content: center;
border: none;
box-shadow: 0 8rpx 20rpx rgba(41, 141, 255, 0.3);
&:active {
transform: scale(0.95);
}
}
}
.recording-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.recording-content {
background-color: rgba(41, 141, 255, 0.9);
padding: 48rpx 64rpx;
border-radius: 32rpx;
text-align: center;
width: 480rpx;
}
.recording-icon {
position: relative;
width: 120rpx;
height: 120rpx;
margin: 0 auto 32rpx;
}
.pulse-animation {
position: absolute;
top: 0;
left: 0;
width: 120rpx;
height: 120rpx;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 50%;
animation: pulse 1.5s infinite;
}
.recording-text {
color: white;
font-size: 32rpx;
display: block;
margin-bottom: 16rpx;
font-weight: 500;
}
.recording-tip {
color: rgba(255, 255, 255, 0.8);
font-size: 24rpx;
}
}
.loading-indicator {
display: flex;
justify-content: center;
padding: 32rpx;
font-size: 24rpx;
color: #666;
}
@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0.7;
}
50% {
transform: scale(1.2);
opacity: 0.3;
}
100% {
transform: scale(0.8);
opacity: 0.7;
}
}
</style>