feat: add notification sound settings component and sound management utilities

- Implemented NotificationSoundSettings.vue for managing notification sounds.
- Added functionality for selecting and previewing single and urgent notification sounds.
- Integrated autoplay warning for first-time audio playback.
- Created soundList.js to manage available sound files and playback functions.
- Included methods for resetting sounds to default settings.

Co-authored-by: tempChanghong <tuantule622@gmail.com>
This commit is contained in:
Sunwuyuan
2026-01-11 15:30:28 +08:00
co-authored by tempChanghong
parent d83dd3993d
commit 4d634095b9
45 changed files with 993 additions and 277 deletions
+254 -66
View File
@@ -1,32 +1,92 @@
<template>
<!-- 学生姓名选择对话框 -->
<!-- 统一姓名设置对话框(学生 / 教师) -->
<v-dialog
v-model="showDialog"
max-width="500"
max-width="720"
persistent
>
<v-card>
<v-card-title>设置学生姓名</v-card-title>
<v-card-title>{{ dialogTitle }}</v-card-title>
<v-card-text>
<div class="mb-2">
请从列表中选择您的姓名:
</div>
<v-autocomplete
v-model="selectedName"
:items="studentList"
clearable
hide-details
item-title="name"
item-value="name"
label="学生姓名"
placeholder="选择您的姓名"
/>
<div
v-if="studentList.length > 0"
class="mt-2 text-caption text-medium-emphasis"
>
共 {{ studentList.length }} 位学生
</div>
<!-- 学生模式 -->
<template v-if="isStudentToken">
<div
class="mb-2"
>
请从列表中选择您的姓名:
</div>
<v-autocomplete
v-model="selectedName"
:items="studentList"
clearable
hide-details
item-title="name"
item-value="name"
label="学生姓名"
placeholder="选择您的姓名"
/>
<div
v-if="studentList.length > 0"
class="mt-2 text-caption text-medium-emphasis"
>
共 {{ studentList.length }} 位学生
</div>
</template>
<!-- 教师模式 -->
<template v-else-if="isTeacherToken">
<!-- 名称输入框 -->
<v-text-field
v-model="teacherForm.name"
label="教师姓名"
placeholder="输入姓名或从下方建议中选择"
clearable
/>
<!-- 建议列表 -->
<div
class="mt-2 mb-4"
>
<div class="d-flex flex-wrap gap-2">
<v-chip
v-for="teacher in filteredTeacherSuggestions"
:key="teacher.name"
size="small"
@click="selectTeacherFromSuggestion(teacher)"
>
{{ teacher.name }}
<span
v-if="teacher.isHeadTeacher"
class="ms-1 text-error"
>
👨‍🏫
</span>
</v-chip>
</div>
</div>
<!-- 班主任开关 -->
<v-switch
v-model="teacherForm.isHeadTeacher"
inset
color="primary"
:label="teacherForm.isHeadTeacher ? '班主任' : '非班主任'"
/>
<!-- 任教科目 -->
<v-combobox
v-model="teacherForm.subjects"
multiple
chips
clearable
label="任教科目"
hint="可直接输入并回车添加多个科目"
persistent-hint
/>
</template>
<v-alert
v-if="error"
class="mt-3"
@@ -43,8 +103,9 @@
>
稍后设置
</v-btn>
<v-spacer/>
<v-spacer />
<v-btn
v-if="isStudentToken"
:disabled="!selectedName || saving"
:loading="saving"
color="primary"
@@ -52,6 +113,15 @@
>
确认
</v-btn>
<v-btn
v-else-if="isTeacherToken"
:disabled="!teacherForm.name || saving"
:loading="saving"
color="primary"
@click="saveTeacherName"
>
确认
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -61,6 +131,7 @@
:is-student="isStudentToken"
:open-dialog="openDialog"
:student-name="currentStudentName"
:teacher-name="currentTeacherName"
name="header-display"
/>
</template>
@@ -81,15 +152,25 @@ const saving = ref(false)
const error = ref('')
const tokenInfo = ref(null)
// 教师相关状态
const teacherList = ref([])
const selectedTeacherName = ref('')
const currentTeacherName = ref('')
const teacherForm = ref({ name: '', isHeadTeacher: false, subjects: [] })
const isStudentToken = computed(() => tokenInfo.value?.deviceType === 'student')
const isTeacherToken = computed(() => tokenInfo.value?.deviceType === 'teacher')
const isReadOnly = computed(() => tokenInfo.value?.isReadOnly === true)
const displayName = computed(() => tokenInfo.value?.note || '设置名称')
const hasToken = computed(() => !!kvToken.value)
const kvToken = computed(() => getSetting('server.kvToken'))
const provider = computed(() => getSetting('server.provider'))
const isKvProvider = computed(() => provider.value === 'kv-server' || provider.value === 'classworkscloud')
const dialogTitle = computed(() => (isStudentToken.value ? '设置学生姓名' : isTeacherToken.value ? '设置教师姓名' : '设置姓名'))
// 教师建议列表(显示所有教师)
const filteredTeacherSuggestions = computed(() => teacherList.value)
// 检查是否需要设置学生姓名
// 检查是否需要设置姓名(学生/教师)
const checkStudentNameStatus = async () => {
if (!isKvProvider.value || !kvToken.value) {
return
@@ -108,37 +189,57 @@ const checkStudentNameStatus = async () => {
tokenInfo.value = tokenResponse.data
// 如果不是学生类型,不需要处理
if (tokenInfo.value.deviceType !== 'student') {
return
}
// 学生设备处理
if (tokenInfo.value.deviceType === 'student') {
currentStudentName.value = tokenInfo.value.note || ''
// 保存当前学生姓名
currentStudentName.value = tokenInfo.value.note || ''
const listResponse = await axios.get(`${serverUrl}/kv/classworks-list-main`, {
headers: { Authorization: `Bearer ${kvToken.value}` }
})
const list = listResponse.data.value || []
studentList.value = Array.isArray(list) ? list : []
// 获取学生列表
const listResponse = await axios.get(`${serverUrl}/kv/classworks-list-main`, {
headers: {
Authorization: `Bearer ${kvToken.value}`
if (studentList.value.length > 0) {
const currentNote = tokenInfo.value.note || ''
const nameExists = studentList.value.some(s => s.name === currentNote)
if (!currentNote || !nameExists) {
showDialog.value = true
selectedName.value = ''
}
}
})
const list = listResponse.data.value || []
studentList.value = Array.isArray(list) ? list : []
// 如果学生列表为空,不需要提示
if (studentList.value.length === 0) {
return
}
// 检查当前姓名是否在学生列表中
const currentNote = tokenInfo.value.note || ''
const nameExists = studentList.value.some(student => student.name === currentNote)
// 教师设备处理
if (tokenInfo.value.deviceType === 'teacher') {
currentTeacherName.value = tokenInfo.value.note || ''
// 如果姓名为空或不在列表中,显示对话框
if (!currentNote || !nameExists) {
showDialog.value = true
selectedName.value = ''
try {
const listResponse = await axios.get(`${serverUrl}/kv/classworks-list-teacher`, {
headers: { Authorization: `Bearer ${kvToken.value}` }
})
const list = listResponse.data.value || []
teacherList.value = Array.isArray(list) ? list : []
} catch (err) {
// 如果列表不存在(404),初始化为空数组
if (err?.response?.status === 404) {
console.log('教师列表不存在,初始化为空')
teacherList.value = []
} else {
console.error('加载教师列表失败:', err)
teacherList.value = []
}
}
if (teacherList.value.length > 0) {
const currentNote = tokenInfo.value.note || ''
const nameExists = teacherList.value.some(t => t.name === currentNote)
if (!currentNote || !nameExists) {
showDialog.value = true
selectedTeacherName.value = ''
}
}
return
}
} catch (err) {
@@ -187,6 +288,74 @@ const saveStudentName = async () => {
}
}
// 保存教师姓名
const saveTeacherName = async () => {
if (!teacherForm.value.name || saving.value) return
error.value = ''
saving.value = true
try {
const serverUrl = getSetting('server.domain')
const token = kvToken.value
// 构建教师数据
const entry = {
name: teacherForm.value.name.trim(),
isHeadTeacher: !!teacherForm.value.isHeadTeacher,
subjects: Array.isArray(teacherForm.value.subjects)
? teacherForm.value.subjects.filter(s => s && String(s).trim()).map(s => String(s).trim())
: []
}
// 先更新本地列表
const idx = teacherList.value.findIndex(t => t.name === entry.name)
if (idx >= 0) {
teacherList.value[idx] = entry
} else {
teacherList.value.push(entry)
}
// 保存列表到 KV
const res = await dataProvider.saveData('classworks-list-teacher', teacherList.value)
if (res?.success === false) {
throw new Error(res?.error?.message || '保存列表失败')
}
// 设置教师名称
const response = await axios.post(
`${serverUrl}/apps/tokens/${token}/set-teacher-name`,
{ name: entry.name }
)
if (response.data.success) {
currentTeacherName.value = entry.name
showDialog.value = false
await checkStudentNameStatus()
emit('token-info-updated')
}
} catch (err) {
const status = err?.response?.status
if (status === 400) {
error.value = '该名称不在教师列表中,请选择正确的姓名'
} else if (status === 403) {
error.value = '只有教师类型的 Token 可以设置姓名'
} else if (status === 404) {
error.value = '设备未设置教师列表或 Token 不存在'
} else {
error.value = err?.response?.data?.error?.message || err?.message || '设置失败,请稍后重试'
}
} finally {
saving.value = false
}
}
// 从建议中选择教师,自动填充班主任和科目
const selectTeacherFromSuggestion = (teacher) => {
teacherForm.value.name = teacher.name
teacherForm.value.isHeadTeacher = teacher.isHeadTeacher || false
teacherForm.value.subjects = Array.isArray(teacher.subjects) ? [...teacher.subjects] : []
}
// 跳过设置
const skipSetting = () => {
showDialog.value = false
@@ -199,29 +368,46 @@ const openDialog = async () => {
console.log('studentList.length:', studentList.value.length)
console.log('currentStudentName:', currentStudentName.value)
if (!isStudentToken.value) {
console.log('Not a student token, cannot open dialog')
if (isStudentToken.value) {
const resp = await dataProvider.loadData('classworks-list-main')
studentList.value = Array.isArray(resp?.value) ? resp.value : (Array.isArray(resp) ? resp : [])
if (studentList.value.length === 0) {
console.log('Student list is empty, trying to load...')
await checkStudentNameStatus()
selectedName.value = currentStudentName.value
showDialog.value = true
} else {
selectedName.value = currentStudentName.value
showDialog.value = true
}
return
}
studentList.value = await dataProvider.loadData('classworks-list-main')
// 如果是学生 token,即使列表为空也应该打开对话框
// 可能是列表还在加载中
if (studentList.value.length === 0) {
console.log('Student list is empty, trying to load...')
// 重新加载学生列表
checkStudentNameStatus().then(() => {
if (studentList.value.length > 0) {
selectedName.value = currentStudentName.value
showDialog.value = true
} else {
console.warn('Student list is still empty after reload')
if (isTeacherToken.value) {
try {
const resp = await dataProvider.loadData('classworks-list-teacher')
teacherList.value = Array.isArray(resp?.value) ? resp.value : (Array.isArray(resp) ? resp : [])
} catch {
// 如果列表不存在,初始化为空
console.log('教师列表不存在或加载失败,允许手动创建')
teacherList.value = []
}
// 重置教师表单
teacherForm.value = { name: currentTeacherName.value, isHeadTeacher: false, subjects: [] }
// 如果当前有教师名称,尝试从列表中读取班主任和科目信息
if (currentTeacherName.value) {
const found = teacherList.value.find(t => t.name === currentTeacherName.value)
if (found) {
teacherForm.value.isHeadTeacher = found.isHeadTeacher || false
teacherForm.value.subjects = Array.isArray(found.subjects) ? [...found.subjects] : []
}
})
} else {
selectedName.value = currentStudentName.value
}
showDialog.value = true
console.log('Dialog opened, showDialog:', showDialog.value)
console.log('Dialog opened (teacher), showDialog:', showDialog.value)
return
}
console.log('Not a student/teacher token, cannot open dialog')
}
// 监听 token 变化
@@ -249,7 +435,9 @@ defineExpose({
checkStudentNameStatus,
openDialog,
currentStudentName,
currentTeacherName,
isStudentToken,
isTeacherToken,
isReadOnly,
displayName,
hasToken,
+132 -43
View File
@@ -15,40 +15,11 @@
<div class="urgent-title mb-6">
{{ currentNotification?.content?.message || "无内容" }}
</div>
<div class="urgent-subtitle mb-6">
{{ senderName }} {{ deviceType }} {{ formatTime(currentNotification?.timestamp) }}
</div>
<!-- 发送者信息(使用 Vuetify Card) -->
<v-card variant="flat" color="white">
<v-card-title>发送者信息</v-card-title>
<v-card-text>
<v-chip
class="mr-2 mb-2"
color="primary"
variant="outlined"
size="small"
>
<v-icon left size="16"> mdi-account </v-icon>
{{ senderName }}
</v-chip>
<v-chip
class="mr-2 mb-2"
color="info"
variant="outlined"
size="small"
>
<v-icon left size="16"> mdi-devices </v-icon>
{{ deviceType }}
</v-chip>
<v-chip
class="mb-2"
color="success"
variant="outlined"
size="small"
>
<v-icon left size="16"> mdi-clock </v-icon>
{{ formatTime(currentNotification?.timestamp) }}
</v-chip>
</v-card-text>
</v-card>
<!-- 多通知导航 -->
<div v-if="hasMultipleNotifications" class="navigation-controls mt-6">
@@ -103,6 +74,8 @@
<script>
import EventSender from "@/components/EventSender.vue";
import { getSetting } from "@/utils/settings.js";
import { playSound, stopSound } from "@/utils/soundList.js";
export default {
name: "UrgentNotification",
@@ -116,6 +89,7 @@ export default {
currentIndex: 0, // 当前显示的通知索引
autoCloseTimer: null,
urgentSoundTimer: null,
currentAudio: null, // 当前播放的音频对象
};
},
computed: {
@@ -165,6 +139,9 @@ export default {
const senderInfo =
this.currentNotification?.senderInfo ||
this.currentNotification?.content?.senderInfo;
if(senderInfo?.deviceType=='teacher') return "教师";
if(senderInfo?.deviceType=='student') return "学生";
if(senderInfo?.deviceType=='classroom') return "教室";
return senderInfo?.deviceType || "未知类型";
},
targetDevices() {
@@ -178,6 +155,8 @@ export default {
if (this.urgentSoundTimer) {
clearInterval(this.urgentSoundTimer);
}
// 停止音频播放
this.stopNotificationSound();
},
methods: {
show(notificationData) {
@@ -202,6 +181,9 @@ export default {
this.sendDisplayedReceipt();
this.playNotificationSound();
// 发送浏览器通知
this.sendBrowserNotification(notificationData);
// 如果是加急通知,启动定时音效
if (this.isUrgent) {
this.startUrgentSound();
@@ -212,7 +194,11 @@ export default {
this.currentIndex = this.notificationQueue.length - 1;
this.sendDisplayedReceipt();
this.playNotificationSound();
this.sendBrowserNotification(notificationData);
this.startUrgentSound();
} else {
// 即使不立即显示,也发送浏览器通知
this.sendBrowserNotification(notificationData);
}
}
},
@@ -313,7 +299,29 @@ export default {
},
playNotificationSound() {
try {
// 统一的通知音效
// 停止之前的音频
this.stopNotificationSound();
// 根据通知类型选择铃声
const soundFile = this.isUrgent
? getSetting('notification.urgentSound')
: getSetting('notification.singleSound');
// 使用配置的铃声文件
this.currentAudio = playSound(soundFile, false);
if (!this.currentAudio) {
// 如果播放失败,使用备用蜂鸣音
this.playFallbackSound();
}
} catch (error) {
console.warn("无法播放通知音效:", error);
this.playFallbackSound();
}
},
// 备用蜂鸣音(原有的实现)
playFallbackSound() {
try {
const audioContext = new (window.AudioContext ||
window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
@@ -330,7 +338,14 @@ export default {
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.3); // 300ms
} catch (error) {
console.warn("无法播放通知音效:", error);
console.warn("无法播放备用音效:", error);
}
},
// 停止通知音效
stopNotificationSound() {
if (this.currentAudio) {
stopSound(this.currentAudio);
this.currentAudio = null;
}
},
// 发送显示回执
@@ -393,14 +408,23 @@ export default {
startUrgentSound() {
this.stopUrgentSound(); // 先清除之前的定时器
// 每秒播放一次提示音
this.urgentSoundTimer = setInterval(() => {
if (this.visible && this.isUrgent) {
this.playNotificationSound();
} else {
this.stopUrgentSound();
}
}, 1000);
// 停止之前的音频
this.stopNotificationSound();
// 播放循环铃声
const soundFile = getSetting('notification.urgentSound');
this.currentAudio = playSound(soundFile, true); // 循环播放
if (!this.currentAudio) {
// 如果播放失败,使用备用方案:每秒播放一次提示音
this.urgentSoundTimer = setInterval(() => {
if (this.visible && this.isUrgent) {
this.playFallbackSound();
} else {
this.stopUrgentSound();
}
}, 1000);
}
},
// 停止加急音效
stopUrgentSound() {
@@ -408,6 +432,65 @@ export default {
clearInterval(this.urgentSoundTimer);
this.urgentSoundTimer = null;
}
this.stopNotificationSound();
},
// 发送浏览器通知
async sendBrowserNotification(notificationData) {
// 检查浏览器是否支持通知API
if (!('Notification' in window)) {
console.warn('浏览器不支持通知API');
return;
}
// 请求通知权限
try {
let permission = Notification.permission;
if (permission === 'default') {
permission = await Notification.requestPermission();
}
if (permission !== 'granted') {
console.warn('用户未授予通知权限');
return;
}
// 构建通知内容
const message = notificationData.content?.message || '新通知';
const senderInfo = notificationData.senderInfo || notificationData.content?.senderInfo;
const senderName = senderInfo?.deviceName || senderInfo?.deviceType || '未知发送者';
const isUrgent = notificationData.content?.isUrgent || false;
// 创建浏览器通知
const notification = new Notification(
isUrgent ? '🚨 紧急通知' : '📢 通知消息',
{
body: `${message}\n\n来自: ${senderName}`,
icon: '/pwa/image/icon-192.png', // 使用应用图标
badge: '/pwa/image/icon-192.png',
tag: notificationData.content?.notificationId || `notification-${Date.now()}`,
requireInteraction: isUrgent, // 紧急通知需要用户交互
silent: false, // 使用系统默认声音
vibrate: isUrgent ? [200, 100, 200, 100, 200] : [200], // 震动模式
timestamp: notificationData.timestamp || Date.now(),
}
);
// 点击通知时聚焦到窗口
notification.onclick = () => {
window.focus();
notification.close();
};
// 自动关闭非紧急通知
if (!isUrgent) {
setTimeout(() => {
notification.close();
}, 10000); // 10秒后自动关闭
}
} catch (error) {
console.error('发送浏览器通知失败:', error);
}
},
},
};
@@ -448,6 +531,12 @@ export default {
line-height: 1.2;
}
.urgent-subtitle {
font-size: 2rem;
font-weight: bold;
color: white;
line-height: 1.2;
}
.notification-content {
font-size: 1.4rem;
color: rgba(255, 255, 255, 0.95);
@@ -0,0 +1,276 @@
<template>
<v-card class="notification-sound-settings">
<v-card-title>
通知铃声设置
</v-card-title>
<v-card-text>
<!-- 自动播放提示 -->
<v-alert
v-if="showAutoplayWarning"
type="info"
variant="tonal"
closable
class="mb-4"
@click:close="showAutoplayWarning = false"
>
<div class="d-flex align-center">
<v-icon class="mr-2">mdi-information</v-icon>
<span>首次使用请点击试听按钮测试音频播放是否正常</span>
</div>
</v-alert>
<v-row>
<!-- 单次通知铃声 -->
<v-col cols="12">
<v-card border>
<v-card-title class="text-subtitle-1">
<v-icon left>mdi-bell-ring</v-icon>
单次通知铃声
</v-card-title>
<v-card-text>
<v-select
v-model="singleSound"
:items="soundOptions"
label="选择铃声"
prepend-icon="mdi-music-note"
variant="outlined"
density="comfortable"
@update:model-value="onSingleSoundChange"
>
<template #item="{ props, item }">
<v-list-item
v-bind="props"
@click="previewSound(item.value)"
>
<template #prepend>
<v-icon>mdi-music-note</v-icon>
</template>
<template #append>
<v-btn
icon
size="small"
variant="text"
@click.stop="previewSound(item.value)"
>
<v-icon>mdi-play</v-icon>
</v-btn>
</template>
</v-list-item>
</template>
</v-select>
<div class="mt-3 d-flex gap-2">
<v-btn
color="primary"
variant="tonal"
@click="previewSound(singleSound)"
>
<v-icon left>mdi-play</v-icon>
试听
</v-btn>
<v-btn
@click="resetSingleSound"
>
<v-icon left>mdi-restore</v-icon>
恢复
</v-btn>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- 持续通知铃声(紧急通知) -->
<v-col cols="12">
<v-card border>
<v-card-title class="text-subtitle-1">
<v-icon left color="error">mdi-bell-alert</v-icon>
紧急通知铃声
</v-card-title>
<v-card-text>
<v-select
v-model="urgentSound"
:items="soundOptions"
label="选择铃声"
prepend-icon="mdi-music-note"
variant="outlined"
density="comfortable"
@update:model-value="onUrgentSoundChange"
>
<template #item="{ props, item }">
<v-list-item
v-bind="props"
@click="previewSound(item.value)"
>
<template #prepend>
<v-icon>mdi-music-note</v-icon>
</template>
<template #append>
<v-btn
icon
size="small"
variant="text"
@click.stop="previewSound(item.value)"
>
<v-icon>mdi-play</v-icon>
</v-btn>
</template>
</v-list-item>
</template>
</v-select>
<div class="mt-3 d-flex gap-2">
<v-btn
color="error"
variant="tonal"
@click="previewSound(urgentSound)"
>
<v-icon left>mdi-play</v-icon>
试听
</v-btn>
<v-btn
@click="resetUrgentSound"
>
<v-icon left>mdi-restore</v-icon>
恢复
</v-btn>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import { getSetting, setSetting } from '@/utils/settings.js';
import { soundFiles, stopSound } from '@/utils/soundList.js';
export default {
name: 'NotificationSoundSettings',
data() {
return {
singleSound: '',
urgentSound: '',
currentAudio: null,
showAutoplayWarning: true,
};
},
computed: {
soundOptions() {
return soundFiles.map(file => ({
title: file.replace('.mp3', ''),
value: file,
}));
},
},
mounted() {
this.loadSettings();
},
beforeUnmount() {
this.stopPreview();
},
methods: {
loadSettings() {
this.singleSound = getSetting('notification.singleSound');
this.urgentSound = getSetting('notification.urgentSound');
},
onSingleSoundChange(value) {
setSetting('notification.singleSound', value);
this.$message?.success('设置已保存', `单次通知铃声: ${value}`);
},
onUrgentSoundChange(value) {
setSetting('notification.urgentSound', value);
this.$message?.success('设置已保存', `紧急通知铃声: ${value}`);
},
async previewSound(filename) {
// 隐藏自动播放警告
this.showAutoplayWarning = false;
// 先停止当前播放
this.stopPreview();
try {
// 播放新音频(不循环)
const audio = await this.playSoundWithPromise(filename, false);
this.currentAudio = audio;
// 音频播放结束后清除引用
if (this.currentAudio) {
this.currentAudio.addEventListener('ended', () => {
this.currentAudio = null;
}, { once: true });
}
} catch (error) {
console.error('播放音频失败:', error);
// 提示用户
if (error.name === 'NotAllowedError') {
this.$message?.warning('无法播放音频', '浏览器阻止了自动播放,请再次点击试听按钮');
} else {
this.$message?.error('播放失败', '音频文件加载失败,请稍后重试');
}
}
},
// 使用Promise包装音频播放,以便捕获错误
playSoundWithPromise(filename, loop = false) {
return new Promise((resolve, reject) => {
const path = this.getSoundPath(filename);
if (!path) {
reject(new Error('音频文件不存在'));
return;
}
try {
// eslint-disable-next-line no-undef
const audio = new Audio(path);
audio.loop = loop;
// 尝试播放
audio.play()
.then(() => {
resolve(audio);
})
.catch(err => {
reject(err);
});
} catch (error) {
reject(error);
}
});
},
getSoundPath(filename) {
if (!filename) return null;
try {
// 使用相对于public目录的路径,这样Vite不会在构建时打包所有音频
// 而是按需加载
return `/src/assets/sounds/${filename}`;
} catch {
return null;
}
},
stopPreview() {
if (this.currentAudio) {
stopSound(this.currentAudio);
this.currentAudio = null;
}
},
resetSingleSound() {
this.singleSound = 'Teams 默认.mp3';
setSetting('notification.singleSound', this.singleSound);
this.$message?.success('已恢复单次通知铃声默认设置');
},
resetUrgentSound() {
this.urgentSound = 'Teams 默认通话铃.mp3';
setSetting('notification.urgentSound', this.urgentSound);
this.$message?.success('已恢复紧急通知铃声默认设置');
},
},
};
</script>
<style scoped>
.notification-sound-settings {
margin: 16px 0;
}
</style>