2025-08-11 10:54:45 +08:00
|
|
|
|
<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 = () => {
|
2025-08-11 14:49:48 +08:00
|
|
|
|
console.log('设置WebSocket监听器')
|
|
|
|
|
|
|
|
|
|
// 监听新消息
|
2025-08-11 10:54:45 +08:00
|
|
|
|
websocketService.on('message', (message) => {
|
|
|
|
|
console.log('收到WebSocket消息:', message)
|
2025-08-11 14:49:48 +08:00
|
|
|
|
|
|
|
|
|
// 如果消息包含通知信息,添加到通知服务
|
|
|
|
|
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'
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-08-11 10:54:45 +08:00
|
|
|
|
})
|
|
|
|
|
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 监听审批状态变更
|
2025-08-11 10:54:45 +08:00
|
|
|
|
websocketService.on('approvalStatusChanged', (data) => {
|
|
|
|
|
console.log('审批状态变更:', data)
|
2025-08-11 14:49:48 +08:00
|
|
|
|
|
|
|
|
|
// 添加审批状态变更通知
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-08-11 10:54:45 +08:00
|
|
|
|
})
|
|
|
|
|
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 监听设备状态变更
|
2025-08-11 10:54:45 +08:00
|
|
|
|
websocketService.on('equipmentStatusChanged', (data) => {
|
|
|
|
|
console.log('设备状态变更:', data)
|
2025-08-11 14:49:48 +08:00
|
|
|
|
|
|
|
|
|
// 添加设备状态变更通知
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-08-11 10:54:45 +08:00
|
|
|
|
})
|
2025-08-11 14:49:48 +08:00
|
|
|
|
|
|
|
|
|
// 监听新审批申请
|
|
|
|
|
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', () => {})
|
2025-08-11 10:54:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 定期检查提醒
|
|
|
|
|
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(() => {
|
2025-08-11 14:49:48 +08:00
|
|
|
|
cleanupWebSocketListeners()
|
2025-08-11 10:54:45 +08:00
|
|
|
|
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>
|