Files
xk-admin/apps/web-antd/src/views/business/chat/components/MessageBubble.vue

1428 lines
42 KiB
Vue
Raw Normal View History

<script setup>
2026-05-06 15:53:45 +08:00
import { computed, ref, watch } from 'vue';
2025-08-02 16:57:57 +08:00
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { Avatar, Button, Image } from 'ant-design-vue';
2025-08-02 16:57:57 +08:00
import { getChatMessageRegisterInfoApi } from '#/views/business/chat/api';
import { useChatStore } from '#/views/business/chat/stores/chat';
import { sendMessage } from '#/views/business/chat/utils/request';
2025-12-23 11:05:12 +08:00
// 辅助函数:按需解析消息内容
const getParsedContent = (messageType, messageContent) => {
2026-02-04 08:53:52 +08:00
// 对于需要 JSON 解析的消息类型4, 9, 10, 11, 12, 13, 14
if ([4, 9, 10, 11, 12, 13, 14].includes(messageType)) {
2025-12-23 11:05:12 +08:00
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',
2026-02-04 08:53:52 +08:00
14: 'transfer-prescription',
2025-12-23 11:05:12 +08:00
};
return types[messageType] || 'unknown';
};
import { getPrescriptionCheckStatusApi } from '#/views/business/order/prescription/api';
import { receptionApi } from '#/views/doctor/doctor-reception/api';
2025-08-02 16:57:57 +08:00
import PrescriptionDetail from '#/views/doctor/doctor-reception/components/PrescriptionDetail.vue';
import RefusalOfTreatmentModal from '#/views/doctor/doctor-reception/components/RefusalOfTreatmentModal.vue';
import previewMedia from '../composables/useMediaPreview.ts';
import { useThemeStore } from '../stores/theme.ts';
2025-08-30 15:31:03 +08:00
import { useUserStore as chatUseUserStore } from '../stores/user';
import AudioMessage from './AudioMessage.vue';
import { XgVideoPlayer } from '#/components/video-player';
2025-12-23 11:05:12 +08:00
import PatientExperienceCard from '#/views/doctor/online-consultation/components/PatientExperienceCard.vue';
2026-05-06 15:53:45 +08:00
import FollowUpDrugAssistantCard from './FollowUpDrugAssistantCard.vue';
2025-12-23 11:05:12 +08:00
import ProductCard from '#/views/doctor/online-consultation/components/ProductCard.vue';
import EndConsultationCard from '#/views/doctor/online-consultation/components/EndConsultationCard.vue';
2026-02-04 08:53:52 +08:00
import TransferPrescriptionMessageCard from '#/views/doctor/online-consultation/components/TransferPrescriptionMessageCard.vue';
import TransferPrescriptionViewDrawer from '#/views/doctor/online-consultation/components/TransferPrescriptionViewDrawer.vue';
2025-08-30 15:31:03 +08:00
const props = defineProps({
message: {
type: Object,
required: true,
},
isSent: {
type: Boolean,
default: false,
},
senderInfo: {
type: Object,
required: true,
},
});
2026-02-04 08:53:52 +08:00
const emit = defineEmits(['show-user-profile', 'open-prescription', 'transfer-import-success']);
const chatUserStore = chatUseUserStore();
const userStore = useUserStore();
const chatStore = useChatStore();
const themeStore = useThemeStore();
2026-02-04 08:53:52 +08:00
// 转接方查看抽屉相关
const showTransferDrawer = ref(false);
const transferRegisterId = ref(null);
const [RefusalOfTreatmentModals, RefusalOfTreatmentModalApi] = useVbenModal({
connectedComponent: RefusalOfTreatmentModal,
});
const avatarStyle = computed(() => ({
background: props.senderInfo.color,
}));
2025-08-02 16:57:57 +08:00
const statusInfo = ref({
status: 0,
text: '',
});
2025-08-18 16:08:37 +08:00
// 挂号操作状态
const registerOperating = ref(false);
2025-08-02 16:57:57 +08:00
// 添加处方状态映射
const prescriptionStatusMap = {
0: '待审核',
1: '已审核',
2: '未通过',
};
2025-08-30 15:31:03 +08:00
// 添加挂号状态映射 0待支付1已支付待接诊2接诊中3已结束4已取消5待评价6已评价7已拒诊
2025-08-18 16:08:37 +08:00
const registerStatusMap = {
2025-08-30 15:31:03 +08:00
0: '待支付',
1: '待接诊',
2: '接诊中',
3: '已结束',
4: '已取消',
5: '待评价',
6: '已评价',
7: '已拒诊',
2025-08-18 16:08:37 +08:00
};
// 添加支付状态映射
const payStatusMap = {
0: '未支付',
2025-08-30 15:31:03 +08:00
1: '已支付',
2025-08-18 16:08:37 +08:00
};
2025-12-23 11:05:12 +08:00
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) => {
2025-08-02 16:57:57 +08:00
statusInfo.value.status = res.status;
statusInfo.value.text = prescriptionStatusMap[res.status];
return prescriptionStatusMap[res.status];
});
2025-12-23 11:05:12 +08:00
}
2025-08-02 16:57:57 +08:00
};
2025-12-23 11:05:12 +08:00
// 计算属性:解析后的消息内容
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);
});
2026-05-06 15:53:45 +08:00
/** type10接口补全的挂号卡字段不写回 props避免破坏响应式 */
const registerCardPayload = ref(null);
const registerCardDisplay = computed(() => {
if (props.message.message_type !== 10) {
return {};
}
let base = {};
const raw = props.message.message_content || props.message.content || '';
try {
base =
typeof raw === 'string' && raw
? JSON.parse(raw)
: typeof raw === 'object' && raw
? { ...raw }
: {};
} catch {
base = {};
}
const enrich = registerCardPayload.value;
if (enrich && typeof enrich === 'object' && !Array.isArray(enrich)) {
return { ...base, ...enrich };
}
return base;
});
const registerDrugNamesLine = computed(() => {
const pc = registerCardDisplay.value;
if (!pc || typeof pc !== 'object') return '';
const arr = pc.selected_western_drugs;
if (Array.isArray(arr) && arr.length) {
return arr.map((d) => d?.name).filter(Boolean).join('、');
}
if (pc.drug?.name) return pc.drug.name;
return '';
});
const isFollowUpDrugAssistant = computed(() => {
if (props.message.message_type !== 11) return false;
const pc = parsedContent.value;
return !!(pc && typeof pc === 'object' && pc.flow === 'follow_up_drug');
});
function fetchRegisterCard() {
if (props.message.message_type !== 10) return;
const content = getParsedContent(
props.message.message_type,
props.message.message_content,
);
2026-05-06 15:53:45 +08:00
const id = content && typeof content === 'object' ? content.id : undefined;
if (!id) return;
getChatMessageRegisterInfoApi({ id }).then((res) => {
const body = res?.data?.data ?? res?.data ?? res;
registerCardPayload.value =
body && typeof body === 'object' && !Array.isArray(body) ? body : null;
});
}
watch(
() => [
props.message.message_type,
props.message.id,
props.message.message_content,
],
() => {
registerCardPayload.value = null;
fetchRegisterCard();
},
{ immediate: true },
);
2025-08-30 15:31:03 +08:00
2025-08-18 16:08:37 +08:00
// 接诊操作
const acceptRegister = async (registerId, orderNo) => {
if (registerOperating.value) return;
registerOperating.value = true;
try {
// 模拟API调用
2025-08-30 15:31:03 +08:00
receptionApi(registerId).then(async (value) => {
// message.success('接诊成功');
// 更新消息状态
2026-05-06 15:53:45 +08:00
fetchRegisterCard();
2025-08-30 15:31:03 +08:00
const messageObj = {
2025-12-23 11:05:12 +08:00
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,
2025-08-30 15:31:03 +08:00
isSent: true,
read: true,
duration: 0,
};
2026-05-06 15:53:45 +08:00
localStorage.setItem('currentRegisterId', String(registerId));
// chatStore.currentFriend?.register_id = reregisterId;
2025-12-23 11:05:12 +08:00
chatStore.addMessage(messageObj, chatUserStore.currentUser.doctor_id);
2025-08-30 15:31:03 +08:00
await sendMessage({
2025-12-23 11:05:12 +08:00
senderId: chatUserStore.currentUser.doctor_id,
2025-08-30 15:31:03 +08:00
receiverId: chatStore.currentFriend.id,
roomId: chatStore.currentFriend.room_id,
type: 'text',
content: '我已接诊,请简单说明您的情况~',
});
});
2025-08-18 16:08:37 +08:00
} catch (error) {
console.error('接诊失败:', error);
} finally {
registerOperating.value = false;
}
};
2026-02-04 08:53:52 +08:00
// 查看转接方详情
const handleViewTransferDetail = (transferData) => {
transferRegisterId.value = chatStore.currentFriend?.register_id;
showTransferDrawer.value = true;
};
// 处理转接方导入成功
const handleTransferImportSuccess = (prescriptionData) => {
// 触发打开处方模态框事件
const registerId = transferRegisterId.value || chatStore.currentFriend?.register_id;
if (registerId) {
emit('open-prescription', registerId);
}
};
2025-08-18 16:08:37 +08:00
// 拒诊操作
const rejectRegister = async (registerId, orderNo) => {
if (registerOperating.value) return;
registerOperating.value = true;
2025-08-30 15:31:03 +08:00
RefusalOfTreatmentModalApi.setData({
values: {
register_id: registerId,
},
});
RefusalOfTreatmentModalApi.open();
registerOperating.value = false;
2025-08-18 16:08:37 +08:00
};
const bubbleClasses = computed(() => {
const baseClasses = ['message-bubble'];
if (props.isSent) {
baseClasses.push('sent');
} else {
baseClasses.push('received');
}
2025-12-23 11:05:12 +08:00
if (props.message.message_type === 0) {
2025-07-31 16:03:07 +08:00
baseClasses.push('bubble-bg');
}
if (themeStore.isDarkMode) {
baseClasses.push('dark');
}
return baseClasses;
});
const handlePreviewMedia = (content, type) => {
previewMedia(content, type);
};
const showUserProfile = () => {
emit('show-user-profile', props.senderInfo);
};
const formatDuration = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const formatTextWithLinks = (text) => {
console.log(text, props.message);
2025-08-30 15:31:03 +08:00
if (!text) return text; // 或 return text (但可能返回 undefined/null根据需求调整)
2025-12-23 11:05:12 +08:00
// 如果 text 是对象,转换为字符串
if (typeof text === 'object') {
text = JSON.stringify(text);
}
const urlRegex = /(https?:\/\/\S+)/g;
return text.replaceAll(
urlRegex,
'<a href="$1" target="_blank" rel="noopener noreferrer" class="text-link">$1</a>',
);
};
const openUrl = (url) => {
window.open(url, '_blank', 'noopener,noreferrer');
};
const getCallStatusText = (status) => {
const statusMap = {
invite: '发起通话',
accepted: '通话已接通',
rejected: '通话被拒绝',
ended: '通话已结束',
missed: '未接通话',
};
return statusMap[status] || '通话消息';
};
const handleImageError = (event) => {
event.target.src = '/placeholder.svg?height=200&width=300';
};
const handleVideoError = (event) => {
console.error('视频加载失败:', event);
};
2025-07-31 16:03:07 +08:00
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail,
});
const viewPrescription = (item) => {
PrescriptionDetailModalApi.setData({
values: item.id,
});
2025-07-31 16:03:07 +08:00
PrescriptionDetailModalApi.open();
};
2025-08-02 16:57:57 +08:00
2025-08-18 16:08:37 +08:00
// 格式化日期时间
const formatDateTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
2025-08-18 16:08:37 +08:00
});
};
// 获取性别文本
const getSexText = (sex) => {
return sex === 1 ? '男' : '女';
};
</script>
2025-07-19 16:47:52 +08:00
<template>
<div
:class="isSent ? 'flex-row-reverse' : 'flex-row'"
class="mb-4 flex items-start gap-3"
>
2025-08-30 15:31:03 +08:00
<RefusalOfTreatmentModals />
2025-07-31 16:03:07 +08:00
<PrescriptionDetailModal />
2025-07-19 16:47:52 +08:00
<!-- 头像 -->
<div
v-if="isSent"
2025-07-19 16:47:52 +08:00
:style="avatarStyle"
class="cursor-pointer rounded-full"
2025-07-19 16:47:52 +08:00
@click="showUserProfile"
>
<Avatar
v-if="userStore.userInfo?.avatar"
:size="48"
:src="userStore.userInfo?.avatar"
/>
<Avatar v-else :size="48">
{{ userStore.userInfo?.nick_name.charAt(0) }}
</Avatar>
</div>
<div
v-else
:style="avatarStyle"
class="cursor-pointer rounded-full"
@click="showUserProfile"
>
<Avatar v-if="senderInfo.avatar" :size="48" :src="senderInfo.avatar" />
<Avatar v-else :size="48">{{ senderInfo.nick_name.charAt(0) }}</Avatar>
2025-07-19 16:47:52 +08:00
</div>
<!-- 消息内容区域 -->
<div
:class="isSent ? 'items-end' : 'items-start'"
class="flex max-w-xs flex-col md:max-w-md lg:max-w-lg xl:max-w-xl"
>
2025-07-19 16:47:52 +08:00
<!-- 发送者名称 -->
<div
v-if="isSent"
class="mb-1 px-2 text-xs text-gray-500 dark:text-gray-400"
>
{{ userStore.userInfo?.nick_name }}
</div>
<div v-else class="mb-1 px-2 text-xs text-gray-500 dark:text-gray-400">
2025-07-19 16:47:52 +08:00
{{ senderInfo.nick_name }}
</div>
<!-- 消息气泡 -->
<div :class="bubbleClasses" class="relative rounded-2xl p-4 shadow-sm">
2025-07-19 16:47:52 +08:00
<!-- 图片消息 -->
<div
2025-12-23 11:05:12 +08:00
v-if="message.message_type === 1"
2026-01-09 11:04:03 +08:00
class="image-message relative cursor-pointer"
style="width: 400px;"
2025-12-23 11:05:12 +08:00
@click="handlePreviewMedia(parsedContent, 'image')"
2025-07-19 16:47:52 +08:00
>
<Image
2025-12-23 11:05:12 +08:00
:src="parsedContent"
alt="图片消息"
class="max-w-full rounded-lg"
2025-07-19 16:47:52 +08:00
@error="handleImageError"
/>
</div>
<!-- 视频消息 -->
<div
2025-12-23 11:05:12 +08:00
v-else-if="message.message_type === 3"
class="video-message relative cursor-pointer"
2025-12-23 11:05:12 +08:00
@click="handlePreviewMedia(parsedContent, 'video')"
2025-07-19 16:47:52 +08:00
>
<XgVideoPlayer
2025-12-23 11:05:12 +08:00
:src="parsedContent"
class="video-thumbnail max-w-full rounded-lg"
thumbnail
/>
2025-07-19 16:47:52 +08:00
<div class="video-overlay">
<div class="play-button">
<i class="fas fa-play"></i>
</div>
<div v-if="message.duration" class="video-duration">
2025-07-19 16:47:52 +08:00
{{ formatDuration(message.duration) }}
</div>
</div>
</div>
<!-- URL链接消息 -->
2025-12-23 11:05:12 +08:00
<div v-else-if="messageTypeName === 'url'" class="url-message">
2025-07-19 16:47:52 +08:00
<div class="url-preview" @click="openUrl(message.url)">
<div class="url-favicon">
<img
v-if="message.favicon"
:alt="message.domain"
:src="message.favicon"
/>
<i v-else class="fas fa-link"></i>
2025-07-19 16:47:52 +08:00
</div>
<div class="url-content">
<div class="url-title">{{ message.title || message.url }}</div>
<div v-if="message.description" class="url-description">
2025-07-19 16:47:52 +08:00
{{ message.description }}
</div>
<div class="url-domain">{{ message.domain }}</div>
</div>
<div v-if="message.thumbnail" class="url-thumbnail">
<img :alt="message.title" :src="message.thumbnail" />
2025-07-19 16:47:52 +08:00
</div>
</div>
<div v-if="message.text" class="url-text">
2025-08-02 16:57:57 +08:00
<Button type="link">{{ message.text }}</Button>
2025-07-19 16:47:52 +08:00
</div>
</div>
<!-- 音频消息 -->
<AudioMessage
2025-12-23 11:05:12 +08:00
v-else-if="message.message_type === 2"
:is-sent="isSent"
:message="message"
/>
2025-07-19 16:47:52 +08:00
2025-08-18 16:08:37 +08:00
<!-- 挂号卡片消息 -->
2025-12-23 11:05:12 +08:00
<div v-else-if="message.message_type === 10" class="register-card">
2025-08-18 16:08:37 +08:00
<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"
>
<i
class="fas fa-calendar-check text-xl text-green-500 dark:text-green-300"
></i>
2025-08-18 16:08:37 +08:00
</div>
<div class="min-w-0 flex-1">
2025-08-18 16:08:37 +08:00
<div class="flex items-center justify-between">
<div class="font-semibold text-gray-800 dark:text-gray-100">
挂号信息
</div>
2025-08-18 16:08:37 +08:00
<div
:class="{
'bg-yellow-100 text-yellow-800':
2026-05-06 15:53:45 +08:00
registerCardDisplay.status === 0,
'bg-blue-100 text-blue-800': registerCardDisplay.status === 1,
'bg-red-100 text-red-800': registerCardDisplay.status === 2,
'bg-red-100 text-red-800': registerCardDisplay.status === 3,
'bg-gray-100 text-gray-800': registerCardDisplay.status === 4,
'bg-purple-100 text-purple-800':
2026-05-06 15:53:45 +08:00
registerCardDisplay.status === 5,
'bg-indigo-100 text-indigo-800':
2026-05-06 15:53:45 +08:00
registerCardDisplay.status === 6,
'bg-pink-100 text-pink-800': registerCardDisplay.status === 7,
2025-08-18 16:08:37 +08:00
}"
class="rounded-full px-2 py-1 text-xs font-medium"
2025-08-18 16:08:37 +08:00
>
2026-05-06 15:53:45 +08:00
{{ registerStatusMap[registerCardDisplay.status] || '未知状态' }}
2025-08-18 16:08:37 +08:00
</div>
</div>
<div
class="mt-1 flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300"
>
2025-08-18 16:08:37 +08:00
<div class="flex items-center gap-1">
<i class="fas fa-hashtag text-xs"></i>
<span class="max-w-[120px] truncate">{{
2026-05-06 15:53:45 +08:00
registerCardDisplay.order_no
}}</span>
2025-08-18 16:08:37 +08:00
</div>
<div class="flex items-center gap-1">
<i class="fas fa-list-ol text-xs"></i>
2026-05-06 15:53:45 +08:00
<span>{{ registerCardDisplay.order_number }}</span>
2025-08-18 16:08:37 +08:00
</div>
</div>
</div>
</div>
<!-- 患者信息 -->
<div class="mt-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-800">
<div class="mb-2 flex items-center justify-between">
<div class="font-medium text-gray-700 dark:text-gray-200">
患者信息
</div>
2025-08-18 16:08:37 +08:00
<div
:class="{
2026-05-06 15:53:45 +08:00
'bg-red-100 text-red-800': registerCardDisplay.is_pay === 0,
'bg-green-100 text-green-800': registerCardDisplay.is_pay === 1,
2025-08-18 16:08:37 +08:00
}"
class="rounded-full px-2 py-1 text-xs font-medium"
2025-08-18 16:08:37 +08:00
>
2026-05-06 15:53:45 +08:00
{{ payStatusMap[registerCardDisplay.is_pay] || '未知' }}
2025-08-18 16:08:37 +08:00
</div>
</div>
<div
class="grid grid-cols-2 gap-2 text-sm text-gray-600 dark:text-gray-300"
>
2025-08-18 16:08:37 +08:00
<div class="flex items-center gap-1">
<i class="fas fa-user text-xs"></i>
2026-05-06 15:53:45 +08:00
<span>{{ registerCardDisplay.user_patient?.name || '未知' }}</span>
2025-08-18 16:08:37 +08:00
</div>
<div class="flex items-center gap-1">
<i class="fas fa-birthday-cake text-xs"></i>
2026-05-06 15:53:45 +08:00
<span>{{ registerCardDisplay.user_patient?.age || 0 }}</span>
2025-08-18 16:08:37 +08:00
</div>
<div class="flex items-center gap-1">
<i class="fas fa-venus-mars text-xs"></i>
2026-05-06 15:53:45 +08:00
<span>{{ getSexText(registerCardDisplay.user_patient?.sex) }}</span>
2025-08-18 16:08:37 +08:00
</div>
<div class="flex items-center gap-1">
<i class="fas fa-phone text-xs"></i>
<span>{{
2026-05-06 15:53:45 +08:00
registerCardDisplay.user_patient?.mobile ? '已绑定' : '未绑定'
}}</span>
2025-08-18 16:08:37 +08:00
</div>
</div>
</div>
2026-05-06 15:53:45 +08:00
<!-- 主诉 / 所选药品 / 数量与小程序挂号卡对齐 -->
<div
v-if="registerCardDisplay.chief_complaint || registerDrugNamesLine || (registerCardDisplay.number != null && registerCardDisplay.number !== '')"
class="mt-3 space-y-2 rounded-lg border border-gray-100 p-3 text-sm dark:border-gray-600"
>
<div v-if="registerCardDisplay.chief_complaint">
<span class="font-medium text-gray-600 dark:text-gray-300">主诉</span>
<span class="text-gray-800 dark:text-gray-100">{{
registerCardDisplay.chief_complaint
}}</span>
</div>
<div v-if="registerDrugNamesLine">
<span class="font-medium text-gray-600 dark:text-gray-300">所选药品</span>
<span class="text-gray-800 dark:text-gray-100">{{
registerDrugNamesLine
}}</span>
</div>
<div
v-if="registerCardDisplay.number != null && registerCardDisplay.number !== ''"
>
<span class="font-medium text-gray-600 dark:text-gray-300">数量</span>
<span class="text-gray-800 dark:text-gray-100"
> {{ registerCardDisplay.number }} </span
>
</div>
</div>
2025-08-18 16:08:37 +08:00
<!-- 操作按钮区域 -->
<div class="mt-3 border-t border-gray-200 pt-3 dark:border-gray-600">
<div class="mb-2 flex items-center justify-between">
2025-08-18 16:08:37 +08:00
<div class="text-sm font-medium text-gray-700 dark:text-gray-200">
挂号费用:
2026-05-06 15:53:45 +08:00
<span class="text-green-500 dark:text-green-400">¥{{ registerCardDisplay.price }}</span>
2025-08-18 16:08:37 +08:00
</div>
</div>
<!-- 接诊/拒诊按钮 - 只在待就诊状态显示 -->
2026-05-06 15:53:45 +08:00
<div v-if="registerCardDisplay.status === 1" class="mt-2 flex gap-2">
2025-08-18 16:08:37 +08:00
<Button
:loading="registerOperating"
class="flex-1"
size="small"
type="primary"
@click="
2026-05-06 15:53:45 +08:00
acceptRegister(registerCardDisplay.id, registerCardDisplay.order_no)
"
2025-08-18 16:08:37 +08:00
>
<i class="fas fa-check mr-1"></i>
接诊
</Button>
<Button
:loading="registerOperating"
class="flex-1"
danger
size="small"
@click="
2026-05-06 15:53:45 +08:00
rejectRegister(registerCardDisplay.id, registerCardDisplay.order_no)
"
2025-08-18 16:08:37 +08:00
>
<i class="fas fa-times mr-1"></i>
拒诊
</Button>
</div>
<!-- 查看详情按钮 -->
<!-- <div class="flex items-center justify-center mt-2">-->
<!-- <div class="flex items-center text-blue-500 dark:text-blue-300 hover:text-blue-600 dark:hover:text-blue-200 transition-colors cursor-pointer">-->
<!-- <span class="text-sm font-medium">查看详情</span>-->
<!-- <i class="fas fa-chevron-right ml-1 text-xs"></i>-->
<!-- </div>-->
<!-- </div>-->
2025-08-18 16:08:37 +08:00
</div>
<!-- 时间信息 -->
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
2026-05-06 15:53:45 +08:00
创建时间: {{ registerCardDisplay.created_at }}
<span v-if="registerCardDisplay.pay_time" class="ml-2">
支付时间: {{ formatDateTime(registerCardDisplay.pay_time) }}
2025-08-18 16:08:37 +08:00
</span>
</div>
</div>
2025-07-31 16:03:07 +08:00
<!-- 处方卡片消息 -->
<div
2025-12-23 11:05:12 +08:00
v-else-if="message.message_type === 4"
2025-07-31 16:03:07 +08:00
class="prescription-card"
2025-12-23 11:05:12 +08:00
@click="viewPrescription(parsedContent)"
2025-07-31 16:03:07 +08:00
>
<div class="flex items-center gap-3">
<div
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900"
>
<i
class="fas fa-file-prescription text-xl text-blue-500 dark:text-blue-300"
></i>
2025-07-31 16:03:07 +08:00
</div>
<div class="min-w-0 flex-1">
2025-07-31 16:03:07 +08:00
<div class="flex items-center justify-between">
<div class="font-semibold text-gray-800 dark:text-gray-100">
电子处方
</div>
2025-07-31 16:03:07 +08:00
<div
:class="{
'bg-yellow-100 text-yellow-800': statusInfo.status === 0,
'bg-green-100 text-green-800': statusInfo.status === 1,
'bg-red-100 text-red-800': statusInfo.status === 2,
'bg-blue-100 text-blue-800': statusInfo.status === 3,
'bg-gray-100 text-gray-800': statusInfo.status === 4,
}"
class="rounded-full px-2 py-1 text-xs font-medium"
2025-07-31 16:03:07 +08:00
>
{{
2025-12-23 11:05:12 +08:00
checkPrescriptionStatus(parsedContent) ||
statusInfo.text ||
'加载中'
}}
2025-07-31 16:03:07 +08:00
</div>
</div>
<div
class="mt-1 flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300"
>
2025-07-31 16:03:07 +08:00
<div class="flex items-center gap-1">
<i class="fas fa-hashtag text-xs"></i>
<span class="max-w-[100px] truncate">{{
2025-12-23 11:05:12 +08:00
parsedContent.order_no
}}</span>
2025-07-31 16:03:07 +08:00
</div>
<div class="flex items-center gap-1">
<i class="fas fa-truck text-xs"></i>
2025-12-23 11:05:12 +08:00
<span>{{ parsedContent.express_name }}</span>
2025-07-31 16:03:07 +08:00
</div>
</div>
</div>
</div>
<div
class="mt-3 flex items-center justify-between border-t border-gray-200 pt-2 dark:border-gray-600"
>
2025-07-31 16:03:07 +08:00
<div class="text-sm font-medium text-gray-700 dark:text-gray-200">
支付金额:
<span class="text-red-500 dark:text-red-400">
2025-12-23 11:05:12 +08:00
¥{{ parsedContent.total_pay_price }}
</span>
2025-07-31 16:03:07 +08:00
</div>
<div
class="flex items-center text-blue-500 transition-colors hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-200"
>
2025-07-31 16:03:07 +08:00
<span class="text-sm font-medium">查看处方</span>
<i class="fas fa-chevron-right ml-1 text-xs"></i>
</div>
</div>
</div>
<!-- 在线复诊|购药信息 -->
<div
2025-12-23 11:05:12 +08:00
v-else-if="messageTypeName === 'follow-up-visit'"
class="group transition-all duration-200"
>
<!-- 卡片容器 -->
<div
:class="{
2025-12-23 11:05:12 +08:00
'border-green-200': parsedContent.hasVisited === '1', // 已到访状态
'border-yellow-200': parsedContent.hasVisited === '0', // 未到访状态
}"
class="rounded-lg border p-4 shadow-sm"
>
<!-- 标题区域 -->
<div class="mb-3 flex items-center justify-between">
<h3 class="flex items-center text-lg font-semibold">
<svg
class="mr-2 h-4 w-4 text-green-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
2025-12-23 11:05:12 +08:00
{{ parsedContent.drug_name }} 复诊信息
</h3>
<span
:class="{
2025-12-23 11:05:12 +08:00
'text-green-800': parsedContent.hasVisited === '1',
'bg-yellow-100 text-yellow-800':
2025-12-23 11:05:12 +08:00
parsedContent.hasVisited === '0',
}"
class="rounded-full px-2 py-1 text-xs"
>
2025-12-23 11:05:12 +08:00
{{ parsedContent.hasVisited === '1' ? '就诊过' : '未就诊过' }}
</span>
</div>
<!-- 症状信息 -->
<div class="space-y-2">
<p class="flex items-start text-sm">
<span class="w-20">症状描述</span>
2025-12-23 11:05:12 +08:00
<span class="flex-1">{{ parsedContent.illnessInfo }}</span>
</p>
<!-- 用药状态 -->
<p
:class="{
'font-medium text-blue-600':
2025-12-23 11:05:12 +08:00
parsedContent.hasUsedDrug === '1',
'text-gray-500': parsedContent.hasUsedDrug === '0',
}"
class="text-sm"
>
<span class="w-20 text-gray-500">用药情况</span>
{{
2025-12-23 11:05:12 +08:00
parsedContent.hasUsedDrug === '1' ? '使用过' : '未使用过'
}}
</p>
<!-- 医生信息 -->
<p
class="mt-3 border-t border-gray-100 pt-3 text-sm text-gray-500"
>
<span class="w-20 text-gray-400">主诊医生</span>
2025-12-23 11:05:12 +08:00
ID: {{ parsedContent.doctor_id }}
</p>
</div>
</div>
</div>
2026-01-07 14:22:01 +08:00
<!-- 系统消息/价格更新卡片 (type=9) -->
<div
v-else-if="message.message_type === 9"
class="system-message-card"
>
<div
v-if="parsedContent?.type === 'price_update'"
class="price-update-card"
>
<div class="price-update-header">
2026-01-09 11:04:03 +08:00
<div class="header-icon">
<i class="fas fa-tag"></i>
</div>
2026-01-07 14:22:01 +08:00
<span class="price-title">订单价格更新</span>
</div>
<div class="price-update-content">
<p class="price-message">{{ parsedContent.message }}</p>
2026-01-09 11:04:03 +08:00
<!-- 订单信息 -->
<div class="order-info">
<div class="info-item">
<span class="info-label">
<i class="fas fa-hashtag"></i>
订单号
</span>
<span class="info-value">{{ parsedContent.order_no }}</span>
</div>
</div>
<!-- 价格详情 -->
<div class="price-info-section">
<div class="price-info-row">
<span class="price-label">
<i class="fas fa-shipping-fast"></i>
运费
</span>
<span class="price-value shipping-fee">
<span v-if="parsedContent.is_free_shipping === 1 || parsedContent.is_free_shipping === '1'" class="free-shipping-tag">
<i class="fas fa-check-circle"></i>
包邮
</span>
<span v-else>¥{{ parsedContent.trans_expenses || '0.00' }}</span>
</span>
</div>
<div class="price-info-row total-row">
<span class="price-label">
<i class="fas fa-money-bill-wave"></i>
总价
</span>
<span class="price-amount">¥{{ parsedContent.total_pay_price }}</span>
</div>
2026-01-07 14:22:01 +08:00
</div>
</div>
</div>
<div v-else class="system-message-text">
{{ typeof parsedContent === 'string' ? parsedContent : '系统消息' }}
</div>
</div>
2026-05-06 15:53:45 +08:00
<!-- type11复诊助理用药确认与药店患者就诊经历卡片分流 -->
<FollowUpDrugAssistantCard
v-else-if="message.message_type === 11 && isFollowUpDrugAssistant"
:content="parsedContent"
/>
2025-12-23 11:05:12 +08:00
<!-- 患者就诊经历卡片 (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"
/>
2026-02-04 08:53:52 +08:00
<!-- 转接方消息卡片 (type=14) -->
<TransferPrescriptionMessageCard
v-else-if="message.message_type === 14"
:content="parsedContent"
:register-id="chatStore.currentFriend?.register_id"
@view-detail="handleViewTransferDetail"
@import-success="handleTransferImportSuccess"
/>
2025-07-19 16:47:52 +08:00
<!-- 通话消息 -->
<div
v-else-if="
2025-12-23 11:05:12 +08:00
message.message_type === 6 || message.message_type === 7
"
class="call-message"
>
2025-07-19 16:47:52 +08:00
<div class="call-info">
<i
2025-12-23 11:05:12 +08:00
:class="message.message_type === 6 ? 'fa-video' : 'fa-phone'"
class="fas"
></i>
2025-07-19 16:47:52 +08:00
<span>{{ getCallStatusText(message.callStatus) }}</span>
</div>
<div v-if="message.duration" class="call-duration">
2025-07-19 16:47:52 +08:00
通话时长: {{ formatDuration(message.duration) }}
</div>
</div>
<!-- 文本消息 -->
<div v-else class="whitespace-pre-wrap text-base leading-relaxed">
2025-12-23 11:05:12 +08:00
<span v-html="formatTextWithLinks(parsedContent || message.message_content || message.content || '')"></span>
2025-07-19 16:47:52 +08:00
</div>
</div>
<!-- 消息时间和状态 -->
<div
class="mt-1 flex items-center gap-1 px-2 text-xs text-gray-400 dark:text-gray-500"
>
2025-07-19 16:47:52 +08:00
<span>{{ message.time }}</span>
<i
v-if="isSent && message.read"
class="fas fa-check-double text-green-500"
></i>
<i
v-else-if="isSent && !message.read"
class="fas fa-clock text-gray-400"
></i>
2025-07-19 16:47:52 +08:00
</div>
</div>
2026-02-04 08:53:52 +08:00
<!-- 转接方查看抽屉 -->
<TransferPrescriptionViewDrawer
v-model:visible="showTransferDrawer"
:register-id="transferRegisterId"
@import-success="handleTransferImportSuccess"
/>
2025-07-19 16:47:52 +08:00
</div>
</template>
<style scoped>
.message-bubble {
position: relative;
max-width: 100%;
word-wrap: break-word;
transition: all 0.2s ease;
}
.message-bubble.sent {
color: white;
border-bottom-right-radius: 4px;
}
2025-07-31 16:03:07 +08:00
.bubble-bg {
background: linear-gradient(135deg, #4361ee, #3f37c9);
}
.bubble-bg.dark {
background: linear-gradient(135deg, #4361ee, #3f37c9);
}
.message-bubble.received.bubble-bg {
2025-07-19 16:47:52 +08:00
background: white;
color: #1a202c;
border: 1px solid #e2e8f0;
border-bottom-left-radius: 4px;
}
2025-07-31 16:03:07 +08:00
.message-bubble.received.dark.bubble-bg {
2025-07-19 16:47:52 +08:00
background: #374151;
color: #f7fafc;
border-color: #4b5563;
}
.video-message {
position: relative;
border-radius: 12px;
overflow: hidden;
max-width: 300px;
}
.video-thumbnail {
width: 100%;
height: auto;
max-height: 200px;
object-fit: cover;
display: block;
}
.video-overlay {
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 0, 0, 0.3) 100%
);
2025-07-19 16:47:52 +08:00
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-message:hover .video-overlay {
opacity: 1;
}
.play-button {
width: 60px;
height: 60px;
background: rgba(0, 0, 0, 0.7);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.play-button:hover {
background: rgba(0, 0, 0, 0.8);
transform: scale(1.1);
}
.video-duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.call-message {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 0;
}
.call-info {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.call-info i {
width: 16px;
text-align: center;
}
.call-duration {
font-size: 12px;
opacity: 0.8;
}
.url-message {
max-width: 400px;
}
.url-preview {
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
background: white;
display: flex;
align-items: flex-start;
padding: 12px;
gap: 12px;
}
.url-preview:hover {
border-color: #4361ee;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(67, 97, 238, 0.15);
}
.message-bubble.sent .url-preview {
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
.message-bubble.received.dark .url-preview {
border-color: #4b5563;
background: #4b5563;
}
.url-favicon {
width: 20px;
height: 20px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.url-favicon img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 2px;
}
.url-content {
flex: 1;
min-width: 0;
}
.url-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
color: #1a202c;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.message-bubble.sent .url-title {
color: white;
}
.message-bubble.received.dark .url-title {
color: #f7fafc;
}
.url-description {
font-size: 12px;
color: #64748b;
margin-bottom: 4px;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.message-bubble.sent .url-description {
color: rgba(255, 255, 255, 0.8);
}
.message-bubble.received.dark .url-description {
color: #9ca3af;
}
.url-domain {
font-size: 11px;
color: #94a3b8;
font-weight: 500;
}
.message-bubble.sent .url-domain {
color: rgba(255, 255, 255, 0.7);
}
.message-bubble.received.dark .url-domain {
color: #6b7280;
}
.url-thumbnail {
width: 60px;
height: 60px;
flex-shrink: 0;
border-radius: 8px;
overflow: hidden;
}
.url-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.url-text {
margin-top: 8px;
font-size: 14px;
line-height: 1.5;
}
:deep(.text-link) {
color: #4361ee;
text-decoration: underline;
transition: color 0.2s ease;
}
:deep(.text-link:hover) {
color: #3f37c9;
}
.message-bubble.sent :deep(.text-link) {
color: rgba(255, 255, 255, 0.9);
}
.message-bubble.sent :deep(.text-link:hover) {
color: white;
}
2025-07-31 16:03:07 +08:00
2025-08-18 16:08:37 +08:00
/* 挂号卡片样式 */
.register-card {
@apply w-full max-w-xs cursor-pointer rounded-xl bg-white p-4 shadow-sm transition-all duration-300 md:max-w-sm dark:bg-gray-700;
@apply border border-gray-200 hover:border-green-300 hover:shadow-md dark:border-gray-600 dark:hover:border-green-500;
2025-08-18 16:08:37 +08:00
}
.message-bubble.sent .register-card {
@apply border-green-200/50 bg-green-50/30 dark:border-green-700/70 dark:bg-green-900/30;
2025-08-18 16:08:37 +08:00
}
.register-card:hover {
@apply -translate-y-0.5 transform;
2025-08-18 16:08:37 +08:00
}
2025-07-31 16:03:07 +08:00
/* 处方卡片样式 - 使用TailwindCSS类替代 */
.prescription-card {
@apply w-full max-w-xs cursor-pointer rounded-xl bg-white p-4 shadow-sm transition-all duration-300 md:max-w-sm dark:bg-gray-700;
@apply border border-gray-200 hover:border-blue-300 hover:shadow-md dark:border-gray-600 dark:hover:border-blue-500;
2025-07-31 16:03:07 +08:00
}
.message-bubble.sent .prescription-card {
@apply border-blue-200/50 bg-blue-50/30 dark:border-blue-700/70 dark:bg-blue-900/30;
2025-07-31 16:03:07 +08:00
}
.prescription-card:hover {
@apply -translate-y-0.5 transform;
2025-07-31 16:03:07 +08:00
}
2026-01-07 14:22:01 +08:00
/* 系统消息/价格更新卡片样式 */
.system-message-card {
@apply w-full max-w-md rounded-xl p-4;
}
.price-update-card {
@apply rounded-xl border border-orange-200 bg-gradient-to-r from-orange-50 to-amber-50 p-4 shadow-sm dark:border-orange-700 dark:from-orange-900/30 dark:to-amber-900/30;
2026-01-09 11:04:03 +08:00
transition: all 0.3s ease;
}
.price-update-card:hover {
@apply shadow-md;
transform: translateY(-2px);
}
.message-bubble.sent .price-update-card {
@apply border-orange-200/50 bg-orange-50/30 dark:border-orange-700/70 dark:bg-orange-900/30;
2026-01-07 14:22:01 +08:00
}
.price-update-header {
2026-01-09 11:04:03 +08:00
@apply mb-3 flex items-center gap-3;
2026-01-07 14:22:01 +08:00
}
2026-01-09 11:04:03 +08:00
.header-icon {
@apply flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100 text-orange-500 dark:bg-orange-900/50 dark:text-orange-300;
2026-01-07 14:22:01 +08:00
}
.price-title {
2026-01-09 11:04:03 +08:00
@apply flex-1 text-base font-semibold text-orange-700 dark:text-orange-300;
2026-01-07 14:22:01 +08:00
}
.price-update-content {
2026-01-09 11:04:03 +08:00
@apply space-y-3;
2026-01-07 14:22:01 +08:00
}
.price-message {
2026-01-09 11:04:03 +08:00
@apply rounded-lg bg-white/60 p-2 text-sm text-gray-700 dark:bg-gray-800/60 dark:text-gray-300;
}
.order-info {
@apply rounded-lg bg-white/60 p-3 dark:bg-gray-800/60;
}
.info-item {
@apply flex items-center justify-between;
}
.info-label {
@apply flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400;
}
.info-label i {
@apply text-orange-500 dark:text-orange-400;
}
.info-value {
@apply text-sm font-medium text-gray-700 dark:text-gray-200;
}
.price-info-section {
@apply space-y-2 rounded-lg border-t border-orange-200/50 bg-white/60 pt-3 dark:border-orange-700/50 dark:bg-gray-800/60;
}
.price-info-row {
@apply flex items-center justify-between;
2026-01-07 14:22:01 +08:00
}
2026-01-09 11:04:03 +08:00
.price-label {
@apply flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-300;
2026-01-07 14:22:01 +08:00
}
2026-01-09 11:04:03 +08:00
.price-label i {
@apply text-orange-500 dark:text-orange-400;
}
.price-value {
@apply text-sm font-medium text-gray-700 dark:text-gray-200;
}
.shipping-fee {
@apply flex items-center;
}
.free-shipping-tag {
@apply inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900/50 dark:text-green-300;
}
.free-shipping-tag i {
@apply text-green-600 dark:text-green-400;
}
.total-row {
@apply border-t border-orange-200/50 pt-2 dark:border-orange-700/50;
}
.total-row .price-label {
@apply text-base font-semibold;
2026-01-07 14:22:01 +08:00
}
.price-amount {
@apply text-lg font-bold text-orange-600 dark:text-orange-400;
}
.system-message-text {
@apply rounded-lg bg-gray-100 p-3 text-center text-sm text-gray-600 dark:bg-gray-700 dark:text-gray-300;
}
2026-01-09 11:04:03 +08:00
/* 患者就诊经历卡片发送样式 */
.message-bubble.sent .patient-experience-card {
@apply border-blue-200/50 bg-blue-50/30 dark:border-blue-700/70 dark:bg-blue-900/30;
}
/* 商品卡片发送样式 */
.message-bubble.sent .product-card {
@apply border-purple-200/50 bg-purple-50/30 dark:border-purple-700/70 dark:bg-purple-900/30;
}
/* 结束问诊卡片发送样式 */
.message-bubble.sent .end-consultation-card {
@apply border-gray-200/50 bg-gray-50/30 dark:border-gray-700/70 dark:bg-gray-900/30;
}
/* 图片和视频消息发送样式 */
.message-bubble.sent .image-message,
.message-bubble.sent .video-message {
opacity: 0.9;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
}
/* 通话消息发送样式 */
.message-bubble.sent .call-message {
color: white;
}
2025-07-19 16:47:52 +08:00
</style>