298 lines
8.2 KiB
Vue
298 lines
8.2 KiB
Vue
<template>
|
||
<Teleport to="body">
|
||
<TransitionGroup
|
||
name="chat-notify-slide"
|
||
tag="div"
|
||
class="chat-notify-container"
|
||
>
|
||
<div
|
||
v-for="item in notifications"
|
||
:key="item.id"
|
||
class="chat-notify-card"
|
||
:class="{
|
||
'chat-notify-card-complete': item.type === 'consultation_complete',
|
||
'chat-notify-card-left': item.type === 'patient_left_chat'
|
||
}"
|
||
@click="handleClick(item)"
|
||
>
|
||
<div
|
||
class="chat-notify-icon"
|
||
:class="{
|
||
'chat-notify-icon-complete': item.type === 'consultation_complete',
|
||
'chat-notify-icon-left': item.type === 'patient_left_chat'
|
||
}"
|
||
>
|
||
<el-icon :size="22">
|
||
<ChatDotRound v-if="item.type !== 'consultation_complete' && item.type !== 'patient_left_chat'" />
|
||
<CircleCheck v-else-if="item.type === 'consultation_complete'" />
|
||
<Right v-else />
|
||
</el-icon>
|
||
</div>
|
||
<div class="chat-notify-body">
|
||
<div class="chat-notify-title">
|
||
{{
|
||
item.type === 'consultation_complete'
|
||
? '面诊结束'
|
||
: item.type === 'patient_left_chat'
|
||
? '患者离开会话'
|
||
: '患者打开会话'
|
||
}}
|
||
</div>
|
||
<div class="chat-notify-desc">
|
||
<template v-if="item.type === 'consultation_complete'">
|
||
<span class="patient-name">{{ item.patient_name || '患者' }}</span>
|
||
的面诊已由
|
||
<span class="doctor-name">{{ item.doctor_name || '医生' }}</span>
|
||
完成,请及时跟进
|
||
</template>
|
||
<template v-else-if="item.type === 'patient_left_chat'">
|
||
<span class="patient-name">{{ item.patient_name || '患者' }}</span>
|
||
已离开问诊会话页面
|
||
</template>
|
||
<template v-else>
|
||
<span class="patient-name">{{ item.patient_name || '患者' }}</span>
|
||
已打开与您的会话,请及时查看
|
||
</template>
|
||
</div>
|
||
<div class="chat-notify-time">{{ formatTime(item.created_at) }}</div>
|
||
</div>
|
||
<button class="chat-notify-close" @click.stop="remove(item.id)" aria-label="关闭">
|
||
<el-icon :size="14"><Close /></el-icon>
|
||
</button>
|
||
</div>
|
||
</TransitionGroup>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ChatDotRound, CircleCheck, Close, Right } from '@element-plus/icons-vue'
|
||
import { getChatNotifications } from '@/api/chat'
|
||
|
||
const router = useRouter()
|
||
|
||
export interface ChatNotifyItem {
|
||
id: string
|
||
type?: 'patient_opened_chat' | 'patient_left_chat' | 'consultation_complete'
|
||
doctor_id?: number
|
||
assistant_id?: number
|
||
patient_id?: string
|
||
patient_name: string
|
||
doctor_name?: string
|
||
diagnosis_id?: number
|
||
created_at: number
|
||
}
|
||
|
||
const notifications = ref<ChatNotifyItem[]>([])
|
||
const pollTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||
const POLL_INTERVAL = 5000
|
||
|
||
function formatTime(ts: number) {
|
||
if (!ts) return ''
|
||
const d = new Date(ts * 1000)
|
||
const now = new Date()
|
||
const diff = now.getTime() - d.getTime()
|
||
if (diff < 60000) return '刚刚'
|
||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
|
||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
|
||
return d.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||
}
|
||
|
||
function remove(id: string) {
|
||
notifications.value = notifications.value.filter((n) => n.id !== id)
|
||
}
|
||
|
||
function handleClick(item: ChatNotifyItem) {
|
||
remove(item.id)
|
||
// 面诊结束:跳转到诊单列表并自动打开编辑
|
||
if (item.type === 'consultation_complete' && item.diagnosis_id) {
|
||
router.push({ path: '/tcm/diagnosis', query: { id: item.diagnosis_id } })
|
||
}
|
||
}
|
||
|
||
async function fetchNotifications() {
|
||
try {
|
||
const list = (await getChatNotifications()) as ChatNotifyItem[]
|
||
if (Array.isArray(list) && list.length > 0) {
|
||
notifications.value = [...list, ...notifications.value].slice(0, 10)
|
||
}
|
||
} catch {
|
||
// 静默失败,避免干扰用户
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchNotifications()
|
||
pollTimer.value = setInterval(fetchNotifications, POLL_INTERVAL)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (pollTimer.value) {
|
||
clearInterval(pollTimer.value)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.chat-notify-container {
|
||
position: fixed;
|
||
top: 72px;
|
||
right: 24px;
|
||
z-index: 3000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
max-width: 360px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.chat-notify-container > * {
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.chat-notify-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
padding: 14px 16px;
|
||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(34, 197, 94, 0.06);
|
||
cursor: pointer;
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
html.dark .chat-notify-card {
|
||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||
border-color: rgba(34, 197, 94, 0.3);
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.chat-notify-card:hover {
|
||
transform: translateX(-2px);
|
||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(34, 197, 94, 0.12);
|
||
}
|
||
|
||
html.dark .chat-notify-card:hover {
|
||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.chat-notify-icon {
|
||
flex-shrink: 0;
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||
color: #fff;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.chat-notify-icon-complete {
|
||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||
}
|
||
|
||
.chat-notify-card-left {
|
||
border-color: rgba(148, 163, 184, 0.35);
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(148, 163, 184, 0.08);
|
||
}
|
||
|
||
.chat-notify-icon-left {
|
||
background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%);
|
||
}
|
||
|
||
.chat-notify-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.chat-notify-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #0f172a;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
html.dark .chat-notify-title {
|
||
color: #f1f5f9;
|
||
}
|
||
|
||
.chat-notify-desc {
|
||
font-size: 13px;
|
||
color: #475569;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
html.dark .chat-notify-desc {
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.patient-name {
|
||
font-weight: 600;
|
||
color: #16a34a;
|
||
}
|
||
|
||
.doctor-name {
|
||
font-weight: 600;
|
||
color: #2563eb;
|
||
}
|
||
|
||
.chat-notify-time {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
html.dark .chat-notify-time {
|
||
color: #64748b;
|
||
}
|
||
|
||
.chat-notify-close {
|
||
flex-shrink: 0;
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 6px;
|
||
color: #94a3b8;
|
||
cursor: pointer;
|
||
transition: color 0.2s, background 0.2s;
|
||
}
|
||
|
||
.chat-notify-close:hover {
|
||
color: #64748b;
|
||
background: rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
html.dark .chat-notify-close:hover {
|
||
color: #94a3b8;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
/* 入场动画 */
|
||
.chat-notify-slide-enter-active,
|
||
.chat-notify-slide-leave-active {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.chat-notify-slide-enter-from {
|
||
opacity: 0;
|
||
transform: translateX(24px);
|
||
}
|
||
|
||
.chat-notify-slide-leave-to {
|
||
opacity: 0;
|
||
transform: translateX(24px);
|
||
}
|
||
|
||
.chat-notify-slide-move {
|
||
transition: transform 0.3s ease;
|
||
}
|
||
</style>
|