Industrial-image-management.../src/layout/components/HeaderRightBar/index.vue

332 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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>
<!-- 消息通知中心 -->
<NotificationCenter ref="notificationCenterRef" />
<!-- 全屏切换组件 -->
<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">
import { Modal, Notification } from '@arco-design/web-vue'
import { useFullscreen } from '@vueuse/core'
import { onMounted, ref, nextTick, computed } from 'vue'
import NotificationCenter from '@/components/NotificationCenter/index.vue'
import SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue'
import notificationService from '@/services/notificationService'
import websocketService from '@/services/websocketService'
import { useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { useBreakpoint, useDevice } from '@/hooks'
defineOptions({ name: 'HeaderRight' })
const { isDesktop } = useDevice()
const { breakpoint } = useBreakpoint()
const notificationCenterRef = ref()
// 使用通知服务的未读消息数量
const unreadMessageCount = computed(() => notificationService.unreadCount.value)
// 语音提示功能
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)
}
// 设置通知监听器
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'
})
}
})
}
// 暴露测试函数到全局,方便在控制台测试
if (typeof window !== 'undefined') {
(window as any).testNotification = {
playSound: playNotificationSound,
flashTitle: flashPageTitle,
showNotification: () => {
notificationService.addNotification({
type: 'SYSTEM',
title: '测试通知',
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
priority: 'HIGH',
category: '测试',
source: 'TEST'
})
},
testAll: () => {
playNotificationSound()
flashPageTitle()
notificationService.addNotification({
type: 'SYSTEM',
title: '测试通知',
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
priority: 'HIGH',
category: '测试',
source: 'TEST'
})
},
// 添加调试函数
debugNotification: () => {
console.log('=== 通知服务调试信息 ===')
console.log('未读消息数量:', unreadMessageCount.value)
console.log('所有通知:', notificationService.getAllNotifications())
console.log('通知统计:', notificationService.getStats())
console.log('WebSocket状态:', websocketService.getStatus())
},
// 手动添加测试通知
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
}
notificationService.addNotification(testNotification)
console.log('已添加测试通知:', testNotification)
}
}
// 暴露通知服务到全局,方便调试
;(window as any).notificationService = notificationService
;(window as any).websocketService = websocketService
;(window as any).unreadMessageCount = unreadMessageCount
}
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(() => {
// 设置通知监听器
setupNotificationListeners()
// 确保WebSocket服务已连接
if (getToken() && !websocketService.connected.value) {
console.log('初始化WebSocket连接')
websocketService.connect()
}
})
})
</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;
}
}
// 通知徽章样式
.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);
}
}
</style>