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

460 lines
12 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, onBeforeUnmount } from 'vue'
import NotificationCenter from '@/components/NotificationCenter/index.vue'
import SettingDrawer from './SettingDrawer.vue'
import Search from './Search.vue'
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()
let socket: WebSocket | null = null
// 清理函数
onBeforeUnmount(() => {
if (socket) {
socket.close()
socket = null
}
// 清理标题闪烁
if (titleFlashInterval) {
clearInterval(titleFlashInterval)
titleFlashInterval = null
}
})
const unreadMessageCount = ref(0)
// 语音提示功能
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)
}
// 暴露测试函数到全局,方便在控制台测试
if (typeof window !== 'undefined') {
(window as any).testNotification = {
playSound: playNotificationSound,
flashTitle: flashPageTitle,
showNotification: () => {
Notification.info({
title: '测试通知',
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
duration: 5000,
closable: true,
position: 'topRight'
})
unreadMessageCount.value++
},
testAll: () => {
playNotificationSound()
flashPageTitle()
Notification.info({
title: '测试通知',
content: '这是一个测试通知,用于验证通知功能是否正常工作。',
duration: 5000,
closable: true,
position: 'topRight'
})
unreadMessageCount.value++
},
// 添加调试函数
debugWebSocket: () => {
console.log('=== WebSocket 调试信息 ===')
console.log('Socket对象:', socket)
console.log('Socket状态:', socket ? socket.readyState : '未连接')
console.log('Token:', getToken())
console.log('环境变量:', import.meta.env.VITE_API_WS_URL)
console.log('未读消息计数:', unreadMessageCount.value)
console.log('用户Token:', userStore.token)
},
// 手动触发WebSocket消息处理
simulateWebSocketMessage: () => {
const mockMessage = {
type: "PROCUREMENT_APPLICATION",
title: "新的采购申请",
content: "收到来自 测试用户 的设备采购申请:测试设备"
}
const event = new MessageEvent('message', {
data: JSON.stringify(mockMessage)
})
if (socket && socket.onmessage) {
console.log('模拟WebSocket消息:', mockMessage)
socket.onmessage(event)
} else {
console.error('WebSocket连接不存在或onmessage未设置')
}
},
// 强制重新连接WebSocket
reconnectWebSocket: () => {
console.log('强制重新连接WebSocket')
const token = getToken()
if (token) {
if (socket) {
socket.close()
socket = null
}
initWebSocket(token)
} else {
console.error('Token不存在无法重新连接')
}
}
}
// 暴露socket对象到全局方便调试
;(window as any).socket = socket
;(window as any).unreadMessageCount = unreadMessageCount
}
// 初始化 WebSocket - 使用防抖避免重复连接
let initTimer: NodeJS.Timeout | null = null
const initWebSocket = (token: string) => {
if (initTimer) {
clearTimeout(initTimer)
}
initTimer = setTimeout(() => {
// 如果已有连接,先关闭
if (socket) {
socket.close()
socket = null
}
try {
// 修复WebSocket URL确保使用正确的端口
const wsUrl = import.meta.env.VITE_API_WS_URL || 'ws://localhost:8888'
const wsEndpoint = wsUrl.replace('8000', '8888') // 确保使用8888端口
console.log('正在连接WebSocket:', `${wsEndpoint}/websocket?token=${token}`)
socket = new WebSocket(`${wsEndpoint}/websocket?token=${token}`)
socket.onopen = () => {
console.log('WebSocket连接成功')
}
socket.onmessage = (event) => {
console.log('收到WebSocket消息:', event.data)
try {
const data = JSON.parse(event.data)
// 处理通知消息
if (data.type && data.title && data.content) {
console.log('处理通知消息:', data)
// 播放语音提示
playNotificationSound()
// 显示通知
Notification.info({
title: data.title,
content: data.content,
duration: 5000,
closable: true,
position: 'topRight'
})
// 增加未读消息计数
unreadMessageCount.value++
// 触发页面标题闪烁
flashPageTitle()
} else {
// 处理简单的数字消息(兼容旧版本)
const count = Number.parseInt(event.data)
if (!isNaN(count)) {
unreadMessageCount.value = count
}
}
} catch (error) {
console.error('解析WebSocket消息失败:', error)
// 尝试解析为数字(兼容旧版本)
const count = Number.parseInt(event.data)
if (!isNaN(count)) {
unreadMessageCount.value = count
}
}
}
socket.onerror = (error) => {
console.error('WebSocket连接错误:', error)
}
socket.onclose = (event) => {
console.log('WebSocket连接关闭:', event.code, event.reason)
socket = null
}
} catch (error) {
console.error('创建WebSocket连接失败:', error)
}
initTimer = null
}, 100) // 100ms防抖
}
// 查询未读消息数量
const getMessageCount = async () => {
try {
const token = getToken()
console.log('获取到token:', token ? '存在' : '不存在')
if (token && !socket) {
console.log('准备初始化WebSocket连接')
nextTick(() => {
initWebSocket(token)
})
} else if (!token) {
console.warn('Token不存在无法建立WebSocket连接')
} else if (socket) {
console.log('WebSocket连接已存在')
}
} catch (error) {
console.error('Failed to get message count:', error)
}
}
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(() => {
// 立即尝试初始化WebSocket
getMessageCount()
// 如果第一次失败1秒后重试
setTimeout(() => {
if (!socket) {
console.log('首次连接失败重试WebSocket连接')
getMessageCount()
}
}, 1000)
})
})
// 监听用户登录状态变化
watch(() => userStore.token, (newToken, oldToken) => {
console.log('Token变化:', { oldToken: oldToken ? '存在' : '不存在', newToken: newToken ? '存在' : '不存在' })
if (newToken && !socket) {
console.log('用户登录初始化WebSocket连接')
getMessageCount()
} else if (!newToken && socket) {
console.log('用户登出关闭WebSocket连接')
socket.close()
socket = null
}
}, { immediate: true })
</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>