Files
xk-admin/apps/web-antd/src/views/business/chat/components/MessageBubble.vue
lq 09a423b8dc
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
fix: 各种提交(日志相关、组件封装相关、订单相关
2025-11-08 15:05:52 +08:00

1021 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
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';
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';
import RefusalOfTreatmentModal from '#/views/doctor/doctor-reception/components/RefusalOfTreatmentModal.vue';
import previewMedia from '../composables/useMediaPreview.ts';
import { useThemeStore } from '../stores/theme.ts';
import { useUserStore as chatUseUserStore } from '../stores/user';
import AudioMessage from './AudioMessage.vue';
const props = defineProps({
message: {
type: Object,
required: true,
},
isSent: {
type: Boolean,
default: false,
},
senderInfo: {
type: Object,
required: true,
},
});
const emit = defineEmits(['show-user-profile']);
const chatUserStore = chatUseUserStore();
const userStore = useUserStore();
const chatStore = useChatStore();
const themeStore = useThemeStore();
const [RefusalOfTreatmentModals, RefusalOfTreatmentModalApi] = useVbenModal({
connectedComponent: RefusalOfTreatmentModal,
});
const avatarStyle = computed(() => ({
background: props.senderInfo.color,
}));
const statusInfo = ref({
status: 0,
text: '',
});
// 挂号操作状态
const registerOperating = ref(false);
// 添加处方状态映射
const prescriptionStatusMap = {
0: '待审核',
1: '已审核',
2: '未通过',
};
// 添加挂号状态映射 0待支付1已支付待接诊2接诊中3已结束4已取消5待评价6已评价7已拒诊
const registerStatusMap = {
0: '待支付',
1: '待接诊',
2: '接诊中',
3: '已结束',
4: '已取消',
5: '待评价',
6: '已评价',
7: '已拒诊',
};
// 添加支付状态映射
const payStatusMap = {
0: '未支付',
1: '已支付',
};
const checkPrescriptionStatus = (id) => {
if (!id) return;
getPrescriptionCheckStatusApi({ id }).then((res) => {
statusInfo.value.status = res.status;
statusInfo.value.text = prescriptionStatusMap[res.status];
return prescriptionStatusMap[res.status];
});
};
const getRegisterCardInfo = () => {
getChatMessageRegisterInfoApi({ id: props.message.content.id }).then(
(res) => {
// eslint-disable-next-line vue/no-mutating-props
props.message.content = res;
},
);
};
// 接诊操作
const acceptRegister = async (registerId, orderNo) => {
if (registerOperating.value) return;
registerOperating.value = true;
try {
// 模拟API调用
receptionApi(registerId).then(async (value) => {
// message.success('接诊成功');
// 更新消息状态
getRegisterCardInfo();
const messageObj = {
type: 'text',
content: '我已接诊,请简单说明您的情况~',
time: new Date().toLocaleTimeString().slice(0, 5),
senderId: `${chatUserStore.currentUser.id}`,
receiverId: chatStore.currentFriend.id,
isSent: true,
read: true,
duration: 0,
};
localStorage.setItem('currentRegisterId', reregisterId);
// chatStore.currentFriend?.register_id = reregisterId;
chatStore.addMessage(messageObj, chatUserStore.currentUser.id);
await sendMessage({
senderId: chatUserStore.currentUser.id,
receiverId: chatStore.currentFriend.id,
roomId: chatStore.currentFriend.room_id,
type: 'text',
content: '我已接诊,请简单说明您的情况~',
});
});
} catch (error) {
console.error('接诊失败:', error);
} finally {
registerOperating.value = false;
}
};
// 拒诊操作
const rejectRegister = async (registerId, orderNo) => {
if (registerOperating.value) return;
registerOperating.value = true;
RefusalOfTreatmentModalApi.setData({
values: {
register_id: registerId,
},
});
RefusalOfTreatmentModalApi.open();
registerOperating.value = false;
};
const bubbleClasses = computed(() => {
const baseClasses = ['message-bubble'];
if (props.isSent) {
baseClasses.push('sent');
} else {
baseClasses.push('received');
}
if (props.message.type === 'text') {
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);
if (!text) return text; // 或 return text (但可能返回 undefined/null,根据需求调整)
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);
};
const [PrescriptionDetailModal, PrescriptionDetailModalApi] = useVbenModal({
connectedComponent: PrescriptionDetail,
});
const viewPrescription = (item) => {
PrescriptionDetailModalApi.setData({
values: item.id,
});
PrescriptionDetailModalApi.open();
};
// 格式化日期时间
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',
});
};
// 获取性别文本
const getSexText = (sex) => {
return sex === 1 ? '男' : '女';
};
if (props.message.type === 'register') {
getRegisterCardInfo();
}
</script>
<template>
<div
:class="isSent ? 'flex-row-reverse' : 'flex-row'"
class="mb-4 flex items-start gap-3"
>
<RefusalOfTreatmentModals />
<PrescriptionDetailModal />
<!-- 头像 -->
<div
v-if="isSent"
:style="avatarStyle"
class="cursor-pointer rounded-full"
@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>
</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"
>
<!-- 发送者名称 -->
<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">
{{ senderInfo.nick_name }}
</div>
<!-- 消息气泡 -->
<div :class="bubbleClasses" class="relative rounded-2xl p-4 shadow-sm">
<!-- 图片消息 -->
<div
v-if="message.type === 'image'"
class="relative cursor-pointer"
@click="handlePreviewMedia(message.content, 'image')"
>
<Image
:src="message.content"
alt="图片消息"
class="max-w-full rounded-lg"
@error="handleImageError"
/>
</div>
<!-- 视频消息 -->
<div
v-else-if="message.type === 'video'"
class="video-message relative cursor-pointer"
@click="handlePreviewMedia(message.content, 'video')"
>
<video
:src="message.content"
class="video-thumbnail max-w-full rounded-lg"
preload="metadata"
@error="handleVideoError"
></video>
<div class="video-overlay">
<div class="play-button">
<i class="fas fa-play"></i>
</div>
<div v-if="message.duration" class="video-duration">
{{ formatDuration(message.duration) }}
</div>
</div>
</div>
<!-- URL链接消息 -->
<div v-else-if="message.type === 'url'" class="url-message">
<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>
</div>
<div class="url-content">
<div class="url-title">{{ message.title || message.url }}</div>
<div v-if="message.description" class="url-description">
{{ 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" />
</div>
</div>
<div v-if="message.text" class="url-text">
<Button type="link">{{ message.text }}</Button>
</div>
</div>
<!-- 音频消息 -->
<AudioMessage
v-else-if="message.type === 'audio'"
:is-sent="isSent"
:message="message"
/>
<!-- 挂号卡片消息 -->
<div v-else-if="message.type === 'register'" 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"
>
<i
class="fas fa-calendar-check text-xl text-green-500 dark:text-green-300"
></i>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between">
<div class="font-semibold text-gray-800 dark:text-gray-100">
挂号信息
</div>
<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,
'bg-purple-100 text-purple-800':
message.content.status === 5,
'bg-indigo-100 text-indigo-800':
message.content.status === 6,
'bg-pink-100 text-pink-800': message.content.status === 7,
}"
class="rounded-full px-2 py-1 text-xs font-medium"
>
{{ registerStatusMap[message.content.status] || '未知状态' }}
</div>
</div>
<div
class="mt-1 flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300"
>
<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
}}</span>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-list-ol text-xs"></i>
<span>第{{ message.content.order_number }}号</span>
</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>
<div
:class="{
'bg-red-100 text-red-800': message.content.is_pay === 0,
'bg-green-100 text-green-800': message.content.is_pay === 1,
}"
class="rounded-full px-2 py-1 text-xs font-medium"
>
{{ payStatusMap[message.content.is_pay] || '未知' }}
</div>
</div>
<div
class="grid grid-cols-2 gap-2 text-sm text-gray-600 dark:text-gray-300"
>
<div class="flex items-center gap-1">
<i class="fas fa-user text-xs"></i>
<span>{{ message.content.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>
</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>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-phone text-xs"></i>
<span>{{
message.content.user_patient?.mobile ? '已绑定' : '未绑定'
}}</span>
</div>
</div>
</div>
<!-- 操作按钮区域 -->
<div class="mt-3 border-t border-gray-200 pt-3 dark:border-gray-600">
<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>
</div>
</div>
<!-- 接诊/拒诊按钮 - 只在待就诊状态显示 -->
<div v-if="message.content.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)
"
>
<i class="fas fa-check mr-1"></i>
接诊
</Button>
<Button
:loading="registerOperating"
class="flex-1"
danger
size="small"
@click="
rejectRegister(message.content.id, message.content.order_no)
"
>
<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>-->
</div>
<!-- 时间信息 -->
<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) }}
</span>
</div>
</div>
<!-- 处方卡片消息 -->
<div
v-else-if="message.type === 'prescription'"
class="prescription-card"
@click="viewPrescription(message.content)"
>
<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>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between">
<div class="font-semibold text-gray-800 dark:text-gray-100">
电子处方
</div>
<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"
>
{{
checkPrescriptionStatus(message.content.id) ||
statusInfo.text ||
'加载中'
}}
</div>
</div>
<div
class="mt-1 flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300"
>
<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
}}</span>
</div>
<div class="flex items-center gap-1">
<i class="fas fa-truck text-xs"></i>
<span>{{ message.content.express_name }}</span>
</div>
</div>
</div>
</div>
<div
class="mt-3 flex items-center justify-between border-t border-gray-200 pt-2 dark:border-gray-600"
>
<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 }}
</span>
</div>
<div
class="flex items-center text-blue-500 transition-colors hover:text-blue-600 dark:text-blue-300 dark:hover:text-blue-200"
>
<span class="text-sm font-medium">查看处方</span>
<i class="fas fa-chevron-right ml-1 text-xs"></i>
</div>
</div>
</div>
<!-- 在线复诊|购药信息 -->
<div
v-else-if="message.type === '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', // 未到访状态
}"
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>
{{ message.content.drug_name }} 复诊信息
</h3>
<span
:class="{
'text-green-800': message.content.hasVisited === '1',
'bg-yellow-100 text-yellow-800':
message.content.hasVisited === '0',
}"
class="rounded-full px-2 py-1 text-xs"
>
{{ message.content.hasVisited === '1' ? '就诊过' : '未就诊过' }}
</span>
</div>
<!-- 症状信息 -->
<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>
</p>
<!-- 用药状态 -->
<p
:class="{
'font-medium text-blue-600':
message.content.hasUsedDrug === '1',
'text-gray-500': message.content.hasUsedDrug === '0',
}"
class="text-sm"
>
<span class="w-20 text-gray-500">用药情况:</span>
{{
message.content.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>
ID: {{ message.content.doctor_id }}
</p>
</div>
</div>
</div>
<!-- 通话消息 -->
<div
v-else-if="
message.type === 'video-call' || message.type === 'voice-call'
"
class="call-message"
>
<div class="call-info">
<i
:class="message.type === 'video-call' ? 'fa-video' : 'fa-phone'"
class="fas"
></i>
<span>{{ getCallStatusText(message.callStatus) }}</span>
</div>
<div v-if="message.duration" class="call-duration">
通话时长: {{ formatDuration(message.duration) }}
</div>
</div>
<!-- 文本消息 -->
<div v-else class="whitespace-pre-wrap text-base leading-relaxed">
<span v-html="formatTextWithLinks(message.content)"></span>
</div>
</div>
<!-- 消息时间和状态 -->
<div
class="mt-1 flex items-center gap-1 px-2 text-xs text-gray-400 dark:text-gray-500"
>
<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>
</div>
</div>
</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;
}
.bubble-bg {
background: linear-gradient(135deg, #4361ee, #3f37c9);
}
.bubble-bg.dark {
background: linear-gradient(135deg, #4361ee, #3f37c9);
}
.message-bubble.received.bubble-bg {
background: white;
color: #1a202c;
border: 1px solid #e2e8f0;
border-bottom-left-radius: 4px;
}
.message-bubble.received.dark.bubble-bg {
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%
);
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;
}
/* 挂号卡片样式 */
.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;
}
.message-bubble.sent .register-card {
@apply border-green-200/50 bg-green-50/30 dark:border-green-700/70 dark:bg-green-900/30;
}
.register-card:hover {
@apply -translate-y-0.5 transform;
}
/* 处方卡片样式 - 使用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;
}
.message-bubble.sent .prescription-card {
@apply border-blue-200/50 bg-blue-50/30 dark:border-blue-700/70 dark:bg-blue-900/30;
}
.prescription-card:hover {
@apply -translate-y-0.5 transform;
}
</style>