优化客服消息通知提醒

This commit is contained in:
yml2213
2026-06-08 06:36:31 +08:00
parent 9d6b1d945c
commit 79ea3a476c
6 changed files with 385 additions and 168 deletions
@@ -6,6 +6,12 @@ export type NotificationPermission = 'default' | 'granted' | 'denied'
const permission = ref<NotificationPermission>('default')
const isSupported = 'Notification' in window
const isSpeechSupported = 'speechSynthesis' in window && 'SpeechSynthesisUtterance' in window
const voiceEnabled = {
user: ref(readVoiceEnabled('user')),
admin: ref(readVoiceEnabled('admin')),
}
let lastVoiceAt = 0
/**
* 桌面通知 composable
@@ -38,22 +44,28 @@ export function useDesktopNotification(scope: 'user' | 'admin') {
/**
* 检查是否应该发送通知
*/
function shouldNotify(conversationId: number, currentConversationId: number | null): boolean {
// 不支持通知
if (!isSupported || permission.value !== 'granted') return false
function shouldAlert(conversationId: number, currentConversationId: number | null): boolean {
// 页面在前台且正在查看该会话 - 不需要通知
if (!document.hidden && currentConversationId === conversationId) return false
return true
}
/**
* 检查是否应该发送桌面通知
*/
function shouldNotify(conversationId: number, currentConversationId: number | null): boolean {
// 不支持通知
if (!isSupported || permission.value !== 'granted') return false
return shouldAlert(conversationId, currentConversationId)
}
/**
* 发送桌面通知
*/
function notify(event: ChatEvent, currentConversationId: number | null) {
if (!event.message) return
if (!shouldNotify(event.conversation_id, currentConversationId)) return
const { sender_name, sender_role, content, attachment_urls } = event.message
@@ -78,33 +90,40 @@ export function useDesktopNotification(scope: 'user' | 'admin') {
const roleLabel = roleMap[sender_role] || '成员'
const title = `${roleLabel} · ${sender_name}`
try {
const notification = new Notification(title, {
body: body.length > 100 ? body.substring(0, 100) + '...' : body,
icon: '/favicon.ico',
badge: '/favicon.ico',
tag: `chat-${event.conversation_id}`, // 相同会话的通知会被替换
requireInteraction: false,
silent: false,
})
if (!shouldAlert(event.conversation_id, currentConversationId)) return
// 点击通知跳转到对应会话
notification.onclick = () => {
window.focus()
const path = scope === 'admin'
? `/admin/chats/${event.conversation_id}`
: `/messages/${event.conversation_id}`
router.push(path)
notification.close()
if (shouldNotify(event.conversation_id, currentConversationId)) {
try {
const notification = new Notification(title, {
body: body.length > 100 ? body.substring(0, 100) + '...' : body,
icon: '/favicon.ico',
badge: '/favicon.ico',
tag: `chat-${event.conversation_id}`, // 相同会话的通知会被替换
requireInteraction: false,
silent: false,
})
// 点击通知跳转到对应会话
notification.onclick = () => {
window.focus()
const path =
scope === 'admin'
? `/admin/chats/${event.conversation_id}`
: `/messages/${event.conversation_id}`
router.push(path)
notification.close()
}
// 5秒后自动关闭
setTimeout(() => {
notification.close()
}, 5000)
} catch (error) {
console.error('Failed to show notification:', error)
}
// 5秒后自动关闭
setTimeout(() => {
notification.close()
}, 5000)
} catch (error) {
console.error('Failed to show notification:', error)
}
speakMessage(roleLabel, sender_name, body)
}
/**
@@ -124,11 +143,68 @@ export function useDesktopNotification(scope: 'user' | 'admin') {
document.addEventListener('keydown', handleInteraction, { once: true })
}
function setVoiceEnabled(enabled: boolean) {
if (!isSpeechSupported) return
voiceEnabled[scope].value = enabled
localStorage.setItem(voiceStorageKey(scope), enabled ? '1' : '0')
if (enabled) speakText('语音提醒已开启')
}
function speakTest() {
speakText('这是一条语音提醒测试')
}
function speakMessage(roleLabel: string, senderName: string, body: string) {
if (!isSpeechSupported || !voiceEnabled[scope].value) return
const now = Date.now()
if (now - lastVoiceAt < 1200) return
lastVoiceAt = now
const message = sanitizeSpeechText(body)
const prefix = scope === 'admin' ? '后台收到新消息' : '收到新消息'
speakText(`${prefix}${roleLabel}${senderName}${message ? `说,${message}` : ''}`)
}
return {
permission,
isSupported,
isSpeechSupported,
voiceEnabled: voiceEnabled[scope],
requestPermission,
notify,
requestPermissionSilently,
setVoiceEnabled,
speakTest,
}
}
function voiceStorageKey(scope: 'user' | 'admin') {
return `hfb.${scope}.notification.voice`
}
function readVoiceEnabled(scope: 'user' | 'admin') {
if (typeof localStorage === 'undefined') return false
return localStorage.getItem(voiceStorageKey(scope)) === '1'
}
function sanitizeSpeechText(value: string) {
return value
.replace(/https?:\/\/\\S+/g, '链接')
.replace(/\\s+/g, ' ')
.trim()
.slice(0, 60)
}
function speakText(text: string) {
if (!isSpeechSupported || !text) return
try {
window.speechSynthesis.cancel()
const utterance = new SpeechSynthesisUtterance(text)
utterance.lang = 'zh-CN'
utterance.rate = 1
utterance.pitch = 1
utterance.volume = 1
window.speechSynthesis.speak(utterance)
} catch (error) {
console.error('Failed to speak notification:', error)
}
}