实现了消息中心显示最新申请消息,并可以查看消息然后点击跳转到具体的审批台

This commit is contained in:
Mr.j 2025-08-11 10:54:45 +08:00
parent 16743cfc7f
commit 48632707cc
11 changed files with 3371 additions and 84 deletions

View File

@ -0,0 +1,620 @@
<template>
<div class="approval-assistant">
<!-- 助手头部 -->
<div class="assistant-header">
<div class="header-left">
<IconRobot style="font-size: 20px; color: var(--color-primary); margin-right: 8px;" />
<h3>智能审批助手</h3>
</div>
<div class="header-right">
<a-switch
v-model="assistantEnabled"
size="small"
@change="toggleAssistant"
>
<template #checked>启用</template>
<template #unchecked>禁用</template>
</a-switch>
</div>
</div>
<!-- 助手内容 -->
<div v-if="assistantEnabled && currentApproval" class="assistant-content">
<!-- 审批建议 -->
<div class="assistant-section">
<div class="section-header">
<IconLightbulb style="color: var(--color-warning); margin-right: 8px;" />
<span>审批建议</span>
</div>
<div class="section-content">
<div class="recommendation-card" :class="getRecommendationClass()">
<div class="recommendation-icon">
<IconCheckCircle v-if="recommendation === 'APPROVE'" />
<IconCloseCircle v-else-if="recommendation === 'REJECT'" />
<IconExclamationCircle v-else />
</div>
<div class="recommendation-content">
<div class="recommendation-title">
{{ getRecommendationTitle() }}
</div>
<div class="recommendation-reason">
{{ getRecommendationReason() }}
</div>
</div>
</div>
</div>
</div>
<!-- 风险评估 -->
<div class="assistant-section">
<div class="section-header">
<IconShield style="color: var(--color-danger); margin-right: 8px;" />
<span>风险评估</span>
</div>
<div class="section-content">
<div class="risk-indicators">
<div
v-for="risk in riskFactors"
:key="risk.type"
class="risk-item"
:class="getRiskClass(risk.level)"
>
<div class="risk-icon">
<IconExclamationCircle v-if="risk.level === 'HIGH'" />
<IconWarning v-else-if="risk.level === 'MEDIUM'" />
<IconCheckCircle v-else />
</div>
<div class="risk-info">
<div class="risk-title">{{ risk.title }}</div>
<div class="risk-description">{{ risk.description }}</div>
</div>
<div class="risk-score">{{ risk.score }}%</div>
</div>
</div>
</div>
</div>
<!-- 快速审批 -->
<div class="assistant-section">
<div class="section-header">
<IconThunderbolt style="color: var(--color-success); margin-right: 8px;" />
<span>快速审批</span>
</div>
<div class="section-content">
<div class="quick-actions">
<a-button
type="primary"
size="small"
:disabled="recommendation !== 'APPROVE'"
@click="handleQuickApprove"
>
<template #icon>
<IconCheckCircle />
</template>
快速通过
</a-button>
<a-button
type="text"
size="small"
status="danger"
:disabled="recommendation !== 'REJECT'"
@click="handleQuickReject"
>
<template #icon>
<IconCloseCircle />
</template>
快速拒绝
</a-button>
<a-button
type="text"
size="small"
@click="handleManualReview"
>
<template #icon>
<IconEdit />
</template>
手动审批
</a-button>
</div>
</div>
</div>
<!-- 审批历史分析 -->
<div class="assistant-section">
<div class="section-header">
<IconBarChart style="color: var(--color-info); margin-right: 8px;" />
<span>审批历史分析</span>
</div>
<div class="section-content">
<div class="history-stats">
<div class="stat-item">
<div class="stat-number">{{ historyStats.total }}</div>
<div class="stat-label">总申请数</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ historyStats.approved }}</div>
<div class="stat-label">通过率</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ historyStats.avgTime }}</div>
<div class="stat-label">平均审批时间</div>
</div>
</div>
</div>
</div>
</div>
<!-- 助手未启用提示 -->
<div v-else-if="!assistantEnabled" class="assistant-disabled">
<IconRobot style="font-size: 48px; color: var(--color-text-3); margin-bottom: 16px;" />
<p>智能审批助手已禁用</p>
<a-button type="primary" size="small" @click="assistantEnabled = true">
启用助手
</a-button>
</div>
<!-- 无审批数据提示 -->
<div v-else class="no-approval-data">
<IconFile style="font-size: 48px; color: var(--color-text-3); margin-bottom: 16px;" />
<p>请选择要审批的申请</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import {
IconRobot,
IconLightbulb,
IconShield,
IconThunderbolt,
IconBarChart,
IconFile,
IconCheckCircle,
IconCloseCircle,
IconExclamationCircle,
IconWarning,
IconEdit
} from '@arco-design/web-vue/es/icon'
import message from '@arco-design-design/web-vue/es/message'
import type { EquipmentApprovalResp } from '@/apis/equipment/type'
import { BusinessType, ApprovalStatus } from '@/apis/equipment/type'
defineOptions({ name: 'ApprovalAssistant' })
interface Props {
approvalData?: EquipmentApprovalResp | null
}
interface RiskFactor {
type: string
title: string
description: string
level: 'LOW' | 'MEDIUM' | 'HIGH'
score: number
}
interface HistoryStats {
total: number
approved: number
avgTime: number
}
const props = withDefaults(defineProps<Props>(), {
approvalData: null
})
const emit = defineEmits<{
quickApprove: [data: EquipmentApprovalResp]
quickReject: [data: EquipmentApprovalResp]
manualReview: [data: EquipmentApprovalResp]
}>()
//
const assistantEnabled = ref(true)
const currentApproval = computed(() => props.approvalData)
//
const recommendation = computed(() => {
if (!currentApproval.value) return null
const approval = currentApproval.value
//
if (approval.businessType === BusinessType.PROCUREMENT) {
//
if (approval.purchasePrice && approval.purchasePrice > 100000) {
return 'REJECT' //
}
if (approval.applicantName && approval.applicantName.includes('测试')) {
return 'REJECT' //
}
return 'APPROVE'
} else if (approval.businessType === BusinessType.BORROW) {
//
if (approval.applyReason && approval.applyReason.length < 10) {
return 'REJECT' //
}
return 'APPROVE'
} else if (approval.businessType === BusinessType.RETURN) {
//
return 'APPROVE' //
}
return 'REVIEW' //
})
//
const riskFactors = computed((): RiskFactor[] => {
if (!currentApproval.value) return []
const approval = currentApproval.value
const factors: RiskFactor[] = []
//
if (approval.purchasePrice && approval.purchasePrice > 50000) {
factors.push({
type: 'PRICE',
title: '价格风险',
description: '采购价格较高,建议详细审查',
level: 'MEDIUM',
score: 70
})
}
//
if (approval.applicantName && approval.applicantName.includes('新员工')) {
factors.push({
type: 'APPLICANT',
title: '申请人风险',
description: '新员工申请,建议加强指导',
level: 'LOW',
score: 30
})
}
//
if (approval.equipmentType && ['危险设备', '精密设备'].includes(approval.equipmentType)) {
factors.push({
type: 'EQUIPMENT',
title: '设备类型风险',
description: '特殊设备类型,需要专业评估',
level: 'HIGH',
score: 85
})
}
//
if (approval.applyTime) {
const applyDate = new Date(approval.applyTime)
const now = new Date()
const diffDays = Math.ceil((now.getTime() - applyDate.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays > 30) {
factors.push({
type: 'TIMING',
title: '时间风险',
description: '申请时间较长,可能影响业务',
level: 'MEDIUM',
score: 60
})
}
}
return factors
})
//
const historyStats = computed((): HistoryStats => {
// API
// 使
return {
total: 156,
approved: 89,
avgTime: 2.5
}
})
//
const toggleAssistant = (enabled: boolean) => {
if (enabled) {
message.success('智能审批助手已启用')
} else {
message.info('智能审批助手已禁用')
}
}
const getRecommendationClass = () => {
switch (recommendation.value) {
case 'APPROVE':
return 'recommendation-approve'
case 'REJECT':
return 'recommendation-reject'
default:
return 'recommendation-review'
}
}
const getRecommendationTitle = () => {
switch (recommendation.value) {
case 'APPROVE':
return '建议通过'
case 'REJECT':
return '建议拒绝'
default:
return '需要人工审查'
}
}
const getRecommendationReason = () => {
if (!currentApproval.value) return ''
const approval = currentApproval.value
switch (recommendation.value) {
case 'APPROVE':
if (approval.businessType === BusinessType.PROCUREMENT) {
return '采购申请信息完整,价格合理,建议通过'
} else if (approval.businessType === BusinessType.BORROW) {
return '借用申请理由充分,设备状态良好,建议通过'
} else {
return '申请信息完整,符合审批条件,建议通过'
}
case 'REJECT':
if (approval.businessType === BusinessType.PROCUREMENT) {
return '采购价格过高或信息不完整,建议拒绝'
} else if (approval.businessType === BusinessType.BORROW) {
return '借用理由不充分或设备状态不佳,建议拒绝'
} else {
return '申请信息不完整或不符合条件,建议拒绝'
}
default:
return '申请情况复杂,需要审批人员详细审查后决定'
}
}
const getRiskClass = (level: string) => {
switch (level) {
case 'HIGH':
return 'risk-high'
case 'MEDIUM':
return 'risk-medium'
default:
return 'risk-low'
}
}
const handleQuickApprove = () => {
if (currentApproval.value) {
emit('quickApprove', currentApproval.value)
message.success('已触发快速审批通过')
}
}
const handleQuickReject = () => {
if (currentApproval.value) {
emit('quickReject', currentApproval.value)
message.success('已触发快速审批拒绝')
}
}
const handleManualReview = () => {
if (currentApproval.value) {
emit('manualReview', currentApproval.value)
message.info('请进行手动审批')
}
}
//
watch(() => props.approvalData, (newData) => {
if (newData) {
console.log('审批助手收到新的审批数据:', newData)
}
}, { immediate: true })
</script>
<style scoped lang="scss">
.approval-assistant {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.assistant-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: var(--color-fill-1);
border-bottom: 1px solid var(--color-border);
.header-left {
display: flex;
align-items: center;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
}
}
.assistant-content {
padding: 16px;
.assistant-section {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 12px;
font-weight: 500;
color: var(--color-text-1);
}
.section-content {
.recommendation-card {
display: flex;
align-items: center;
padding: 16px;
border-radius: 8px;
border: 1px solid var(--color-border);
&.recommendation-approve {
background: var(--color-success-light-1);
border-color: var(--color-success);
}
&.recommendation-reject {
background: var(--color-danger-light-1);
border-color: var(--color-danger);
}
&.recommendation-review {
background: var(--color-warning-light-1);
border-color: var(--color-warning);
}
.recommendation-icon {
margin-right: 12px;
font-size: 24px;
.arco-icon {
&.recommendation-approve & {
color: var(--color-success);
}
&.recommendation-reject & {
color: var(--color-danger);
}
&.recommendation-review & {
color: var(--color-warning);
}
}
}
.recommendation-content {
flex: 1;
.recommendation-title {
font-weight: 600;
margin-bottom: 4px;
}
.recommendation-reason {
font-size: 13px;
color: var(--color-text-2);
line-height: 1.4;
}
}
}
.risk-indicators {
.risk-item {
display: flex;
align-items: center;
padding: 12px;
margin-bottom: 8px;
border-radius: 6px;
border: 1px solid var(--color-border);
&:last-child {
margin-bottom: 0;
}
&.risk-high {
background: var(--color-danger-light-1);
border-color: var(--color-danger);
}
&.risk-medium {
background: var(--color-warning-light-1);
border-color: var(--color-warning);
}
&.risk-low {
background: var(--color-success-light-1);
border-color: var(--color-success);
}
.risk-icon {
margin-right: 12px;
font-size: 18px;
}
.risk-info {
flex: 1;
.risk-title {
font-weight: 500;
margin-bottom: 2px;
}
.risk-description {
font-size: 12px;
color: var(--color-text-2);
}
}
.risk-score {
font-weight: 600;
font-size: 14px;
color: var(--color-text-1);
}
}
}
.quick-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.history-stats {
display: flex;
gap: 16px;
.stat-item {
text-align: center;
flex: 1;
.stat-number {
font-size: 24px;
font-weight: 600;
color: var(--color-primary);
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: var(--color-text-3);
}
}
}
}
}
}
.assistant-disabled,
.no-approval-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 16px;
text-align: center;
color: var(--color-text-3);
p {
margin: 0 0 16px 0;
}
}
}
</style>

View File

@ -0,0 +1,736 @@
<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 = () => {
websocketService.on('message', (message) => {
//
console.log('收到WebSocket消息:', message)
})
websocketService.on('approvalStatusChanged', (data) => {
console.log('审批状态变更:', data)
})
websocketService.on('equipmentStatusChanged', (data) => {
console.log('设备状态变更:', data)
})
}
//
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(() => {
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>

View File

@ -12,29 +12,8 @@
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<!-- 消息通知 --> <!-- 消息通知中心 -->
<a-popover <NotificationCenter ref="notificationCenterRef" />
position="bottom"
trigger="click"
:content-style="{ marginTop: '-5px', padding: 0, border: 'none' }"
:arrow-style="{ width: 0, height: 0 }"
>
<a-badge
:count="unreadMessageCount"
:dot="unreadMessageCount > 0"
:show-zero="false"
class="notification-badge"
>
<a-button size="mini" class="gi_hover_btn notification-btn">
<template #icon>
<icon-notification :size="18" />
</template>
</a-button>
</a-badge>
<template #content>
<Message @readall-success="getMessageCount" />
</template>
</a-popover>
<!-- 全屏切换组件 --> <!-- 全屏切换组件 -->
<a-tooltip v-if="!['xs', 'sm'].includes(breakpoint)" content="全屏切换" position="bottom"> <a-tooltip v-if="!['xs', 'sm'].includes(breakpoint)" content="全屏切换" position="bottom">
@ -82,7 +61,7 @@
import { Modal, Notification } from '@arco-design/web-vue' import { Modal, Notification } from '@arco-design/web-vue'
import { useFullscreen } from '@vueuse/core' import { useFullscreen } from '@vueuse/core'
import { onMounted, ref, nextTick, onBeforeUnmount } from 'vue' 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 SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue' import Search from './Search.vue'
@ -94,6 +73,7 @@ defineOptions({ name: 'HeaderRight' })
const { isDesktop } = useDevice() const { isDesktop } = useDevice()
const { breakpoint } = useBreakpoint() const { breakpoint } = useBreakpoint()
const notificationCenterRef = ref()
let socket: WebSocket | null = null let socket: WebSocket | null = null
// //

View File

@ -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<string, any>
// 新增字段
source?: string // 消息来源
actionRequired?: boolean // 是否需要操作
expiresAt?: string // 过期时间
relatedId?: string // 关联的业务ID
// 新增智能提醒字段
reminderTime?: string // 提醒时间
reminderType?: 'IMMEDIATE' | 'DELAYED' | 'RECURRING' // 提醒类型
reminderInterval?: number // 重复提醒间隔(分钟)
lastReminderTime?: string // 上次提醒时间
}
class NotificationService {
private notifications = ref<NotificationItem[]>([])
// 计算属性
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<NotificationItem, 'id' | 'createTime' | 'read'>) {
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<string, NotificationItem[]> = {}
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<string, Function[]> = 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

View File

@ -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<string, any>
}
export interface ApprovalRecord {
id: string
approverName: string
action: 'APPROVE' | 'REJECT' | 'COMMENT'
comment?: string
timestamp: string
status: 'PENDING' | 'COMPLETED'
}
class ProcurementSyncService {
private procurements = ref<ProcurementItem[]>([])
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<ProcurementItem, 'id' | 'createTime' | 'updateTime' | 'approvalHistory'>) {
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<string, ProcurementItem[]> = {}
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

View File

@ -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<string, Function[]> = new Map()
// 消息队列 - 用于离线时缓存消息
private messageQueue: WebSocketMessage[] = []
private maxQueueSize = 100
// 消息去重 - 避免重复处理
private processedMessages = new Set<string>()
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

View File

@ -33,6 +33,15 @@
padding: $padding; padding: $padding;
} }
// 高z-index类用于确保组件在最上层显示
.gi_z_index_high {
z-index: 10001 !important;
}
.gi_z_index_highest {
z-index: 100001 !important;
}
.gi_relative { .gi_relative {
position: relative; position: relative;
} }

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @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') import('vue')
} }

View File

@ -7,66 +7,8 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Avatar: typeof import('./../components/Avatar/index.vue')['default'] NotificationCenter: typeof import('./../components/NotificationCenter/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']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] 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']
} }
} }

View File

@ -82,6 +82,7 @@ import { IconInfoCircle, IconEdit } from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message' import message from '@arco-design/web-vue/es/message'
import type { EquipmentApprovalResp, EquipmentApprovalReq } from '@/apis/equipment/type' import type { EquipmentApprovalResp, EquipmentApprovalReq } from '@/apis/equipment/type'
import { equipmentApprovalApi } from '@/apis/equipment/approval' import { equipmentApprovalApi } from '@/apis/equipment/approval'
import notificationService from '@/services/notificationService'
defineOptions({ name: 'ApprovalActionModal' }) defineOptions({ name: 'ApprovalActionModal' })
@ -173,9 +174,23 @@ const handleSubmit = async () => {
if (props.actionType === 'approve') { if (props.actionType === 'approve') {
await equipmentApprovalApi.approve(approvalId, submitData) await equipmentApprovalApi.approve(approvalId, submitData)
message.success('审批通过成功') message.success('审批通过成功')
//
notificationService.addApprovalNotification(
props.approvalData.equipmentName,
'APPROVED',
formData.approverName || '审批人'
)
} else { } else {
await equipmentApprovalApi.reject(approvalId, submitData) await equipmentApprovalApi.reject(approvalId, submitData)
message.success('审批拒绝成功') message.success('审批拒绝成功')
//
notificationService.addApprovalNotification(
props.approvalData.equipmentName,
'REJECTED',
formData.approverName || '审批人'
)
} }
emit('success') emit('success')

View File

@ -175,6 +175,7 @@
import { ref, reactive, watch } from 'vue' import { ref, reactive, watch } from 'vue'
import { Message } from '@arco-design/web-vue' import { Message } from '@arco-design/web-vue'
import { equipmentApprovalApi } from '@/apis/equipment/approval' import { equipmentApprovalApi } from '@/apis/equipment/approval'
import notificationService from '@/services/notificationService'
interface Props { interface Props {
visible: boolean visible: boolean
@ -296,6 +297,15 @@ const handleSubmit = async () => {
await equipmentApprovalApi.submitProcurementApplication(submitData) await equipmentApprovalApi.submitProcurementApplication(submitData)
//
const currentUser = JSON.parse(localStorage.getItem('userInfo') || '{}')
const applicantName = currentUser.nickname || currentUser.username || '未知用户'
notificationService.addProcurementNotification(
submitData.equipmentName,
applicantName
)
Message.success('采购申请提交成功') Message.success('采购申请提交成功')
emit('success') emit('success')
handleCancel() handleCancel()