fix: 在线复诊

This commit is contained in:
2025-12-23 11:05:12 +08:00
parent f2d7317982
commit e9b79b2963
23 changed files with 2526 additions and 254 deletions

View File

@@ -71,7 +71,7 @@ const initWebsocket = () => {
// 当userInfo加载完成后再执行初始化
watch(userInfoLoaded, (loaded) => {
if (loaded) {
// initWebsocket();
initWebsocket();
}
});
</script>

View File

@@ -79,9 +79,9 @@ export const usePrescriptionStore = defineStore('prescription', () => {
const methodSelected = ref();
const syndromeSelected = ref();
// 获取localStorage key
// 获取localStorage key(修改为在线复诊前缀)
const getStorageKey = () =>
`prescriptionData-chat-${currentRegisterId.value}`;
`onlineConsultation-prescriptionData${currentRegisterId.value}`;
// 计算属性
const totalProductCost = computed(() => {
@@ -188,9 +188,10 @@ export const usePrescriptionStore = defineStore('prescription', () => {
};
// 完整初始化(包含患者信息,仅用于 PrescriptionModal
const initializePrescription = async (registerId: number | string) => {
console.log('初始化处方registerId:', registerId);
const initializePrescription = async (
registerId: number | string,
shouldFetchPatientInfo: boolean = false
) => {
// 设置当前注册ID
currentRegisterId.value = registerId;
@@ -202,13 +203,15 @@ export const usePrescriptionStore = defineStore('prescription', () => {
await initializeBasicData();
}
// 获取患者信息(只在 PrescriptionModal 中调用
// 获取患者信息(只在需要时调用,如 PrescriptionModal
if (shouldFetchPatientInfo) {
if (
!isPatientInfoLoaded.value ||
patientInfo.value?.register_id !== registerId
) {
await getPatientInfo();
isPatientInfoLoaded.value = true;
}
}
};
@@ -641,7 +644,7 @@ export const usePrescriptionStore = defineStore('prescription', () => {
total: totalCost.value,
category: category.value,
drug_type: 2,
register_id: patientInfo.value.id,
register_id: currentRegisterId.value,
treatment_price: treatmentPrice.value,
package_method_id: packageMethodId.value,
process_rule_id: processRuleId.value,
@@ -661,22 +664,28 @@ export const usePrescriptionStore = defineStore('prescription', () => {
message.success('处方已发送');
sendMessage({
roomId: chatStore.currentFriend.room_id,
senderId: userStore.currentUser.id,
senderId: userStore.currentUser.doctor_id,
receiverId: chatStore.currentFriend.id,
type: 'prescription',
content: JSON.stringify(res),
});
chatStore.addMessage(
{
roomId: chatStore.currentFriend.room_id,
senderId: userStore.currentUser.id,
receiverId: chatStore.currentFriend.id,
type: 'prescription',
content: res,
id: Date.now().toString(),
room_id: chatStore.currentFriend.room_id,
sender_user_id: `doctor-${userStore.currentUser.doctor_id}`,
receiver_user_id: chatStore.currentFriend.id,
message_type: 4,
message_content: JSON.stringify(res),
messageTypeName: 'prescription',
created_at: Date.now(),
created_at_text: new Date().toLocaleTimeString().slice(0, 5),
timestamp: Date.now(),
isSent: true,
time: new Date().toLocaleTimeString().slice(0, 5),
read: true,
duration: 0,
},
userStore.currentUser.id,
userStore.currentUser.doctor_id,
);
resetForm();
});
@@ -704,7 +713,7 @@ export const usePrescriptionStore = defineStore('prescription', () => {
activeCategory.value = categoryValue;
updateCurrentDrugs([]);
localStorage.setItem(
`activeCategory-chat-${currentRegisterId.value}`,
`onlineConsultation-activeCategory${currentRegisterId.value}`,
categoryValue.toString(),
);
};

View File

@@ -61,8 +61,8 @@ export const usePrescriptionStore = () => {
const doctorSecondSign = ref(0)
const checkData = ref<any>({})
// 获取localStorage key
const getStorageKey = () => `prescriptionData-chat-${currentRegisterId.value}`
// 获取localStorage key(修改为在线复诊前缀)
const getStorageKey = () => `onlineConsultation-prescriptionData${currentRegisterId.value}`
// 计算属性
const totalProductCost = computed(() => {
@@ -579,7 +579,7 @@ export const usePrescriptionStore = () => {
const changeCategory = (categoryValue: number) => {
activeCategory.value = categoryValue
updateCurrentDrugs([])
localStorage.setItem(`activeCategory-chat-${currentRegisterId.value}`, categoryValue.toString())
localStorage.setItem(`onlineConsultation-activeCategory${currentRegisterId.value}`, categoryValue.toString())
}
// 工具函数

View File

@@ -91,7 +91,8 @@ const getAudioDuration = () => {
}
// 否则创建临时音频获取元数据
const tempAudio = new Audio(props.message.content);
const audioUrl = props.message.message_content || props.message.content || '';
const tempAudio = new Audio(audioUrl);
tempAudio.addEventListener('loadedmetadata', () => {
if (tempAudio.duration && tempAudio.duration > 0) {
audioDuration.value = Math.round(tempAudio.duration);
@@ -180,7 +181,8 @@ const getWaveHeight = (index) => {
const togglePlay = () => {
if (!audio.value) {
audio.value = new Audio(props.message.content);
const audioUrl = props.message.message_content || props.message.content || '';
audio.value = new Audio(audioUrl);
// 确保获取音频时长
audio.value.addEventListener('loadedmetadata', () => {

View File

@@ -102,7 +102,7 @@ const viewFriendProfile = () => {
</script>
<template>
<div :class="{ dark: themeStore.isDarkMode }" class="chat-header">
<div class="chat-header bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4">
<div class="header-content">
<div class="friend-info">
<div
@@ -129,7 +129,7 @@ const viewFriendProfile = () => {
<span class="status-text">
{{ isFriendOnline ? '在线' : '离线' }}
</span>
<span v-if="callStatus" class="call-status-text">
<span v-if="callStatus" class="call-status-text text-blue-500 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/20">
{{ callStatusText }}
</span>
</p>
@@ -182,17 +182,9 @@ const viewFriendProfile = () => {
<style scoped>
.chat-header {
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
padding: 16px 24px;
position: relative;
}
.chat-header.dark {
background: #2d2d2d;
border-bottom-color: #374151;
}
.header-content {
display: flex;
align-items: center;
@@ -227,14 +219,9 @@ const viewFriendProfile = () => {
.friend-name {
font-size: 18px;
font-weight: 600;
color: #1a202c;
margin: 0;
}
.chat-header.dark .friend-name {
color: #f7fafc;
}
.friend-status {
display: flex;
align-items: center;
@@ -259,11 +246,7 @@ const viewFriendProfile = () => {
}
.status-text {
color: #64748b;
}
.chat-header.dark .status-text {
color: #9ca3af;
/* 颜色已在模板中使用 Tailwind 类 */
}
.status-text.online {
@@ -278,18 +261,11 @@ const viewFriendProfile = () => {
.call-status-text {
margin-left: 8px;
font-size: 13px;
color: #3b82f6;
font-weight: 500;
background: rgba(59, 130, 246, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.chat-header.dark .call-status-text {
color: #93c5fd;
background: rgba(59, 130, 246, 0.2);
}
.header-actions {
display: flex;
align-items: center;
@@ -344,13 +320,13 @@ const viewFriendProfile = () => {
color: #475569;
}
.chat-header.dark .more-btn {
.dark .more-btn {
background: #374151;
color: #9ca3af;
border-color: #4b5563;
}
.chat-header.dark .more-btn:hover {
.dark .more-btn:hover {
background: #4b5563;
color: #d1d5db;
}
@@ -369,7 +345,7 @@ const viewFriendProfile = () => {
margin-top: 8px;
}
.chat-header.dark .more-actions-menu {
.dark .more-actions-menu {
background: #374151;
border-color: #4b5563;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
@@ -390,11 +366,11 @@ const viewFriendProfile = () => {
background: #f8fafc;
}
.chat-header.dark .menu-item {
.dark .menu-item {
color: #d1d5db;
}
.chat-header.dark .menu-item:hover {
.dark .menu-item:hover {
background: #4b5563;
}
@@ -404,7 +380,7 @@ const viewFriendProfile = () => {
color: #64748b;
}
.chat-header.dark .menu-item i {
.dark .menu-item i {
color: #9ca3af;
}
@@ -426,7 +402,7 @@ const viewFriendProfile = () => {
/* 响应式设计 */
@media (max-width: 768px) {
.chat-header {
padding: 12px 16px;
@apply px-4 py-3;
}
.friend-avatar {

View File

@@ -1,7 +1,7 @@
<template>
<div
class="custom-textarea-container"
:class="{ 'dark': isDark, 'focused': isFocused, 'disabled': disabled }"
class="custom-textarea-container bg-white dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-600"
:class="{ 'focused': isFocused, 'disabled': disabled }"
@drop="handleDrop"
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@@ -38,10 +38,7 @@
</template>
<script setup>
import {ref, computed, nextTick, watch} from 'vue';
import {useThemeStore} from '../stores/theme.ts';
const themeStore = useThemeStore();
import {ref, nextTick, watch} from 'vue';
const props = defineProps({
modelValue: {
@@ -91,8 +88,6 @@ const emit = defineEmits(['update:modelValue', 'focus', 'blur', 'clear', 'keydow
const textareaRef = ref(null);
const isFocused = ref(false);
const isDark = computed(() => themeStore.isDarkMode);
const handleInput = (event) => {
emit('update:modelValue', event.target.value);
@@ -203,8 +198,6 @@ defineExpose({
<style scoped>
.custom-textarea-container {
position: relative;
background: #ffffff;
border: 2px solid #e2e8f0;
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
@@ -214,32 +207,26 @@ defineExpose({
border-color: #cbd5e1;
}
.dark .custom-textarea-container:hover {
border-color: #6b7280;
}
.custom-textarea-container.focused {
border-color: #4361ee;
box-shadow: 0 0 0 4px rgba(67, 97, 238, 0.1);
}
.dark .custom-textarea-container.focused {
box-shadow: 0 0 0 4px rgba(67, 97, 238, 0.2);
}
.custom-textarea-container.disabled {
background: #f8fafc;
border-color: #e2e8f0;
cursor: not-allowed;
}
.custom-textarea-container.dark {
background: #374151;
border-color: #4b5563;
}
.custom-textarea-container.dark:hover {
border-color: #6b7280;
}
.custom-textarea-container.dark.focused {
border-color: #4361ee;
box-shadow: 0 0 0 4px rgba(67, 97, 238, 0.2);
}
.custom-textarea-container.dark.disabled {
.dark .custom-textarea-container.disabled {
background: #1f2937;
border-color: #374151;
}
@@ -251,7 +238,6 @@ defineExpose({
background: transparent;
padding: 12px 16px;
font-size: 14px;
color: #1a202c;
line-height: 1.5;
resize: none;
font-family: inherit;
@@ -261,20 +247,16 @@ defineExpose({
color: #94a3b8;
}
.dark .custom-textarea::placeholder {
color: #6b7280;
}
.custom-textarea:disabled {
cursor: not-allowed;
color: #94a3b8;
}
.custom-textarea-container.dark .custom-textarea {
color: #f7fafc;
}
.custom-textarea-container.dark .custom-textarea::placeholder {
color: #6b7280;
}
.custom-textarea-container.dark .custom-textarea:disabled {
.dark .custom-textarea:disabled {
color: #6b7280;
}
@@ -304,7 +286,7 @@ defineExpose({
background: #f1f5f9;
}
.custom-textarea-container.dark .clear-btn:hover {
.dark .clear-btn:hover {
color: #d1d5db;
background: #4b5563;
}

View File

@@ -46,7 +46,7 @@ const getListTitle = () => {
const selectFriend = (friend) => {
chatStore.setCurrentFriend(friend);
// 清除未读消息并加载聊天记录
chatStore.switchFriend(friend, userStore.currentUser.id);
chatStore.switchFriend(friend, userStore.currentUser.doctor_id);
};
// 监听当前视图变化,清空搜索

View File

@@ -74,7 +74,7 @@ const filteredFriends = computed(() => {
const startChat = (friend) => {
chatStore.setCurrentFriend(friend);
chatStore.switchFriend(friend, userStore.currentUser.id);
chatStore.switchFriend(friend, userStore.currentUser.doctor_id);
};
</script>

View File

@@ -9,6 +9,41 @@ import { Avatar, Button, Image } from 'ant-design-vue';
import { getChatMessageRegisterInfoApi } from '#/views/business/chat/api';
import { useChatStore } from '#/views/business/chat/stores/chat';
import { sendMessage } from '#/views/business/chat/utils/request';
// 辅助函数:按需解析消息内容
const getParsedContent = (messageType, messageContent) => {
// 对于需要 JSON 解析的消息类型4, 10, 11, 12, 13
if ([4, 10, 11, 12, 13].includes(messageType)) {
try {
return JSON.parse(messageContent);
} catch (e) {
console.error('解析消息内容失败:', e);
return messageContent;
}
}
return messageContent;
};
// 辅助函数:获取消息类型名称
const getMessageTypeName = (messageType) => {
const types = {
0: 'text',
1: 'image',
2: 'audio',
3: 'video',
4: 'prescription',
5: 'medical-record',
6: 'video-call',
7: 'audio-call',
8: 'file',
9: 'system',
10: 'register',
11: 'patient-experience',
12: 'product-card',
13: 'end-consultation',
};
return types[messageType] || 'unknown';
};
import { getPrescriptionCheckStatusApi } from '#/views/business/order/prescription/api';
import { receptionApi } from '#/views/doctor/doctor-reception/api';
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
@@ -18,6 +53,9 @@ import previewMedia from '../composables/useMediaPreview.ts';
import { useThemeStore } from '../stores/theme.ts';
import { useUserStore as chatUseUserStore } from '../stores/user';
import AudioMessage from './AudioMessage.vue';
import PatientExperienceCard from '#/views/doctor/online-consultation/components/PatientExperienceCard.vue';
import ProductCard from '#/views/doctor/online-consultation/components/ProductCard.vue';
import EndConsultationCard from '#/views/doctor/online-consultation/components/EndConsultationCard.vue';
const props = defineProps({
message: {
@@ -80,20 +118,41 @@ const payStatusMap = {
1: '已支付',
};
const checkPrescriptionStatus = (id) => {
if (!id) return;
getPrescriptionCheckStatusApi({ id }).then((res) => {
const checkPrescriptionStatus = (parsedContent) => {
// 优先使用 message_content 中的 status 字段(由定时任务更新)
if (parsedContent && parsedContent.status !== undefined) {
statusInfo.value.status = parsedContent.status;
statusInfo.value.text = prescriptionStatusMap[parsedContent.status];
return prescriptionStatusMap[parsedContent.status];
}
// 如果 status 不存在,才调用 API作为备用方案
if (parsedContent && parsedContent.id) {
getPrescriptionCheckStatusApi({ id: parsedContent.id }).then((res) => {
statusInfo.value.status = res.status;
statusInfo.value.text = prescriptionStatusMap[res.status];
return prescriptionStatusMap[res.status];
});
}
};
// 计算属性:解析后的消息内容
const parsedContent = computed(() => {
// 确保 message_content 字段存在,优先使用 message_content其次使用 content
const messageContent = props.message.message_content || props.message.content || '';
return getParsedContent(props.message.message_type, messageContent);
});
// 计算属性:消息类型名称
const messageTypeName = computed(() => {
return getMessageTypeName(props.message.message_type);
});
const getRegisterCardInfo = () => {
getChatMessageRegisterInfoApi({ id: props.message.content.id }).then(
const content = getParsedContent(props.message.message_type, props.message.message_content);
getChatMessageRegisterInfoApi({ id: content.id }).then(
(res) => {
// eslint-disable-next-line vue/no-mutating-props
props.message.content = res;
props.message.message_content = JSON.stringify(res);
},
);
};
@@ -113,20 +172,20 @@ const acceptRegister = async (registerId, orderNo) => {
getRegisterCardInfo();
const messageObj = {
type: 'text',
content: '我已接诊,请简单说明您的情况~',
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: `${chatUserStore.currentUser.id}`,
receiverId: chatStore.currentFriend.id,
message_type: 0,
message_content: '我已接诊,请简单说明您的情况~',
created_at_text: new Date().toLocaleTimeString().slice(0, 5),
sender_user_id: `doctor-${chatUserStore.currentUser.doctor_id}`,
receiver_user_id: chatStore.currentFriend.id,
isSent: true,
read: true,
duration: 0,
};
localStorage.setItem('currentRegisterId', reregisterId);
// chatStore.currentFriend?.register_id = reregisterId;
chatStore.addMessage(messageObj, chatUserStore.currentUser.id);
chatStore.addMessage(messageObj, chatUserStore.currentUser.doctor_id);
await sendMessage({
senderId: chatUserStore.currentUser.id,
senderId: chatUserStore.currentUser.doctor_id,
receiverId: chatStore.currentFriend.id,
roomId: chatStore.currentFriend.room_id,
type: 'text',
@@ -164,7 +223,7 @@ const bubbleClasses = computed(() => {
baseClasses.push('received');
}
if (props.message.type === 'text') {
if (props.message.message_type === 0) {
baseClasses.push('bubble-bg');
}
@@ -192,6 +251,10 @@ const formatDuration = (seconds) => {
const formatTextWithLinks = (text) => {
console.log(text, props.message);
if (!text) return text; // 或 return text (但可能返回 undefined/null根据需求调整)
// 如果 text 是对象,转换为字符串
if (typeof text === 'object') {
text = JSON.stringify(text);
}
const urlRegex = /(https?:\/\/\S+)/g;
return text.replaceAll(
urlRegex,
@@ -251,7 +314,7 @@ const getSexText = (sex) => {
return sex === 1 ? '男' : '女';
};
if (props.message.type === 'register') {
if (props.message.message_type === 10) {
getRegisterCardInfo();
}
</script>
@@ -309,12 +372,12 @@ if (props.message.type === 'register') {
<div :class="bubbleClasses" class="relative rounded-2xl p-4 shadow-sm">
<!-- 图片消息 -->
<div
v-if="message.type === 'image'"
v-if="message.message_type === 1"
class="relative cursor-pointer"
@click="handlePreviewMedia(message.content, 'image')"
@click="handlePreviewMedia(parsedContent, 'image')"
>
<Image
:src="message.content"
:src="parsedContent"
alt="图片消息"
class="max-w-full rounded-lg"
@error="handleImageError"
@@ -323,12 +386,12 @@ if (props.message.type === 'register') {
<!-- 视频消息 -->
<div
v-else-if="message.type === 'video'"
v-else-if="message.message_type === 3"
class="video-message relative cursor-pointer"
@click="handlePreviewMedia(message.content, 'video')"
@click="handlePreviewMedia(parsedContent, 'video')"
>
<video
:src="message.content"
:src="parsedContent"
class="video-thumbnail max-w-full rounded-lg"
preload="metadata"
@error="handleVideoError"
@@ -344,7 +407,7 @@ if (props.message.type === 'register') {
</div>
<!-- URL链接消息 -->
<div v-else-if="message.type === 'url'" class="url-message">
<div v-else-if="messageTypeName === 'url'" class="url-message">
<div class="url-preview" @click="openUrl(message.url)">
<div class="url-favicon">
<img
@@ -372,13 +435,13 @@ if (props.message.type === 'register') {
<!-- 音频消息 -->
<AudioMessage
v-else-if="message.type === 'audio'"
v-else-if="message.message_type === 2"
:is-sent="isSent"
:message="message"
/>
<!-- 挂号卡片消息 -->
<div v-else-if="message.type === 'register'" class="register-card">
<div v-else-if="message.message_type === 10" class="register-card">
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-green-100 dark:bg-green-900"
@@ -396,20 +459,20 @@ if (props.message.type === 'register') {
<div
:class="{
'bg-yellow-100 text-yellow-800':
message.content.status === 0,
'bg-blue-100 text-blue-800': message.content.status === 1,
'bg-green-100 text-green-800': message.content.status === 2,
'bg-red-100 text-red-800': message.content.status === 3,
'bg-gray-100 text-gray-800': message.content.status === 4,
parsedContent.status === 0,
'bg-blue-100 text-blue-800': parsedContent.status === 1,
'bg-red-100 text-red-800': parsedContent.status === 2,
'bg-red-100 text-red-800': parsedContent.status === 3,
'bg-gray-100 text-gray-800': parsedContent.status === 4,
'bg-purple-100 text-purple-800':
message.content.status === 5,
parsedContent.status === 5,
'bg-indigo-100 text-indigo-800':
message.content.status === 6,
'bg-pink-100 text-pink-800': message.content.status === 7,
parsedContent.status === 6,
'bg-pink-100 text-pink-800': parsedContent.status === 7,
}"
class="rounded-full px-2 py-1 text-xs font-medium"
>
{{ registerStatusMap[message.content.status] || '未知状态' }}
{{ registerStatusMap[parsedContent.status] || '未知状态' }}
</div>
</div>
@@ -419,13 +482,13 @@ if (props.message.type === 'register') {
<div class="flex items-center gap-1">
<i class="fas fa-hashtag text-xs"></i>
<span class="max-w-[120px] truncate">{{
message.content.order_no
parsedContent.order_no
}}</span>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-list-ol text-xs"></i>
<span>{{ message.content.order_number }}</span>
<span>{{ parsedContent.order_number }}</span>
</div>
</div>
</div>
@@ -439,12 +502,12 @@ if (props.message.type === 'register') {
</div>
<div
:class="{
'bg-red-100 text-red-800': message.content.is_pay === 0,
'bg-green-100 text-green-800': message.content.is_pay === 1,
'bg-red-100 text-red-800': parsedContent.is_pay === 0,
'bg-green-100 text-green-800': parsedContent.is_pay === 1,
}"
class="rounded-full px-2 py-1 text-xs font-medium"
>
{{ payStatusMap[message.content.is_pay] || '未知' }}
{{ payStatusMap[parsedContent.is_pay] || '未知' }}
</div>
</div>
@@ -453,20 +516,20 @@ if (props.message.type === 'register') {
>
<div class="flex items-center gap-1">
<i class="fas fa-user text-xs"></i>
<span>{{ message.content.user_patient?.name || '未知' }}</span>
<span>{{ parsedContent.user_patient?.name || '未知' }}</span>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-birthday-cake text-xs"></i>
<span>{{ message.content.user_patient?.age || 0 }}</span>
<span>{{ parsedContent.user_patient?.age || 0 }}</span>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-venus-mars text-xs"></i>
<span>{{ getSexText(message.content.user_patient?.sex) }}</span>
<span>{{ getSexText(parsedContent.user_patient?.sex) }}</span>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-phone text-xs"></i>
<span>{{
message.content.user_patient?.mobile ? '已绑定' : '未绑定'
parsedContent.user_patient?.mobile ? '已绑定' : '未绑定'
}}</span>
</div>
</div>
@@ -477,19 +540,19 @@ if (props.message.type === 'register') {
<div class="mb-2 flex items-center justify-between">
<div class="text-sm font-medium text-gray-700 dark:text-gray-200">
挂号费用:
<span class="text-green-500 dark:text-green-400">¥{{ message.content.price }}</span>
<span class="text-green-500 dark:text-green-400">¥{{ parsedContent.price }}</span>
</div>
</div>
<!-- 接诊/拒诊按钮 - 只在待就诊状态显示 -->
<div v-if="message.content.status === 1" class="mt-2 flex gap-2">
<div v-if="parsedContent.status === 1" class="mt-2 flex gap-2">
<Button
:loading="registerOperating"
class="flex-1"
size="small"
type="primary"
@click="
acceptRegister(message.content.id, message.content.order_no)
acceptRegister(parsedContent.id, parsedContent.order_no)
"
>
<i class="fas fa-check mr-1"></i>
@@ -501,7 +564,7 @@ if (props.message.type === 'register') {
danger
size="small"
@click="
rejectRegister(message.content.id, message.content.order_no)
rejectRegister(parsedContent.id, parsedContent.order_no)
"
>
<i class="fas fa-times mr-1"></i>
@@ -520,18 +583,18 @@ if (props.message.type === 'register') {
<!-- 时间信息 -->
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
创建时间: {{ message.content.created_at }}
<span v-if="message.content.pay_time" class="ml-2">
支付时间: {{ formatDateTime(message.content.pay_time) }}
创建时间: {{ parsedContent.created_at }}
<span v-if="parsedContent.pay_time" class="ml-2">
支付时间: {{ formatDateTime(parsedContent.pay_time) }}
</span>
</div>
</div>
<!-- 处方卡片消息 -->
<div
v-else-if="message.type === 'prescription'"
v-else-if="message.message_type === 4"
class="prescription-card"
@click="viewPrescription(message.content)"
@click="viewPrescription(parsedContent)"
>
<div class="flex items-center gap-3">
<div
@@ -558,7 +621,7 @@ if (props.message.type === 'register') {
class="rounded-full px-2 py-1 text-xs font-medium"
>
{{
checkPrescriptionStatus(message.content.id) ||
checkPrescriptionStatus(parsedContent) ||
statusInfo.text ||
'加载中'
}}
@@ -571,13 +634,13 @@ if (props.message.type === 'register') {
<div class="flex items-center gap-1">
<i class="fas fa-hashtag text-xs"></i>
<span class="max-w-[100px] truncate">{{
message.content.order_no
parsedContent.order_no
}}</span>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-truck text-xs"></i>
<span>{{ message.content.express_name }}</span>
<span>{{ parsedContent.express_name }}</span>
</div>
</div>
</div>
@@ -589,7 +652,7 @@ if (props.message.type === 'register') {
<div class="text-sm font-medium text-gray-700 dark:text-gray-200">
支付金额:
<span class="text-red-500 dark:text-red-400">
¥{{ message.content.total_pay_price }}
¥{{ parsedContent.total_pay_price }}
</span>
</div>
@@ -604,14 +667,14 @@ if (props.message.type === 'register') {
<!-- 在线复诊|购药信息 -->
<div
v-else-if="message.type === 'follow-up-visit'"
v-else-if="messageTypeName === 'follow-up-visit'"
class="group transition-all duration-200"
>
<!-- 卡片容器 -->
<div
:class="{
'border-green-200': message.content.hasVisited === '1', // 已到访状态
'border-yellow-200': message.content.hasVisited === '0', // 未到访状态
'border-green-200': parsedContent.hasVisited === '1', // 已到访状态
'border-yellow-200': parsedContent.hasVisited === '0', // 未到访状态
}"
class="rounded-lg border p-4 shadow-sm"
>
@@ -632,17 +695,17 @@ if (props.message.type === 'register') {
stroke-width="2"
/>
</svg>
{{ message.content.drug_name }} 复诊信息
{{ parsedContent.drug_name }} 复诊信息
</h3>
<span
:class="{
'text-green-800': message.content.hasVisited === '1',
'text-green-800': parsedContent.hasVisited === '1',
'bg-yellow-100 text-yellow-800':
message.content.hasVisited === '0',
parsedContent.hasVisited === '0',
}"
class="rounded-full px-2 py-1 text-xs"
>
{{ message.content.hasVisited === '1' ? '就诊过' : '未就诊过' }}
{{ parsedContent.hasVisited === '1' ? '就诊过' : '未就诊过' }}
</span>
</div>
@@ -650,21 +713,21 @@ if (props.message.type === 'register') {
<div class="space-y-2">
<p class="flex items-start text-sm">
<span class="w-20">症状描述</span>
<span class="flex-1">{{ message.content.illnessInfo }}</span>
<span class="flex-1">{{ parsedContent.illnessInfo }}</span>
</p>
<!-- 用药状态 -->
<p
:class="{
'font-medium text-blue-600':
message.content.hasUsedDrug === '1',
'text-gray-500': message.content.hasUsedDrug === '0',
parsedContent.hasUsedDrug === '1',
'text-gray-500': parsedContent.hasUsedDrug === '0',
}"
class="text-sm"
>
<span class="w-20 text-gray-500">用药情况</span>
{{
message.content.hasUsedDrug === '1' ? '使用过' : '未使用过'
parsedContent.hasUsedDrug === '1' ? '使用过' : '未使用过'
}}
</p>
@@ -673,22 +736,41 @@ if (props.message.type === 'register') {
class="mt-3 border-t border-gray-100 pt-3 text-sm text-gray-500"
>
<span class="w-20 text-gray-400">主诊医生</span>
ID: {{ message.content.doctor_id }}
ID: {{ parsedContent.doctor_id }}
</p>
</div>
</div>
</div>
<!-- 患者就诊经历卡片 (type=11) -->
<PatientExperienceCard
v-else-if="message.message_type === 11"
:content="parsedContent"
/>
<!-- 商品卡片 (type=12) -->
<ProductCard
v-else-if="message.message_type === 12"
:content="parsedContent"
@click-product="(product) => console.log('点击商品:', product)"
/>
<!-- 结束问诊卡片 (type=13) -->
<EndConsultationCard
v-else-if="message.message_type === 13"
:content="parsedContent"
/>
<!-- 通话消息 -->
<div
v-else-if="
message.type === 'video-call' || message.type === 'voice-call'
message.message_type === 6 || message.message_type === 7
"
class="call-message"
>
<div class="call-info">
<i
:class="message.type === 'video-call' ? 'fa-video' : 'fa-phone'"
:class="message.message_type === 6 ? 'fa-video' : 'fa-phone'"
class="fas"
></i>
<span>{{ getCallStatusText(message.callStatus) }}</span>
@@ -700,7 +782,7 @@ if (props.message.type === 'register') {
<!-- 文本消息 -->
<div v-else class="whitespace-pre-wrap text-base leading-relaxed">
<span v-html="formatTextWithLinks(message.content)"></span>
<span v-html="formatTextWithLinks(parsedContent || message.message_content || message.content || '')"></span>
</div>
</div>

View File

@@ -1,7 +1,8 @@
<script setup>
import { ref, provide, nextTick, onMounted, onUnmounted } from 'vue';
import { ref, provide, nextTick, onMounted, onUnmounted, computed } from 'vue';
import { message } from 'ant-design-vue';
import { message, Modal } from 'ant-design-vue';
import { PlusOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { useFileUpload } from '../composables/useFileUpload.ts';
import { useRecording } from '../composables/useRecording.ts';
@@ -9,9 +10,13 @@ import { useChatStore } from '../stores/chat.ts';
import { useThemeStore } from '../stores/theme.ts';
import { useUserStore } from '../stores/user.ts';
import { sendMessage } from '../utils/request.ts';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
import { endConsultation } from '#/views/doctor/online-consultation/api/index';
import CustomTextarea from './CustomTextarea.vue';
import EmojiPicker from './EmojiPicker.vue';
import FileUploadPreview from './FileUploadPreview.vue';
import QuickReplyBubbles from '#/views/doctor/online-consultation/components/QuickReplyBubbles.vue';
// 定义 props
const props = defineProps({
@@ -24,6 +29,7 @@ const props = defineProps({
const userStore = useUserStore();
const chatStore = useChatStore();
const themeStore = useThemeStore();
const prescriptionStore = usePrescriptionStore();
const messageText = ref('');
const messageInputRef = ref(null);
@@ -46,21 +52,36 @@ const { isRecording, startRecording, stopRecording } = useRecording();
const sendUploadedFile = () => {
if (!uploadPreview.value) return;
// 消息类型映射
const messageTypeMap = {
text: 0,
image: 1,
audio: 2,
video: 3,
prescription: 4,
'medical-record': 5,
'video-call': 6,
'audio-call': 7,
file: 8,
};
const messageObj = {
type: uploadPreview.value.type,
content: uploadPreview.value.url,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: `${userStore.currentUser.id}`,
receiverId: chatStore.currentFriend.id,
message_type: messageTypeMap[uploadPreview.value.type] || 0,
message_content: uploadPreview.value.url,
created_at_text: new Date().toLocaleTimeString().slice(0, 5),
created_at: Date.now(),
sender_user_id: `doctor-${userStore.currentUser.doctor_id}`,
receiver_user_id: chatStore.currentFriend.id,
room_id: chatStore.currentFriend.room_id,
read: true,
duration: 0,
};
chatStore.addMessage(messageObj, userStore.currentUser.id);
chatStore.addMessage(messageObj, userStore.currentUser.doctor_id);
// 发送到服务器
sendMessage({
senderId: userStore.currentUser.id,
senderId: userStore.currentUser.doctor_id,
receiverId: chatStore.currentFriend.id,
type: uploadPreview.value.type,
content: uploadPreview.value.url,
@@ -92,26 +113,45 @@ const sendTextMessage = async () => {
uploadPreview.value = null;
}
// 创建消息对象
// 消息类型映射
const messageTypeMap = {
text: 0,
image: 1,
audio: 2,
video: 3,
prescription: 4,
'medical-record': 5,
'video-call': 6,
'audio-call': 7,
file: 8,
register: 10,
'patient-experience': 11,
'product-card': 12,
'end-consultation': 13,
};
// 创建消息对象 - 使用数据库字段名
const messageObj = {
type: messageType,
content: messageContent,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: `${userStore.currentUser.id}`,
receiverId: chatStore.currentFriend.id,
message_type: messageTypeMap[messageType] || 0,
message_content: messageContent,
created_at_text: new Date().toLocaleTimeString().slice(0, 5),
created_at: Date.now(),
sender_user_id: `doctor-${userStore.currentUser.doctor_id}`,
receiver_user_id: chatStore.currentFriend.id,
room_id: chatStore.currentFriend.room_id,
isSent: true,
read: true,
duration: 0,
};
// return console.log('发送消息:' + userStore.currentUser.id, messageObj);
// return console.log('发送消息:' + userStore.currentUser.doctor_id, messageObj);
// 添加到本地消息列表
chatStore.addMessage(messageObj, userStore.currentUser.id);
chatStore.addMessage(messageObj, userStore.currentUser.doctor_id);
// 发送到服务器
try {
await sendMessage({
senderId: userStore.currentUser.id,
senderId: userStore.currentUser.doctor_id,
receiverId: chatStore.currentFriend.id,
roomId: chatStore.currentFriend.room_id,
type: messageType,
@@ -208,21 +248,23 @@ const handleAudioRecorded = (event) => {
const { url, duration } = event.detail;
const messageObj = {
type: 'audio',
content: url,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: `${userStore.currentUser.id}`,
receiverId: chatStore.currentFriend.id,
message_type: 2, // audio
message_content: url,
created_at_text: new Date().toLocaleTimeString().slice(0, 5),
created_at: Date.now(),
sender_user_id: `doctor-${userStore.currentUser.doctor_id}`,
receiver_user_id: chatStore.currentFriend.id,
room_id: chatStore.currentFriend.room_id,
isSent: true,
read: true,
duration,
};
chatStore.addMessage(messageObj, userStore.currentUser.id);
chatStore.addMessage(messageObj, userStore.currentUser.doctor_id);
// 发送到服务器
sendMessage({
senderId: userStore.currentUser.id,
senderId: userStore.currentUser.doctor_id,
receiverId: chatStore.currentFriend.id,
roomId: chatStore.currentFriend.room_id,
isSent: true,
@@ -262,7 +304,6 @@ const emit = defineEmits([
// 开方功能触发函数
const handleOpenPrescription = () => {
// 获取当前会话的register_id这里假设可以通过某种方式获取
console.log(chatStore.currentFriend.register_id, 'sssssssssssssss')
// 例如从聊天存储或props中获取
const registerId = chatStore.currentFriend?.register_id || 66;
@@ -275,10 +316,160 @@ const handleOpenPrescription = () => {
}
};
// 处理快速回复选择
const handleQuickReplySelect = (content) => {
// 设置输入框内容并发送
messageText.value = content;
sendTextMessage();
};
// 判断是否显示在线复诊相关按钮
const showOnlineConsultationButtons = computed(() => {
// 检查是否有 register_id如果有则说明是在线复诊场景
return !!chatStore.currentFriend?.register_id;
});
// 将患者就诊经历中的药品加入处方单
const handleAddPatientProductsToPrescription = async () => {
// 检查处方store是否已初始化
if (!prescriptionStore.currentRegisterId) {
const registerId = chatStore.currentFriend?.register_id;
if (registerId) {
prescriptionStore.initializePrescription(registerId, false);
} else {
message.warning('请先接诊患者');
return;
}
}
// 从聊天记录中筛选出所有 message_type=11 的消息(患者就诊经历)
const experienceMessages = chatStore.messages.filter(
(msg) => msg.message_type === 11
);
if (experienceMessages.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取诊所ID
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
}
}
// 按时间排序,取最新的就诊经历
const sortedMessages = [...experienceMessages].sort((a, b) => {
const timeA = a.created_at ? new Date(a.created_at).getTime() : (a.timestamp || 0);
const timeB = b.created_at ? new Date(b.created_at).getTime() : (b.timestamp || 0);
return timeB - timeA; // 降序排列,最新的在前
});
const latestMessage = sortedMessages[0];
try {
// 解析最新消息的内容
let experienceData;
try {
experienceData = typeof latestMessage.message_content === 'string'
? JSON.parse(latestMessage.message_content)
: latestMessage.message_content;
} catch (e) {
console.error('解析就诊经历消息失败:', e);
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取药品名称
const drugName = experienceData.drug_name;
if (!drugName) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('该药品已在处方中');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已将${drugData.drug.drug_name}添加到处方单`);
// 触发打开处方模态框事件
emit('openPrescription', prescriptionStore.currentRegisterId || chatStore.currentFriend?.register_id);
}
} catch (error) {
console.error('添加药品失败:', error);
message.error('添加药品失败,请重试');
}
};
// 结束问诊
const handleEndConsultation = () => {
const registerId = chatStore.currentFriend?.register_id;
if (!registerId) {
message.warning('患者信息不完整,无法结束接诊');
return;
}
const patientName = chatStore.currentFriend?.nick_name || '患者';
Modal.confirm({
title: '确认结束接诊',
content: `确定要结束${patientName}的复诊吗?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await endConsultation({
register_id: registerId,
doctor_id: userStore.currentUser?.doctor_id || '39',
reason: '医生主动结束'
});
message.success('结束接诊成功');
} catch (error) {
console.error('结束接诊失败:', error);
message.error('结束接诊失败,请重试');
}
}
});
};
</script>
<template>
<div class="message-input-container" :class="{ dark: themeStore.isDarkMode }">
<div class="message-input-container flex-shrink-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
<!-- 快速回复气泡仅在线复诊场景显示 -->
<QuickReplyBubbles
v-if="chatStore.currentFriend"
:doctor-id="'32'"
@select-reply="handleQuickReplySelect"
/>
<!-- 文件上传预览 -->
<FileUploadPreview v-if="uploadPreview" />
@@ -328,6 +519,24 @@ const handleOpenPrescription = () => {
>
<i class="fas fa-microphone"></i>
</button>
<!-- 添加药品按钮仅在线复诊场景显示 -->
<button
v-if="showOnlineConsultationButtons"
class="function-btn"
title="将患者就诊经历中的药品加入处方单"
@click="handleAddPatientProductsToPrescription"
>
<PlusOutlined />
</button>
<!-- 结束问诊按钮仅在线复诊场景显示 -->
<button
v-if="showOnlineConsultationButtons"
class="function-btn end-consultation-btn"
title="结束问诊"
@click="handleEndConsultation"
>
<CloseOutlined />
</button>
</div>
<div class="input-container">
@@ -378,14 +587,12 @@ const handleOpenPrescription = () => {
<style scoped>
.message-input-container {
padding: 20px;
border-top: 1px solid #e2e8f0;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
position: relative;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
}
.message-input-container.dark {
border-top-color: #374151;
background: linear-gradient(135deg, #2d2d2d 0%, #1f2937 100%);
.dark .message-input-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
}
.input-container {
@@ -398,7 +605,11 @@ const handleOpenPrescription = () => {
display: flex;
align-items: center;
padding: 8px;
border-top: 1px solid #666666;
border-top: 1px solid #e2e8f0;
}
.dark .toolbar {
border-top-color: #374151;
}
.function-btn {
@@ -413,10 +624,18 @@ const handleOpenPrescription = () => {
color: #666;
}
.dark .function-btn {
color: #9ca3af;
}
.function-btn:hover {
background-color: #f0f0f0;
}
.dark .function-btn:hover {
background-color: #374151;
}
.prescription-button {
color: #455cda;
font-weight: 500;
@@ -426,6 +645,21 @@ const handleOpenPrescription = () => {
background-color: #e6e9ff;
}
.end-consultation-btn {
color: #ff4d4f;
font-weight: 500;
}
.end-consultation-btn:hover {
background-color: #fff1f0;
color: #ff4d4f;
}
.dark .end-consultation-btn:hover {
background-color: rgba(255, 77, 79, 0.2);
color: #ff4d4f;
}
.function-buttons {
display: flex;
gap: 12px;
@@ -521,12 +755,12 @@ const handleOpenPrescription = () => {
height: 100px;
}
.message-input-container.dark .function-btn {
.dark .function-btn {
background: linear-gradient(135deg, #374151, #4b5563);
color: #9ca3af;
}
.message-input-container.dark .function-btn:hover {
.dark .function-btn:hover {
background: linear-gradient(135deg, #4361ee, #3f37c9);
color: white;
}

View File

@@ -20,7 +20,7 @@ const oldScrollHeight = ref(0); // 已加载次数
// 获取发送者信息
const getSenderInfo = (message) => {
return message.senderId === userStore.currentUser.id
return message.senderId === userStore.currentUser.doctor_id
? {
nick_name: userStore.currentUser.nick_name,
avatar: userStore.currentUser.nick_name.charAt(0),
@@ -126,8 +126,9 @@ onMounted(() => {
<template>
<div
ref="messagesContainer"
class="message-list-background flex-1 overflow-y-auto bg-gray-50 p-5 dark:bg-gray-900"
class="message-list-background flex-1 overflow-y-auto bg-gray-50 p-5 dark:bg-gray-900 min-h-0"
@scroll="handleScroll"
style="max-height: 45vh;"
>
<Spin :spinning="loadingMessages" tip="加载消息中...">
<!-- 空状态 -->

View File

@@ -78,9 +78,10 @@ const [Modal, modalApi] = useVbenModal({
modalData.value = modalApi.getData<Record<string, any>>();
if (modalData.value?.registerId) {
// 使用完整初始化,包含患者信息
// 使用完整初始化,包含患者信息(传递 true 以获取患者信息)
await prescriptionStore.initializePrescription(
modalData.value.registerId,
true,
);
prescriptionStore.loadFromLocalStorage();
} else {

View File

@@ -149,7 +149,7 @@ onUnmounted(() => {
<PrescriptionModalComponent />
<!-- 聊天区域 -->
<div class="min-w-0 flex-1">
<div class="min-w-0 flex-1 h-full overflow-hidden">
<ChatArea
v-if="chatStore.currentFriend && currentNav === 'chat'"
@open-prescription="openPrescriptionModule"

View File

@@ -58,7 +58,9 @@ export const useChatStore = defineStore('chat', () => {
hasMore.value = false;
return;
}
const newMessages = checkMyMessage(res.list, currentFriend.value.id);
// 使用当前用户的IDdoctor_id来判断消息是否是自己发送的
const currentUserDoctorId = userStore.currentUser?.doctor_id;
const newMessages = checkMyMessage(res.list, currentUserDoctorId);
messages.value.unshift(...newMessages);
console.log('load message ok', hasMore.value);
});
@@ -148,8 +150,12 @@ export const useChatStore = defineStore('chat', () => {
throw new Error('缺少通话状态');
}
// 确保 sender_user_id 有 doctor- 前缀
const senderId = userStore.currentUser.doctor_id.toString();
const normalizedSenderId = senderId.startsWith('doctor-') ? senderId : `doctor-${senderId}`;
const requestData = {
sender_user_id: userStore.currentUser.id,
sender_user_id: normalizedSenderId,
receiver_user_id: signal.receiver_user_id,
message_type:
signal.call_type || (signal.call_status === 'invite' ? 6 : 7),
@@ -223,30 +229,48 @@ export const useChatStore = defineStore('chat', () => {
room_id: friend.room_id,
}).then((res) => {
lastMessageId.value = res.last_message_id;
messages.value = checkMyMessage(res.list, friend.id);
// 使用当前用户的IDdoctor_id来判断消息是否是自己发送的
const currentUserDoctorId = userStore.currentUser?.doctor_id;
messages.value = checkMyMessage(res.list, currentUserDoctorId);
});
};
/**
* 转化消息格式
* 转化消息格式 - 直接返回数据库字段,不进行转换
* @param messages
* @param currentUserId
*/
const checkMyMessage = (messages, currentUserId) => {
return messages.map((message) => {
// isSent 表示消息是否是自己发送的
// 需要处理不同的 sender_user_id 格式:
// - 如果 sender_user_id 等于 currentUserId说明是自己发送的
// - 如果 sender_user_id 是 "doctor-{currentUserId}" 格式,说明是自己发送的
// - 如果 sender_user_id 是 "user-{currentUserId}" 格式,说明是自己发送的
// 使用 == 而不是 === 进行类型宽松比较,避免类型转换
const senderId = message.sender_user_id;
// 只在需要调用 startsWith 时才检查类型
const currentUserIdStr = typeof currentUserId === 'string' ? currentUserId : (currentUserId != null ? String(currentUserId) : '');
const isSent =
senderId == currentUserId ||
senderId == `doctor-${currentUserId}` ||
senderId == `user-${currentUserId}` ||
(typeof currentUserIdStr === 'string' && currentUserIdStr.startsWith('doctor-') && senderId == currentUserIdStr.replace('doctor-', '')) ||
(typeof currentUserIdStr === 'string' && currentUserIdStr.startsWith('user-') && senderId == currentUserIdStr.replace('user-', ''));
return {
id: message.id,
type: getMessageType(message.message_type),
content:
message.message_type === 4 || message.message_type === 10 || message.message_type === 11
? JSON.parse(message.message_content)
: message.message_content,
time: message.created_at_text,
timestamp: message.created_at,
senderId: message.sender_user_id,
read: true,
sender_user_id: message.sender_user_id,
receiver_user_id: message.receiver_user_id,
message_type: message.message_type,
message_content: message.message_content,
duration: message.duration,
room_id: message.room_id,
created_at: message.created_at,
created_at_text: message.created_at_text,
timestamp: message.created_at,
read: true,
chatKey: message.id,
isSent: message.sender_user_id !== currentUserId,
isSent: isSent,
};
});
};
@@ -256,16 +280,20 @@ export const useChatStore = defineStore('chat', () => {
messages.value.push(message);
const friend = friends.value.find(
(f) => f.id === message.senderId || f.id === currentFriend.value?.id,
(f) => f.id === message.sender_user_id || f.id === currentFriend.value?.id,
);
if (friend) {
let lastMessage = message.content;
if (message.type === 'image') lastMessage = '[图片]';
if (message.type === 'video') lastMessage = '[视频]';
if (message.type === 'audio') lastMessage = '[语音]';
if (message.type === 'prescription') lastMessage = '[处方信息]';
if (message.type === 'register') lastMessage = '[挂号信息]';
if (message.type === 'video-call' || message.type === 'audio-call')
let lastMessage = getParsedContent(message.message_type, message.message_content);
if (typeof lastMessage === 'object') {
lastMessage = JSON.stringify(lastMessage);
}
const messageTypeName = getMessageTypeName(message.message_type);
if (messageTypeName === 'image') lastMessage = '[图片]';
if (messageTypeName === 'video') lastMessage = '[视频]';
if (messageTypeName === 'audio') lastMessage = '[语音]';
if (messageTypeName === 'prescription') lastMessage = '[处方信息]';
if (messageTypeName === 'register') lastMessage = '[挂号信息]';
if (messageTypeName === 'video-call' || messageTypeName === 'audio-call')
lastMessage = '[通话]';
friend.last_message =
@@ -294,6 +322,12 @@ export const useChatStore = defineStore('chat', () => {
// }
};
// 辅助函数移除ID前缀统一格式比较
const getIdWithoutPrefix = (id) => {
if (!id) return '';
return String(id).replace(/^(user-|doctor-)/, '');
};
// 处理接收到的消息
const handleIncomingMessage = async (messageData, currentUserId) => {
// if (messageData.isFromHttp) {
@@ -304,37 +338,83 @@ export const useChatStore = defineStore('chat', () => {
const senderId = messageData.sender_user_id;
const receiverId = messageData.receiver_user_id;
if (receiverId !== `doctor-${currentUserId}`) return;
// 首先检查房间号匹配,确保只显示当前房间的消息
const currentRoomId = currentFriend.value?.room_id;
if (messageData.room_id && currentRoomId && messageData.room_id !== currentRoomId) {
return; // 房间号不匹配,忽略消息
}
// 确保 receiver_user_id 匹配时使用正确的前缀
// 如果当前用户是医生接收者应该是患者user-前缀)
// 如果当前用户是患者接收者应该是医生doctor-前缀)
// 使用 == 而不是 === 进行类型宽松比较,避免类型转换
// 只在需要调用 startsWith 时才检查类型
const currentUserIdStr = typeof currentUserId === 'string' ? currentUserId : (currentUserId != null ? String(currentUserId) : '');
const expectedReceiverId = typeof currentUserIdStr === 'string' && currentUserIdStr.startsWith('doctor-')
? `user-${currentUserIdStr.replace('doctor-', '')}`
: `doctor-${currentUserId}`;
// 如果 receiverId 匹配预期值或者是当前用户ID或者是当前好友的ID则显示消息
// 使用 == 而不是 === 进行类型宽松比较
const friendId = currentFriend.value?.id;
if (receiverId != expectedReceiverId && receiverId != currentUserId && receiverId != friendId) {
return;
}
if (messageData.call_id || messageData.call_status) {
handleCallSignal(messageData);
return;
}
// 判断消息是否是自己发送的(使用 checkMyMessage 的逻辑)
// currentUserIdStr 已在第346行声明直接使用
const isSent =
senderId == currentUserId ||
senderId == `doctor-${currentUserId}` ||
senderId == `user-${currentUserId}` ||
(typeof currentUserIdStr === 'string' && currentUserIdStr.startsWith('doctor-') && senderId == currentUserIdStr.replace('doctor-', '')) ||
(typeof currentUserIdStr === 'string' && currentUserIdStr.startsWith('user-') && senderId == currentUserIdStr.replace('user-', ''));
const newMessage = {
type: getMessageType(messageData.message_type),
content: messageData.content,
time: new Date().toLocaleTimeString().slice(0, 5),
senderId,
read: false,
id: messageData.id || Date.now(),
sender_user_id: senderId,
receiver_user_id: receiverId,
message_type: messageData.message_type,
message_content: messageData.message_content || messageData.content,
duration: messageData.duration || 0,
timestamp: Date.now(),
room_id: messageData.room_id,
created_at: messageData.created_at || Date.now(),
created_at_text: messageData.created_at_text || new Date().toLocaleTimeString().slice(0, 5),
timestamp: messageData.created_at || Date.now(),
read: false,
isSent: isSent,
};
try {
if (
newMessage.type === 'register' ||
messageData.type === 'prescription'
) {
newMessage.content = JSON.parse(messageData.content);
}
// 不再需要手动解析getParsedContent 会在组件中按需解析
// await chatDB.saveMessage(newMessage, currentUserId, senderId);
if (currentFriend.value && senderId == currentFriend.value.id) {
// 如果房间号匹配,说明是当前聊天窗口的消息,应该直接添加
if (currentRoomId && messageData.room_id === currentRoomId) {
// 使用统一的ID比较方式判断是否是当前好友发送的消息
const senderIdWithoutPrefix = getIdWithoutPrefix(senderId);
const currentFriendIdWithoutPrefix = getIdWithoutPrefix(currentFriend.value?.id);
if (senderIdWithoutPrefix === currentFriendIdWithoutPrefix) {
// 是当前好友发送的消息,标记为已读并添加到消息列表
newMessage.read = true;
messages.value.push(newMessage);
} else {
const friend = friends.value.find((f) => f.id == senderId);
// 不是当前好友发送的,但房间号匹配,可能是系统消息或其他情况,也添加到消息列表
messages.value.push(newMessage);
}
} else {
// 房间号不匹配,只更新未读数
const friend = friends.value.find((f) => {
const friendIdWithoutPrefix = getIdWithoutPrefix(f.id);
const senderIdWithoutPrefix = getIdWithoutPrefix(senderId);
return friendIdWithoutPrefix === senderIdWithoutPrefix;
});
if (friend) {
friend.un_read_count = (friend.un_read_count || 0) + 1;
}
@@ -342,11 +422,15 @@ export const useChatStore = defineStore('chat', () => {
const friend = friends.value.find((f) => f.id == senderId);
if (friend) {
let lastMessage = newMessage.content;
if (newMessage.type === 'image') lastMessage = '[图片]';
if (newMessage.type === 'video') lastMessage = '[视频]';
if (newMessage.type === 'audio') lastMessage = '[语音]';
if (newMessage.type === 'prescription') lastMessage = '[处方信息]';
let lastMessage = getParsedContent(newMessage.message_type, newMessage.message_content);
if (typeof lastMessage === 'object') {
lastMessage = JSON.stringify(lastMessage);
}
const messageTypeName = getMessageTypeName(newMessage.message_type);
if (messageTypeName === 'image') lastMessage = '[图片]';
if (messageTypeName === 'video') lastMessage = '[视频]';
if (messageTypeName === 'audio') lastMessage = '[语音]';
if (messageTypeName === 'prescription') lastMessage = '[处方信息]';
if (newMessage.type === 'register') lastMessage = '[挂号信息]';
if (
newMessage.type === 'video-call' ||
@@ -369,8 +453,8 @@ export const useChatStore = defineStore('chat', () => {
}
};
// 根据消息类型编号获取类型字符串
const getMessageType = (type) => {
// 辅助函数:根据消息类型编号获取类型名称(不转换字段名)
const getMessageTypeName = (type) => {
const types = [
'text',
'image',
@@ -381,11 +465,27 @@ export const useChatStore = defineStore('chat', () => {
'video-call',
'audio-call',
'file',
'',
'system',
'register',
'follow-up-visit',
'patient-experience',
'product-card',
'end-consultation',
];
return types[type] || 'text';
return types[type] || 'unknown';
};
// 辅助函数:按需解析消息内容
const getParsedContent = (messageType, messageContent) => {
// 对于需要 JSON 解析的消息类型4, 10, 11, 12, 13
if ([4, 10, 11, 12, 13].includes(messageType)) {
try {
return JSON.parse(messageContent);
} catch (e) {
console.error('解析消息内容失败:', e);
return messageContent;
}
}
return messageContent;
};
// 处理通话信令
@@ -1262,7 +1362,7 @@ export const useChatStore = defineStore('chat', () => {
// 发送通话请求
try {
logWithTime('log', '准备发送通话邀请:', {
senderUserId: userStore.currentUser.id,
senderUserId: userStore.currentUser.doctor_id,
receiverUserId: peerId,
callId,
callType: type === 'video' ? 6 : 7,
@@ -1543,5 +1643,7 @@ export const useChatStore = defineStore('chat', () => {
resetCallState,
sendCallSignal: sendCallSignalInternal,
clearCallTimeout,
getMessageTypeName,
getParsedContent,
};
});

View File

@@ -124,11 +124,18 @@ export const sendMessage = (data) => {
file: 8,
};
// 确保 receiver_user_id 有 user- 前缀(如果是患者端)
const receiverId = data.receiverId.toString();
const normalizedReceiverId = receiverId.startsWith('user-') ? receiverId : `user-${receiverId}`;
// 确保 sender_user_id 有 doctor- 前缀(医生端)
const senderId = data.senderId.toString();
const normalizedSenderId = senderId.startsWith('doctor-') ? senderId : `doctor-${senderId}`;
const requestData = {
room_id: data.roomId,
// sender_user_id: 'doctor-' + data.senderId.toString(),
sender_user_id: data.senderId.toString(),
receiver_user_id: data.receiverId.toString(),
sender_user_id: normalizedSenderId,
receiver_user_id: normalizedReceiverId,
message_type: messageTypeMap[data.type] || 0,
message_content: data.content || '',
duration: data.duration || 0,
@@ -178,10 +185,16 @@ export const sendCallSignal = (signalData) => {
throw new Error('缺少通话状态');
}
// 确保 receiver_user_id 有 user- 前缀(如果是患者端)
const receiverId = signalData.receiver_user_id;
const normalizedReceiverId = receiverId.startsWith('user-') || receiverId.startsWith('doctor-')
? receiverId
: `user-${receiverId}`;
// 构建通话信令请求数据
const requestData = {
sender_user_id: signalData.sender_user_id,
receiver_user_id: signalData.receiver_user_id,
receiver_user_id: normalizedReceiverId,
message_type: signalData.message_type || 6, // 默认视频通话
message_content:
signalData.message_content || JSON.stringify(signalData.data || {}),

View File

@@ -0,0 +1,90 @@
import { requestClient } from '#/api/request';
const prefix = 'online-consultation/';
/**
* 获取默认医生ID
*/
export async function getDefaultDoctorId() {
return requestClient.get<any>(`${prefix}get-default-doctor-id`);
}
/**
* 创建或获取聊天房间
* @param data
*/
export async function getOrCreateChatRoom(data: any) {
return requestClient.post<any>(`${prefix}get-or-create-chat-room`, data);
}
/**
* 结束在线复诊
* @param data
*/
export async function endConsultation(data: any) {
return requestClient.post<any>(`${prefix}end-consultation`, data);
}
/**
* 获取快速回复模板列表
* @param data
*/
export async function getQuickReplyList(data: any) {
return requestClient.get<any>(`${prefix}get-quick-reply-list`, { params: data });
}
/**
* 创建快速回复模板
* @param data
*/
export async function createQuickReply(data: any) {
return requestClient.post<any>(`${prefix}create-quick-reply`, data);
}
/**
* 更新快速回复模板
* @param data
*/
export async function updateQuickReply(data: any) {
return requestClient.post<any>(`${prefix}update-quick-reply`, data);
}
/**
* 删除快速回复模板
* @param data
*/
export async function deleteQuickReply(data: any) {
return requestClient.post<any>(`${prefix}delete-quick-reply`, data);
}
/**
* 获取在线复诊患者列表
* @param type 类型1=当前待接诊2=历史(已接诊)
*/
export async function getPatientList(type: number) {
return requestClient.get<any>(`${prefix}get-patient-list`, { params: { type } });
}
/**
* 获取患者详情
* @param roomId 房间ID
* @param registerId 挂号ID可选
*/
export async function getPatientDetail(roomId: string, registerId?: number) {
const params: any = { room_id: roomId };
if (registerId) {
params.register_id = registerId;
}
return requestClient.get<any>(`${prefix}get-patient-detail`, { params });
}
/**
* 接诊
* @param registerId 挂号ID
*/
export async function reception(registerId: number) {
return requestClient.post<any>(`${prefix}reception`, {
register_id: registerId
});
}

View File

@@ -0,0 +1,79 @@
<script setup>
import { computed } from 'vue';
import { Card } from 'ant-design-vue';
const props = defineProps({
content: {
type: Object,
required: true,
},
});
// 解析内容
const endData = computed(() => {
if (typeof props.content === 'string') {
try {
return JSON.parse(props.content);
} catch {
return {};
}
}
return props.content;
});
</script>
<template>
<Card class="end-consultation-card" :bordered="true" style="max-width: 350px">
<div class="end-consultation-content">
<div class="title">本次问诊已结束</div>
<div v-if="endData.end_reason" class="reason">
<div class="label">结束原因</div>
<div class="value">{{ endData.end_reason }}</div>
</div>
<div v-if="endData.end_time" class="time">
<div class="label">结束时间</div>
<div class="value">{{ endData.end_time }}</div>
</div>
</div>
</Card>
</template>
<style scoped>
.end-consultation-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #f5f5f5;
}
.end-consultation-content {
padding: 12px;
text-align: center;
}
.title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: #999;
}
.reason,
.time {
margin-top: 8px;
text-align: left;
}
.label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.value {
font-size: 14px;
color: #333;
}
</style>

View File

@@ -0,0 +1,193 @@
<script setup>
import { computed, ref } from 'vue';
import { Card, Button, message } from 'ant-design-vue';
import { usePrescriptionStore } from '#/store/prescription';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
const props = defineProps({
content: {
type: Object,
required: true,
},
});
const prescriptionStore = usePrescriptionStore();
const addingDrug = ref(false);
// 解析内容
const experienceData = computed(() => {
if (typeof props.content === 'string') {
try {
return JSON.parse(props.content);
} catch {
return {};
}
}
return props.content;
});
const hasVisitedText = computed(() => {
return experienceData.value.has_visited === '1' || experienceData.value.has_visited === 1
? '已就诊过'
: '未就诊过';
});
const hasUsedDrugText = computed(() => {
return experienceData.value.has_used_drug === '1' || experienceData.value.has_used_drug === 1
? '已使用过药物'
: '未使用过药物';
});
// 添加药品到处方单
const handleAddToPrescription = async () => {
if (!experienceData.value.drug_name) {
message.warning('该就诊经历中没有药品信息');
return;
}
// 检查处方store是否已初始化
if (!prescriptionStore.currentRegisterId) {
message.warning('请先接诊患者');
return;
}
addingDrug.value = true;
try {
// 通过药品名称搜索药品
// 如果myStoreId为0尝试使用myStoreList的第一个
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
}
}
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: experienceData.value.drug_name,
});
if (!res || res.length === 0) {
message.warning(`未找到药品:${experienceData.value.drug_name}`);
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('该药品已在处方中');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已将${drugData.drug.drug_name}添加到处方单`);
}
} catch (error) {
console.error('添加药品失败:', error);
message.error('添加药品失败,请重试');
} finally {
addingDrug.value = false;
}
};
</script>
<template>
<Card class="patient-experience-card" :bordered="true" style="max-width: 400px">
<div class="patient-experience-content">
<div class="title">患者就诊经历</div>
<div v-if="experienceData.symptom_description" class="info-item">
<div class="label">症状描述</div>
<div class="value">{{ experienceData.symptom_description }}</div>
</div>
<div class="info-item">
<div class="label">就诊情况</div>
<div class="value">{{ hasVisitedText }}</div>
</div>
<div class="info-item">
<div class="label">用药情况</div>
<div class="value">{{ hasUsedDrugText }}</div>
</div>
<div v-if="experienceData.drug_name" class="info-item">
<div class="label">使用药物</div>
<div class="value">{{ experienceData.drug_name }}</div>
</div>
<div v-if="experienceData.illness_info" class="info-item">
<div class="label">病情信息</div>
<div class="value">{{ experienceData.illness_info }}</div>
</div>
<!-- 添加到处方单按钮 -->
<div v-if="experienceData.drug_name" class="action-section">
<Button
type="primary"
size="small"
:loading="addingDrug"
@click="handleAddToPrescription"
>
添加药品到处方单
</Button>
</div>
</div>
</Card>
</template>
<style scoped>
.patient-experience-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.patient-experience-content {
padding: 8px;
}
.title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: #1890ff;
}
.info-item {
margin-bottom: 8px;
display: flex;
flex-direction: column;
}
.label {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.value {
font-size: 14px;
color: #333;
word-break: break-word;
}
.action-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,317 @@
<script setup>
import { ref, watch } from 'vue';
import { Descriptions, Button, Empty } from 'ant-design-vue';
import { getPatientDetail, reception } from '../api/index.ts';
import { message } from 'ant-design-vue';
import { sendMessage } from '#/views/business/chat/utils/request';
import { useUserStore } from '#/views/business/chat/stores/user';
const userStore = useUserStore();
const props = defineProps({
patient: {
type: Object,
default: null
},
doctorId: {
type: String,
default: '39' // 改为39与后端保持一致虽然不再使用但保持一致性
}
});
const emit = defineEmits(['reception-success', 'enter-chat']);
const patientDetail = ref(null);
const loading = ref(false);
const receptionLoading = ref(false);
// 获取患者详情
const fetchPatientDetail = async () => {
if (!props.patient || !props.patient.room_id) {
patientDetail.value = null;
return;
}
loading.value = true;
try {
// 如果患者数据中有register_id传递它来查询对应的就诊记录
let registerId;
if (props.patient.register_id) {
const id = Number(props.patient.register_id);
if (!isNaN(id) && id > 0) {
registerId = id;
}
}
console.log('查询患者详情register_id:', registerId, 'patient:', props.patient);
const res = await getPatientDetail(props.patient.room_id, registerId);
console.log('患者详情API响应:', res);
// 确保正确解析返回数据
const data = res?.result || res?.data || res || null;
patientDetail.value = data;
console.log('患者详情数据:', patientDetail.value);
console.log('就诊记录数量:', patientDetail.value?.register_infos?.length || 0);
} catch (error) {
console.error('获取患者详情失败:', error);
message.error('获取患者详情失败,请重试');
patientDetail.value = null;
} finally {
loading.value = false;
}
};
// 接诊
const handleReception = async () => {
if (!props.patient || !props.patient.register_id) {
message.error('患者信息不存在或缺少挂号ID');
return;
}
receptionLoading.value = true;
try {
// 传递挂号ID
const res = await reception(props.patient.register_id);
console.log('接诊API响应:', res);
// res 已经是 data.result 的内容,无需判断 code请求封装会自动判断失败会抛出异常
message.success('接诊成功');
const receptionResult = {
...props.patient,
register_id: res.register_id || props.patient.register_id,
status: 2, // 接诊中
};
emit('reception-success', receptionResult);
await fetchPatientDetail();
} catch (error) {
console.error('接诊失败:', error);
message.error('接诊失败,请重试');
} finally {
receptionLoading.value = false;
}
};
// 获取性别文本
const getSexText = (sex) => {
if (sex === 1) return '男';
if (sex === 2) return '女';
return '未填写';
};
// 进入聊天
const handleEnterChat = () => {
if (!props.patient || !props.patient.room_id) {
message.warning('患者信息不完整,无法进入聊天');
return;
}
emit('enter-chat', props.patient);
};
// 监听患者变化
watch(
() => props.patient,
(newPatient) => {
if (newPatient) {
fetchPatientDetail();
} else {
patientDetail.value = null;
}
},
{ immediate: true }
);
</script>
<template>
<div class="patient-info-panel bg-white dark:bg-gray-900">
<div v-if="loading" class="loading-state">
<text>加载中...</text>
</div>
<div v-else-if="!patientDetail && !patient" class="empty-state">
<Empty description="请选择患者" />
</div>
<div v-else-if="patientDetail" class="patient-content">
<!-- 患者信息头部标题和进入聊天按钮 -->
<div class="patient-header">
<h2 class="patient-title">患者信息</h2>
<Button
v-if="patient && patient.room_id"
type="primary"
@click="handleEnterChat"
class="enter-chat-btn"
>
进入聊天
</Button>
</div>
<!-- 患者基本信息 -->
<Descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 2, xs: 2 }"
bordered
class="patient-descriptions"
>
<Descriptions.Item label="患者姓名">
{{ patientDetail.user_patient?.name || patientDetail.user?.nick_name || '未知' }}
</Descriptions.Item>
<Descriptions.Item label="患者年龄">
{{ patientDetail.user_patient?.age || 0 }}
</Descriptions.Item>
<Descriptions.Item label="患者性别">
{{ getSexText(patientDetail.user_patient?.sex) }}
</Descriptions.Item>
<Descriptions.Item label="手机号">
{{ patientDetail.user_patient?.mobile || patientDetail.user?.mobile || '未填写' }}
</Descriptions.Item>
<Descriptions.Item v-if="patientDetail.register_order?.order_no" label="订单号">
{{ patientDetail.register_order.order_no }}
</Descriptions.Item>
</Descriptions>
<!-- 就诊记录 -->
<div v-if="patientDetail.register_infos && patientDetail.register_infos.length > 0" class="register-info-section">
<h3 class="section-title text-gray-900 dark:text-gray-100 border-b-2 border-blue-600 dark:border-blue-400">就诊记录</h3>
<div class="register-info-list">
<div
v-for="info in patientDetail.register_infos"
:key="info.id"
class="register-info-item bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-600"
>
<div v-if="info.illnessInfo" class="illness-info">
<strong>症状</strong>{{ info.illnessInfo }}
</div>
<div v-if="info.drug" class="drug-info">
<strong>药品</strong>{{ info.drug.name }}
</div>
<div class="meta-info">
<span>就诊过{{ info.has_visited ? '是' : '否' }}</span>
<span style="margin-left: 16px;">使用过药品{{ info.has_used_drug ? '是' : '否' }}</span>
</div>
</div>
</div>
</div>
<!-- 接诊按钮 -->
<div v-if="!patientDetail.is_recepted" class="action-section border-t border-gray-200 dark:border-gray-700">
<Button
type="primary"
:loading="receptionLoading"
size="large"
block
@click="handleReception"
>
接诊
</Button>
</div>
<div v-else class="recepted-info bg-green-50 dark:bg-green-900/20">
<text class="recepted-text text-green-600 dark:text-green-400">已接诊</text>
</div>
</div>
</div>
</template>
<style scoped>
.patient-info-panel {
height: 100%;
overflow-y: auto;
padding: 20px;
}
.loading-state,
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.patient-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.patient-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.patient-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
.enter-chat-btn {
flex-shrink: 0;
}
.dark .patient-title {
color: #f3f4f6;
}
.patient-descriptions {
margin-bottom: 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 8px;
}
.register-info-section {
margin-top: 24px;
}
.register-info-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.register-info-item {
padding: 16px;
border-radius: 8px;
border: 1px solid;
}
.illness-info,
.drug-info {
margin-bottom: 8px;
font-size: 14px;
line-height: 1.6;
}
.illness-info strong,
.drug-info strong {
margin-right: 8px;
}
.meta-info {
margin-top: 8px;
font-size: 12px;
}
.action-section {
margin-top: 24px;
padding-top: 24px;
}
.recepted-info {
margin-top: 24px;
padding: 16px;
text-align: center;
border-radius: 8px;
}
.recepted-text {
font-size: 16px;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,265 @@
<script setup>
import { ref, watch, onMounted } from 'vue';
import { RadioButton, RadioGroup, Button } from 'ant-design-vue';
import { ReloadOutlined } from '@ant-design/icons-vue';
import { getPatientList } from '../api/index.ts';
const props = defineProps({
doctorId: {
type: String,
default: '32'
}
});
const emit = defineEmits(['select-patient']);
const listType = ref(1); // 1=当前2=历史
const patients = ref([]);
const selectedPatientId = ref(null);
const loading = ref(false);
// 获取患者列表
const fetchPatientList = async () => {
loading.value = true;
try {
const res = await getPatientList(listType.value);
// 确保正确解析返回数据jok函数返回格式为 { code: 0, message: "...", result: [...] }
const data = res?.result || res?.data || res || [];
patients.value = Array.isArray(data) ? data : [];
console.log('患者列表数据:', patients.value);
} catch (error) {
console.error('获取患者列表失败:', error);
patients.value = [];
} finally {
loading.value = false;
}
};
// 获取患者的唯一标识符
const getPatientUniqueId = (patient) => {
if (!patient) return null;
return patient.register_id || patient.room_id || patient.id || null;
};
// 切换列表类型
const handleTypeChange = () => {
selectedPatientId.value = null;
fetchPatientList();
};
// 选择患者
const selectPatient = (patient) => {
selectedPatientId.value = getPatientUniqueId(patient);
emit('select-patient', patient);
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
1: '待接诊',
2: '接诊中',
3: '已结束',
};
return statusMap[status] || '未知';
};
// 获取状态样式类
const getStatusClass = (status) => {
const classMap = {
1: 'status-pending',
2: 'status-consulting',
3: 'status-ended',
};
return classMap[status] || '';
};
// 格式化时间
const formatTime = (time) => {
if (!time) return '';
const date = new Date(time * 1000 || time);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return '刚刚';
if (minutes < 60) return `${minutes}分钟前`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}小时前`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}天前`;
return date.toLocaleDateString('zh-CN');
};
onMounted(() => {
fetchPatientList();
// 定时刷新列表每10分钟
setInterval(fetchPatientList, 600000);
});
// 监听列表类型变化
watch(listType, handleTypeChange);
</script>
<template>
<div class="patient-list-container bg-white dark:bg-gray-900">
<div class="list-header border-b border-gray-200 dark:border-gray-700">
<div class="header-top">
<RadioGroup v-model:value="listType" class="type-switch" @change="handleTypeChange">
<RadioButton :value="1">当前</RadioButton>
<RadioButton :value="2">历史</RadioButton>
</RadioGroup>
<Button
type="text"
:loading="loading"
@click="fetchPatientList"
class="refresh-btn"
>
<template #icon>
<ReloadOutlined />
</template>
刷新
</Button>
</div>
</div>
<div v-if="loading" class="loading-state">
<text>加载中...</text>
</div>
<div v-else-if="patients.length === 0" class="empty-state">
<text>暂无患者</text>
</div>
<div v-else class="patient-list">
<div
v-for="(patient, index) in patients"
:key="`patient-${getPatientUniqueId(patient) || index}`"
:class="['patient-card', 'border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800', { 'active border-l-4 border-l-blue-600 bg-blue-50 dark:bg-blue-900/20': selectedPatientId === getPatientUniqueId(patient) }]"
@click.stop="selectPatient(patient)"
>
<div class="patient-info">
<h3 class="patient-name">
{{ patient.user_patient?.name || patient.user?.nick_name || '未知患者' }}
</h3>
<p class="patient-phone">{{ patient.user_patient?.mobile || patient.user?.mobile || '' }}</p>
<p v-if="patient.order_no" class="order-no">订单号{{ patient.order_no }}</p>
<div class="patient-meta">
<span :class="['status', getStatusClass(patient.status)]">
{{ getStatusText(patient.status) }}
</span>
<span class="time text-gray-500 dark:text-gray-400">{{ formatTime(patient.last_message_time) }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.patient-list-container {
height: 100%;
display: flex;
flex-direction: column;
}
.list-header {
padding: 16px;
}
.header-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.type-switch {
flex: 1;
display: flex;
}
.refresh-btn {
flex-shrink: 0;
}
.type-switch :deep(.ant-radio-button-wrapper) {
flex: 1;
text-align: center;
}
.loading-state,
.empty-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.patient-list {
flex: 1;
overflow-y: auto;
}
.patient-card {
padding: 16px;
cursor: pointer;
transition: all 0.3s;
}
.patient-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.patient-name {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.patient-phone {
font-size: 14px;
margin: 0;
}
.order-no {
font-size: 12px;
margin: 4px 0 0 0;
}
.patient-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
}
.status {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.status-pending {
background: #fef0f0;
color: #f56c6c;
}
.status-consulting {
background: #f0f9eb;
color: #67c23a;
}
.status-ended {
background: #f4f4f5;
color: #909399;
}
.time {
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,117 @@
<script setup>
import { computed } from 'vue';
import { Card, Image, Button } from 'ant-design-vue';
const props = defineProps({
content: {
type: Object,
required: true,
},
});
const emit = defineEmits(['click-product']);
// 解析内容
const productData = computed(() => {
if (typeof props.content === 'string') {
try {
return JSON.parse(props.content);
} catch {
return {};
}
}
return props.content;
});
const handleClick = () => {
emit('click-product', productData.value);
};
</script>
<template>
<Card class="product-card" :bordered="true" style="max-width: 300px">
<div class="product-content" @click="handleClick">
<div v-if="productData.image" class="product-image">
<Image :src="productData.image" :preview="false" style="width: 100%; height: 150px; object-fit: cover;" />
</div>
<div class="product-info">
<div class="product-name">{{ productData.drug_name || '商品名称' }}</div>
<div v-if="productData.specification" class="product-spec">
规格{{ productData.specification }}
</div>
<div v-if="productData.function" class="product-function">
功效{{ productData.function }}
</div>
<div class="product-price">
<span class="price-label">价格</span>
<span class="price-value">¥{{ productData.price || '0.00' }}</span>
</div>
</div>
</div>
</Card>
</template>
<style scoped>
.product-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
}
.product-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.product-content {
padding: 8px;
}
.product-image {
margin-bottom: 8px;
border-radius: 4px;
overflow: hidden;
}
.product-info {
padding: 0 4px;
}
.product-name {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: #333;
}
.product-spec,
.product-function {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.product-price {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #eee;
}
.price-label {
font-size: 12px;
color: #666;
}
.price-value {
font-size: 18px;
font-weight: 600;
color: #ff4d4f;
margin-left: 4px;
}
</style>

View File

@@ -0,0 +1,256 @@
<script setup>
import { ref, onMounted, watch } from 'vue';
import { Button, Popconfirm } from 'ant-design-vue';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import {
getQuickReplyList,
createQuickReply,
updateQuickReply,
deleteQuickReply
} from '../api/index';
const props = defineProps({
doctorId: {
type: String,
default: '32',
},
});
const emit = defineEmits(['select-reply']);
const quickReplies = ref([]);
const showAddModal = ref(false);
const editingItem = ref(null);
const newContent = ref('');
// 加载快速回复列表
const loadQuickReplies = async () => {
try {
const res = await getQuickReplyList({ doctor_id: props.doctorId });
quickReplies.value = res || [];
} catch (error) {
console.error('加载快速回复失败:', error);
}
};
// 选择快速回复
const handleSelectReply = (content) => {
emit('select-reply', content);
};
// 添加快速回复
const handleAddReply = async () => {
if (!newContent.value.trim()) {
return;
}
try {
await createQuickReply({
doctor_id: props.doctorId,
content: newContent.value.trim(),
sort_order: quickReplies.value.length,
});
newContent.value = '';
showAddModal.value = false;
await loadQuickReplies();
} catch (error) {
console.error('添加快速回复失败:', error);
}
};
// 编辑快速回复
const handleEditReply = async (item) => {
editingItem.value = item;
newContent.value = item.content;
showAddModal.value = true;
};
// 更新快速回复
const handleUpdateReply = async () => {
if (!newContent.value.trim() || !editingItem.value) {
return;
}
try {
await updateQuickReply({
id: editingItem.value.id,
doctor_id: props.doctorId,
content: newContent.value.trim(),
});
newContent.value = '';
editingItem.value = null;
showAddModal.value = false;
await loadQuickReplies();
} catch (error) {
console.error('更新快速回复失败:', error);
}
};
// 删除快速回复
const handleDeleteReply = async (id) => {
try {
await deleteQuickReply({
id,
doctor_id: props.doctorId,
});
await loadQuickReplies();
} catch (error) {
console.error('删除快速回复失败:', error);
}
};
// 取消编辑
const handleCancel = () => {
newContent.value = '';
editingItem.value = null;
showAddModal.value = false;
};
onMounted(() => {
loadQuickReplies();
});
watch(() => props.doctorId, () => {
loadQuickReplies();
});
</script>
<template>
<div class="quick-reply-bubbles bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="bubbles-container">
<div
v-for="item in quickReplies"
:key="item.id"
class="reply-bubble bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20"
@click="handleSelectReply(item.content)"
>
<span class="bubble-text">{{ item.content }}</span>
<div class="bubble-actions" @click.stop>
<Button
v-if="item.doctor_id !== '0' && item.doctor_id !== 0"
type="text"
size="small"
@click="handleEditReply(item)"
>
<template #icon><EditOutlined /></template>
</Button>
<Popconfirm
v-if="item.doctor_id !== '0' && item.doctor_id !== 0"
title="确定删除这条快速回复吗?"
@confirm="handleDeleteReply(item.id)"
>
<Button
type="text"
size="small"
danger
>
<template #icon><DeleteOutlined /></template>
</Button>
</Popconfirm>
</div>
</div>
<div
v-if="!showAddModal"
class="add-bubble bg-white dark:bg-gray-700 border-dashed border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 hover:text-blue-500 dark:hover:text-blue-400"
@click="showAddModal = true"
>
<PlusOutlined />
<span>添加常用语</span>
</div>
<div v-else class="add-modal bg-white dark:bg-gray-700 border-blue-500 dark:border-blue-400">
<input
v-model="newContent"
type="text"
placeholder="输入常用语"
class="add-input bg-transparent placeholder-gray-400 dark:placeholder-gray-500"
@keyup.enter="editingItem ? handleUpdateReply() : handleAddReply()"
@keyup.esc="handleCancel"
/>
<div class="add-actions">
<Button size="small" @click="handleCancel">取消</Button>
<Button
type="primary"
size="small"
@click="editingItem ? handleUpdateReply() : handleAddReply()"
>
{{ editingItem ? '更新' : '添加' }}
</Button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.quick-reply-bubbles {
padding: 8px 12px;
}
.bubbles-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.reply-bubble {
position: relative;
display: inline-flex;
align-items: center;
padding: 6px 12px;
border: 1px solid;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s;
}
.reply-bubble:hover .bubble-actions {
display: flex;
}
.bubble-text {
font-size: 13px;
}
.bubble-actions {
display: none;
margin-left: 8px;
gap: 4px;
}
.add-bubble {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px dashed;
border-radius: 16px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s;
}
.add-modal {
display: flex;
align-items: center;
gap: 8px;
padding: 4px;
border: 1px solid;
border-radius: 16px;
}
.add-input {
flex: 1;
padding: 4px 8px;
border: none;
outline: none;
font-size: 13px;
}
.add-actions {
display: flex;
gap: 4px;
}
</style>

View File

@@ -0,0 +1,553 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useVbenModal } from '@vben/common-ui';
import { Button, Modal, message } from 'ant-design-vue';
import ChatArea from '#/views/business/chat/components/ChatArea.vue';
import ConnectionStatus from '#/views/business/chat/components/ConnectionStatus.vue';
import EmptyState from '#/views/business/chat/components/EmptyState.vue';
import PrescriptionModal from '#/views/business/chat/components/PrescriptionModal.vue';
import { useChatStore } from '#/views/business/chat/stores/chat.ts';
import { useUserStore } from '#/views/business/chat/stores/user';
import { usePrescriptionStore } from '#/store/prescription';
import PatientList from './components/PatientList.vue';
import PatientInfoPanel from './components/PatientInfoPanel.vue';
import { endConsultation } from './api/index.ts';
import { getProductListDoctorReception } from '#/views/doctor/doctor-reception/api';
const route = useRoute();
const router = useRouter();
const chatStore = useChatStore();
const userStore = useUserStore();
const prescriptionStore = usePrescriptionStore();
// 使用 VbenModal 管理处方模态框
const [PrescriptionModalComponent, prescriptionModalApi] = useVbenModal({
connectedComponent: PrescriptionModal,
});
// 当前患者信息
const currentPatient = ref(null);
const registerId = ref(null);
// 左侧列表显示状态
const leftShow = ref(true);
// 当前选中的患者
const selectedPatient = ref(null);
// 视图模式:'list' = 列表模式(显示患者列表和信息),'chat' = 聊天模式(显示聊天界面)
const viewMode = ref('list');
// 获取左侧列表显示状态
function getLeftShow() {
const localStorageLeftShow = localStorage.getItem('onlineConsultationLeftShow');
leftShow.value = localStorageLeftShow !== 'false';
}
getLeftShow();
// 监听leftShow存入缓存
watch(
() => leftShow.value,
(enable) => {
localStorage.setItem('onlineConsultationLeftShow', enable);
},
{ immediate: true }
);
// 从路由参数获取registerId
onMounted(() => {
const id = route.params.id || route.query.registerId;
if (id) {
registerId.value = id;
// 初始化处方store但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(id, false);
}
});
// 监听当前好友变化,更新患者信息
watch(
() => chatStore.currentFriend,
(friend) => {
if (friend) {
currentPatient.value = friend;
// 更新registerId
if (friend.registerId) {
registerId.value = friend.registerId;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(friend.registerId, false);
}
}
},
{ immediate: true }
);
// 显示开方模块
const openPrescriptionModule = (id) => {
const targetRegisterId = id || registerId.value;
if (!targetRegisterId) {
console.error('registerId不能为空');
return;
}
prescriptionModalApi.setData({
registerId: targetRegisterId,
onPrescriptionSent: handlePrescriptionSent,
});
prescriptionModalApi.open();
};
// 处理处方发送完成事件
const handlePrescriptionSent = () => {
console.log('处方发送完成');
// 可以在这里添加其他逻辑,如刷新聊天记录等
};
// 处理选择患者
const handleSelectPatient = (patient) => {
selectedPatient.value = patient;
// 切换到列表模式以显示患者信息
viewMode.value = 'list';
// 设置当前好友,以便聊天功能正常工作
if (patient && patient.room_id) {
// 设置聊天好友信息
const friendInfo = {
id: patient.user_id || patient.user?.id,
room_id: patient.room_id,
register_id: patient.register_id,
nick_name: patient.user_patient?.name || patient.user?.nick_name || '患者',
avatar: patient.user?.avatar || '',
un_read_count: 0,
};
chatStore.setCurrentFriend(friendInfo);
// 切换到该好友并加载消息(如果已接诊)
if (friendInfo.room_id && userStore.currentUser?.doctor_id && patient.status === 2) {
chatStore.switchFriend(friendInfo, userStore.currentUser.doctor_id);
}
// 更新registerId
if (patient.register_id) {
registerId.value = patient.register_id;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(patient.register_id, false);
}
}
};
// 处理进入聊天
const handleEnterChat = (patient) => {
// 切换到聊天模式
viewMode.value = 'chat';
// 设置当前好友,以便聊天功能正常工作
if (patient && patient.room_id) {
// 设置聊天好友信息参考handleSelectPatient的逻辑
const friendInfo = {
id: patient.user_id || patient.user?.id,
room_id: patient.room_id,
register_id: patient.register_id,
nick_name: patient.user_patient?.name || patient.user?.nick_name || '患者',
avatar: patient.user?.avatar || '',
un_read_count: 0,
};
chatStore.setCurrentFriend(friendInfo);
// 切换到该好友并加载消息
if (friendInfo.room_id && userStore.currentUser?.id) {
chatStore.switchFriend(friendInfo, userStore.currentUser.id);
}
// 更新registerId
if (patient.register_id) {
registerId.value = patient.register_id;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(patient.register_id, false);
}
}
};
// 处理接诊成功
const handleReceptionSuccess = (patient) => {
// 更新 selectedPatient
selectedPatient.value = {
...selectedPatient.value,
...patient,
status: 2, // 接诊中
};
// 接诊成功后,切换到聊天模式
viewMode.value = 'chat';
// 确保 room_id 存在
if (!patient.room_id && selectedPatient.value?.room_id) {
patient.room_id = selectedPatient.value.room_id;
}
// 设置当前好友,以便聊天功能正常工作
if (patient && patient.room_id) {
// 设置聊天好友信息参考handleSelectPatient的逻辑
const friendInfo = {
id: patient.user_id || patient.user?.id,
room_id: patient.room_id,
register_id: patient.register_id,
nick_name: patient.user_patient?.name || patient.user?.nick_name || '患者',
avatar: patient.user?.avatar || '',
un_read_count: 0,
};
chatStore.setCurrentFriend(friendInfo);
// 切换到该好友并加载消息
if (friendInfo.room_id && userStore.currentUser?.doctor_id) {
chatStore.switchFriend(friendInfo, userStore.currentUser.doctor_id);
}
}
// 更新registerId
if (patient.register_id) {
registerId.value = patient.register_id;
// 初始化处方数据但不获取患者信息避免调用错误的API
prescriptionStore.initializePrescription(patient.register_id, false);
}
};
// 结束接诊
const handleEndConsultation = () => {
if (!selectedPatient.value) {
message.warning('请先选择患者');
return;
}
if (!selectedPatient.value.register_id) {
message.warning('患者信息不完整,无法结束接诊');
return;
}
const patientName = selectedPatient.value.user_patient?.name ||
selectedPatient.value.user?.nick_name ||
'患者';
Modal.confirm({
title: '确认结束接诊',
content: `确定要结束${patientName}的复诊吗?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await endConsultation({
register_id: selectedPatient.value.register_id,
doctor_id: userStore.currentUser?.doctor_id || '39',
reason: '医生主动结束'
});
// res 已经是 result 内容,无需判断 code
message.success('结束接诊成功');
// 更新患者状态
if (selectedPatient.value) {
selectedPatient.value.status = 3; // 已结束
}
// 可以切换到列表模式或刷新当前患者信息
viewMode.value = 'list';
} catch (error) {
console.error('结束接诊失败:', error);
message.error('结束接诊失败,请重试');
}
}
});
};
// 将患者购买的药品加入处方单
const handleAddPatientProductsToPrescription = async () => {
// 检查处方store是否已初始化
if (!prescriptionStore.currentRegisterId) {
message.warning('请先接诊患者');
return;
}
// 从聊天记录中筛选出所有 message_type=11 的消息(患者就诊经历)
const productMessages = chatStore.messages.filter(
(msg) => msg.message_type === 11
);
if (productMessages.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取诊所ID
let storeId = prescriptionStore.myStoreId;
if (!storeId || storeId === 0) {
if (prescriptionStore.myStoreList && prescriptionStore.myStoreList.length > 0) {
storeId = prescriptionStore.myStoreList[0].id;
} else {
message.warning('请先选择诊所');
return;
}
}
let addedCount = 0;
let skippedCount = 0;
const errors = [];
try {
// 遍历每条商品消息,添加到处方单
for (const msg of productMessages) {
try {
// 解析消息内容
let productData;
try {
productData = typeof msg.message_content === 'string'
? JSON.parse(msg.message_content)
: msg.message_content;
} catch (e) {
console.error('解析就诊经历消息失败:', e);
continue;
}
// 获取药品名称(从就诊经历中提取 drug_name
const drugName = productData.drug_name;
if (!drugName) {
skippedCount++;
continue;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
errors.push(`未找到药品:${drugName}`);
skippedCount++;
continue;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
skippedCount++;
continue;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
addedCount++;
}
} catch (error) {
console.error('添加药品失败:', error);
errors.push(`添加药品失败:${error.message || '未知错误'}`);
}
}
// 显示结果消息
if (addedCount > 0) {
message.success(`已成功添加 ${addedCount} 个药品到处方单`);
// 打开处方模态框
prescriptionModalApi.setData({
registerId: prescriptionStore.currentRegisterId,
onPrescriptionSent: handlePrescriptionSent,
});
prescriptionModalApi.open();
} else if (skippedCount > 0) {
// 如果所有药品都无法添加,尝试添加最新的那个
// 按时间排序取最新的药品created_at 或 timestamp 最大的)
const sortedMessages = [...productMessages].sort((a, b) => {
const timeA = a.created_at ? new Date(a.created_at).getTime() : (a.timestamp || 0);
const timeB = b.created_at ? new Date(b.created_at).getTime() : (b.timestamp || 0);
return timeB - timeA; // 降序排列,最新的在前
});
if (sortedMessages.length > 0) {
const latestMessage = sortedMessages[0];
try {
// 解析最新消息的内容
let productData;
try {
productData = typeof latestMessage.message_content === 'string'
? JSON.parse(latestMessage.message_content)
: latestMessage.message_content;
} catch (e) {
console.error('解析商品消息失败:', e);
message.warning('当前聊天没有可添加的药品');
return;
}
// 获取药品名称
const drugName = productData.drug_name || productData.name;
if (!drugName) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 通过药品名称搜索药品
const res = await getProductListDoctorReception({
store_id: storeId,
type: 2, // 中成(西)药
name: drugName,
});
if (!res || res.length === 0) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 使用第一个匹配的药品
const drugData = res[0];
// 检查药品是否已在处方中
const existItem = prescriptionStore.currentDrugs.find(
(item) => item.index_id === drugData.id,
);
if (existItem) {
message.warning('当前聊天没有可添加的药品');
return;
}
// 添加药品到处方单
const success = prescriptionStore.addProducts(drugData);
if (success) {
message.success(`已成功添加 ${drugData.drug.drug_name} 到处方单`);
// 打开处方模态框
prescriptionModalApi.setData({
registerId: prescriptionStore.currentRegisterId,
onPrescriptionSent: handlePrescriptionSent,
});
prescriptionModalApi.open();
} else {
message.warning('当前聊天没有可添加的药品');
}
} catch (error) {
console.error('添加最新药品失败:', error);
message.warning('当前聊天没有可添加的药品');
}
} else {
message.warning('当前聊天没有可添加的药品');
}
}
// 如果有错误,显示错误信息
if (errors.length > 0) {
console.warn('添加药品时的错误:', errors);
}
} catch (error) {
console.error('批量添加药品失败:', error);
message.error('添加药品失败,请重试');
}
};
</script>
<template>
<div
class="chat-box-body flex bg-gradient-to-br from-blue-900 via-purple-900 to-pink-300 p-5"
style="height: 95vh"
>
<!-- 主容器 -->
<div
:class="leftShow ? 'container-box-grid' : 'container-box'"
class="z-10 mx-auto flex flex-1 overflow-hidden rounded-2xl bg-white/90 shadow-2xl backdrop-blur-sm"
>
<!-- 连接状态指示器 -->
<ConnectionStatus />
<!-- 处方模态框 - 使用 VbenModal 模式 -->
<PrescriptionModalComponent />
<!-- 左侧患者列表 -->
<div class="left-panel-wrapper">
<Button
:class="leftShow ? 'w-full mb-2' : 'sticky top-5 left-5'"
type="primary"
@click="leftShow = !leftShow"
>
{{ leftShow ? '收起患者列表' : '展开患者列表' }}
</Button>
<PatientList
v-if="leftShow"
:doctor-id="'39'"
@select-patient="handleSelectPatient"
class="patient-list-panel"
/>
</div>
<!-- 右侧内容区域 -->
<div class="right-content-area">
<!-- 列表模式显示患者信息和接诊 -->
<template v-if="viewMode === 'list'">
<PatientInfoPanel
:patient="selectedPatient"
:doctor-id="'39'"
@reception-success="handleReceptionSuccess"
@enter-chat="handleEnterChat"
class="patient-info-panel"
/>
</template>
<!-- 聊天模式显示聊天界面 -->
<template v-else>
<div class="min-w-0 flex-1">
<ChatArea
v-if="chatStore.currentFriend"
@open-prescription="openPrescriptionModule"
/>
<EmptyState v-else />
</div>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.container-box {
min-height: 90vh;
display: flex;
flex-direction: column;
}
.container-box-grid {
min-height: 90vh;
display: grid;
grid-template-columns: 300px 1fr;
gap: 0;
}
.left-panel-wrapper {
border-right: 1px solid #e4e7ed;
padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.patient-list-panel {
flex: 1;
overflow: hidden;
}
.right-content-area {
display: flex;
flex-direction: column;
overflow: hidden;
}
.patient-info-panel {
flex: 1;
overflow-y: auto;
}
.dark .left-panel-wrapper {
border-right-color: #333;
}
.dark .container-box-grid {
background: #1f2937;
}
</style>