diff --git a/UNIFIED_LINK_GENERATOR.md b/UNIFIED_LINK_GENERATOR.md
deleted file mode 100644
index 68bea2d..0000000
--- a/UNIFIED_LINK_GENERATOR.md
+++ /dev/null
@@ -1,159 +0,0 @@
-# 统一链接生成器功能测试
-
-## 功能概述
-
-新的统一链接生成器将预配置认证信息和设置分享功能合并到一个链接中,用户可以:
-
-1. 输入命名空间和认证码(预配置认证)
-2. 选择需要分享的设置项
-3. 生成包含两种信息的统一链接
-
-## 核心特性
-
-### ✅ **统一链接生成**
-- 同时包含预配置认证参数和设置配置
-- 一个链接完成设备认证和设置应用
-- 自动编码和参数组合
-
-### ✅ **智能界面设计**
-- 分层式界面:预配置认证 + 设置分享 + 链接生成
-- 实时预览和状态显示
-- 安全提醒和敏感信息保护
-
-### ✅ **安全优化**
-- 数据源设置和已变更设置默认排除 `server.kvToken`
-- 敏感设置项标记和值遮盖
-- 多重安全提醒
-
-## 链接格式
-
-### 基础预配置链接
-```
-https://domain.com/?namespace=classroom-001&authCode=pass123&autoExecute=true
-```
-
-### 包含设置的统一链接
-```
-https://domain.com/?namespace=classroom-001&authCode=pass123&autoExecute=true&config=eyJ...
-```
-
-其中:
-- `namespace`: 设备命名空间
-- `authCode`: 认证码(可选)
-- `autoExecute`: 是否自动执行认证
-- `config`: Base64编码的设置JSON对象
-
-## 使用流程
-
-1. **输入预配置信息**
- - 命名空间(必填)
- - 认证码(可选)
- - 是否自动执行认证
-
-2. **选择设置项**
- - 快速选择:数据源设置、已变更设置、全选
- - 手动选择:通过详细列表选择特定设置
- - 安全保护:敏感设置默认不选中
-
-3. **生成统一链接**
- - 点击"生成统一链接"按钮
- - 链接实时生成和预览
- - 一键复制和测试
-
-## 技术实现
-
-### 参数组合逻辑
-```javascript
-// 1. 添加预配置参数
-params.append("namespace", namespace);
-params.append("authCode", authCode);
-params.append("autoExecute", "true");
-
-// 2. 添加设置配置
-if (selectedSettings.length > 0) {
- const configObj = {};
- selectedSettings.forEach(key => {
- configObj[key] = allSettings[key];
- });
-
- const base64Config = btoa(JSON.stringify(configObj));
- params.append("config", base64Config);
-}
-```
-
-### 自动更新机制
-- 监听预配置表单变化
-- 监听设置选择变化
-- 实时生成统一链接
-
-## 使用场景
-
-### 1. **设备批量部署 + 环境配置**
-```
-https://classworks.example.com/?namespace=classroom-01&authCode=device01&autoExecute=true&config=eyJzZXJ2ZXIuZG9tYWluIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20ifQ==
-```
-- 自动认证为指定设备
-- 自动配置服务器地址等设置
-
-### 2. **演示环境快速部署**
-```
-https://classworks.example.com/?namespace=demo&autoExecute=true&config=eyJkaXNwbGF5LnRoZW1lIjoiZGFyayIsImVkaXQubW9kZSI6InJlYWRvbmx5In0=
-```
-- 自动认证为演示账号
-- 自动应用演示环境设置
-
-### 3. **培训环境标准化**
-```
-https://classworks.example.com/?namespace=training&authCode=train123&config=eyJkaXNwbGF5LnNob3dIZWxwIjp0cnVlLCJlZGl0LmVuYWJsZUd1aWRlIjp0cnVlfQ==
-```
-- 预配置培训账号
-- 启用帮助和引导功能
-
-## 安全考虑
-
-### ✅ **默认安全**
-- Token等敏感信息默认不包含
-- 快速选择按钮智能排除敏感设置
-- 明确的安全警告和提醒
-
-### ✅ **用户控制**
-- 用户仍可手动选择包含敏感设置
-- 敏感设置有明确标记
-- 提供充分的风险提示
-
-### ✅ **传输安全**
-- 建议HTTPS传输
-- URL参数会被自动清理
-- 设置信息经过Base64编码
-
-## 兼容性
-
-- ✅ 向后兼容现有的预配置功能
-- ✅ 向后兼容现有的设置分享功能
-- ✅ 新增统一链接格式
-- ✅ 保持所有现有API接口
-
-## 测试建议
-
-1. **基础功能测试**
- - 仅预配置信息的链接生成
- - 预配置 + 设置的统一链接生成
- - 链接复制和测试功能
-
-2. **安全功能测试**
- - 验证敏感设置默认不选中
- - 验证敏感设置标记显示
- - 验证安全提醒展示
-
-3. **兼容性测试**
- - 测试生成的链接是否正常工作
- - 测试预配置认证是否正常
- - 测试设置应用是否正常
-
-## 优势总结
-
-1. **用户体验**: 一个链接完成所有配置,无需多步操作
-2. **部署效率**: 批量设备部署更加便捷
-3. **管理简化**: 减少链接管理复杂度
-4. **安全平衡**: 在便捷性和安全性之间找到平衡
-5. **功能完整**: 涵盖认证和配置的完整解决方案
\ No newline at end of file
diff --git a/package.json b/package.json
index 4cb18b1..2f4e86c 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview",
- "lint": "eslint . --fix"
+ "lint": "eslint . --fix",
+ "prebuild": "node scripts/generate-sound-list.js"
},
"dependencies": {
"@fingerprintjs/fingerprintjs": "^5.0.1",
diff --git a/scripts/generate-sound-list.js b/scripts/generate-sound-list.js
new file mode 100644
index 0000000..45a796d
--- /dev/null
+++ b/scripts/generate-sound-list.js
@@ -0,0 +1,149 @@
+/**
+ * 自动生成音频文件列表脚本
+ * 读取 src/assets/sounds 文件夹中的所有音频文件并生成列表
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// 音频文件路径
+const soundsDir = path.join(__dirname, '../src/assets/sounds');
+const outputFile = path.join(__dirname, '../src/utils/soundList.js');
+
+// 读取音频文件
+function getSoundFiles() {
+ try {
+ const files = fs.readdirSync(soundsDir);
+ // 过滤出音频文件(.mp3, .wav, .ogg等)
+ const audioFiles = files.filter(file => {
+ const ext = path.extname(file).toLowerCase();
+ return ['.mp3', '.wav', '.ogg', '.m4a', '.aac'].includes(ext);
+ });
+
+ return audioFiles.sort();
+ } catch (error) {
+ console.error('读取音频文件夹失败:', error);
+ return [];
+ }
+}
+
+// 生成文件内容
+function generateSoundList() {
+ const soundFiles = getSoundFiles();
+
+ const fileContent = `/**
+ * 自动生成的音频文件列表
+ * 由 scripts/generate-sound-list.js 生成
+ * 请勿手动修改此文件
+ */
+
+// 所有可用的音频文件列表
+export const soundFiles = ${JSON.stringify(soundFiles, null, 2)};
+
+// 默认的单次通知铃声
+export const defaultSingleSound = 'Teams 默认.mp3';
+
+// 默认的持续通知铃声
+export const defaultUrgentSound = 'Teams 默认通话铃.mp3';
+
+// 获取音频文件的完整路径
+export function getSoundPath(filename) {
+ if (!filename) return null;
+ // 使用动态路径,避免Vite在构建时加载所有音频文件
+ // 这样只有在真正需要播放时才会加载对应的音频文件
+ return \`/src/assets/sounds/\${filename}\`;
+}
+
+// 播放音频文件
+export function playSound(filename, loop = false) {
+ const path = getSoundPath(filename);
+ if (!path) {
+ console.warn('音频文件不存在:', filename);
+ return null;
+ }
+
+ try {
+ // eslint-disable-next-line no-undef
+ const audio = new Audio(path);
+ audio.loop = loop;
+
+ // 尝试播放,但不阻止返回audio对象
+ const playPromise = audio.play();
+ if (playPromise !== undefined) {
+ playPromise.catch(err => {
+ // 静默处理错误,调用者应该自己处理
+ console.warn('播放音频失败:', err.name, err.message);
+ });
+ }
+
+ return audio;
+ } catch (error) {
+ console.error('创建音频对象失败:', error);
+ return null;
+ }
+}
+
+// 播放音频文件(Promise版本,用于更好的错误处理)
+export function playSoundAsync(filename, loop = false) {
+ return new Promise((resolve, reject) => {
+ const path = getSoundPath(filename);
+ if (!path) {
+ reject(new Error('音频文件不存在'));
+ return;
+ }
+
+ try {
+ // eslint-disable-next-line no-undef
+ const audio = new Audio(path);
+ audio.loop = loop;
+
+ // 预加载音频文件
+ audio.preload = 'auto';
+
+ audio.play()
+ .then(() => resolve(audio))
+ .catch(err => reject(err));
+ } catch (error) {
+ reject(error);
+ }
+ });
+}
+
+// 停止音频播放
+export function stopSound(audio) {
+ if (audio) {
+ audio.pause();
+ audio.currentTime = 0;
+ }
+}
+`;
+
+ return fileContent;
+}
+
+// 写入文件
+function writeSoundList() {
+ try {
+ const content = generateSoundList();
+
+ // 确保输出目录存在
+ const outputDir = path.dirname(outputFile);
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ fs.writeFileSync(outputFile, content, 'utf-8');
+ console.log('✓ 音频文件列表生成成功:', outputFile);
+ console.log('✓ 共找到', getSoundFiles().length, '个音频文件');
+ } catch (error) {
+ console.error('✗ 生成音频列表失败:', error);
+ process.exit(1);
+ }
+}
+
+// 执行生成
+writeSoundList();
diff --git a/src/assets/sounds/Teams Ping.mp3 b/src/assets/sounds/Teams Ping.mp3
new file mode 100644
index 0000000..5f3e1c2
Binary files /dev/null and b/src/assets/sounds/Teams Ping.mp3 differ
diff --git a/src/assets/sounds/Teams Remix.mp3 b/src/assets/sounds/Teams Remix.mp3
new file mode 100644
index 0000000..f50f267
Binary files /dev/null and b/src/assets/sounds/Teams Remix.mp3 differ
diff --git a/src/assets/sounds/Teams bounce.mp3 b/src/assets/sounds/Teams bounce.mp3
new file mode 100644
index 0000000..0b4e136
Binary files /dev/null and b/src/assets/sounds/Teams bounce.mp3 differ
diff --git a/src/assets/sounds/Teams incoming-ringtone-level30.mp3 b/src/assets/sounds/Teams incoming-ringtone-level30.mp3
new file mode 100644
index 0000000..ce9c3b8
Binary files /dev/null and b/src/assets/sounds/Teams incoming-ringtone-level30.mp3 differ
diff --git a/src/assets/sounds/Teams incoming-ringtone-level40.mp3 b/src/assets/sounds/Teams incoming-ringtone-level40.mp3
new file mode 100644
index 0000000..41e1ef5
Binary files /dev/null and b/src/assets/sounds/Teams incoming-ringtone-level40.mp3 differ
diff --git a/src/assets/sounds/Teams meetup_ring.mp3 b/src/assets/sounds/Teams meetup_ring.mp3
new file mode 100644
index 0000000..16ff8f7
Binary files /dev/null and b/src/assets/sounds/Teams meetup_ring.mp3 differ
diff --git a/src/assets/sounds/Teams screenshare_ring.mp3 b/src/assets/sounds/Teams screenshare_ring.mp3
new file mode 100644
index 0000000..0e19996
Binary files /dev/null and b/src/assets/sounds/Teams screenshare_ring.mp3 differ
diff --git a/src/assets/sounds/Teams teams_meet_up_reminder.mp3 b/src/assets/sounds/Teams teams_meet_up_reminder.mp3
new file mode 100644
index 0000000..0e19996
Binary files /dev/null and b/src/assets/sounds/Teams teams_meet_up_reminder.mp3 differ
diff --git a/src/assets/sounds/Teams teams_notification.mp3 b/src/assets/sounds/Teams teams_notification.mp3
new file mode 100644
index 0000000..f4a0a3f
Binary files /dev/null and b/src/assets/sounds/Teams teams_notification.mp3 differ
diff --git a/src/assets/sounds/Teams 优先处理.mp3 b/src/assets/sounds/Teams 优先处理.mp3
new file mode 100644
index 0000000..910efd0
Binary files /dev/null and b/src/assets/sounds/Teams 优先处理.mp3 differ
diff --git a/src/assets/sounds/Teams 共鸣.mp3 b/src/assets/sounds/Teams 共鸣.mp3
new file mode 100644
index 0000000..7f46f4e
Binary files /dev/null and b/src/assets/sounds/Teams 共鸣.mp3 differ
diff --git a/src/assets/sounds/Teams 召唤.mp3 b/src/assets/sounds/Teams 召唤.mp3
new file mode 100644
index 0000000..0b4aae2
Binary files /dev/null and b/src/assets/sounds/Teams 召唤.mp3 differ
diff --git a/src/assets/sounds/Teams 叮铃.mp3 b/src/assets/sounds/Teams 叮铃.mp3
new file mode 100644
index 0000000..a099399
Binary files /dev/null and b/src/assets/sounds/Teams 叮铃.mp3 differ
diff --git a/src/assets/sounds/Teams 增强.mp3 b/src/assets/sounds/Teams 增强.mp3
new file mode 100644
index 0000000..03f7249
Binary files /dev/null and b/src/assets/sounds/Teams 增强.mp3 differ
diff --git a/src/assets/sounds/Teams 尤里卡.mp3 b/src/assets/sounds/Teams 尤里卡.mp3
new file mode 100644
index 0000000..fb5cdb8
Binary files /dev/null and b/src/assets/sounds/Teams 尤里卡.mp3 differ
diff --git a/src/assets/sounds/Teams 弹拨.mp3 b/src/assets/sounds/Teams 弹拨.mp3
new file mode 100644
index 0000000..a31cd6c
Binary files /dev/null and b/src/assets/sounds/Teams 弹拨.mp3 differ
diff --git a/src/assets/sounds/Teams 提醒.mp3 b/src/assets/sounds/Teams 提醒.mp3
new file mode 100644
index 0000000..fce78c9
Binary files /dev/null and b/src/assets/sounds/Teams 提醒.mp3 differ
diff --git a/src/assets/sounds/Teams 摇摆.mp3 b/src/assets/sounds/Teams 摇摆.mp3
new file mode 100644
index 0000000..65fd935
Binary files /dev/null and b/src/assets/sounds/Teams 摇摆.mp3 differ
diff --git a/src/assets/sounds/Teams 时空.mp3 b/src/assets/sounds/Teams 时空.mp3
new file mode 100644
index 0000000..bcf62e7
Binary files /dev/null and b/src/assets/sounds/Teams 时空.mp3 differ
diff --git a/src/assets/sounds/Teams 气泡(大声).mp3 b/src/assets/sounds/Teams 气泡(大声).mp3
new file mode 100644
index 0000000..212db3a
Binary files /dev/null and b/src/assets/sounds/Teams 气泡(大声).mp3 differ
diff --git a/src/assets/sounds/Teams 气泡.mp3 b/src/assets/sounds/Teams 气泡.mp3
new file mode 100644
index 0000000..92896fe
Binary files /dev/null and b/src/assets/sounds/Teams 气泡.mp3 differ
diff --git a/src/assets/sounds/Teams 波普.mp3 b/src/assets/sounds/Teams 波普.mp3
new file mode 100644
index 0000000..ee69f22
Binary files /dev/null and b/src/assets/sounds/Teams 波普.mp3 differ
diff --git a/src/assets/sounds/Teams 波纹.mp3 b/src/assets/sounds/Teams 波纹.mp3
new file mode 100644
index 0000000..966701b
Binary files /dev/null and b/src/assets/sounds/Teams 波纹.mp3 differ
diff --git a/src/assets/sounds/Teams 滴水.mp3 b/src/assets/sounds/Teams 滴水.mp3
new file mode 100644
index 0000000..e61ad75
Binary files /dev/null and b/src/assets/sounds/Teams 滴水.mp3 differ
diff --git a/src/assets/sounds/Teams 点击.mp3 b/src/assets/sounds/Teams 点击.mp3
new file mode 100644
index 0000000..fadcfa5
Binary files /dev/null and b/src/assets/sounds/Teams 点击.mp3 differ
diff --git a/src/assets/sounds/Teams 蜂鸣声.mp3 b/src/assets/sounds/Teams 蜂鸣声.mp3
new file mode 100644
index 0000000..765e504
Binary files /dev/null and b/src/assets/sounds/Teams 蜂鸣声.mp3 differ
diff --git a/src/assets/sounds/Teams 警报.mp3 b/src/assets/sounds/Teams 警报.mp3
new file mode 100644
index 0000000..c5da548
Binary files /dev/null and b/src/assets/sounds/Teams 警报.mp3 differ
diff --git a/src/assets/sounds/Teams 赋予希望.mp3 b/src/assets/sounds/Teams 赋予希望.mp3
new file mode 100644
index 0000000..7562489
Binary files /dev/null and b/src/assets/sounds/Teams 赋予希望.mp3 differ
diff --git a/src/assets/sounds/Teams 轻弹.mp3 b/src/assets/sounds/Teams 轻弹.mp3
new file mode 100644
index 0000000..f509a4e
Binary files /dev/null and b/src/assets/sounds/Teams 轻弹.mp3 differ
diff --git a/src/assets/sounds/Teams 进阶.mp3 b/src/assets/sounds/Teams 进阶.mp3
new file mode 100644
index 0000000..9c1d68e
Binary files /dev/null and b/src/assets/sounds/Teams 进阶.mp3 differ
diff --git a/src/assets/sounds/Teams 重复振铃.mp3 b/src/assets/sounds/Teams 重复振铃.mp3
new file mode 100644
index 0000000..7cd5fcf
Binary files /dev/null and b/src/assets/sounds/Teams 重复振铃.mp3 differ
diff --git a/src/assets/sounds/Teams 颤振.mp3 b/src/assets/sounds/Teams 颤振.mp3
new file mode 100644
index 0000000..9ff2793
Binary files /dev/null and b/src/assets/sounds/Teams 颤振.mp3 differ
diff --git a/src/assets/sounds/Teams 高分.mp3 b/src/assets/sounds/Teams 高分.mp3
new file mode 100644
index 0000000..53d2891
Binary files /dev/null and b/src/assets/sounds/Teams 高分.mp3 differ
diff --git a/src/assets/sounds/Teams 默认.mp3 b/src/assets/sounds/Teams 默认.mp3
new file mode 100644
index 0000000..639c092
Binary files /dev/null and b/src/assets/sounds/Teams 默认.mp3 differ
diff --git a/src/assets/sounds/Teams 默认通话铃.mp3 b/src/assets/sounds/Teams 默认通话铃.mp3
new file mode 100644
index 0000000..36d569f
Binary files /dev/null and b/src/assets/sounds/Teams 默认通话铃.mp3 differ
diff --git a/src/components/StudentNameManager.vue b/src/components/StudentNameManager.vue
index d5b6ecf..fe8742c 100644
--- a/src/components/StudentNameManager.vue
+++ b/src/components/StudentNameManager.vue
@@ -1,32 +1,92 @@
-
+
- 设置学生姓名
+ {{ dialogTitle }}
-
- 请从列表中选择您的姓名:
-
-
-
- 共 {{ studentList.length }} 位学生
-
+
+
+
+ 请从列表中选择您的姓名:
+
+
+
+ 共 {{ studentList.length }} 位学生
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ teacher.name }}
+
+ 👨🏫
+
+
+
+
+
+
+
+
+
+
+
+
稍后设置
-
+
确认
+
+ 确认
+
@@ -61,6 +131,7 @@
:is-student="isStudentToken"
:open-dialog="openDialog"
:student-name="currentStudentName"
+ :teacher-name="currentTeacherName"
name="header-display"
/>
@@ -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,
diff --git a/src/components/UrgentNotification.vue b/src/components/UrgentNotification.vue
index a34ceec..c32ae37 100644
--- a/src/components/UrgentNotification.vue
+++ b/src/components/UrgentNotification.vue
@@ -15,40 +15,11 @@
{{ currentNotification?.content?.message || "无内容" }}
+
+ {{ senderName }} {{ deviceType }} {{ formatTime(currentNotification?.timestamp) }}
+
+
-
-
- 发送者信息
-
-
- mdi-account
- {{ senderName }}
-
-
- mdi-devices
- {{ deviceType }}
-
-
- mdi-clock
- {{ formatTime(currentNotification?.timestamp) }}
-
-
-
@@ -103,6 +74,8 @@
+
+
diff --git a/src/pages/index.vue b/src/pages/index.vue
index a674d50..17391fd 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -944,12 +944,14 @@ export default {
this.updateSettings();
});
- // 连接学生姓名管理组件
+ // 连接学生姓名管理组件(支持学生和教师)
this.$nextTick(() => {
const studentNameManager = this.$refs.studentNameManager;
if (studentNameManager) {
- this.studentNameInfo.name = studentNameManager.currentStudentName;
+ // 优先使用学生名称,如果不是学生则使用教师名称
+ this.studentNameInfo.name = studentNameManager.currentStudentName || studentNameManager.currentTeacherName || '';
this.studentNameInfo.isStudent = studentNameManager.isStudentToken;
+ this.studentNameInfo.isTeacher = studentNameManager.isTeacherToken;
this.studentNameInfo.openDialog = () =>
studentNameManager.openDialog();
@@ -958,12 +960,31 @@ export default {
() => studentNameManager.currentStudentName,
(newName) => {
this.studentNameInfo.name = newName;
+ this.updateTokenDisplayInfo();
+ }
+ );
+ // 监听教师姓名变化
+ this.$watch(
+ () => studentNameManager.currentTeacherName,
+ (newName) => {
+ if (studentNameManager.isTeacherToken) {
+ this.studentNameInfo.name = newName;
+ this.updateTokenDisplayInfo();
+ }
}
);
this.$watch(
() => studentNameManager.isStudentToken,
(isStudent) => {
this.studentNameInfo.isStudent = isStudent;
+ this.updateTokenDisplayInfo();
+ }
+ );
+ this.$watch(
+ () => studentNameManager.isTeacherToken,
+ (isTeacher) => {
+ this.studentNameInfo.isTeacher = isTeacher;
+ this.updateTokenDisplayInfo();
}
);
}
@@ -1116,32 +1137,35 @@ export default {
const displayName = manager.displayName;
const isReadOnly = manager.isReadOnly;
const isStudent = manager.isStudentToken;
+ const isTeacher = manager.isTeacherToken;
// 设置只读状态(对所有类型的 token 都显示)
this.tokenDisplayInfo.readonly = isReadOnly;
- // 只有学生类型的 token 才显示名称 chip
- if (!isStudent) {
+ // 学生和教师都显示名称 chip
+ if (!isStudent && !isTeacher) {
this.tokenDisplayInfo.show = false;
return;
}
- // 设置学生名称显示(始终蓝色)
+ // 设置名称显示(始终蓝色)
this.tokenDisplayInfo.text = displayName;
this.tokenDisplayInfo.color = "primary";
- this.tokenDisplayInfo.icon = "mdi-account";
+ // 学生用人头图标,教师用学校图标
+ this.tokenDisplayInfo.icon = isTeacher ? "mdi-school" : "mdi-account";
this.tokenDisplayInfo.disabled = isReadOnly; // 只读时不可点击
this.tokenDisplayInfo.show = true;
},
- // 处理 Token Chip 点击
+ // 处理 Token Chip 点击(学生和教师都支持)
handleTokenChipClick() {
console.log("Token chip clicked");
const manager = this.$refs.studentNameManager;
console.log("Manager:", manager);
console.log("Is student token:", manager?.isStudentToken);
+ console.log("Is teacher token:", manager?.isTeacherToken);
- if (manager && manager.isStudentToken) {
+ if (manager && (manager.isStudentToken || manager.isTeacherToken)) {
console.log("Opening dialog...");
manager.openDialog();
} else {
diff --git a/src/pages/settings.vue b/src/pages/settings.vue
index cebe40e..63c5e4a 100644
--- a/src/pages/settings.vue
+++ b/src/pages/settings.vue
@@ -181,6 +181,10 @@
/>
+
+
+
+
@@ -277,6 +281,7 @@ import HomeworkTemplateCard from "@/components/settings/cards/HomeworkTemplateCa
import SubjectManagementCard from "@/components/settings/cards/SubjectManagementCard.vue";
import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue";
import HitokotoSettings from "@/components/HitokotoSettings.vue";
+import NotificationSoundSettings from "@/components/settings/NotificationSoundSettings.vue";
export default {
name: "Settings",
@@ -299,6 +304,7 @@ export default {
SubjectManagementCard,
KvDatabaseCard,
HitokotoSettings,
+ NotificationSoundSettings,
},
setup() {
const {mobile} = useDisplay();
@@ -420,6 +426,11 @@ export default {
icon: "mdi-theme-light-dark",
value: "theme",
},
+ {
+ title: "通知铃声",
+ icon: "mdi-bell-ring",
+ value: "notification",
+ },
{
title: "一言",
icon: "mdi-comment-quote",
diff --git a/src/utils/settings.js b/src/utils/settings.js
index c74cb1b..c88aa7f 100644
--- a/src/utils/settings.js
+++ b/src/utils/settings.js
@@ -400,6 +400,22 @@ const settingsDefinitions = {
// 设置应用的主题模式,可选亮色或暗色主题
},
+ // 通知铃声设置
+ "notification.singleSound": {
+ type: "string",
+ default: "Teams 默认.mp3",
+ description: "单次通知铃声",
+ icon: "mdi-bell-ring",
+ // 设置单次通知时播放的音频文件
+ },
+ "notification.urgentSound": {
+ type: "string",
+ default: "Teams 默认通话铃.mp3",
+ description: "持续通知铃声",
+ icon: "mdi-bell-alert",
+ // 设置紧急通知时循环播放的音频文件
+ },
+
// 随机点名设置
"randomPicker.enabled": {
type: "boolean",
diff --git a/src/utils/soundList.js b/src/utils/soundList.js
new file mode 100644
index 0000000..11bea0c
--- /dev/null
+++ b/src/utils/soundList.js
@@ -0,0 +1,121 @@
+/**
+ * 自动生成的音频文件列表
+ * 由 scripts/generate-sound-list.js 生成
+ * 请勿手动修改此文件
+ */
+
+// 所有可用的音频文件列表
+export const soundFiles = [
+ "Teams Ping.mp3",
+ "Teams Remix.mp3",
+ "Teams bounce.mp3",
+ "Teams incoming-ringtone-level30.mp3",
+ "Teams incoming-ringtone-level40.mp3",
+ "Teams meetup_ring.mp3",
+ "Teams screenshare_ring.mp3",
+ "Teams teams_meet_up_reminder.mp3",
+ "Teams teams_notification.mp3",
+ "Teams 优先处理.mp3",
+ "Teams 共鸣.mp3",
+ "Teams 召唤.mp3",
+ "Teams 叮铃.mp3",
+ "Teams 增强.mp3",
+ "Teams 尤里卡.mp3",
+ "Teams 弹拨.mp3",
+ "Teams 提醒.mp3",
+ "Teams 摇摆.mp3",
+ "Teams 时空.mp3",
+ "Teams 气泡(大声).mp3",
+ "Teams 气泡.mp3",
+ "Teams 波普.mp3",
+ "Teams 波纹.mp3",
+ "Teams 滴水.mp3",
+ "Teams 点击.mp3",
+ "Teams 蜂鸣声.mp3",
+ "Teams 警报.mp3",
+ "Teams 赋予希望.mp3",
+ "Teams 轻弹.mp3",
+ "Teams 进阶.mp3",
+ "Teams 重复振铃.mp3",
+ "Teams 颤振.mp3",
+ "Teams 高分.mp3",
+ "Teams 默认.mp3",
+ "Teams 默认通话铃.mp3"
+];
+
+// 默认的单次通知铃声
+export const defaultSingleSound = 'Teams 默认.mp3';
+
+// 默认的持续通知铃声
+export const defaultUrgentSound = 'Teams 默认通话铃.mp3';
+
+// 获取音频文件的完整路径
+export function getSoundPath(filename) {
+ if (!filename) return null;
+ // 使用动态路径,避免Vite在构建时加载所有音频文件
+ // 这样只有在真正需要播放时才会加载对应的音频文件
+ return `/src/assets/sounds/${filename}`;
+}
+
+// 播放音频文件
+export function playSound(filename, loop = false) {
+ const path = getSoundPath(filename);
+ if (!path) {
+ console.warn('音频文件不存在:', filename);
+ return null;
+ }
+
+ try {
+ // eslint-disable-next-line no-undef
+ const audio = new Audio(path);
+ audio.loop = loop;
+
+ // 尝试播放,但不阻止返回audio对象
+ const playPromise = audio.play();
+ if (playPromise !== undefined) {
+ playPromise.catch(err => {
+ // 静默处理错误,调用者应该自己处理
+ console.warn('播放音频失败:', err.name, err.message);
+ });
+ }
+
+ return audio;
+ } catch (error) {
+ console.error('创建音频对象失败:', error);
+ return null;
+ }
+}
+
+// 播放音频文件(Promise版本,用于更好的错误处理)
+export function playSoundAsync(filename, loop = false) {
+ return new Promise((resolve, reject) => {
+ const path = getSoundPath(filename);
+ if (!path) {
+ reject(new Error('音频文件不存在'));
+ return;
+ }
+
+ try {
+ // eslint-disable-next-line no-undef
+ const audio = new Audio(path);
+ audio.loop = loop;
+
+ // 预加载音频文件
+ audio.preload = 'auto';
+
+ audio.play()
+ .then(() => resolve(audio))
+ .catch(err => reject(err));
+ } catch (error) {
+ reject(error);
+ }
+ });
+}
+
+// 停止音频播放
+export function stopSound(audio) {
+ if (audio) {
+ audio.pause();
+ audio.currentTime = 0;
+ }
+}