From 3d04175ef3c287de61b42c81f128ce6d4600292d Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 29 Oct 2026 21:53:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=A2=9E=E5=BC=BA=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=8F=90=E9=86=92=E5=A3=B0=E9=9F=B3=E4=B8=8E=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E6=9C=97=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/NotificationPreferences.tsx | 287 +++++++++++ web/src/components/layout/AgentSidebar.tsx | 53 +- web/src/lib/notificationAudio.ts | 230 +++++++++ web/src/lib/notificationSettings.ts | 137 +++++ .../pages/agent/settings/NotifySection.tsx | 13 +- web/src/pages/agent/settings/tabs.tsx | 2 +- web/src/stores/notifications.tsx | 479 ++++++++++++++++-- 7 files changed, 1140 insertions(+), 61 deletions(-) create mode 100644 web/src/components/common/NotificationPreferences.tsx create mode 100644 web/src/lib/notificationAudio.ts create mode 100644 web/src/lib/notificationSettings.ts diff --git a/web/src/components/common/NotificationPreferences.tsx b/web/src/components/common/NotificationPreferences.tsx new file mode 100644 index 0000000..7dfb6e6 --- /dev/null +++ b/web/src/components/common/NotificationPreferences.tsx @@ -0,0 +1,287 @@ +import { useState } from 'react' +import { + AudioOutlined, + DeleteOutlined, + PlayCircleOutlined, + SoundOutlined, + UploadOutlined, +} from '@ant-design/icons' +import { Button, Popconfirm, Select, Slider, Switch, Tag, Upload, message as antMsg } from 'antd' +import { useAgentNotifications } from '@/stores/notifications' +import { + MAX_CUSTOM_SOUND_BYTES, + MAX_CUSTOM_SOUND_SECONDS, + SOUND_PRESETS, + SPEAK_MODES, + type SoundPresetId, +} from '@/lib/notificationSettings' + +interface NotificationPreferencesProps { + className?: string +} + +function formatDuration(seconds: number): string { + if (!Number.isFinite(seconds) || seconds <= 0) return '' + return `${seconds.toFixed(1)}s` +} + +export default function NotificationPreferences({ className = '' }: NotificationPreferencesProps) { + const { + soundEnabled, + setSoundEnabled, + soundSource, + preset, + setPreset, + setSoundSource, + customSound, + volume, + setVolume, + previewSound, + uploadCustomSound, + removeCustomSound, + speakEnabled, + setSpeakEnabled, + speakMode, + setSpeakMode, + speakRate, + setSpeakRate, + voiceURI, + setVoiceURI, + voices, + ttsSupported, + previewSpeech, + } = useAgentNotifications() + const [uploading, setUploading] = useState(false) + const [removing, setRemoving] = useState(false) + const [previewing, setPreviewing] = useState(false) + + const soundOptions = [ + ...SOUND_PRESETS.map(item => ({ value: item.value, label: item.label })), + ...(customSound ? [{ value: 'custom', label: `自定义:${customSound.name}` }] : []), + ] + const selectedSound = soundSource === 'custom' && customSound ? 'custom' : preset + const voiceOptions = [ + { value: '', label: '系统默认' }, + ...voices.map(item => ({ + value: item.voiceURI, + label: `${item.name}${item.lang ? ` · ${item.lang}` : ''}`, + })), + ] + + const handleSoundChange = (value: string) => { + if (value === 'custom') { + setSoundSource('custom') + return + } + setPreset(value as SoundPresetId) + } + + const handlePreview = async () => { + setPreviewing(true) + try { + await previewSound() + } catch { + antMsg.error('试听失败,请检查浏览器音频权限') + } finally { + window.setTimeout(() => setPreviewing(false), 500) + } + } + + const handleUpload = async (file: File) => { + setUploading(true) + try { + await uploadCustomSound(file) + antMsg.success('自定义提示音已保存') + await previewSound() + } catch (e) { + antMsg.error(e instanceof Error ? e.message : '上传音频失败') + } finally { + setUploading(false) + } + } + + const handleRemove = async () => { + setRemoving(true) + try { + await removeCustomSound() + antMsg.success('已删除自定义提示音') + } catch (e) { + antMsg.error(e instanceof Error ? e.message : '删除失败') + } finally { + setRemoving(false) + } + } + + const handleSpeechPreview = () => { + if (!ttsSupported) { + antMsg.warning('当前浏览器不支持语音朗读') + return + } + if (!previewSpeech()) antMsg.error('语音试听失败,请检查系统语音设置') + } + + return ( +
+
+
+
+ + 声音与朗读 +
+
仅保存在当前浏览器,换设备后需要重新设置
+
+ 本机设置 +
+ +
+
+
+
声音提醒
+
收到访客消息时播放提示音
+
+ +
+ + {soundEnabled && ( +
+
+
提示音
+ ({ value: item.value, label: item.label }))} + onChange={setSpeakMode} + /> +
+
+ {SPEAK_MODES.find(item => item.value === speakMode)?.desc} +
+
+
语速
+ + + {speakRate.toFixed(2)}x + +
+
+
音色
+