优化客服消息通知提醒
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user