2025-07-30 09:13:52 +08:00
|
|
|
|
<template>
|
|
|
|
|
<a-row justify="end" align="center">
|
|
|
|
|
<a-space size="medium">
|
|
|
|
|
<!-- 搜索 -->
|
|
|
|
|
<Search v-if="isDesktop" />
|
|
|
|
|
<!-- 项目配置 -->
|
|
|
|
|
<a-tooltip content="项目配置" position="bl">
|
|
|
|
|
<a-button size="mini" class="gi_hover_btn" @click="SettingDrawerRef?.open">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<icon-settings :size="18" />
|
|
|
|
|
</template>
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
2025-08-11 10:54:45 +08:00
|
|
|
|
<!-- 消息通知中心 -->
|
|
|
|
|
<NotificationCenter ref="notificationCenterRef" />
|
2025-07-30 09:13:52 +08:00
|
|
|
|
|
|
|
|
|
<!-- 全屏切换组件 -->
|
|
|
|
|
<a-tooltip v-if="!['xs', 'sm'].includes(breakpoint)" content="全屏切换" position="bottom">
|
|
|
|
|
<a-button size="mini" class="gi_hover_btn" @click="toggle">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<icon-fullscreen v-if="!isFullscreen" :size="18" />
|
|
|
|
|
<icon-fullscreen-exit v-else :size="18" />
|
|
|
|
|
</template>
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
|
|
|
|
<!-- 暗黑模式切换 -->
|
|
|
|
|
<a-tooltip content="主题切换" position="bottom">
|
|
|
|
|
<GiThemeBtn></GiThemeBtn>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
|
|
|
|
<!-- 管理员账户 -->
|
|
|
|
|
<a-dropdown trigger="hover">
|
|
|
|
|
<a-row align="center" :wrap="false" class="user">
|
|
|
|
|
<!-- 管理员头像 -->
|
|
|
|
|
<Avatar :src="userStore.avatar" :name="userStore.nickname" :size="32" />
|
|
|
|
|
<span class="username">{{ userStore.nickname }}</span>
|
|
|
|
|
<icon-down />
|
|
|
|
|
</a-row>
|
|
|
|
|
<template #content>
|
|
|
|
|
<a-doption @click="router.push('/user/profile')">
|
|
|
|
|
<span>个人中心</span>
|
|
|
|
|
</a-doption>
|
|
|
|
|
<a-doption @click="router.push('/user/message')">
|
|
|
|
|
<span>消息中心</span>
|
|
|
|
|
</a-doption>
|
|
|
|
|
<a-divider :margin="0" />
|
|
|
|
|
<a-doption @click="logout">
|
|
|
|
|
<span>退出登录</span>
|
|
|
|
|
</a-doption>
|
|
|
|
|
</template>
|
|
|
|
|
</a-dropdown>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<SettingDrawer ref="SettingDrawerRef"></SettingDrawer>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-08-08 16:37:36 +08:00
|
|
|
|
import { Modal, Notification } from '@arco-design/web-vue'
|
2025-07-30 09:13:52 +08:00
|
|
|
|
import { useFullscreen } from '@vueuse/core'
|
2025-08-11 14:49:48 +08:00
|
|
|
|
import { onMounted, ref, nextTick, computed } from 'vue'
|
2025-08-11 10:54:45 +08:00
|
|
|
|
import NotificationCenter from '@/components/NotificationCenter/index.vue'
|
2025-07-30 09:13:52 +08:00
|
|
|
|
import SettingDrawer from './SettingDrawer.vue'
|
|
|
|
|
import Search from './Search.vue'
|
2025-08-11 14:49:48 +08:00
|
|
|
|
import notificationService from '@/services/notificationService'
|
|
|
|
|
import websocketService from '@/services/websocketService'
|
2025-08-06 14:53:27 +08:00
|
|
|
|
|
2025-07-30 09:13:52 +08:00
|
|
|
|
import { useUserStore } from '@/stores'
|
|
|
|
|
import { getToken } from '@/utils/auth'
|
|
|
|
|
import { useBreakpoint, useDevice } from '@/hooks'
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'HeaderRight' })
|
|
|
|
|
|
|
|
|
|
const { isDesktop } = useDevice()
|
|
|
|
|
const { breakpoint } = useBreakpoint()
|
2025-08-11 10:54:45 +08:00
|
|
|
|
const notificationCenterRef = ref()
|
2025-07-30 09:13:52 +08:00
|
|
|
|
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 使用通知服务的未读消息数量
|
|
|
|
|
const unreadMessageCount = computed(() => notificationService.unreadCount.value)
|
2025-07-30 09:13:52 +08:00
|
|
|
|
|
2025-08-08 17:35:49 +08:00
|
|
|
|
// 语音提示功能
|
|
|
|
|
const 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) // 800Hz
|
|
|
|
|
oscillator.type = 'sine'
|
|
|
|
|
|
|
|
|
|
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime)
|
|
|
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5)
|
|
|
|
|
|
|
|
|
|
// 播放音频
|
|
|
|
|
oscillator.start(audioContext.currentTime)
|
|
|
|
|
oscillator.stop(audioContext.currentTime + 0.5)
|
|
|
|
|
|
|
|
|
|
console.log('播放语音提示')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('播放语音提示失败:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 页面标题闪烁功能
|
|
|
|
|
let titleFlashInterval: NodeJS.Timeout | null = null
|
|
|
|
|
const flashPageTitle = () => {
|
|
|
|
|
const originalTitle = document.title
|
|
|
|
|
let flashCount = 0
|
|
|
|
|
const maxFlashes = 6 // 闪烁3次(开-关-开-关-开-关)
|
|
|
|
|
|
|
|
|
|
// 清除之前的闪烁
|
|
|
|
|
if (titleFlashInterval) {
|
|
|
|
|
clearInterval(titleFlashInterval)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
titleFlashInterval = setInterval(() => {
|
|
|
|
|
if (flashCount >= maxFlashes) {
|
|
|
|
|
document.title = originalTitle
|
|
|
|
|
if (titleFlashInterval) {
|
|
|
|
|
clearInterval(titleFlashInterval)
|
|
|
|
|
titleFlashInterval = null
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.title = flashCount % 2 === 0 ? '🔔 新的采购申请' : originalTitle
|
|
|
|
|
flashCount++
|
|
|
|
|
}, 500)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 设置通知监听器
|
|
|
|
|
const setupNotificationListeners = () => {
|
|
|
|
|
// 监听新通知添加事件
|
|
|
|
|
notificationService.on('add', (notification) => {
|
|
|
|
|
console.log('收到新通知:', notification)
|
|
|
|
|
|
|
|
|
|
// 播放语音提示
|
|
|
|
|
playNotificationSound()
|
|
|
|
|
|
|
|
|
|
// 触发页面标题闪烁
|
|
|
|
|
flashPageTitle()
|
|
|
|
|
|
|
|
|
|
// 显示桌面通知
|
|
|
|
|
if (notification.priority === 'HIGH' || notification.priority === 'URGENT') {
|
|
|
|
|
Notification.info({
|
|
|
|
|
title: notification.title,
|
|
|
|
|
content: notification.content,
|
|
|
|
|
duration: 5000,
|
|
|
|
|
closable: true,
|
|
|
|
|
position: 'topRight'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 17:35:49 +08:00
|
|
|
|
// 暴露测试函数到全局,方便在控制台测试
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
(window as any).testNotification = {
|
|
|
|
|
playSound: playNotificationSound,
|
|
|
|
|
flashTitle: flashPageTitle,
|
|
|
|
|
showNotification: () => {
|
2025-08-11 14:49:48 +08:00
|
|
|
|
notificationService.addNotification({
|
|
|
|
|
type: 'SYSTEM',
|
2025-08-08 17:35:49 +08:00
|
|
|
|
title: '测试通知',
|
|
|
|
|
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
|
2025-08-11 14:49:48 +08:00
|
|
|
|
priority: 'HIGH',
|
|
|
|
|
category: '测试',
|
|
|
|
|
source: 'TEST'
|
2025-08-08 17:35:49 +08:00
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
testAll: () => {
|
|
|
|
|
playNotificationSound()
|
|
|
|
|
flashPageTitle()
|
2025-08-11 14:49:48 +08:00
|
|
|
|
notificationService.addNotification({
|
|
|
|
|
type: 'SYSTEM',
|
2025-08-08 17:35:49 +08:00
|
|
|
|
title: '测试通知',
|
|
|
|
|
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
|
2025-08-11 14:49:48 +08:00
|
|
|
|
priority: 'HIGH',
|
|
|
|
|
category: '测试',
|
|
|
|
|
source: 'TEST'
|
2025-08-08 17:35:49 +08:00
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
// 添加调试函数
|
2025-08-11 14:49:48 +08:00
|
|
|
|
debugNotification: () => {
|
|
|
|
|
console.log('=== 通知服务调试信息 ===')
|
|
|
|
|
console.log('未读消息数量:', unreadMessageCount.value)
|
|
|
|
|
console.log('所有通知:', notificationService.getAllNotifications())
|
|
|
|
|
console.log('通知统计:', notificationService.getStats())
|
|
|
|
|
console.log('WebSocket状态:', websocketService.getStatus())
|
2025-08-08 17:35:49 +08:00
|
|
|
|
},
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 手动添加测试通知
|
|
|
|
|
addTestNotification: (type = 'PROCUREMENT') => {
|
|
|
|
|
const testNotification = {
|
|
|
|
|
type: type as any,
|
|
|
|
|
title: `测试${type}通知`,
|
|
|
|
|
content: `这是一个测试${type}通知,时间:${new Date().toLocaleString()}`,
|
|
|
|
|
priority: 'HIGH' as any,
|
|
|
|
|
category: '测试',
|
|
|
|
|
source: 'TEST',
|
|
|
|
|
actionRequired: true
|
2025-08-08 17:35:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 14:49:48 +08:00
|
|
|
|
notificationService.addNotification(testNotification)
|
|
|
|
|
console.log('已添加测试通知:', testNotification)
|
2025-08-08 17:35:49 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 暴露通知服务到全局,方便调试
|
|
|
|
|
;(window as any).notificationService = notificationService
|
|
|
|
|
;(window as any).websocketService = websocketService
|
2025-08-08 17:35:49 +08:00
|
|
|
|
;(window as any).unreadMessageCount = unreadMessageCount
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 09:13:52 +08:00
|
|
|
|
const { isFullscreen, toggle } = useFullscreen()
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
const SettingDrawerRef = ref<InstanceType<typeof SettingDrawer>>()
|
|
|
|
|
|
|
|
|
|
// 退出登录
|
|
|
|
|
const logout = () => {
|
|
|
|
|
Modal.warning({
|
|
|
|
|
title: '提示',
|
|
|
|
|
content: '确认退出登录?',
|
|
|
|
|
hideCancel: false,
|
|
|
|
|
closable: true,
|
|
|
|
|
onBeforeOk: async () => {
|
|
|
|
|
try {
|
|
|
|
|
await userStore.logout()
|
|
|
|
|
await router.replace('/login')
|
|
|
|
|
return true
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
nextTick(() => {
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 设置通知监听器
|
|
|
|
|
setupNotificationListeners()
|
2025-08-08 17:35:49 +08:00
|
|
|
|
|
2025-08-11 14:49:48 +08:00
|
|
|
|
// 确保WebSocket服务已连接
|
|
|
|
|
if (getToken() && !websocketService.connected.value) {
|
|
|
|
|
console.log('初始化WebSocket连接')
|
|
|
|
|
websocketService.connect()
|
|
|
|
|
}
|
2025-07-30 09:13:52 +08:00
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.arco-dropdown-open .arco-icon-down {
|
|
|
|
|
transform: rotate(180deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.user {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: var(--color-text-1);
|
|
|
|
|
|
|
|
|
|
.username {
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arco-icon-down {
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
margin-left: 2px;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-08 17:35:49 +08:00
|
|
|
|
|
|
|
|
|
// 通知徽章样式
|
|
|
|
|
.notification-badge {
|
|
|
|
|
.arco-badge-dot {
|
|
|
|
|
background-color: #f53f3f;
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(245, 63, 63, 0.2);
|
|
|
|
|
animation: pulse 2s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.arco-badge-count {
|
|
|
|
|
background-color: #f53f3f;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
animation: bounce 0.6s ease-in-out;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.notification-btn {
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 脉冲动画
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
0% {
|
|
|
|
|
box-shadow: 0 0 0 0 rgba(245, 63, 63, 0.7);
|
|
|
|
|
}
|
|
|
|
|
70% {
|
|
|
|
|
box-shadow: 0 0 0 10px rgba(245, 63, 63, 0);
|
|
|
|
|
}
|
|
|
|
|
100% {
|
|
|
|
|
box-shadow: 0 0 0 0 rgba(245, 63, 63, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 弹跳动画
|
|
|
|
|
@keyframes bounce {
|
|
|
|
|
0%, 20%, 53%, 80%, 100% {
|
|
|
|
|
transform: translate3d(0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
40%, 43% {
|
|
|
|
|
transform: translate3d(0, -8px, 0);
|
|
|
|
|
}
|
|
|
|
|
70% {
|
|
|
|
|
transform: translate3d(0, -4px, 0);
|
|
|
|
|
}
|
|
|
|
|
90% {
|
|
|
|
|
transform: translate3d(0, -2px, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-30 09:13:52 +08:00
|
|
|
|
</style>
|