diff --git a/src/components/ApprovalAssistant/index.vue b/src/components/ApprovalAssistant/index.vue new file mode 100644 index 0000000..a023d94 --- /dev/null +++ b/src/components/ApprovalAssistant/index.vue @@ -0,0 +1,620 @@ + + + + + + + 智能审批助手 + + + + 启用 + 禁用 + + + + + + + + + + + 审批建议 + + + + + + + + + + + {{ getRecommendationTitle() }} + + + {{ getRecommendationReason() }} + + + + + + + + + + + 风险评估 + + + + + + + + + + + {{ risk.title }} + {{ risk.description }} + + {{ risk.score }}% + + + + + + + + + + 快速审批 + + + + + + + + 快速通过 + + + + + + 快速拒绝 + + + + + + 手动审批 + + + + + + + + + + 审批历史分析 + + + + + {{ historyStats.total }} + 总申请数 + + + {{ historyStats.approved }} + 通过率 + + + {{ historyStats.avgTime }} + 平均审批时间 + + + + + + + + + + 智能审批助手已禁用 + + 启用助手 + + + + + + + 请选择要审批的申请 + + + + + + + diff --git a/src/components/NotificationCenter/index.vue b/src/components/NotificationCenter/index.vue new file mode 100644 index 0000000..8bf24ad --- /dev/null +++ b/src/components/NotificationCenter/index.vue @@ -0,0 +1,736 @@ + + + + + + + + + + 消息中心 + + + + + + + + + + 消息中心 + {{ unreadCount }} 条未读 + + + + 全部已读 + + + 清空已读 + + + + + + + + + + 全部 ({{ totalCount }}) + + + + + 待审批 ({{ pendingCount }}) + + + + + 设备 ({{ equipmentCount }}) + + + + + 紧急 ({{ urgentCount }}) + + + + + + + + + + 暂无消息 + + + + + + + + + + + + {{ notification.title }} + + 需操作 + + + {{ getReminderTypeText(notification.reminderType) }} + + + {{ notification.content }} + + {{ formatTime(notification.createTime) }} + {{ notification.category }} + {{ notification.source }} + + + + + + + + + + + + + + + + + + + + + + + + + + 立即提醒 + 延迟提醒 + 重复提醒 + + + + + + + + + + + + + + + + + + diff --git a/src/layout/components/HeaderRightBar/index.vue b/src/layout/components/HeaderRightBar/index.vue index 64719de..ab8b2f9 100644 --- a/src/layout/components/HeaderRightBar/index.vue +++ b/src/layout/components/HeaderRightBar/index.vue @@ -12,29 +12,8 @@ - - - - - - - - - - - - - + + @@ -82,7 +61,7 @@ import { Modal, Notification } from '@arco-design/web-vue' import { useFullscreen } from '@vueuse/core' import { onMounted, ref, nextTick, onBeforeUnmount } from 'vue' -import Message from './Message.vue' +import NotificationCenter from '@/components/NotificationCenter/index.vue' import SettingDrawer from './SettingDrawer.vue' import Search from './Search.vue' @@ -94,6 +73,7 @@ defineOptions({ name: 'HeaderRight' }) const { isDesktop } = useDevice() const { breakpoint } = useBreakpoint() +const notificationCenterRef = ref() let socket: WebSocket | null = null // 清理函数 diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts new file mode 100644 index 0000000..efa753e --- /dev/null +++ b/src/services/notificationService.ts @@ -0,0 +1,696 @@ +import { ref, computed } from 'vue' + +export interface NotificationItem { + id: string + type: 'APPROVAL' | 'PENDING' | 'SYSTEM' | 'PROCUREMENT' | 'EQUIPMENT_BORROW' | 'EQUIPMENT_RETURN' | 'EQUIPMENT_MAINTENANCE' | 'EQUIPMENT_ALERT' | 'WORKFLOW' + title: string + content: string + createTime: string + read: boolean + targetUrl?: string + priority?: 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT' + category?: string + metadata?: Record + // 新增字段 + source?: string // 消息来源 + actionRequired?: boolean // 是否需要操作 + expiresAt?: string // 过期时间 + relatedId?: string // 关联的业务ID + // 新增智能提醒字段 + reminderTime?: string // 提醒时间 + reminderType?: 'IMMEDIATE' | 'DELAYED' | 'RECURRING' // 提醒类型 + reminderInterval?: number // 重复提醒间隔(分钟) + lastReminderTime?: string // 上次提醒时间 +} + +class NotificationService { + private notifications = ref([]) + + // 计算属性 + public readonly unreadCount = computed(() => + this.notifications.value.filter(n => !n.read).length + ) + + public readonly pendingCount = computed(() => + this.notifications.value.filter(n => + n.type === 'PENDING' && !n.read + ).length + ) + + public readonly approvalCount = computed(() => + this.notifications.value.filter(n => + n.type === 'APPROVAL' && !n.read + ).length + ) + + public readonly procurementCount = computed(() => + this.notifications.value.filter(n => + n.type === 'PROCUREMENT' && !n.read + ).length + ) + + // 新增:设备借用通知数量 + public readonly equipmentBorrowCount = computed(() => + this.notifications.value.filter(n => + n.type === 'EQUIPMENT_BORROW' && !n.read + ).length + ) + + // 新增:设备归还通知数量 + public readonly equipmentReturnCount = computed(() => + this.notifications.value.filter(n => + n.type === 'EQUIPMENT_RETURN' && !n.read + ).length + ) + + // 新增:设备维护通知数量 + public readonly equipmentMaintenanceCount = computed(() => + this.notifications.value.filter(n => + n.type === 'EQUIPMENT_MAINTENANCE' && !n.read + ).length + ) + + // 新增:设备告警通知数量 + public readonly equipmentAlertCount = computed(() => + this.notifications.value.filter(n => + n.type === 'EQUIPMENT_ALERT' && !n.read + ).length + ) + + // 新增:工作流通知数量 + public readonly workflowCount = computed(() => + this.notifications.value.filter(n => + n.type === 'WORKFLOW' && !n.read + ).length + ) + + // 新增:需要操作的通知数量 + public readonly actionRequiredCount = computed(() => + this.notifications.value.filter(n => + n.actionRequired && !n.read + ).length + ) + + // 新增:紧急通知数量 + public readonly urgentCount = computed(() => + this.notifications.value.filter(n => + n.priority === 'URGENT' && !n.read + ).length + ) + + // 新增:今日通知数量 + public readonly todayCount = computed(() => { + const today = new Date() + today.setHours(0, 0, 0, 0) + return this.notifications.value.filter(n => { + const createDate = new Date(n.createTime) + return createDate >= today + }).length + }) + + // 获取所有通知 + public getAllNotifications() { + return this.notifications.value + } + + // 获取未读通知 + public getUnreadNotifications() { + return this.notifications.value.filter(n => !n.read) + } + + // 获取特定类型的通知 + public getNotificationsByType(type: NotificationItem['type']) { + return this.notifications.value.filter(n => n.type === type) + } + + // 新增:获取需要操作的通知 + public getActionRequiredNotifications() { + return this.notifications.value.filter(n => n.actionRequired && !n.read) + } + + // 新增:获取紧急通知 + public getUrgentNotifications() { + return this.notifications.value.filter(n => n.priority === 'URGENT' && !n.read) + } + + // 新增:获取今日通知 + public getTodayNotifications() { + const today = new Date() + today.setHours(0, 0, 0, 0) + return this.notifications.value.filter(n => { + const createDate = new Date(n.createTime) + return createDate >= today + }) + } + + // 新增:获取相关通知 + public getRelatedNotifications(relatedId: string) { + return this.notifications.value.filter(n => n.relatedId === relatedId) + } + + // 新增:获取需要提醒的通知 + public getReminderNotifications() { + const now = new Date() + return this.notifications.value.filter(n => { + if (!n.reminderTime) return false + + const reminderDate = new Date(n.reminderTime) + if (reminderDate <= now) { + // 检查重复提醒 + if (n.reminderType === 'RECURRING' && n.reminderInterval) { + const lastReminder = n.lastReminderTime ? new Date(n.lastReminderTime) : reminderDate + const nextReminder = new Date(lastReminder.getTime() + n.reminderInterval * 60 * 1000) + return nextReminder <= now + } + return true + } + return false + }) + } + + // 新增:设置通知提醒 + public setNotificationReminder(notificationId: string, reminderTime: string, reminderType: 'IMMEDIATE' | 'DELAYED' | 'RECURRING' = 'IMMEDIATE', reminderInterval?: number) { + const notification = this.notifications.value.find(n => n.id === notificationId) + if (notification) { + notification.reminderTime = reminderTime + notification.reminderType = reminderType + notification.reminderInterval = reminderInterval + this.saveToStorage() + } + } + + // 新增:取消通知提醒 + public cancelNotificationReminder(notificationId: string) { + const notification = this.notifications.value.find(n => n.id === notificationId) + if (notification) { + notification.reminderTime = undefined + notification.reminderType = undefined + notification.reminderInterval = undefined + notification.lastReminderTime = undefined + this.saveToStorage() + } + } + + // 新增:标记提醒已处理 + public markReminderProcessed(notificationId: string) { + const notification = this.notifications.value.find(n => n.id === notificationId) + if (notification && notification.reminderType === 'RECURRING') { + notification.lastReminderTime = new Date().toISOString() + this.saveToStorage() + } + } + + // 添加通知 + public addNotification(notification: Omit) { + const newNotification: NotificationItem = { + ...notification, + id: this.generateId(), + createTime: new Date().toISOString(), + read: false, + // 设置默认值 + priority: notification.priority || 'NORMAL', + actionRequired: notification.actionRequired || false, + source: notification.source || 'SYSTEM' + } + + this.notifications.value.unshift(newNotification) + + // 限制通知数量,避免内存泄漏 + if (this.notifications.value.length > 200) { + this.notifications.value = this.notifications.value.slice(0, 200) + } + + // 触发事件 + this.emitNotificationEvent('add', newNotification) + + // 自动保存 + this.saveToStorage() + + // 检查是否需要播放提示音 + if (newNotification.priority === 'URGENT' || newNotification.priority === 'HIGH') { + this.playNotificationSound() + } + + return newNotification + } + + // 标记通知为已读 + public markAsRead(id: string) { + const notification = this.notifications.value.find(n => n.id === id) + if (notification) { + notification.read = true + this.emitNotificationEvent('read', notification) + this.saveToStorage() + } + } + + // 标记所有通知为已读 + public markAllAsRead() { + this.notifications.value.forEach(n => n.read = true) + this.emitNotificationEvent('readAll', null) + this.saveToStorage() + } + + // 标记特定类型通知为已读 + public markTypeAsRead(type: NotificationItem['type']) { + this.notifications.value + .filter(n => n.type === type) + .forEach(n => n.read = true) + this.emitNotificationEvent('readType', { type }) + this.saveToStorage() + } + + // 新增:标记相关通知为已读 + public markRelatedAsRead(relatedId: string) { + this.notifications.value + .filter(n => n.relatedId === relatedId) + .forEach(n => n.read = true) + this.emitNotificationEvent('readRelated', { relatedId }) + this.saveToStorage() + } + + // 删除通知 + public removeNotification(id: string) { + const index = this.notifications.value.findIndex(n => n.id === id) + if (index > -1) { + const notification = this.notifications.value[index] + this.notifications.value.splice(index, 1) + this.emitNotificationEvent('remove', notification) + this.saveToStorage() + } + } + + // 清空所有通知 + public clearAll() { + this.notifications.value = [] + this.emitNotificationEvent('clear', null) + this.saveToStorage() + } + + // 清空已读通知 + public clearRead() { + this.notifications.value = this.notifications.value.filter(n => !n.read) + this.emitNotificationEvent('clearRead', null) + this.saveToStorage() + } + + // 新增:清空过期通知 + public clearExpired() { + const now = new Date() + const beforeCount = this.notifications.value.length + + this.notifications.value = this.notifications.value.filter(n => { + if (!n.expiresAt) return true + const expireDate = new Date(n.expiresAt) + return expireDate > now + }) + + const afterCount = this.notifications.value.length + if (beforeCount !== afterCount) { + this.emitNotificationEvent('clearExpired', { clearedCount: beforeCount - afterCount }) + this.saveToStorage() + } + } + + // 获取通知统计 + public getStats() { + const total = this.notifications.value.length + const unread = this.unreadCount.value + const pending = this.pendingCount.value + const approval = this.approvalCount.value + const procurement = this.procurementCount.value + const actionRequired = this.actionRequiredCount.value + const urgent = this.urgentCount.value + const today = this.todayCount.value + + return { + total, + unread, + pending, + approval, + procurement, + actionRequired, + urgent, + today, + read: total - unread + } + } + + // 搜索通知 + public searchNotifications(keyword: string) { + if (!keyword.trim()) { + return this.notifications.value + } + + const lowerKeyword = keyword.toLowerCase() + return this.notifications.value.filter(n => + n.title.toLowerCase().includes(lowerKeyword) || + n.content.toLowerCase().includes(lowerKeyword) || + n.category?.toLowerCase().includes(lowerKeyword) || + n.source?.toLowerCase().includes(lowerKeyword) + ) + } + + // 按优先级排序 + public sortByPriority() { + const priorityOrder = { 'URGENT': 4, 'HIGH': 3, 'NORMAL': 2, 'LOW': 1 } + + this.notifications.value.sort((a, b) => { + 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() + }) + } + + // 新增:按时间排序 + public sortByTime() { + this.notifications.value.sort((a, b) => + new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + ) + } + + // 新增:按类型分组 + public groupByType() { + const groups: Record = {} + + this.notifications.value.forEach(notification => { + const type = notification.type + if (!groups[type]) { + groups[type] = [] + } + groups[type].push(notification) + }) + + return groups + } + + // 添加采购申请通知 + public addProcurementNotification(equipmentName: string, applicantName: string) { + return this.addNotification({ + type: 'PROCUREMENT', + title: '新的设备采购申请', + content: `收到来自 ${applicantName} 的设备采购申请:${equipmentName}`, + targetUrl: '/asset-management/device-management/approval', + priority: 'HIGH', + category: '设备采购', + actionRequired: true, + source: 'PROCUREMENT_SYSTEM', + metadata: { + equipmentName, + applicantName, + timestamp: Date.now() + } + }) + } + + // 添加审批结果通知 + public addApprovalNotification(equipmentName: string, result: 'APPROVED' | 'REJECTED', approverName: string) { + const isApproved = result === 'APPROVED' + return this.addNotification({ + type: 'APPROVAL', + title: `采购申请${isApproved ? '已通过' : '已拒绝'}`, + content: `您的设备采购申请"${equipmentName}"${isApproved ? '已通过' : '被拒绝'},审批人:${approverName}`, + targetUrl: '/asset-management/device-management/procurement', + priority: isApproved ? 'NORMAL' : 'HIGH', + category: '审批结果', + actionRequired: !isApproved, // 被拒绝时需要操作 + source: 'APPROVAL_SYSTEM', + metadata: { + equipmentName, + result, + approverName, + timestamp: Date.now() + } + }) + } + + // 添加系统通知 + public addSystemNotification(title: string, content: string, priority: NotificationItem['priority'] = 'NORMAL') { + return this.addNotification({ + type: 'SYSTEM', + title, + content, + priority, + category: '系统通知', + source: 'SYSTEM', + actionRequired: false + }) + } + + // 新增:添加待审批通知 + public addPendingApprovalNotification(equipmentName: string, applicantName: string, businessType: string) { + return this.addNotification({ + type: 'PENDING', + title: '新的审批申请', + content: `收到来自 ${applicantName} 的${businessType}申请:${equipmentName}`, + targetUrl: '/asset-management/device-management/approval', + priority: 'HIGH', + category: '审批申请', + actionRequired: true, + source: 'APPROVAL_SYSTEM', + metadata: { + equipmentName, + applicantName, + businessType, + timestamp: Date.now() + } + }) + } + + // 新增:添加设备借用通知 + public addEquipmentBorrowNotification(equipmentName: string, borrowerName: string, borrowReason: string) { + return this.addNotification({ + type: 'EQUIPMENT_BORROW', + title: '新的设备借用申请', + content: `收到来自 ${borrowerName} 的设备借用申请:${equipmentName},借用原因:${borrowReason}`, + targetUrl: '/asset-management/device-management/device-center', + priority: 'HIGH', + category: '设备借用', + actionRequired: true, + source: 'EQUIPMENT_SYSTEM', + metadata: { + equipmentName, + borrowerName, + borrowReason, + timestamp: Date.now() + } + }) + } + + // 新增:添加设备归还通知 + public addEquipmentReturnNotification(equipmentName: string, returnerName: string, returnCondition: string) { + return this.addNotification({ + type: 'EQUIPMENT_RETURN', + title: '设备归还申请', + content: `收到来自 ${returnerName} 的设备归还申请:${equipmentName},归还状态:${returnCondition}`, + targetUrl: '/asset-management/device-management/device-center', + priority: 'NORMAL', + category: '设备归还', + actionRequired: false, + source: 'EQUIPMENT_SYSTEM', + metadata: { + equipmentName, + returnerName, + returnCondition, + timestamp: Date.now() + } + }) + } + + // 新增:添加设备维护通知 + public addEquipmentMaintenanceNotification(equipmentName: string, maintenanceType: string, estimatedDuration: string) { + return this.addNotification({ + type: 'EQUIPMENT_MAINTENANCE', + title: '设备维护申请', + content: `设备 ${equipmentName} 申请${maintenanceType}维护,预计时长:${estimatedDuration}`, + targetUrl: '/asset-management/device-management/device-center', + priority: 'NORMAL', + category: '设备维护', + actionRequired: true, + source: 'EQUIPMENT_SYSTEM', + metadata: { + equipmentName, + maintenanceType, + estimatedDuration, + timestamp: Date.now() + } + }) + } + + // 新增:添加工作流通知 + public addWorkflowNotification(workflowName: string, currentNode: string, actionRequired: string) { + return this.addNotification({ + type: 'WORKFLOW', + title: '工作流待处理', + content: `工作流"${workflowName}"当前节点:${currentNode},需要操作:${actionRequired}`, + targetUrl: '/asset-management/device-management/approval', + priority: 'HIGH', + category: '工作流', + actionRequired: true, + source: 'WORKFLOW_SYSTEM', + metadata: { + workflowName, + currentNode, + actionRequired, + timestamp: Date.now() + } + }) + } + + // 新增:播放通知提示音 + private playNotificationSound() { + try { + // 创建音频上下文 + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() + const oscillator = audioContext.createOscillator() + const gainNode = audioContext.createGain() + + oscillator.connect(gainNode) + gainNode.connect(audioContext.destination) + + oscillator.frequency.setValueAtTime(800, audioContext.currentTime) + oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1) + + gainNode.gain.setValueAtTime(0.1, audioContext.currentTime) + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3) + + oscillator.start(audioContext.currentTime) + oscillator.stop(audioContext.currentTime + 0.3) + } catch (error) { + console.warn('无法播放通知提示音:', error) + } + } + + // 事件系统 + private eventListeners: Map = new Map() + + public on(event: string, callback: Function) { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, []) + } + this.eventListeners.get(event)!.push(callback) + } + + public off(event: string, callback: Function) { + const listeners = this.eventListeners.get(event) + if (listeners) { + const index = listeners.indexOf(callback) + if (index > -1) { + listeners.splice(index, 1) + } + } + } + + private emitNotificationEvent(event: string, data: any) { + const listeners = this.eventListeners.get(event) + if (listeners) { + listeners.forEach(callback => { + try { + callback(data) + } catch (error) { + console.error('通知事件回调执行失败:', error) + } + }) + } + } + + // 生成唯一ID + private generateId(): string { + return `notification_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } + + // 持久化存储 + public saveToStorage() { + try { + localStorage.setItem('notifications', JSON.stringify(this.notifications.value)) + } catch (error) { + console.error('保存通知到本地存储失败:', error) + } + } + + public loadFromStorage() { + try { + const stored = localStorage.getItem('notifications') + if (stored) { + this.notifications.value = JSON.parse(stored) + + // 清理过期通知 + this.clearExpired() + + // 重新排序 + this.sortByPriority() + } + } catch (error) { + console.error('从本地存储加载通知失败:', error) + } + } + + // 自动保存 + public enableAutoSave() { + // 监听通知变化,自动保存 + this.on('add', () => this.saveToStorage()) + this.on('read', () => this.saveToStorage()) + this.on('remove', () => this.saveToStorage()) + this.on('clear', () => this.saveToStorage()) + } + + // 新增:定期清理过期通知 + public enableAutoCleanup() { + // 每小时清理一次过期通知 + setInterval(() => { + this.clearExpired() + }, 60 * 60 * 1000) + } + + // 新增:导出通知数据 + public exportNotifications() { + try { + const data = JSON.stringify(this.notifications.value, null, 2) + const blob = new Blob([data], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `notifications_${new Date().toISOString().split('T')[0]}.json` + a.click() + URL.revokeObjectURL(url) + } catch (error) { + console.error('导出通知失败:', error) + } + } + + // 新增:导入通知数据 + public importNotifications(data: string) { + try { + const notifications = JSON.parse(data) + if (Array.isArray(notifications)) { + this.notifications.value = [...this.notifications.value, ...notifications] + this.saveToStorage() + return true + } + return false + } catch (error) { + console.error('导入通知失败:', error) + return false + } + } +} + +// 创建单例实例 +const notificationService = new NotificationService() + +// 启用自动保存 +notificationService.enableAutoSave() + +// 启用自动清理 +notificationService.enableAutoCleanup() + +// 从本地存储加载通知 +notificationService.loadFromStorage() + +export default notificationService \ No newline at end of file diff --git a/src/services/procurementSyncService.ts b/src/services/procurementSyncService.ts new file mode 100644 index 0000000..9b8d3cf --- /dev/null +++ b/src/services/procurementSyncService.ts @@ -0,0 +1,492 @@ +import { ref, computed } from 'vue' +import notificationService from './notificationService' +import websocketService from './websocketService' + +export interface ProcurementItem { + id: string + equipmentName: string + applicantName: string + status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED' + createTime: string + updateTime: string + priority: 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT' + category: string + description?: string + budget?: number + quantity: number + unit: string + expectedDelivery?: string + approvalHistory: ApprovalRecord[] + metadata?: Record +} + +export interface ApprovalRecord { + id: string + approverName: string + action: 'APPROVE' | 'REJECT' | 'COMMENT' + comment?: string + timestamp: string + status: 'PENDING' | 'COMPLETED' +} + +class ProcurementSyncService { + private procurements = ref([]) + private isInitialized = ref(false) + + // 计算属性 + public readonly pendingCount = computed(() => + this.procurements.value.filter(p => p.status === 'PENDING').length + ) + + public readonly approvedCount = computed(() => + this.procurements.value.filter(p => p.status === 'APPROVED').length + ) + + public readonly rejectedCount = computed(() => + this.procurements.value.filter(p => p.status === 'REJECTED').length + ) + + public readonly processingCount = computed(() => + this.procurements.value.filter(p => p.status === 'PROCESSING').length + ) + + public readonly completedCount = computed(() => + this.procurements.value.filter(p => p.status === 'COMPLETED').length + ) + + public readonly urgentCount = computed(() => + this.procurements.value.filter(p => p.priority === 'URGENT' && p.status === 'PENDING').length + ) + + public readonly highPriorityCount = computed(() => + this.procurements.value.filter(p => + (p.priority === 'HIGH' || p.priority === 'URGENT') && + p.status === 'PENDING' + ).length + ) + + // 获取所有采购项目 + public getAllProcurements() { + return this.procurements.value + } + + // 获取特定状态的采购项目 + public getProcurementsByStatus(status: ProcurementItem['status']) { + return this.procurements.value.filter(p => p.status === status) + } + + // 获取特定优先级的采购项目 + public getProcurementsByPriority(priority: ProcurementItem['priority']) { + return this.procurements.value.filter(p => p.priority === priority) + } + + // 获取待审批的采购项目 + public getPendingProcurements() { + return this.procurements.value.filter(p => p.status === 'PENDING') + } + + // 获取紧急采购项目 + public getUrgentProcurements() { + return this.procurements.value.filter(p => + p.priority === 'URGENT' && p.status === 'PENDING' + ) + } + + // 根据ID获取采购项目 + public getProcurementById(id: string) { + return this.procurements.value.find(p => p.id === id) + } + + // 添加采购申请 + public addProcurement(procurement: Omit) { + const newProcurement: ProcurementItem = { + ...procurement, + id: this.generateId(), + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + approvalHistory: [] + } + + this.procurements.value.unshift(newProcurement) + + // 限制数量,避免内存泄漏 + if (this.procurements.value.length > 500) { + this.procurements.value = this.procurements.value.slice(0, 500) + } + + // 添加通知 + notificationService.addProcurementNotification( + newProcurement.equipmentName, + newProcurement.applicantName + ) + + // 触发WebSocket更新 + this.broadcastProcurementUpdate('ADD', newProcurement) + + // 保存到本地存储 + this.saveToStorage() + + return newProcurement + } + + // 更新采购状态 + public updateProcurementStatus(id: string, status: ProcurementItem['status'], approverName?: string, comment?: string) { + const procurement = this.procurements.value.find(p => p.id === id) + if (!procurement) { + console.warn(`采购项目 ${id} 不存在`) + return false + } + + const oldStatus = procurement.status + procurement.status = status + procurement.updateTime = new Date().toISOString() + + // 添加审批记录 + if (approverName) { + const approvalRecord: ApprovalRecord = { + id: this.generateId(), + approverName, + action: status === 'APPROVED' ? 'APPROVE' : 'REJECT', + comment, + timestamp: new Date().toISOString(), + status: 'COMPLETED' + } + procurement.approvalHistory.push(approvalRecord) + } + + // 添加通知 + if (status === 'APPROVED' || status === 'REJECTED') { + notificationService.addApprovalNotification( + procurement.equipmentName, + status, + approverName || '系统' + ) + } + + // 触发WebSocket更新 + this.broadcastProcurementUpdate('UPDATE', procurement) + + // 保存到本地存储 + this.saveToStorage() + + console.log(`采购项目 ${id} 状态从 ${oldStatus} 更新为 ${status}`) + return true + } + + // 批量更新状态 + public batchUpdateStatus(ids: string[], status: ProcurementItem['status'], approverName: string, comment?: string) { + const results = ids.map(id => this.updateProcurementStatus(id, status, approverName, comment)) + return results.every(result => result) + } + + // 删除采购项目 + public removeProcurement(id: string) { + const index = this.procurements.value.findIndex(p => p.id === id) + if (index > -1) { + const procurement = this.procurements.value[index] + this.procurements.value.splice(index, 1) + + // 触发WebSocket更新 + this.broadcastProcurementUpdate('REMOVE', { id }) + + // 保存到本地存储 + this.saveToStorage() + + return true + } + return false + } + + // 搜索采购项目 + public searchProcurements(keyword: string) { + if (!keyword.trim()) { + return this.procurements.value + } + + const lowerKeyword = keyword.toLowerCase() + return this.procurements.value.filter(p => + p.equipmentName.toLowerCase().includes(lowerKeyword) || + p.applicantName.toLowerCase().includes(lowerKeyword) || + p.description?.toLowerCase().includes(lowerKeyword) || + p.category.toLowerCase().includes(lowerKeyword) + ) + } + + // 按优先级排序 + public sortByPriority() { + const priorityOrder = { 'URGENT': 4, 'HIGH': 3, 'NORMAL': 2, 'LOW': 1 } + + this.procurements.value.sort((a, b) => { + const aPriority = priorityOrder[a.priority] || 2 + const bPriority = priorityOrder[b.priority] || 2 + + if (aPriority !== bPriority) { + return bPriority - aPriority + } + + // 优先级相同时,按时间倒序 + return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + }) + } + + // 按时间排序 + public sortByTime() { + this.procurements.value.sort((a, b) => + new Date(b.createTime).getTime() - new Date(a.createTime).getTime() + ) + } + + // 按状态分组 + public groupByStatus() { + const groups: Record = {} + + this.procurements.value.forEach(procurement => { + const status = procurement.status + if (!groups[status]) { + groups[status] = [] + } + groups[status].push(procurement) + }) + + return groups + } + + // 获取统计信息 + public getStats() { + const total = this.procurements.value.length + const pending = this.pendingCount.value + const approved = this.approvedCount.value + const rejected = this.rejectedCount.value + const processing = this.processingCount.value + const completed = this.completedCount.value + const urgent = this.urgentCount.value + const highPriority = this.highPriorityCount.value + + return { + total, + pending, + approved, + rejected, + processing, + completed, + urgent, + highPriority, + approvalRate: total > 0 ? ((approved + completed) / total * 100).toFixed(1) : '0.0' + } + } + + // 导出采购数据 + public exportProcurements() { + try { + const data = JSON.stringify(this.procurements.value, null, 2) + const blob = new Blob([data], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `procurements_${new Date().toISOString().split('T')[0]}.json` + a.click() + URL.revokeObjectURL(url) + } catch (error) { + console.error('导出采购数据失败:', error) + } + } + + // 导入采购数据 + public importProcurements(data: string) { + try { + const procurements = JSON.parse(data) + if (Array.isArray(procurements)) { + this.procurements.value = [...this.procurements.value, ...procurements] + this.saveToStorage() + return true + } + return false + } catch (error) { + console.error('导入采购数据失败:', error) + return false + } + } + + // 清空所有数据 + public clearAll() { + this.procurements.value = [] + this.saveToStorage() + } + + // 初始化服务 + public async initialize() { + if (this.isInitialized.value) { + return + } + + try { + // 从本地存储加载数据 + this.loadFromStorage() + + // 设置WebSocket监听 + this.setupWebSocketListeners() + + // 设置定期同步 + this.setupPeriodicSync() + + this.isInitialized.value = true + console.log('采购同步服务初始化完成') + } catch (error) { + console.error('采购同步服务初始化失败:', error) + } + } + + // 设置WebSocket监听 + private setupWebSocketListeners() { + // 监听采购更新消息 + websocketService.on('PROCUREMENT_UPDATE', (data: any) => { + this.handleWebSocketMessage(data) + }) + + // 监听审批更新消息 + websocketService.on('APPROVAL_UPDATE', (data: any) => { + this.handleApprovalUpdate(data) + }) + + // 监听设备状态更新 + websocketService.on('EQUIPMENT_STATUS_UPDATE', (data: any) => { + this.handleEquipmentStatusUpdate(data) + }) + } + + // 处理WebSocket消息 + private handleWebSocketMessage(data: any) { + try { + const { action, procurement } = data + + switch (action) { + case 'ADD': + if (procurement && !this.procurements.value.find(p => p.id === procurement.id)) { + this.procurements.value.unshift(procurement) + } + break + + case 'UPDATE': + if (procurement) { + const index = this.procurements.value.findIndex(p => p.id === procurement.id) + if (index > -1) { + this.procurements.value[index] = procurement + } + } + break + + case 'REMOVE': + if (data.id) { + const index = this.procurements.value.findIndex(p => p.id === data.id) + if (index > -1) { + this.procurements.value.splice(index, 1) + } + } + break + } + + // 保存到本地存储 + this.saveToStorage() + } catch (error) { + console.error('处理WebSocket采购消息失败:', error) + } + } + + // 处理审批更新 + private handleApprovalUpdate(data: any) { + try { + const { procurementId, status, approverName, comment } = data + this.updateProcurementStatus(procurementId, status, approverName, comment) + } catch (error) { + console.error('处理审批更新失败:', error) + } + } + + // 处理设备状态更新 + private handleEquipmentStatusUpdate(data: any) { + try { + const { procurementId, equipmentStatus } = data + const procurement = this.procurements.value.find(p => p.id === procurementId) + + if (procurement) { + // 根据设备状态更新采购状态 + if (equipmentStatus === 'INSTALLED') { + this.updateProcurementStatus(procurementId, 'COMPLETED') + } else if (equipmentStatus === 'IN_TRANSIT') { + this.updateProcurementStatus(procurementId, 'PROCESSING') + } + } + } catch (error) { + console.error('处理设备状态更新失败:', error) + } + } + + // 设置定期同步 + private setupPeriodicSync() { + // 每5分钟同步一次状态 + setInterval(() => { + this.syncProcurementStatus() + }, 5 * 60 * 1000) + } + + // 同步采购状态 + private async syncProcurementStatus() { + try { + // 这里可以调用后端API同步最新状态 + // const response = await api.getProcurementStatus() + // 更新本地状态 + console.log('定期同步采购状态...') + } catch (error) { + console.error('同步采购状态失败:', error) + } + } + + // 广播采购更新 + private broadcastProcurementUpdate(action: string, data: any) { + if (websocketService.connected.value) { + websocketService.send({ + type: 'PROCUREMENT_UPDATE', + data: { action, ...data }, + timestamp: Date.now() + }) + } + } + + // 生成唯一ID + private generateId(): string { + return `procurement_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } + + // 持久化存储 + public saveToStorage() { + try { + localStorage.setItem('procurements', JSON.stringify(this.procurements.value)) + } catch (error) { + console.error('保存采购数据到本地存储失败:', error) + } + } + + public loadFromStorage() { + try { + const stored = localStorage.getItem('procurements') + if (stored) { + this.procurements.value = JSON.parse(stored) + + // 重新排序 + this.sortByPriority() + } + } catch (error) { + console.error('从本地存储加载采购数据失败:', error) + } + } + + // 获取初始化状态 + public getInitializationStatus() { + return this.isInitialized.value + } +} + +// 创建单例实例 +const procurementSyncService = new ProcurementSyncService() + +export default procurementSyncService diff --git a/src/services/websocketService.ts b/src/services/websocketService.ts new file mode 100644 index 0000000..c323227 --- /dev/null +++ b/src/services/websocketService.ts @@ -0,0 +1,787 @@ +import { ref, onMounted, onUnmounted } from 'vue' +import notificationService from './notificationService' + +interface WebSocketMessage { + type: 'NOTIFICATION' | 'APPROVAL_UPDATE' | 'PROCUREMENT_UPDATE' | 'SYSTEM' | 'EQUIPMENT_STATUS_UPDATE' | 'HEARTBEAT' | 'EQUIPMENT_BORROW_UPDATE' | 'EQUIPMENT_RETURN_UPDATE' | 'EQUIPMENT_MAINTENANCE_UPDATE' | 'EQUIPMENT_ALERT' | 'WORKFLOW_UPDATE' + data: any + timestamp: number + messageId?: string +} + +class WebSocketService { + private ws: WebSocket | null = null + private reconnectAttempts = 0 + private maxReconnectAttempts = 5 + private reconnectInterval = 3000 // 3秒 + private heartbeatInterval: NodeJS.Timeout | null = null + private isConnected = ref(false) + private isConnecting = ref(false) + + // 连接状态 + public readonly connected = this.isConnected + public readonly connecting = this.isConnecting + + // 连接配置 + private wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8080/ws' + private token = localStorage.getItem('token') || '' + + // 事件监听器 + private eventListeners: Map = new Map() + + // 消息队列 - 用于离线时缓存消息 + private messageQueue: WebSocketMessage[] = [] + private maxQueueSize = 100 + + // 消息去重 - 避免重复处理 + private processedMessages = new Set() + private maxProcessedMessages = 1000 + + constructor() { + this.setupEventListeners() + this.loadMessageQueue() + } + + // 连接WebSocket + public connect() { + if (this.isConnecting.value || this.isConnected.value) { + return + } + + this.isConnecting.value = true + + try { + // 构建WebSocket URL,包含认证token + const url = `${this.wsUrl}?token=${encodeURIComponent(this.token)}` + this.ws = new WebSocket(url) + + this.ws.onopen = this.handleOpen.bind(this) + this.ws.onmessage = this.handleMessage.bind(this) + this.ws.onclose = this.handleClose.bind(this) + this.ws.onerror = this.handleError.bind(this) + + } catch (error) { + console.error('WebSocket连接失败:', error) + this.isConnecting.value = false + this.scheduleReconnect() + } + } + + // 断开连接 + public disconnect() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + this.heartbeatInterval = null + } + + if (this.ws) { + this.ws.close() + this.ws = null + } + + this.isConnected.value = false + this.isConnecting.value = false + } + + // 发送消息 + public send(message: any) { + if (this.ws && this.isConnected.value) { + try { + this.ws.send(JSON.stringify(message)) + } catch (error) { + console.error('发送WebSocket消息失败:', error) + // 发送失败时,将消息加入队列 + this.addToMessageQueue(message) + } + } else { + console.warn('WebSocket未连接,将消息加入队列') + this.addToMessageQueue(message) + } + } + + // 发送心跳 + public sendHeartbeat() { + this.send({ + type: 'HEARTBEAT', + data: { timestamp: Date.now() }, + timestamp: Date.now() + }) + } + + // 处理连接打开 + private handleOpen() { + console.log('WebSocket连接已建立') + this.isConnected.value = true + this.isConnecting.value = false + this.reconnectAttempts = 0 + + // 启动心跳 + this.startHeartbeat() + + // 发送认证消息 + this.send({ + type: 'AUTH', + data: { token: this.token }, + timestamp: Date.now() + }) + + // 处理离线期间的消息队列 + this.processMessageQueue() + + // 触发连接事件 + this.emit('connected', null) + } + + // 处理消息接收 + private handleMessage(event: MessageEvent) { + try { + const message: WebSocketMessage = JSON.parse(event.data) + this.processMessage(message) + } catch (error) { + console.error('解析WebSocket消息失败:', error) + } + } + + // 处理连接关闭 + private handleClose(event: CloseEvent) { + console.log('WebSocket连接已关闭:', event.code, event.reason) + this.isConnected.value = false + this.isConnecting.value = false + + // 停止心跳 + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + this.heartbeatInterval = null + } + + // 触发断开连接事件 + this.emit('disconnected', { code: event.code, reason: event.reason }) + + // 如果不是主动关闭,尝试重连 + if (event.code !== 1000) { + this.scheduleReconnect() + } + } + + // 处理连接错误 + private handleError(error: Event) { + console.error('WebSocket连接错误:', error) + this.isConnecting.value = false + + // 触发错误事件 + this.emit('error', error) + + // 尝试重连 + this.scheduleReconnect() + } + + // 处理接收到的消息 + private processMessage(message: WebSocketMessage) { + console.log('收到WebSocket消息:', message) + + // 消息去重检查 + if (message.messageId && this.processedMessages.has(message.messageId)) { + console.log('消息已处理,跳过:', message.messageId) + return + } + + // 添加到已处理消息集合 + if (message.messageId) { + this.processedMessages.add(message.messageId) + // 限制已处理消息数量 + if (this.processedMessages.size > this.maxProcessedMessages) { + const firstKey = this.processedMessages.keys().next().value + if (firstKey) { + this.processedMessages.delete(firstKey) + } + } + } + + switch (message.type) { + case 'NOTIFICATION': + this.handleNotificationMessage(message.data) + break + + case 'APPROVAL_UPDATE': + this.handleApprovalUpdateMessage(message.data) + break + + case 'PROCUREMENT_UPDATE': + this.handleProcurementUpdateMessage(message.data) + break + + case 'EQUIPMENT_STATUS_UPDATE': + this.handleEquipmentStatusUpdateMessage(message.data) + break + + case 'EQUIPMENT_BORROW_UPDATE': + this.handleEquipmentBorrowUpdateMessage(message.data) + break + + case 'EQUIPMENT_RETURN_UPDATE': + this.handleEquipmentReturnUpdateMessage(message.data) + break + + case 'EQUIPMENT_MAINTENANCE_UPDATE': + this.handleEquipmentMaintenanceUpdateMessage(message.data) + break + + case 'EQUIPMENT_ALERT': + this.handleEquipmentAlertMessage(message.data) + break + + case 'WORKFLOW_UPDATE': + this.handleWorkflowUpdateMessage(message.data) + break + + case 'SYSTEM': + this.handleSystemMessage(message.data) + break + + case 'HEARTBEAT': + // 心跳响应,不需要特殊处理 + break + + default: + console.warn('未知的WebSocket消息类型:', message.type) + } + + // 触发消息接收事件 + this.emit('message', message) + + // 保存消息到本地存储 + this.saveMessageToStorage(message) + } + + // 处理通知消息 + private handleNotificationMessage(data: any) { + if (data.notification) { + notificationService.addNotification({ + type: data.notification.type || 'SYSTEM', + title: data.notification.title || '新通知', + content: data.notification.content || '', + priority: data.notification.priority || 'NORMAL', + category: data.notification.category, + targetUrl: data.notification.targetUrl, + metadata: data.notification.metadata + }) + } + } + + // 处理审批更新消息 + private handleApprovalUpdateMessage(data: any) { + if (data.approval) { + const approval = data.approval + + // 根据审批状态添加相应通知 + if (approval.status === 'APPROVED') { + notificationService.addApprovalNotification( + approval.equipmentName, + 'APPROVED', + approval.approverName || '审批人' + ) + + // 触发审批状态更新事件 + this.emit('approvalStatusChanged', { + type: 'APPROVED', + approvalId: approval.approvalId, + equipmentId: approval.equipmentId, + status: approval.status + }) + } else if (approval.status === 'REJECTED') { + notificationService.addApprovalNotification( + approval.equipmentName, + 'REJECTED', + approval.approverName || '审批人' + ) + + // 触发审批状态更新事件 + this.emit('approvalStatusChanged', { + type: 'REJECTED', + approvalId: approval.approvalId, + equipmentId: approval.equipmentId, + status: approval.status + }) + } else if (approval.status === 'SUBMITTED') { + // 新增待审批通知 + notificationService.addNotification({ + type: 'PENDING', + title: '新的审批申请', + content: `收到来自 ${approval.applicantName} 的${approval.businessType || '设备'}申请:${approval.equipmentName}`, + targetUrl: '/asset-management/device-management/approval', + priority: 'HIGH', + category: '审批申请', + metadata: { + approvalId: approval.approvalId, + equipmentName: approval.equipmentName, + applicantName: approval.applicantName, + businessType: approval.businessType, + timestamp: Date.now() + } + }) + + // 触发新审批申请事件 + this.emit('newApprovalRequest', approval) + } + } + } + + // 处理采购更新消息 + private handleProcurementUpdateMessage(data: any) { + if (data.procurement) { + const procurement = data.procurement + + if (procurement.status === 'SUBMITTED') { + notificationService.addProcurementNotification( + procurement.equipmentName, + procurement.applicantName || '申请人' + ) + + // 触发采购状态更新事件 + this.emit('procurementStatusChanged', { + type: 'SUBMITTED', + procurementId: procurement.procurementId, + equipmentId: procurement.equipmentId, + status: procurement.status + }) + } else if (procurement.status === 'APPROVED') { + // 采购申请被批准 + notificationService.addNotification({ + type: 'PROCUREMENT', + title: '采购申请已批准', + content: `您的设备采购申请"${procurement.equipmentName}"已获得批准`, + targetUrl: '/asset-management/device-management/procurement', + priority: 'NORMAL', + category: '采购审批', + metadata: { + equipmentName: procurement.equipmentName, + status: procurement.status, + timestamp: Date.now() + } + }) + + // 触发采购状态更新事件 + this.emit('procurementStatusChanged', { + type: 'APPROVED', + procurementId: procurement.procurementId, + equipmentId: procurement.equipmentId, + status: procurement.status + }) + } + } + } + + // 处理设备状态更新消息 + private handleEquipmentStatusUpdateMessage(data: any) { + if (data.equipment) { + const equipment = data.equipment + + // 触发设备状态更新事件 + this.emit('equipmentStatusChanged', { + equipmentId: equipment.equipmentId, + oldStatus: equipment.oldStatus, + newStatus: equipment.newStatus, + updateTime: equipment.updateTime + }) + + // 如果状态变化涉及采购,添加相应通知 + if (equipment.statusChangeType === 'PROCUREMENT') { + notificationService.addNotification({ + type: 'PROCUREMENT', + title: '设备采购状态更新', + content: `设备"${equipment.equipmentName}"的采购状态已更新为:${equipment.newStatus}`, + targetUrl: '/asset-management/device-management/procurement', + priority: 'NORMAL', + category: '设备状态', + metadata: { + equipmentId: equipment.equipmentId, + equipmentName: equipment.equipmentName, + oldStatus: equipment.oldStatus, + newStatus: equipment.newStatus, + timestamp: Date.now() + } + }) + } + } + } + + // 处理设备借用更新消息 + private handleEquipmentBorrowUpdateMessage(data: any) { + if (data.borrow) { + const borrow = data.borrow + + // 触发设备借用状态更新事件 + this.emit('equipmentBorrowChanged', { + equipmentId: borrow.equipmentId, + borrowId: borrow.borrowId, + oldStatus: borrow.oldStatus, + newStatus: borrow.newStatus, + updateTime: borrow.updateTime + }) + + // 添加借用状态更新通知 + notificationService.addNotification({ + type: 'EQUIPMENT_BORROW', + title: '设备借用状态更新', + content: `设备"${borrow.equipmentName}"的借用状态已更新为:${borrow.newStatus}`, + targetUrl: '/asset-management/device-management/device-center', + priority: 'NORMAL', + category: '设备借用', + metadata: { + equipmentId: borrow.equipmentId, + equipmentName: borrow.equipmentName, + borrowId: borrow.borrowId, + oldStatus: borrow.oldStatus, + newStatus: borrow.newStatus, + timestamp: Date.now() + } + }) + } + } + + // 处理设备归还更新消息 + private handleEquipmentReturnUpdateMessage(data: any) { + if (data.return) { + const returnData = data.return + + // 触发设备归还状态更新事件 + this.emit('equipmentReturnChanged', { + equipmentId: returnData.equipmentId, + returnId: returnData.returnId, + oldStatus: returnData.oldStatus, + newStatus: returnData.newStatus, + updateTime: returnData.updateTime + }) + + // 添加归还状态更新通知 + notificationService.addNotification({ + type: 'EQUIPMENT_RETURN', + title: '设备归还状态更新', + content: `设备"${returnData.equipmentName}"的归还状态已更新为:${returnData.newStatus}`, + targetUrl: '/asset-management/device-management/device-center', + priority: 'NORMAL', + category: '设备归还', + metadata: { + equipmentId: returnData.equipmentId, + equipmentName: returnData.equipmentName, + returnId: returnData.returnId, + oldStatus: returnData.oldStatus, + newStatus: returnData.newStatus, + timestamp: Date.now() + } + }) + } + } + + // 处理设备维护更新消息 + private handleEquipmentMaintenanceUpdateMessage(data: any) { + if (data.maintenance) { + const maintenance = data.maintenance + + // 触发设备维护状态更新事件 + this.emit('equipmentMaintenanceChanged', { + equipmentId: maintenance.equipmentId, + maintenanceId: maintenance.maintenanceId, + oldStatus: maintenance.oldStatus, + newStatus: maintenance.newStatus, + updateTime: maintenance.updateTime + }) + + // 添加维护状态更新通知 + notificationService.addNotification({ + type: 'EQUIPMENT_MAINTENANCE', + title: '设备维护状态更新', + content: `设备"${maintenance.equipmentName}"的维护状态已更新为:${maintenance.newStatus}`, + targetUrl: '/asset-management/device-management/device-center', + priority: 'NORMAL', + category: '设备维护', + metadata: { + equipmentId: maintenance.equipmentId, + equipmentName: maintenance.equipmentName, + maintenanceId: maintenance.maintenanceId, + oldStatus: maintenance.oldStatus, + newStatus: maintenance.newStatus, + timestamp: Date.now() + } + }) + } + } + + // 处理设备告警消息 + private handleEquipmentAlertMessage(data: any) { + if (data.alert) { + const alert = data.alert + + // 触发设备告警事件 + this.emit('equipmentAlert', { + equipmentId: alert.equipmentId, + alertId: alert.alertId, + alertType: alert.alertType, + alertLevel: alert.alertLevel, + message: alert.message, + timestamp: alert.timestamp + }) + + // 添加设备告警通知 + notificationService.addNotification({ + type: 'EQUIPMENT_ALERT', + title: `设备告警 - ${alert.alertType}`, + content: `设备"${alert.equipmentName}"发生告警:${alert.message}`, + targetUrl: '/asset-management/device-management/device-center', + priority: alert.alertLevel === 'CRITICAL' ? 'URGENT' : 'HIGH', + category: '设备告警', + actionRequired: true, + metadata: { + equipmentId: alert.equipmentId, + equipmentName: alert.equipmentName, + alertId: alert.alertId, + alertType: alert.alertType, + alertLevel: alert.alertLevel, + message: alert.message, + timestamp: Date.now() + } + }) + } + } + + // 处理工作流更新消息 + private handleWorkflowUpdateMessage(data: any) { + if (data.workflow) { + const workflow = data.workflow + + // 触发工作流状态更新事件 + this.emit('workflowStatusChanged', { + workflowId: workflow.workflowId, + oldStatus: workflow.oldStatus, + newStatus: workflow.newStatus, + currentNode: workflow.currentNode, + updateTime: workflow.updateTime + }) + + // 添加工作流状态更新通知 + notificationService.addNotification({ + type: 'WORKFLOW', + title: '工作流状态更新', + content: `工作流"${workflow.workflowName}"状态已更新为:${workflow.newStatus}`, + targetUrl: '/asset-management/device-management/approval', + priority: 'NORMAL', + category: '工作流', + metadata: { + workflowId: workflow.workflowId, + workflowName: workflow.workflowName, + oldStatus: workflow.oldStatus, + newStatus: workflow.newStatus, + currentNode: workflow.currentNode, + timestamp: Date.now() + } + }) + } + } + + // 处理系统消息 + private handleSystemMessage(data: any) { + if (data.system) { + const system = data.system + + notificationService.addSystemNotification( + system.title || '系统通知', + system.content || '', + system.priority || 'NORMAL' + ) + } + } + + // 启动心跳 + private startHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + } + + this.heartbeatInterval = setInterval(() => { + if (this.isConnected.value) { + this.sendHeartbeat() + } + }, 30000) // 30秒发送一次心跳 + } + + // 安排重连 + private scheduleReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error('WebSocket重连次数已达上限,停止重连') + return + } + + this.reconnectAttempts++ + console.log(`WebSocket将在 ${this.reconnectInterval / 1000} 秒后尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`) + + setTimeout(() => { + this.connect() + }, this.reconnectInterval) + + // 递增重连间隔 + this.reconnectInterval = Math.min(this.reconnectInterval * 1.5, 30000) + } + + // 消息队列管理 + private addToMessageQueue(message: any) { + this.messageQueue.push(message) + if (this.messageQueue.length > this.maxQueueSize) { + this.messageQueue.shift() // 移除最旧的消息 + } + this.saveMessageQueue() + } + + private processMessageQueue() { + if (this.messageQueue.length > 0) { + console.log(`处理离线期间的消息队列,共 ${this.messageQueue.length} 条消息`) + const messages = [...this.messageQueue] + this.messageQueue = [] + + messages.forEach(message => { + this.processMessage(message) + }) + + this.saveMessageQueue() + } + } + + private saveMessageQueue() { + try { + localStorage.setItem('websocket_message_queue', JSON.stringify(this.messageQueue)) + } catch (error) { + console.error('保存消息队列失败:', error) + } + } + + private loadMessageQueue() { + try { + const stored = localStorage.getItem('websocket_message_queue') + if (stored) { + this.messageQueue = JSON.parse(stored) + } + } catch (error) { + console.error('加载消息队列失败:', error) + } + } + + // 消息存储管理 + private saveMessageToStorage(message: WebSocketMessage) { + try { + const messages = this.getStoredMessages() + messages.unshift(message) + + // 限制存储的消息数量 + if (messages.length > 200) { + messages.splice(200) + } + + localStorage.setItem('websocket_messages', JSON.stringify(messages)) + } catch (error) { + console.error('保存消息到本地存储失败:', error) + } + } + + private getStoredMessages(): WebSocketMessage[] { + try { + const stored = localStorage.getItem('websocket_messages') + return stored ? JSON.parse(stored) : [] + } catch (error) { + console.error('获取存储的消息失败:', error) + return [] + } + } + + // 设置事件监听器 + private setupEventListeners() { + // 监听token变化,重新连接 + window.addEventListener('storage', (event) => { + if (event.key === 'token' && event.newValue !== this.token) { + this.token = event.newValue || '' + if (this.isConnected.value) { + this.disconnect() + this.connect() + } + } + }) + } + + // 事件系统 + public on(event: string, callback: Function) { + if (!this.eventListeners.has(event)) { + this.eventListeners.set(event, []) + } + this.eventListeners.get(event)!.push(callback) + } + + public off(event: string, callback: Function) { + const listeners = this.eventListeners.get(event) + if (listeners) { + const index = listeners.indexOf(callback) + if (index > -1) { + listeners.splice(index, 1) + } + } + } + + private emit(event: string, data: any) { + const listeners = this.eventListeners.get(event) + if (listeners) { + listeners.forEach(callback => { + try { + callback(data) + } catch (error) { + console.error('WebSocket事件回调执行失败:', error) + } + }) + } + } + + // 获取连接状态 + public getStatus() { + return { + connected: this.isConnected.value, + connecting: this.isConnecting.value, + reconnectAttempts: this.reconnectAttempts, + maxReconnectAttempts: this.maxReconnectAttempts, + messageQueueSize: this.messageQueue.length, + processedMessagesCount: this.processedMessages.size + } + } + + // 更新认证token + public updateToken(newToken: string) { + this.token = newToken + if (this.isConnected.value) { + // 重新认证 + this.send({ + type: 'AUTH', + data: { token: this.token }, + timestamp: Date.now() + }) + } + } + + // 获取存储的消息 + public getMessages() { + return this.getStoredMessages() + } + + // 清空存储的消息 + public clearMessages() { + try { + localStorage.removeItem('websocket_messages') + localStorage.removeItem('websocket_message_queue') + this.messageQueue = [] + this.processedMessages.clear() + } catch (error) { + console.error('清空消息失败:', error) + } + } +} + +// 创建单例实例 +const websocketService = new WebSocketService() + +// 自动连接 +websocketService.connect() + +export default websocketService diff --git a/src/styles/global.scss b/src/styles/global.scss index 86d49ac..823fc32 100644 --- a/src/styles/global.scss +++ b/src/styles/global.scss @@ -33,6 +33,15 @@ padding: $padding; } +// 高z-index类,用于确保组件在最上层显示 +.gi_z_index_high { + z-index: 10001 !important; +} + +.gi_z_index_highest { + z-index: 100001 !important; +} + .gi_relative { position: relative; } diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts index eab6be6..369aad4 100644 --- a/src/types/auto-imports.d.ts +++ b/src/types/auto-imports.d.ts @@ -70,6 +70,6 @@ declare global { // for type re-export declare global { // @ts-ignore - export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' + export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' import('vue') } diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 19a4246..14a0fe7 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -7,66 +7,8 @@ export {} declare module 'vue' { export interface GlobalComponents { - Avatar: typeof import('./../components/Avatar/index.vue')['default'] - Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default'] - CellCopy: typeof import('./../components/CellCopy/index.vue')['default'] - Chart: typeof import('./../components/Chart/index.vue')['default'] - ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default'] - CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default'] - CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default'] - DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default'] - DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default'] - FilePreview: typeof import('./../components/FilePreview/index.vue')['default'] - GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default'] - GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default'] - GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default'] - GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default'] - GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default'] - GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default'] - GiDot: typeof import('./../components/GiDot/index.tsx')['default'] - GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default'] - GiFooter: typeof import('./../components/GiFooter/index.vue')['default'] - GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default'] - GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default'] - GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default'] - GiIframe: typeof import('./../components/GiIframe/index.vue')['default'] - GiOption: typeof import('./../components/GiOption/index.vue')['default'] - GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default'] - GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default'] - GiSpace: typeof import('./../components/GiSpace/index.vue')['default'] - GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default'] - GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default'] - GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default'] - GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default'] - GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default'] - GiTag: typeof import('./../components/GiTag/index.tsx')['default'] - GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default'] - HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default'] - Icon403: typeof import('./../components/icons/Icon403.vue')['default'] - Icon404: typeof import('./../components/icons/Icon404.vue')['default'] - Icon500: typeof import('./../components/icons/Icon500.vue')['default'] - IconBorders: typeof import('./../components/icons/IconBorders.vue')['default'] - IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default'] - IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default'] - IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default'] - ImageImport: typeof import('./../components/ImageImport/index.vue')['default'] - ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default'] - IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default'] - JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default'] - MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default'] - MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default'] - ParentView: typeof import('./../components/ParentView/index.vue')['default'] + NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] - SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default'] - SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default'] - TextCopy: typeof import('./../components/TextCopy/index.vue')['default'] - TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default'] - UserSelect: typeof import('./../components/UserSelect/index.vue')['default'] - Verify: typeof import('./../components/Verify/index.vue')['default'] - VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default'] - VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default'] - WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default'] - YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default'] } } diff --git a/src/views/system-resource/device-management/approval/components/ApprovalActionModal.vue b/src/views/system-resource/device-management/approval/components/ApprovalActionModal.vue index 5480187..5bc88e5 100644 --- a/src/views/system-resource/device-management/approval/components/ApprovalActionModal.vue +++ b/src/views/system-resource/device-management/approval/components/ApprovalActionModal.vue @@ -82,6 +82,7 @@ import { IconInfoCircle, IconEdit } from '@arco-design/web-vue/es/icon' import message from '@arco-design/web-vue/es/message' import type { EquipmentApprovalResp, EquipmentApprovalReq } from '@/apis/equipment/type' import { equipmentApprovalApi } from '@/apis/equipment/approval' +import notificationService from '@/services/notificationService' defineOptions({ name: 'ApprovalActionModal' }) @@ -173,9 +174,23 @@ const handleSubmit = async () => { if (props.actionType === 'approve') { await equipmentApprovalApi.approve(approvalId, submitData) message.success('审批通过成功') + + // 添加审批通过通知 + notificationService.addApprovalNotification( + props.approvalData.equipmentName, + 'APPROVED', + formData.approverName || '审批人' + ) } else { await equipmentApprovalApi.reject(approvalId, submitData) message.success('审批拒绝成功') + + // 添加审批拒绝通知 + notificationService.addApprovalNotification( + props.approvalData.equipmentName, + 'REJECTED', + formData.approverName || '审批人' + ) } emit('success') diff --git a/src/views/system-resource/device-management/procurement/components/ProcurementApplicationModal.vue b/src/views/system-resource/device-management/procurement/components/ProcurementApplicationModal.vue index bad19be..d77e9f5 100644 --- a/src/views/system-resource/device-management/procurement/components/ProcurementApplicationModal.vue +++ b/src/views/system-resource/device-management/procurement/components/ProcurementApplicationModal.vue @@ -175,6 +175,7 @@ import { ref, reactive, watch } from 'vue' import { Message } from '@arco-design/web-vue' import { equipmentApprovalApi } from '@/apis/equipment/approval' +import notificationService from '@/services/notificationService' interface Props { visible: boolean @@ -296,6 +297,15 @@ const handleSubmit = async () => { await equipmentApprovalApi.submitProcurementApplication(submitData) + // 添加采购申请通知 + const currentUser = JSON.parse(localStorage.getItem('userInfo') || '{}') + const applicantName = currentUser.nickname || currentUser.username || '未知用户' + + notificationService.addProcurementNotification( + submitData.equipmentName, + applicantName + ) + Message.success('采购申请提交成功') emit('success') handleCancel()
智能审批助手已禁用
请选择要审批的申请
暂无消息