Files
zyt/admin/src/components/chat-notify-toast/index.vue
T
2026-05-07 16:42:29 +08:00

298 lines
8.2 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.
<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>