Industrial-image-management.../src/components/NotificationCenter/index.vue

871 lines
24 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>
<div class="notification-center">
<!-- 消息中心图标和徽章 -->
<div class="notification-trigger" @click="toggleDropdown">
<a-badge :count="unreadCount" :dot="hasUrgentNotifications">
<a-button type="text" class="notification-btn" title="消息中心">
<template #icon>
<IconNotification />
</template>
<span class="notification-text">消息中心</span>
</a-button>
</a-badge>
</div>
<!-- 消息中心弹窗 -->
<a-modal
v-model:visible="modalVisible"
title="消息中心"
width="800px"
:footer="false"
:mask-closable="true"
:closable="true"
:destroy-on-close="false"
:z-index="999999"
class="notification-modal"
>
<!-- 消息中心头部 -->
<div class="notification-header">
<div class="header-left">
<h3>消息中心</h3>
<span class="notification-count">{{ unreadCount }} 条未读</span>
</div>
<div class="header-right">
<a-button type="text" size="small" @click="markAllAsRead">
全部已读
</a-button>
<a-button type="text" size="small" @click="clearRead">
清空已读
</a-button>
</div>
</div>
<!-- 消息类型标签 -->
<div class="notification-tabs">
<a-tabs v-model:active-key="activeTab" size="small">
<a-tab-pane key="all" title="全部">
<template #title>
<span>全部 ({{ totalCount }})</span>
</template>
</a-tab-pane>
<a-tab-pane key="pending" title="待审批">
<template #title>
<span>待审批 ({{ pendingCount }})</span>
</template>
</a-tab-pane>
<a-tab-pane key="equipment" title="设备">
<template #title>
<span>设备 ({{ equipmentCount }})</span>
</template>
</a-tab-pane>
<a-tab-pane key="urgent" title="紧急">
<template #title>
<span>紧急 ({{ urgentCount }})</span>
</template>
</a-tab-pane>
</a-tabs>
</div>
<!-- 消息列表 -->
<div class="notification-list">
<div v-if="filteredNotifications.length === 0" class="empty-state">
<IconInfo style="font-size: 48px; color: #d9d9d9; margin-bottom: 16px;" />
<p>暂无消息</p>
</div>
<div
v-for="notification in filteredNotifications"
:key="notification.id"
class="notification-item"
:class="{
'unread': !notification.read,
'urgent': notification.priority === 'URGENT',
'high': notification.priority === 'HIGH'
}"
@click="handleNotificationClick(notification)"
>
<!-- 消息图标 -->
<div class="notification-icon">
<component :is="getNotificationIcon(notification.type)" />
</div>
<!-- 消息内容 -->
<div class="notification-content">
<div class="notification-title">
{{ notification.title }}
<a-tag
v-if="notification.actionRequired"
size="small"
color="red"
>
需操作
</a-tag>
<a-tag
v-if="notification.reminderType"
size="small"
color="blue"
>
{{ getReminderTypeText(notification.reminderType) }}
</a-tag>
</div>
<div class="notification-message">{{ notification.content }}</div>
<div class="notification-meta">
<span class="notification-time">{{ formatTime(notification.createTime) }}</span>
<span class="notification-category">{{ notification.category }}</span>
<span v-if="notification.source" class="notification-source">{{ notification.source }}</span>
</div>
</div>
<!-- 消息操作 -->
<div class="notification-actions">
<a-button
type="text"
size="small"
@click.stop="toggleReminder(notification)"
:title="notification.reminderTime ? '取消提醒' : '设置提醒'"
>
<IconClockCircle v-if="!notification.reminderTime" />
<IconClose v-else />
</a-button>
<a-button
type="text"
size="small"
@click.stop="removeNotification(notification.id)"
title="删除消息"
>
<IconDelete />
</a-button>
</div>
</div>
</div>
<!-- 消息底部 -->
<div class="notification-footer">
<a-button type="text" size="small" @click="viewAllNotifications">
查看全部消息
</a-button>
<a-button type="text" size="small" @click="exportNotifications">
导出消息
</a-button>
</div>
</a-modal>
<!-- 提醒设置弹窗 -->
<a-modal
v-model:visible="reminderModalVisible"
title="设置消息提醒"
width="400px"
@ok="saveReminder"
@cancel="cancelReminder"
>
<a-form :model="reminderForm" layout="vertical">
<a-form-item label="提醒类型">
<a-radio-group v-model="reminderForm.type">
<a-radio value="IMMEDIATE">立即提醒</a-radio>
<a-radio value="DELAYED">延迟提醒</a-radio>
<a-radio value="RECURRING">重复提醒</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="reminderForm.type === 'DELAYED'" label="提醒时间">
<a-date-picker
v-model="reminderForm.time"
show-time
placeholder="选择提醒时间"
style="width: 100%"
/>
</a-form-item>
<a-form-item v-if="reminderForm.type === 'RECURRING'" label="重复间隔">
<a-input-number
v-model="reminderForm.interval"
:min="1"
:max="1440"
placeholder="间隔分钟"
style="width: 100%"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
IconNotification,
IconInfo,
IconClockCircle,
IconClose,
IconDelete,
IconCheckCircle,
IconClockCircle as IconPending,
IconApps,
IconExclamationCircle,
IconExclamationCircle as IconWarning,
IconSettings
} from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message'
import notificationService from '@/services/notificationService'
import websocketService from '@/services/websocketService'
defineOptions({ name: 'NotificationCenter' })
const router = useRouter()
// 响应式数据
const modalVisible = ref(false)
const activeTab = ref('all')
const reminderModalVisible = ref(false)
const currentNotification = ref<any>(null)
// 提醒表单
const reminderForm = ref({
type: 'IMMEDIATE' as 'IMMEDIATE' | 'DELAYED' | 'RECURRING',
time: null as Date | null,
interval: 30
})
// 计算属性
const notifications = computed(() => notificationService.getAllNotifications())
const unreadCount = computed(() => notificationService.unreadCount.value)
const totalCount = computed(() => notifications.value.length)
const pendingCount = computed(() => notificationService.pendingCount.value)
const equipmentCount = computed(() =>
notificationService.equipmentBorrowCount.value +
notificationService.equipmentReturnCount.value +
notificationService.equipmentMaintenanceCount.value +
notificationService.equipmentAlertCount.value
)
const urgentCount = computed(() => notificationService.urgentCount.value)
const hasUrgentNotifications = computed(() => urgentCount.value > 0)
// 过滤后的消息列表
const filteredNotifications = computed(() => {
let filtered = notifications.value
switch (activeTab.value) {
case 'pending':
filtered = filtered.filter(n => n.type === 'PENDING')
break
case 'equipment':
filtered = filtered.filter(n =>
['EQUIPMENT_BORROW', 'EQUIPMENT_RETURN', 'EQUIPMENT_MAINTENANCE', 'EQUIPMENT_ALERT'].includes(n.type)
)
break
case 'urgent':
filtered = filtered.filter(n => n.priority === 'URGENT' || n.priority === 'HIGH')
break
}
// 按优先级和时间排序
return filtered.sort((a, b) => {
const priorityOrder = { 'URGENT': 4, 'HIGH': 3, 'NORMAL': 2, 'LOW': 1 }
const aPriority = priorityOrder[a.priority || 'NORMAL'] || 2
const bPriority = priorityOrder[b.priority || 'NORMAL'] || 2
if (aPriority !== bPriority) {
return bPriority - aPriority
}
return new Date(b.createTime).getTime() - new Date(a.createTime).getTime()
}).slice(0, 20) // 只显示前20条
})
// 方法
const toggleDropdown = () => {
console.log('打开消息中心弹窗')
modalVisible.value = true
}
const markAllAsRead = () => {
notificationService.markAllAsRead()
message.success('已标记所有消息为已读')
}
const clearRead = () => {
notificationService.clearRead()
message.success('已清空已读消息')
}
const handleNotificationClick = (notification: any) => {
console.log('点击消息:', notification)
// 标记为已读
notificationService.markAsRead(notification.id)
// 构建跳转路径
let targetUrl = notification.targetUrl
// 如果没有targetUrl根据消息类型和业务信息构建
if (!targetUrl) {
targetUrl = buildTargetUrl(notification)
}
console.log('构建的目标URL:', targetUrl)
// 如果有目标URL跳转过去
if (targetUrl) {
try {
router.push(targetUrl)
modalVisible.value = false
message.success('正在跳转到相关页面...')
} catch (error) {
console.error('路由跳转失败:', error)
message.error('页面跳转失败,请手动导航')
}
} else {
console.warn('无法构建跳转路径,消息数据:', notification)
message.warning('该消息暂无相关操作页面')
}
}
// 根据消息类型构建跳转路径
const buildTargetUrl = (notification: any): string | null => {
const { type, relatedId, metadata, category } = notification
console.log('构建跳转路径,消息类型:', type, '相关ID:', relatedId, '元数据:', metadata)
switch (type) {
case 'PROCUREMENT':
case 'PENDING':
// 设备采购申请 - 跳转到审批台
return '/asset-management/device-management/approval'
case 'APPROVAL':
// 审批相关 - 跳转到审批台
return '/asset-management/device-management/approval'
case 'EQUIPMENT_BORROW':
// 设备借用 - 跳转到设备中心
return '/asset-management/device-management/device-center'
case 'EQUIPMENT_RETURN':
// 设备归还 - 跳转到设备中心
return '/asset-management/device-management/device-center'
case 'EQUIPMENT_MAINTENANCE':
// 设备维护 - 跳转到设备中心
return '/asset-management/device-management/device-center'
case 'EQUIPMENT_ALERT':
// 设备告警 - 跳转到设备中心
return '/asset-management/device-management/device-center'
case 'WORKFLOW':
// 工作流 - 根据具体类型跳转
if (metadata?.workflowType === 'PROJECT') {
return '/project-management/project-template/project-aproval'
}
return '/asset-management/device-management/approval'
case 'SYSTEM':
// 系统消息 - 通常不需要跳转
return null
default:
// 默认跳转到审批台
return '/asset-management/device-management/approval'
}
}
const getNotificationIcon = (type: string) => {
const iconMap: Record<string, any> = {
'APPROVAL': IconCheckCircle,
'PENDING': IconPending,
'PROCUREMENT': IconApps,
'EQUIPMENT_BORROW': IconApps,
'EQUIPMENT_RETURN': IconApps,
'EQUIPMENT_MAINTENANCE': IconSettings,
'EQUIPMENT_ALERT': IconWarning,
'WORKFLOW': IconSettings,
'SYSTEM': IconExclamationCircle
}
return iconMap[type] || IconNotification
}
const getReminderTypeText = (type: string) => {
const typeMap: Record<string, string> = {
'IMMEDIATE': '立即',
'DELAYED': '延迟',
'RECURRING': '重复'
}
return typeMap[type] || type
}
const formatTime = (time: string) => {
const date = new Date(time)
const now = new Date()
const diff = now.getTime() - date.getTime()
if (diff < 60000) return '刚刚'
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前`
return date.toLocaleDateString()
}
const toggleReminder = (notification: any) => {
if (notification.reminderTime) {
// 取消提醒
notificationService.cancelNotificationReminder(notification.id)
message.success('已取消提醒')
} else {
// 设置提醒
currentNotification.value = notification
reminderModalVisible.value = true
}
}
const saveReminder = () => {
if (currentNotification.value) {
const reminderTime = reminderForm.value.type === 'DELAYED' && reminderForm.value.time
? reminderForm.value.time.toISOString()
: new Date().toISOString()
notificationService.setNotificationReminder(
currentNotification.value.id,
reminderTime,
reminderForm.value.type as any,
reminderForm.value.type === 'RECURRING' ? reminderForm.value.interval : undefined
)
message.success('提醒设置成功')
reminderModalVisible.value = false
resetReminderForm()
}
}
const cancelReminder = () => {
reminderModalVisible.value = false
resetReminderForm()
}
const resetReminderForm = () => {
reminderForm.value = {
type: 'IMMEDIATE',
time: null,
interval: 30
}
currentNotification.value = null
}
const removeNotification = (id: string) => {
notificationService.removeNotification(id)
message.success('消息已删除')
}
const viewAllNotifications = () => {
router.push('/notifications')
modalVisible.value = false
}
const exportNotifications = () => {
notificationService.exportNotifications()
message.success('消息导出成功')
}
// 监听WebSocket事件
const setupWebSocketListeners = () => {
console.log('设置WebSocket监听器')
// 监听新消息
websocketService.on('message', (message) => {
console.log('收到WebSocket消息:', message)
// 如果消息包含通知信息,添加到通知服务
if (message.data && message.data.notification) {
console.log('处理通知消息:', message.data.notification)
notificationService.addNotification({
type: message.data.notification.type || 'SYSTEM',
title: message.data.notification.title || '新通知',
content: message.data.notification.content || '',
priority: message.data.notification.priority || 'NORMAL',
category: message.data.notification.category || '系统',
targetUrl: message.data.notification.targetUrl,
metadata: message.data.notification.metadata,
source: message.data.notification.source || 'WEBSOCKET'
})
}
})
// 监听审批状态变更
websocketService.on('approvalStatusChanged', (data) => {
console.log('审批状态变更:', data)
// 添加审批状态变更通知
if (data.type === 'SUBMITTED') {
notificationService.addNotification({
type: 'PENDING',
title: '新的审批申请',
content: `收到来自 ${data.applicantName || '申请人'} 的${data.businessType || '设备'}申请:${data.equipmentName || '未知设备'}`,
targetUrl: '/asset-management/device-management/approval',
priority: 'HIGH',
category: '审批申请',
actionRequired: true,
source: 'APPROVAL_SYSTEM',
metadata: {
approvalId: data.approvalId,
equipmentName: data.equipmentName,
applicantName: data.applicantName,
businessType: data.businessType,
timestamp: Date.now()
}
})
}
})
// 监听设备状态变更
websocketService.on('equipmentStatusChanged', (data) => {
console.log('设备状态变更:', data)
// 添加设备状态变更通知
notificationService.addNotification({
type: 'EQUIPMENT_ALERT',
title: '设备状态更新',
content: `设备"${data.equipmentName || '未知设备'}"状态已更新为:${data.newStatus}`,
targetUrl: '/asset-management/device-management/device-center',
priority: 'NORMAL',
category: '设备状态',
source: 'EQUIPMENT_SYSTEM',
metadata: {
equipmentId: data.equipmentId,
equipmentName: data.equipmentName,
oldStatus: data.oldStatus,
newStatus: data.newStatus,
timestamp: Date.now()
}
})
})
// 监听采购状态变更
websocketService.on('procurementStatusChanged', (data) => {
console.log('采购状态变更:', data)
if (data.type === 'SUBMITTED') {
notificationService.addNotification({
type: 'PROCUREMENT',
title: '新的采购申请',
content: `收到来自 ${data.applicantName || '申请人'} 的设备采购申请:${data.equipmentName || '未知设备'}`,
targetUrl: '/asset-management/device-management/approval',
priority: 'HIGH',
category: '设备采购',
actionRequired: true,
source: 'PROCUREMENT_SYSTEM',
metadata: {
procurementId: data.procurementId,
equipmentName: data.equipmentName,
applicantName: data.applicantName,
timestamp: Date.now()
}
})
}
})
// 监听新审批申请
websocketService.on('newApprovalRequest', (data) => {
console.log('新审批申请:', data)
notificationService.addNotification({
type: 'PENDING',
title: '新的审批申请',
content: `收到来自 ${data.applicantName || '申请人'} 的${data.businessType || '设备'}申请:${data.equipmentName || '未知设备'}`,
targetUrl: '/asset-management/device-management/approval',
priority: 'HIGH',
category: '审批申请',
actionRequired: true,
source: 'APPROVAL_SYSTEM',
metadata: {
approvalId: data.approvalId,
equipmentName: data.equipmentName,
applicantName: data.applicantName,
businessType: data.businessType,
timestamp: Date.now()
}
})
})
// 监听WebSocket连接状态
websocketService.on('connected', () => {
console.log('WebSocket已连接')
})
websocketService.on('disconnected', (data) => {
console.log('WebSocket已断开:', data)
})
websocketService.on('error', (error) => {
console.error('WebSocket错误:', error)
})
}
// 清理WebSocket监听器
const cleanupWebSocketListeners = () => {
console.log('清理WebSocket监听器')
// 移除所有事件监听器
websocketService.off('message', () => {})
websocketService.off('approvalStatusChanged', () => {})
websocketService.off('equipmentStatusChanged', () => {})
websocketService.off('procurementStatusChanged', () => {})
websocketService.off('newApprovalRequest', () => {})
websocketService.off('connected', () => {})
websocketService.off('disconnected', () => {})
websocketService.off('error', () => {})
}
// 定期检查提醒
let reminderCheckInterval: NodeJS.Timeout | null = null
const startReminderCheck = () => {
reminderCheckInterval = setInterval(() => {
const reminderNotifications = notificationService.getReminderNotifications()
if (reminderNotifications.length > 0) {
// 显示提醒通知
reminderNotifications.forEach(notification => {
message.info(`${notification.title}: ${notification.content}`)
notificationService.markReminderProcessed(notification.id)
})
}
}, 60000) // 每分钟检查一次
}
// 生命周期
onMounted(() => {
setupWebSocketListeners()
startReminderCheck()
})
onUnmounted(() => {
cleanupWebSocketListeners()
if (reminderCheckInterval) {
clearInterval(reminderCheckInterval)
}
})
</script>
<style scoped lang="scss">
.notification-center {
position: relative;
z-index: 999999;
.notification-trigger {
cursor: pointer;
.notification-btn {
color: var(--color-text-1);
display: flex;
align-items: center;
gap: 4px;
&:hover {
color: var(--color-primary);
}
.notification-text {
font-size: 14px;
margin-left: 4px;
}
}
}
}
// 消息中心弹窗样式
.notification-modal {
:deep(.arco-modal) {
z-index: 999999 !important;
}
:deep(.arco-modal-mask) {
z-index: 999998 !important;
}
:deep(.arco-modal-wrapper) {
z-index: 999999 !important;
}
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid var(--color-border);
margin-bottom: 16px;
.header-left {
h3 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-1);
}
.notification-count {
font-size: 13px;
color: var(--color-text-3);
}
}
.header-right {
display: flex;
gap: 8px;
}
}
.notification-tabs {
margin-bottom: 16px;
:deep(.arco-tabs-nav) {
margin-bottom: 0;
}
}
.notification-list {
max-height: 400px;
overflow-y: auto;
.empty-state {
text-align: center;
padding: 40px 0;
color: var(--color-text-3);
}
.notification-item {
display: flex;
align-items: flex-start;
padding: 16px;
border: 1px solid var(--color-border-2);
border-radius: 8px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--color-fill-2);
border-color: var(--color-primary-light-3);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&.unread {
background-color: var(--color-primary-light-1);
border-color: var(--color-primary-light-3);
&:hover {
background-color: var(--color-primary-light-2);
}
}
&.urgent {
border-left: 4px solid var(--color-danger);
background-color: var(--color-danger-light-1);
}
&.high {
border-left: 4px solid var(--color-warning);
background-color: var(--color-warning-light-1);
}
.notification-icon {
margin-right: 16px;
margin-top: 2px;
color: var(--color-text-2);
font-size: 18px;
}
.notification-content {
flex: 1;
min-width: 0;
.notification-title {
font-weight: 600;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
color: var(--color-text-1);
font-size: 15px;
}
.notification-message {
font-size: 14px;
color: var(--color-text-2);
margin-bottom: 12px;
line-height: 1.5;
}
.notification-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: var(--color-text-3);
.notification-time {
color: var(--color-text-2);
font-weight: 500;
}
}
}
.notification-actions {
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s;
}
&:hover .notification-actions {
opacity: 1;
}
}
}
.notification-footer {
display: flex;
justify-content: space-between;
padding: 16px 0;
border-top: 1px solid var(--color-border);
margin-top: 16px;
background-color: var(--color-fill-1);
border-radius: 8px;
padding: 16px;
}
// 确保弹窗在最上层
:deep(.arco-modal) {
z-index: 999999 !important;
}
:deep(.arco-modal-mask) {
z-index: 999998 !important;
}
:deep(.arco-modal-wrapper) {
z-index: 999999 !important;
}
// 针对Arco Design v2的样式
:deep(.arco-overlay) {
z-index: 999999 !important;
}
:deep(.arco-overlay-container) {
z-index: 999999 !important;
}
// 确保消息中心弹窗不被其他元素遮挡
:deep(.arco-modal) {
z-index: 999999 !important;
position: relative !important;
}
// 强制设置最高优先级
:deep(.arco-modal-wrapper) {
z-index: 999999 !important;
position: relative !important;
}
</style>