实现申请采购推送信息到审批台出现语音提示和闪烁提示

This commit is contained in:
Mr.j 2025-08-08 17:35:49 +08:00
parent 37d2d21318
commit a0912edc6a
3 changed files with 245 additions and 6 deletions

View File

@ -19,8 +19,13 @@
:content-style="{ marginTop: '-5px', padding: 0, border: 'none' }"
:arrow-style="{ width: 0, height: 0 }"
>
<a-badge :count="unreadMessageCount" dot>
<a-button size="mini" class="gi_hover_btn">
<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>
@ -97,10 +102,148 @@ onBeforeUnmount(() => {
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) => {
@ -116,10 +259,12 @@ const initWebSocket = (token: string) => {
}
try {
// WebSocket URL使
const wsUrl = import.meta.env.VITE_API_WS_URL || 'ws://localhost:8888'
console.log('正在连接WebSocket:', `${wsUrl}/websocket?token=${token}`)
const wsEndpoint = wsUrl.replace('8000', '8888') // 使8888
console.log('正在连接WebSocket:', `${wsEndpoint}/websocket?token=${token}`)
socket = new WebSocket(`${wsUrl}/websocket?token=${token}`)
socket = new WebSocket(`${wsEndpoint}/websocket?token=${token}`)
socket.onopen = () => {
console.log('WebSocket连接成功')
@ -133,6 +278,10 @@ const initWebSocket = (token: string) => {
//
if (data.type && data.title && data.content) {
console.log('处理通知消息:', data)
//
playNotificationSound()
//
Notification.info({
title: data.title,
@ -144,6 +293,9 @@ const initWebSocket = (token: string) => {
//
unreadMessageCount.value++
//
flashPageTitle()
} else {
//
const count = Number.parseInt(event.data)
@ -181,10 +333,17 @@ const initWebSocket = (token: string) => {
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)
@ -218,9 +377,32 @@ const logout = () => {
onMounted(() => {
nextTick(() => {
// getMessageCount()
// 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">
@ -242,4 +424,56 @@ onMounted(() => {
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>

View File

@ -70,6 +70,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -5,6 +5,11 @@ import createVitePlugins from './config/plugins'
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
// 设置默认的WebSocket URL
if (!env.VITE_API_WS_URL) {
env.VITE_API_WS_URL = 'ws://localhost:8888'
}
return {
// 开发或生产环境服务的公共基础路径
base: env.VITE_BASE,