fix: 在线复诊
This commit is contained in:
@@ -71,7 +71,7 @@ const initWebsocket = () => {
|
||||
// 当userInfo加载完成后再执行初始化
|
||||
watch(userInfoLoaded, (loaded) => {
|
||||
if (loaded) {
|
||||
// initWebsocket();
|
||||
initWebsocket();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
// 监听当前视图变化,清空搜索
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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="加载消息中...">
|
||||
<!-- 空状态 -->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -58,7 +58,9 @@ export const useChatStore = defineStore('chat', () => {
|
||||
hasMore.value = false;
|
||||
return;
|
||||
}
|
||||
const newMessages = checkMyMessage(res.list, currentFriend.value.id);
|
||||
// 使用当前用户的ID(doctor_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);
|
||||
// 使用当前用户的ID(doctor_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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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 || {}),
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
553
apps/web-antd/src/views/doctor/online-consultation/index.vue
Normal file
553
apps/web-antd/src/views/doctor/online-consultation/index.vue
Normal 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>
|
||||
|
||||
Reference in New Issue
Block a user