diff --git a/README.md b/README.md index 4016e08..e1d2cb5 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@ # ExamSchedule -网页版ExamShowboard +**不只是考试看板。** + ![es](./doc/es.png) + +![main](/assets/main.png) + ## 功能 +- 考试看板 + - 实时显示当前时间、当前考试科目、考试起止时间、剩余时间及考试状态。 + - 支持全屏显示。 + - 支持设置时间偏移和考场信息,并保存到浏览器Cookie中。 + - 支持临时编辑消息,并保存到浏览器Cookie中(3天后到期)。 +- 时间广播 + - 支持自定义广播配置。 + - 支持打开本地json配置 -- 实时显示当前时间、当前考试科目、考试起止时间、剩余时间及考试状态。 -- 支持播放考试音频广播。 -- 支持全屏显示。 -- 支持设置时间偏移和考场信息,并保存到浏览器Cookie中。 -- 支持临时编辑消息,并保存到浏览器Cookie中(3天后到期)。 -### 设置说明 +### 考试看板设置说明 点击设置按钮可以打开设置窗口,进行以下配置: @@ -33,6 +40,7 @@ ## 软件截图 ### 主界面 + ![es](./doc/es.png) #### 考试展板界面 diff --git a/assets/dark.png b/assets/dark.png new file mode 100644 index 0000000..70d658f Binary files /dev/null and b/assets/dark.png differ diff --git a/assets/light.png b/assets/light.png new file mode 100644 index 0000000..5c6506b Binary files /dev/null and b/assets/light.png differ diff --git a/assets/main.png b/assets/main.png new file mode 100644 index 0000000..6ed190a Binary files /dev/null and b/assets/main.png differ diff --git a/assets/notification.jpeg b/assets/notification.jpeg new file mode 100644 index 0000000..36ee4e3 Binary files /dev/null and b/assets/notification.jpeg differ diff --git a/assets/settings.png b/assets/settings.png new file mode 100644 index 0000000..e98c98b Binary files /dev/null and b/assets/settings.png differ diff --git a/assets/time.png b/assets/time.png new file mode 100644 index 0000000..d978618 Binary files /dev/null and b/assets/time.png differ diff --git a/notification/course_schedule.json b/notification/course_schedule.json index 08ac924..3273095 100644 --- a/notification/course_schedule.json +++ b/notification/course_schedule.json @@ -1,62 +1,84 @@ -[ - { - "name": "第一节课", - "start": "2025-03-09T08:00:00", - "end": "2025-03-09T08:50:00" - }, - { - "name": "第二节课", - "start": "2025-03-09T09:00:00", - "end": "2025-03-09T09:50:00" - }, - { - "name": "第三节课", - "start": "2025-03-09T10:10:00", - "end": "2025-03-09T11:00:00" - }, - { - "name": "第四节课", - "start": "2025-03-09T11:10:00", - "end": "2025-03-09T12:00:00" - }, - { - "name": "第一节课", - "start": "2025-03-09T13:30:00", - "end": "2025-03-09T14:20:00" - }, - { - "name": "第二节课", - "start": "2025-03-09T14:30:00", - "end": "2025-03-09T15:20:00" - }, - { - "name": "第三节课", - "start": "2025-03-09T15:40:00", - "end": "2025-03-09T16:30:00" - }, - { - "name": "第四节课", - "start": "2025-03-09T16:40:00", - "end": "2025-03-09T17:30:00" - }, - { - "name": "第一节课", - "start": "2025-03-09T18:00:00", - "end": "2025-03-09T18:50:00" - }, - { - "name": "第二节课", - "start": "2025-03-09T19:00:00", - "end": "2025-03-09T19:50:00" - }, - { - "name": "第三节课", - "start": "2025-03-09T20:10:00", - "end": "2025-03-09T21:00:00" - }, - { - "name": "第四节课", - "start": "2025-03-09T21:10:00", - "end": "2025-03-10T00:15:00" - } -] \ No newline at end of file +{ + "examInfos": [ + { + "name": "第一节课", + "start": "2025-03-22T08:00:00", + "end": "2025-03-22T08:50:00" + }, + { + "name": "第二节课", + "start": "2025-03-22T09:00:00", + "end": "2025-03-22T09:50:00" + }, + { + "name": "第三节课", + "start": "2025-03-22T10:10:00", + "end": "2025-03-22T11:00:00" + }, + { + "name": "第四节课", + "start": "2025-03-22T11:10:00", + "end": "2025-03-22T12:00:00" + }, + { + "name": "第一节课", + "start": "2025-03-22T13:30:00", + "end": "2025-03-22T14:20:00" + }, + { + "name": "第二节课", + "start": "2025-03-22T14:30:00", + "end": "2025-03-22T15:20:00" + }, + { + "name": "第三节课", + "start": "2025-03-22T15:40:00", + "end": "2025-03-22T16:30:00" + }, + { + "name": "第四节课", + "start": "2025-03-22T16:40:00", + "end": "2025-03-22T17:30:00" + }, + { + "name": "第一节课", + "start": "2025-03-22T18:00:00", + "end": "2025-03-22T18:50:00" + }, + { + "name": "第二节课", + "start": "2025-03-22T19:00:00", + "end": "2025-03-22T19:50:00" + }, + { + "name": "第三节课", + "start": "2025-03-22T20:10:00", + "end": "2025-03-22T21:00:00" + }, + { + "name": "第四节课", + "start": "2025-03-22T21:10:00", + "end": "2025-03-23T01:30:00" + } + ], + "examName": "考试看板—广播适配", + "message": "1111111", + "room": "剩余时间", + "reminders": [ + { + "condition": "start", + "time": 0, + "audio": "考试开始" + }, + { + "condition": "end", + "time": 0, + "audio": "考试结束" + }, + { + "condition": "beforeEnd", + "time": "15", + "audio": "距离结束还有15分钟" + } + ] +} \ No newline at end of file diff --git a/notification/index.html b/notification/index.html index ba25298..d8ec2c9 100644 --- a/notification/index.html +++ b/notification/index.html @@ -3,9 +3,20 @@ 考试看板—广播适配 - + + + + + + + + + + + + - +
@@ -14,14 +25,22 @@
+
+

考试看板

+
+
+
+
- - +
正在初始化...
-
--:--
+
--:--
@@ -65,5 +84,6 @@ + \ No newline at end of file diff --git a/notification/scripts/audioController.js b/notification/scripts/audioController.js index d38e584..c2fd514 100644 --- a/notification/scripts/audioController.js +++ b/notification/scripts/audioController.js @@ -75,14 +75,38 @@ var audioController = (function() { } function populateAudioSelect() { + if (!Object.keys(soundFiles).length) { + // 如果音频文件还没加载完成,等待加载 + fetch('audio_files.json') + .then(response => response.json()) + .then(data => { + soundFiles = data; + _populateSelectOptions(); + }) + .catch(e => errorSystem.show('音频选项加载失败: ' + e.message, 'error')); + } else { + _populateSelectOptions(); + } + } + + function _populateSelectOptions() { var selects = document.querySelectorAll('select[name="audioSelect"]'); selects.forEach(select => { + // 保存当前选中的值 + var currentValue = select.value; + // 清空现有选项 + select.innerHTML = ''; + // 添加新选项 Object.keys(soundFiles).forEach(function(type) { var option = document.createElement('option'); option.value = type; option.textContent = type; select.appendChild(option); }); + // 恢复之前选中的值(如果该值仍然有效) + if (currentValue && soundFiles[currentValue]) { + select.value = currentValue; + } }); } @@ -102,7 +126,8 @@ var audioController = (function() { play: play, getAudioSrc: getAudioSrc, populateAudioSelect: populateAudioSelect, - removeInvalidAudioOptions: removeInvalidAudioOptions + removeInvalidAudioOptions: removeInvalidAudioOptions, + _populateSelectOptions: _populateSelectOptions }; })(); diff --git a/notification/scripts/config.js b/notification/scripts/config.js index f2729a7..e2b2d65 100644 --- a/notification/scripts/config.js +++ b/notification/scripts/config.js @@ -18,41 +18,116 @@ document.getElementById('importJson').addEventListener('change', function(event) function applyConfig(config) { try { - if (config.examInfos) { - courseSchedule = config.examInfos.map(function(exam) { - return { - name: exam.name, - start: exam.start, - end: exam.end - }; + fetch('audio_files.json') + .then(response => response.json()) + .then(audioFiles => { + const reminderCookie = getCookie("reminders"); + + // 获取有效的音频列表和默认音频 + let validAudioTypes = Object.keys(audioFiles); + let defaultAudio = validAudioTypes[0]; + + if (config.examInfos) { + courseSchedule = config.examInfos; + updateScheduleTable(); + } + if (config.examName) { + document.title = config.examName; + document.getElementById('examTitle').textContent = config.examName; + } + if (config.message) { + document.getElementById('examMessage').textContent = config.message; + } + if (config.room) { + document.getElementById('timeDescription').textContent = '考场: ' + config.room; + } + + // 验证并修复提醒中的音频设置 + if (config.reminders) { + config.reminders = config.reminders.map(reminder => { + if (!validAudioTypes.includes(reminder.audio)) { + errorSystem.show(`音频"${reminder.audio}"不存在,已替换为"${defaultAudio}"`, 'info'); + reminder.audio = defaultAudio; + } + return reminder; + }); + + // 清空现有提醒表 + var table = document.getElementById('reminderTable'); + while (table.rows.length > 2) { + table.deleteRow(1); + } + + // 添加新的提醒设置 + config.reminders.forEach(function(reminder) { + var row = table.insertRow(table.rows.length - 1); + let audioOptions = validAudioTypes + .map(audio => ``) + .join(''); + + row.innerHTML = ` + + + + + + + + + `; + row.cells[0].querySelector('select').addEventListener('change', function() { + row.cells[1].querySelector('input').disabled = this.value === 'start' || this.value === 'end'; + row.cells[1].querySelector('input').placeholder = this.value === 'start' || this.value === 'end' ? '-' : '分钟'; + }); + }); + + // 只更新提醒队列,不保存到 Cookie + loadRemindersToQueue(config.reminders); + } + + errorSystem.show('配置导入成功(临时生效)', 'info'); + }) + .catch(err => { + errorSystem.show('获取音频列表失败: ' + err.message, 'error'); }); - updateScheduleTable(); - } - if (config.examName) { - document.title = config.examName; - } - if (config.message) { - document.getElementById('statusLabel').textContent = config.message; - } - if (config.room) { - document.getElementById('timeDescription').textContent = '考场: ' + config.room; - } - if (config.reminders) { - localStorage.setItem('reminders', JSON.stringify(config.reminders)); - } } catch (err) { errorSystem.show('应用配置失败: ' + err.message, 'error'); } } function exportConfig() { + var table = document.getElementById('reminderTable'); + var reminders = []; + for (var i = 1; i < table.rows.length - 1; i++) { + var row = table.rows[i]; + var condition = row.cells[0].querySelector('select').value; + var timeInput = row.cells[1].querySelector('input'); + var audioSelect = row.cells[2].querySelector('select'); + if (timeInput && audioSelect) { + reminders.push({ + condition: condition, + time: timeInput.value || 0, + audio: audioSelect.value || 'classStart' + }); + } + } + var config = { examInfos: courseSchedule, examName: document.title, - message: document.getElementById('statusLabel').textContent, + message: document.getElementById('examMessage').textContent || "诚信考试,禁止作弊", room: document.getElementById('timeDescription').textContent.replace('考场: ', ''), - reminders: JSON.parse(localStorage.getItem('reminders') || '[]') + reminders: reminders }; + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config)); var downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); diff --git a/notification/scripts/courseSchedule.js b/notification/scripts/courseSchedule.js index 78e4e26..9e189a9 100644 --- a/notification/scripts/courseSchedule.js +++ b/notification/scripts/courseSchedule.js @@ -1,12 +1,84 @@ var courseSchedule = []; -fetch('course_schedule.json') - .then(response => response.json()) - .then(data => { - courseSchedule = data; - updateScheduleTable(); - }) - .catch(error => errorSystem.show('加载课程表失败: ' + error.message, 'error')); +// 将fetch移动到函数中以便控制初始化顺序 +function loadCourseSchedule() { + return fetch('course_schedule.json') + .then(response => response.json()) + .then(data => { + courseSchedule = data.examInfos || []; + document.title = data.examName || '考试看板'; + document.getElementById('examTitle').textContent = data.examName || '考试看板'; + document.getElementById('examMessage').textContent = data.message || ''; + document.getElementById('timeDescription').textContent = data.room ? '考场: ' + data.room : ''; + updateScheduleTable(); + + // 检查Cookie是否存在 + const reminderCookie = getCookie("reminders"); + if (!reminderCookie && data.reminders && Array.isArray(data.reminders)) { + // 如果Cookie不存在,加载配置文件中的提醒设置 + fetch('audio_files.json') + .then(response => response.json()) + .then(audioFiles => { + const validAudioTypes = Object.keys(audioFiles); + const defaultAudio = validAudioTypes[0]; + + // 验证并修复音频设置 + const reminders = data.reminders.map(reminder => { + if (!validAudioTypes.includes(reminder.audio)) { + reminder.audio = defaultAudio; + } + return reminder; + }); + + // 填充提醒表格 + var table = document.getElementById('reminderTable'); + while (table.rows.length > 2) { + table.deleteRow(1); + } + + reminders.forEach(reminder => { + var row = table.insertRow(table.rows.length - 1); + let audioOptions = validAudioTypes + .map(audio => ``) + .join(''); + + row.innerHTML = ` + + + + + + + + + `; + + row.cells[0].querySelector('select').addEventListener('change', function() { + row.cells[1].querySelector('input').disabled = this.value === 'start' || this.value === 'end'; + row.cells[1].querySelector('input').placeholder = this.value === 'start' || this.value === 'end' ? '-' : '分钟'; + }); + }); + + // 更新提醒队列并保存到Cookie + loadRemindersToQueue(reminders); + saveSettingsToCookies(); + }); + } + return courseSchedule; + }) + .catch(error => { + errorSystem.show('加载课程表失败: ' + error.message, 'error'); + return []; + }); +} function parseTime(timeStr) { try { @@ -59,25 +131,30 @@ function getNextCourse() { } } +// 修改更新表格函数,增加数据检查 function updateScheduleTable() { try { - var now = new Date(), - table = document.getElementById('scheduleTable'), - rows = table.querySelectorAll('tr:not(:first-child)'); - rows.forEach(row => row.remove()); // 清空现有行 + if (!Array.isArray(courseSchedule)) { + errorSystem.show('课程表数据格式错误', 'error'); + return; + } + + var now = new Date(); + var table = document.getElementById('scheduleTable'); + // 清空现有行,保留表头 + while (table.rows.length > 1) { + table.deleteRow(1); + } + courseSchedule.forEach(function(course) { var row = table.insertRow(-1); row.innerHTML = '' + course.name + '' + - '' + formatDateTime(course.start) + ' - ' + formatDateTime(course.end) + '' + - ''; - }); - for (var i = 0; i < courseSchedule.length; i++) { - var course = courseSchedule[i]; - if (!course) continue; // 确保不会超出数组边界 - var start = parseTime(course.start), - end = parseTime(course.end), - row = table.rows[i + 1]; // 跳过表头行 - row.className = ''; + '' + formatDateTime(course.start) + ' - ' + formatDateTime(course.end) + '' + + ''; + + var start = parseTime(course.start); + var end = parseTime(course.end); + if (now >= start && now <= end) { row.className = 'current-class'; row.cells[2].textContent = '进行中'; @@ -88,7 +165,7 @@ function updateScheduleTable() { row.className = 'past-class'; row.cells[2].textContent = '已结束'; } - } + }); } catch (e) { errorSystem.show('课程表更新失败: ' + e.message, 'error'); } diff --git a/notification/scripts/script.js b/notification/scripts/script.js index 421f20b..68c8805 100644 --- a/notification/scripts/script.js +++ b/notification/scripts/script.js @@ -1,5 +1,5 @@ // 全局状态变量 -var isFullscreen = false, currentCourse = null, lastCourse = null, timer = null, lastUpdate = Date.now(); +var lastCourse = null, timer = null, lastUpdate = Date.now(); // 新增:安全更新循环函数 function safeUpdate() { @@ -15,29 +15,6 @@ function safeUpdate() { } } -// 修改:全屏切换函数 -function toggleFullscreen() { - try { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen().then(() => { - isFullscreen = true; - document.body.classList.add('fullscreen-mode'); - adjustFontSize(); - document.getElementById('exitFullscreenBtn').style.display = 'block'; - }); - } else { - document.exitFullscreen().then(() => { - isFullscreen = false; - document.body.classList.remove('fullscreen-mode'); - adjustFontSize(); - document.getElementById('exitFullscreenBtn').style.display = 'none'; - }); - } - } catch (e) { - errorSystem.show('全屏切换失败: ' + e.message, 'error'); - } -} - function adjustFontSize() { var elements = document.querySelectorAll('.time-display, .status-label'); elements.forEach(element => { @@ -55,10 +32,15 @@ function adjustFontSize() { } } +function adjustCountdownFontSize() { + var countdownElement = document.getElementById('timeDisplay'); + var currentSize = parseFloat(window.getComputedStyle(countdownElement).fontSize); + countdownElement.style.fontSize = (currentSize + 5) + 'px'; +} + function addReminder() { var table = document.getElementById('reminderTable'); var row = table.insertRow(table.rows.length - 1); - row.draggable = true; row.innerHTML = ` - ☰ `; row.cells[0].querySelector('select').addEventListener('change', function() { row.cells[1].querySelector('input').disabled = this.value === 'start' || this.value === 'end'; row.cells[1].querySelector('input').placeholder = this.value === 'start' || this.value === 'end' ? '-' : '分钟'; }); audioController.populateAudioSelect(); - addDragAndDropHandlers(row); } function removeReminder(button) { @@ -90,29 +70,32 @@ function removeReminder(button) { } function saveConfig() { - var table = document.getElementById('reminderTable'); - var reminders = []; - for (var i = 1; i < table.rows.length - 1; i++) { - var row = table.rows[i]; - var condition = row.cells[0].querySelector('select').value; - var timeInput = row.cells[1].querySelector('input'); - var audioSelect = row.cells[2].querySelector('select'); - if (timeInput && audioSelect) { - var time = timeInput.value || 0; // 确保时间值不为空 - var audio = audioSelect.value || 'classStart'; // 确保音频选择不为空 - reminders.push({ condition: condition, time: time, audio: audio }); + try { + var table = document.getElementById('reminderTable'); + var reminders = []; + for (var i = 1; i < table.rows.length - 1; i++) { + var row = table.rows[i]; + var condition = row.cells[0].querySelector('select').value; + var timeInput = row.cells[1].querySelector('input'); + var audioSelect = row.cells[2].querySelector('select'); + if (timeInput && audioSelect) { + reminders.push({ + condition: condition, + time: timeInput.value || 0, + audio: audioSelect.value + }); + } } + if (reminders.length === 0) { + errorSystem.show('请添加至少一个提醒策略', 'error'); + return; + } + // 保存到 Cookie 并更新提醒队列 + saveSettingsToCookies(); + loadRemindersToQueue(reminders); + } catch (e) { + errorSystem.show('保存设置失败: ' + e.message, 'error'); } - var config = { - reminders: reminders, - examInfos: courseSchedule, - examName: document.title, - message: document.getElementById('statusLabel').textContent, - room: document.getElementById('timeDescription').textContent.replace('考场: ', '') - }; - localStorage.setItem('config', JSON.stringify(config)); - errorSystem.show('配置已保存', 'info'); - loadRemindersToQueue(reminders); } function loadRemindersToQueue(reminders) { @@ -168,55 +151,47 @@ function loadRemindersToQueue(reminders) { // 修改:系统初始化函数 function init() { try { - var table = document.getElementById('scheduleTable'); - courseSchedule.forEach(function(course) { - var row = table.insertRow(-1); - row.innerHTML = '' + course.name + '' + - '' + formatDateTime(course.start) + ' - ' + formatDateTime(course.end) + '' + - ''; - }); - // 初始化设置复选框 - var config = JSON.parse(localStorage.getItem('config') || '{}'); - // 加载提醒设置 - var reminders = config.reminders || []; - var table = document.getElementById('reminderTable'); - reminders.forEach(function(reminder) { - var row = table.insertRow(table.rows.length - 1); - row.draggable = true; - row.innerHTML = ` - - - - - - - - - ☰ - `; - row.cells[0].querySelector('select').addEventListener('change', function() { - row.cells[1].querySelector('input').disabled = this.value === 'start' || this.value === 'end'; - row.cells[1].querySelector('input').placeholder = this.value === 'start' || this.value === 'end' ? '-' : '分钟'; + // 先加载课程表,然后再初始化其他内容 + loadCourseSchedule().then(() => { + // 加载配置 + var config = JSON.parse(localStorage.getItem('config') || '{}'); + + // 加载提醒设置 + var reminders = config.reminders || []; + var table = document.getElementById('reminderTable'); + reminders.forEach(function(reminder) { + var row = table.insertRow(table.rows.length - 1); + row.innerHTML = ` + + + + + + + + + `; + row.cells[0].querySelector('select').addEventListener('change', function() { + row.cells[1].querySelector('input').disabled = this.value === 'start' || this.value === 'end'; + row.cells[1].querySelector('input').placeholder = this.value === 'start' || this.value === 'end' ? '-' : '分钟'; + }); }); - addDragAndDropHandlers(row); + // 启动安全更新循环 + safeUpdate(); + // 加载设置从Cookies + loadSettingsFromCookies(); + // 加载提醒到队列 + loadRemindersToQueue(reminders); }); - // 启动安全更新循环 - safeUpdate(); - // 移除或修改音频权限激活代码(用原音频系统无需特殊激活) - document.body.onclick = null; - // 加载设置从Cookies - loadSettingsFromCookies(); - // 加载提醒到队列 - loadRemindersToQueue(reminders); } catch (e) { errorSystem.show('系统初始化失败: ' + e.message); } @@ -224,42 +199,4 @@ function init() { window.onbeforeunload = function () { if (timer) clearTimeout(timer); }; -init(); - -function addDragAndDropHandlers(row) { - row.addEventListener('dragstart', function(e) { - e.dataTransfer.setData('text/plain', e.target.rowIndex); - e.target.classList.add('dragging'); - }); - - row.addEventListener('dragover', function(e) { - e.preventDefault(); - var draggingRow = document.querySelector('.dragging'); - if (draggingRow && draggingRow !== e.target) { - var table = document.getElementById('reminderTable'); - var rows = Array.from(table.rows).slice(1, -1); - var targetRow = rows.find(row => row === e.target || row.contains(e.target)); - if (targetRow && targetRow.parentNode === table) { - var targetIndex = targetRow.rowIndex; - var draggingIndex = draggingRow.rowIndex; - if (draggingIndex < targetIndex) { - table.insertBefore(draggingRow, targetRow.nextSibling); - } else { - table.insertBefore(draggingRow, targetRow); - } - } - } - }); - - row.addEventListener('drop', function(e) { - e.preventDefault(); - var draggingRow = document.querySelector('.dragging'); - if (draggingRow) { - draggingRow.classList.remove('dragging'); - } - }); - - row.addEventListener('dragend', function(e) { - e.target.classList.remove('dragging'); - }); -} \ No newline at end of file +init(); \ No newline at end of file diff --git a/notification/scripts/settings.js b/notification/scripts/settings.js index c397c8d..f41390e 100644 --- a/notification/scripts/settings.js +++ b/notification/scripts/settings.js @@ -1,55 +1,149 @@ function saveSettingsToCookies() { - var table = document.getElementById('reminderTable'); - var reminders = []; - for (var i = 1; i < table.rows.length - 1; i++) { - var row = table.rows[i]; - var condition = row.cells[0].querySelector('select').value; - var timeInput = row.cells[1].querySelector('input'); - var audioSelect = row.cells[2].querySelector('select'); - if (timeInput && audioSelect) { - var time = timeInput.value || 0; // 确保时间值不为空 - var audio = audioSelect.value || 'classStart'; // 确保音频选择不为空 - reminders.push({ condition: condition, time: time, audio: audio }); + try { + var table = document.getElementById('reminderTable'); + var reminders = []; + for (var i = 1; i < table.rows.length - 1; i++) { + var row = table.rows[i]; + var condition = row.cells[0].querySelector('select').value; + var timeInput = row.cells[1].querySelector('input'); + var audioSelect = row.cells[2].querySelector('select'); + if (timeInput && audioSelect) { + var time = timeInput.value || 0; + var audio = audioSelect.value; + reminders.push({ condition: condition, time: time, audio: audio }); + } } + + if (reminders.length === 0) { + errorSystem.show('请添加至少一个提醒策略', 'error'); + return false; + } + + // 编码并保存到 Cookie + const remindersStr = encodeURIComponent(JSON.stringify(reminders)); + setCookie("reminders", remindersStr, 365); + + // 更新提醒队列 + loadRemindersToQueue(reminders); + + errorSystem.show('提醒设置已保存', 'info'); + return true; + } catch (e) { + errorSystem.show('保存设置失败: ' + e.message, 'error'); + return false; } - document.cookie = "reminders=" + JSON.stringify(reminders); } function loadSettingsFromCookies() { - var cookies = document.cookie.split(';'); - cookies.forEach(function(cookie) { - var parts = cookie.split('='); - var name = parts[0].trim(); - var value = parts[1].trim(); - if (name === 'reminders') { - var reminders = JSON.parse(value); - var table = document.getElementById('reminderTable'); - reminders.forEach(function(reminder) { - var row = table.insertRow(table.rows.length - 1); - row.innerHTML = ` - - - - - - - - - `; - row.cells[0].querySelector('select').addEventListener('change', function() { - row.cells[1].querySelector('input').disabled = this.value === 'start' || this.value === 'end'; - row.cells[1].querySelector('input').placeholder = this.value === 'start' || this.value === 'end' ? '-' : '分钟'; - }); - }); + try { + const reminderCookie = getCookie("reminders"); + if (reminderCookie) { + // 解码并解析 Cookie 值 + const reminders = JSON.parse(decodeURIComponent(reminderCookie)); + if (Array.isArray(reminders)) { + // 清空现有提醒 + var table = document.getElementById('reminderTable'); + while (table.rows.length > 2) { + table.deleteRow(1); + } + + // 使用 audio_files.json 的数据填充提醒表 + fetch('audio_files.json') + .then(response => response.json()) + .then(audioFiles => { + const validAudioTypes = Object.keys(audioFiles); + const defaultAudio = validAudioTypes[0]; + + reminders.forEach(function(reminder) { + // 检查音频是否有效 + if (!validAudioTypes.includes(reminder.audio)) { + reminder.audio = defaultAudio; + } + + var row = table.insertRow(table.rows.length - 1); + let audioOptions = validAudioTypes + .map(audio => ``) + .join(''); + + row.innerHTML = ` + + + + + + + + + `; + + row.cells[0].querySelector('select').addEventListener('change', function() { + row.cells[1].querySelector('input').disabled = this.value === 'start' || this.value === 'end'; + row.cells[1].querySelector('input').placeholder = this.value === 'start' || this.value === 'end' ? '-' : '分钟'; + }); + }); + + // 更新提醒队列 + loadRemindersToQueue(reminders); + }) + .catch(error => { + errorSystem.show('加载音频文件列表失败: ' + error.message, 'error'); + }); + } } - }); + } catch (e) { + errorSystem.show('加载设置失败: ' + e.message, 'error'); + } } + +document.addEventListener("DOMContentLoaded", () => { + const themeToggle = document.getElementById("theme-toggle"); + + let theme = getCookie("theme") || "light"; + + if (theme === "light") { + document.body.classList.remove("dark-mode"); + themeToggle.checked = false; + } else { + document.body.classList.add("dark-mode"); + themeToggle.checked = true; + } + + themeToggle.addEventListener("change", () => { + const theme = themeToggle.checked ? "dark" : "light"; + if (theme === "light") { + document.body.classList.remove("dark-mode"); + } else { + document.body.classList.add("dark-mode"); + } + setCookie("theme", theme, 365); + }); +}); + +function getCookie(name) { + const nameEQ = name + "="; + const ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +function setCookie(name, value, days) { + var expires = ""; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} \ No newline at end of file diff --git a/notification/scripts/utils.js b/notification/scripts/utils.js new file mode 100644 index 0000000..596e588 --- /dev/null +++ b/notification/scripts/utils.js @@ -0,0 +1,48 @@ +function setCookie(name, value, days) { + try { + const d = new Date(); + d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); + const expires = "expires=" + d.toUTCString(); + document.cookie = name + "=" + value + ";" + expires + ";path=/;SameSite=Strict"; + } catch (e) { + console.error('设置 Cookie 失败:', e); + } +} + +function getCookie(name) { + try { + const nameEQ = name + "="; + const cookies = document.cookie.split(';'); + for (let cookie of cookies) { + cookie = cookie.trim(); + if (cookie.indexOf(nameEQ) === 0) { + return cookie.substring(nameEQ.length); + } + } + } catch (e) { + console.error('读取 Cookie 失败:', e); + } + return null; +} + +function formatTimeWithoutSeconds(time) { + return time.slice(0, -3); +} + +const errorSystem = { + show: function(message) { + try { + const container = document.querySelector('.error-container'); + const content = document.getElementById('errorMessage'); + content.textContent = message; + container.style.display = 'flex'; + setTimeout(this.hide, 5000); + } catch(e) { + console.error('错误提示系统异常:', e); + } + }, + hide: function() { + const container = document.querySelector('.error-container'); + if (container) container.style.display = 'none'; + } +}; diff --git a/notification/styles/action-btn.css b/notification/styles/action-btn.css new file mode 100644 index 0000000..21f1d5e --- /dev/null +++ b/notification/styles/action-btn.css @@ -0,0 +1,17 @@ +.action-btn { + background: #27ae60; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 15px; + font-weight: 500; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(39, 174, 96, 0.25); + margin-top: 10px; +} +.action-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(39, 174, 96, 0.35); +} diff --git a/notification/styles/base.css b/notification/styles/base.css new file mode 100644 index 0000000..eeb6c68 --- /dev/null +++ b/notification/styles/base.css @@ -0,0 +1,12 @@ +body { + font-family: 'Segoe UI', Arial, sans-serif; + margin: 0; + padding: 20px; + background: #f5f7fa; + color: #2d3436; +} +.container { + max-width: 800px; + margin: 0 auto; + position: relative; +} diff --git a/notification/styles/control-btn.css b/notification/styles/control-btn.css new file mode 100644 index 0000000..dd34b0d --- /dev/null +++ b/notification/styles/control-btn.css @@ -0,0 +1,24 @@ +.control-bar { + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; + display: flex; + gap: 10px; +} +.control-btn { + background: linear-gradient(135deg, #3498db, #2980b9); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + cursor: pointer; + font-size: 15px; + font-weight: 500; + transition: all 0.2s ease; + box-shadow: 0 4px 12px rgba(52,152,219,0.25); +} +.control-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(52,152,219,0.35); +} diff --git a/notification/styles/dark-mode.css b/notification/styles/dark-mode.css new file mode 100644 index 0000000..f6258ad --- /dev/null +++ b/notification/styles/dark-mode.css @@ -0,0 +1,106 @@ +body.dark-mode { + background: #2d3436; + color: #e0e0e0; +} +body.dark-mode .status-box { + background: linear-gradient(145deg, #2d3436, #3a3a3a); + border: 1px solid #444; +} +body.dark-mode .time-display { + color: #ecf0f1; +} +body.dark-mode .status-label { + color: #bdc3c7; +} +body.dark-mode .schedule-table { + background: #3a3a3a; +} +body.dark-mode .schedule-table th { + background: #444; + color: #ecf0f1; +} +body.dark-mode .schedule-table td { + border-bottom: 1px solid #555; +} +body.dark-mode .current-class { + background: #34495e !important; +} +body.dark-mode .future-class { + background: #3a3a3a !important; +} +body.dark-mode .past-class { + background: #2d3436 !important; + color: #7f8c8d; +} +body.dark-mode .control-btn { + background: linear-gradient(135deg, #2980b9, #3498db); +} +body.dark-mode .control-btn:hover { + box-shadow: 0 6px 16px rgba(52, 152, 219, 0.35); +} +body.dark-mode .settings-panel { + background: rgba(45, 52, 54, 0.95); + border: 1px solid #444; +} +body.dark-mode .settings-panel h3 { + color: #ecf0f1; +} +body.dark-mode .settings-panel label { + color: #bdc3c7; +} +body.dark-mode .settings-panel input[type="number"] { + background: #3a3a3a; + color: #ecf0f1; + border: 1px solid #555; +} +body.dark-mode .settings-panel input[type="file"] { + background: #3a3a3a; + color: #ecf0f1; + border: 1px solid #555; +} +body.dark-mode .file-input-label { + background: #27ae60; +} +body.dark-mode .file-input-label:hover { + background: #219150; +} +body.dark-mode .info-container { + background: #2980b9; +} +body.dark-mode .info-content:before { + color: #2980b9; +} +body.dark-mode .error-container { + background: #c0392b; +} +body.dark-mode .error-content:before { + color: #c0392b; +} +body.dark-mode .reminder-table { + background: #3a3a3a; +} +body.dark-mode .reminder-table th { + background: #444; + color: #ecf0f1; +} +body.dark-mode .reminder-table td { + border-bottom: 1px solid #555; +} +body.dark-mode .reminder-table td input, +body.dark-mode .reminder-table td select { + background: #3a3a3a; + color: #ecf0f1; + border: 1px solid #555; +} +body.dark-mode .reminder-table td button { + background: #e74c3c; +} +body.dark-mode .reminder-table td button:hover { + background: #c0392b; +} +body.dark-mode .action-btn { + background: #27ae60; +} +body.dark-mode .action-btn:hover { + box-shadow: 0 6px 16px rgba(39, 174, 96, 0.35); +} diff --git a/notification/styles/file-input-label.css b/notification/styles/file-input-label.css new file mode 100644 index 0000000..3e8a1e8 --- /dev/null +++ b/notification/styles/file-input-label.css @@ -0,0 +1,15 @@ +.file-input-label { + display: inline-block; + padding: 10px 20px; + background: #27ae60; + color: white; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s ease; +} +.file-input-label:hover { + background: #219150; +} +.file-input-label input[type="file"] { + display: none; +} diff --git a/notification/styles/fullscreen-mode.css b/notification/styles/fullscreen-mode.css new file mode 100644 index 0000000..5b97db7 --- /dev/null +++ b/notification/styles/fullscreen-mode.css @@ -0,0 +1,40 @@ +.fullscreen-mode { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #ffffff; + z-index: 9999; + padding: 0; + margin: 0; + overflow: hidden; +} +.fullscreen-mode .status-box { + width: 100%; + height: 100%; + border: none; + box-shadow: none; + border-radius: 0; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: none; +} +.fullscreen-mode .time-display { + font-size: 25vw; + line-height: 0.9; + color: #2c3e50; +} +.fullscreen-mode .status-label { + font-size: 6vw; + margin-top: 4vw; + color: #57606f; +} +.fullscreen-mode .control-bar, +.fullscreen-mode .settings-panel, +.fullscreen-mode .schedule-table { + display: none !important; +} diff --git a/notification/styles/info-error.css b/notification/styles/info-error.css new file mode 100644 index 0000000..a839488 --- /dev/null +++ b/notification/styles/info-error.css @@ -0,0 +1,69 @@ +.info-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #3498db; + color: white; + padding: 16px; + display: none; + z-index: 10001; + animation: slideUp 0.3s ease; +} +@keyframes slideUp { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} +.info-content { + max-width: 800px; + margin: 0 auto; + font-size: 15px; + display: flex; + align-items: center; + gap: 12px; +} +.info-content:before { + content: 'i'; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: white; + color: #3498db; + border-radius: 50%; + font-weight: bold; +} + +.error-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: #ff6b6b; + color: white; + padding: 16px; + display: none; + z-index: 10001; + animation: slideUp 0.3s ease; +} +.error-content { + max-width: 800px; + margin: 0 auto; + font-size: 15px; + display: flex; + align-items: center; + gap: 12px; +} +.error-content:before { + content: '!'; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: white; + color: #ff6b6b; + border-radius: 50%; + font-weight: bold; +} diff --git a/notification/styles/reminder-table.css b/notification/styles/reminder-table.css new file mode 100644 index 0000000..8839630 --- /dev/null +++ b/notification/styles/reminder-table.css @@ -0,0 +1,55 @@ +.reminder-table { + width: 100%; + border-collapse: collapse; + margin: 15px 0; + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} +.reminder-table th, .reminder-table td { + padding: 12px; + border-bottom: 1px solid #f1f3f6; + text-align: left; +} +.reminder-table th { + background: #f8f9fa; + color: #57606f; + font-weight: 600; +} +.reminder-table td { + padding: 12px; + border-bottom: 1px solid #f1f3f6; + text-align: left; + cursor: move; /* 添加拖拽光标 */ +} +.reminder-table td.drag-handle { + cursor: grab; /* 添加拖拽光标 */ +} +.reminder-table tr.dragging { + opacity: 0.5; /* 拖拽时透明度 */ +} +.reminder-table td input, .reminder-table td select { + width: 100%; + padding: 8px; + border: 1px solid #e0e6ed; + border-radius: 4px; + font-size: 14px; + color: #2d3436; +} +.reminder-table td button { + background: #e74c3c; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s ease; +} +.reminder-table td button:hover { + background: #c0392b; +} +.reminder-table td:last-child { + text-align: center; +} diff --git a/notification/styles/schedule-table.css b/notification/styles/schedule-table.css new file mode 100644 index 0000000..3137192 --- /dev/null +++ b/notification/styles/schedule-table.css @@ -0,0 +1,40 @@ +.schedule-table { + width: 100%; + border-collapse: collapse; + margin: 25px 0; + background: white; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} +.schedule-table th { + background: #f8f9fa; + padding: 16px; + color: #57606f; + font-weight: 600; + border-bottom: 2px solid #e0e6ed; +} +.schedule-table td { + padding: 14px; + border-bottom: 1px solid #f1f3f6; +} +.current-class { + background: #e8f5e9 !important; + position: relative; +} +.current-class:after { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 4px; + background: #2ecc71; +} +.future-class { + background: #f8f9fa !important; +} +.past-class { + background: #fafafa !important; + color: #a4b0be; +} diff --git a/notification/styles/settings-panel.css b/notification/styles/settings-panel.css new file mode 100644 index 0000000..046b2b3 --- /dev/null +++ b/notification/styles/settings-panel.css @@ -0,0 +1,60 @@ +.settings-panel { + background: rgba(255,255,255,0.95); + padding: 25px; + margin: 25px 0; + border-radius: 12px; + border: 1px solid #e0e6ed; + backdrop-filter: blur(8px); +} +.settings-panel h3 { + margin: 0 0 20px; + color: #2c3e50; + font-size: 20px; +} +.settings-panel label { + display: flex; + align-items: center; + gap: 10px; + margin: 12px 0; + font-size: 16px; + color: #57606f; + flex-wrap: wrap; /* 解决文本框显示在页面外的问题 */ +} +.settings-panel input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: #3498db; +} +.settings-panel input[type="number"] { + width: 60px; + padding: 5px; + border: 1px solid #e0e6ed; + border-radius: 4px; + font-size: 14px; + color: #2d3436; + margin-right: 10px; +} +.settings-panel input[type="file"] { + margin-top: 10px; +} +.file-input-label { + display: inline-block; + padding: 10px 20px; + background: #27ae60; + color: white; + border-radius: 8px; + cursor: pointer; + transition: background 0.3s ease; +} +.file-input-label:hover { + background: #219150; +} +.file-input-label input[type="file"] { + display: none; +} +.button-group { + display: flex; + justify-content: space-between; + gap: 10px; + margin-top: 20px; +} diff --git a/notification/styles/status-box.css b/notification/styles/status-box.css new file mode 100644 index 0000000..cf29f97 --- /dev/null +++ b/notification/styles/status-box.css @@ -0,0 +1,24 @@ +.status-box { + background: linear-gradient(145deg, #ffffff, #f8f9fa); + border: 1px solid #e0e6ed; + padding: 30px; + margin: 20px 0; + border-radius: 16px; + box-shadow: 0 8px 24px rgba(0,0,0,0.06); + transition: all 0.3s ease; +} +.time-display { + font-size: 48px; + color: #2c3e50; + font-weight: 700; + margin: 15px 0; + text-align: center; + letter-spacing: 2px; + text-shadow: 0 2px 4px rgba(0,0,0,0.1); +} +.status-label { + font-size: 28px; + text-align: center; + margin: 15px 0; + color: #57606f; +} diff --git a/notification/styles/style.css b/notification/styles/style.css index b657603..f8ac5ed 100644 --- a/notification/styles/style.css +++ b/notification/styles/style.css @@ -12,6 +12,34 @@ body { position: relative; } +/* 考试信息板块 */ +.exam-info { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: white; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} + +.exam-info h1 { + margin: 0; + font-size: 32px; + color: #2c3e50; + margin-bottom: 10px; +} + +.exam-room { + font-size: 18px; + color: #57606f; +} + +.exam-message { + font-size: 20px; + color: #57606f; + margin-top: 10px; +} + /* 状态显示框 */ .status-box { background: linear-gradient(145deg, #ffffff, #f8f9fa); @@ -80,46 +108,9 @@ body { color: #a4b0be; } -/* 全屏模式 */ +/* 移除全屏模式相关样式 */ .fullscreen-mode { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: #ffffff; - z-index: 9999; - padding: 0; - margin: 0; - overflow: hidden; -} -.fullscreen-mode .status-box { - width: 100%; - height: 100%; - border: none; - box-shadow: none; - border-radius: 0; - padding: 0; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background: none; -} -.fullscreen-mode .time-display { - font-size: 25vw; - line-height: 0.9; - color: #2c3e50; -} -.fullscreen-mode .status-label { - font-size: 6vw; - margin-top: 4vw; - color: #57606f; -} -.fullscreen-mode .control-bar, -.fullscreen-mode .settings-panel, -.fullscreen-mode .schedule-table { - display: none !important; + display: none; } /* 控制按钮 */ @@ -374,3 +365,115 @@ body { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(39, 174, 96, 0.35); } + +/* 明暗色切换 */ +body.dark-mode { + background: #2d3436; + color: #e0e0e0; +} +body.dark-mode .status-box { + background: linear-gradient(145deg, #2d3436, #3a3a3a); + border: 1px solid #444; +} +body.dark-mode .time-display { + color: #ecf0f1; +} +body.dark-mode .status-label { + color: #bdc3c7; +} +body.dark-mode .schedule-table { + background: #3a3a3a; +} +body.dark-mode .schedule-table th { + background: #444; + color: #ecf0f1; +} +body.dark-mode .schedule-table td { + border-bottom: 1px solid #555; +} +body.dark-mode .current-class { + background: #34495e !important; +} +body.dark-mode .future-class { + background: #3a3a3a !important; +} +body.dark-mode .past-class { + background: #2d3436 !important; + color: #7f8c8d; +} +body.dark-mode .control-btn { + background: linear-gradient(135deg, #2980b9, #3498db); +} +body.dark-mode .control-btn:hover { + box-shadow: 0 6px 16px rgba(52, 152, 219, 0.35); +} +body.dark-mode .settings-panel { + background: rgba(45, 52, 54, 0.95); + border: 1px solid #444; +} +body.dark-mode .settings-panel h3 { + color: #ecf0f1; +} +body.dark-mode .settings-panel label { + color: #bdc3c7; +} +body.dark-mode .settings-panel input[type="number"] { + background: #3a3a3a; + color: #ecf0f1; + border: 1px solid #555; +} +body.dark-mode .settings-panel input[type="file"] { + background: #3a3a3a; + color: #ecf0f1; + border: 1px solid #555; +} +body.dark-mode .file-input-label { + background: #27ae60; +} +body.dark-mode .file-input-label:hover { + background: #219150; +} +body.dark-mode .info-container { + background: #2980b9; +} +body.dark-mode .info-content:before { + color: #2980b9; +} +body.dark-mode .error-container { + background: #c0392b; +} +body.dark-mode .error-content:before { + color: #c0392b; +} +body.dark-mode .reminder-table { + background: #3a3a3a; +} +body.dark-mode .reminder-table th { + background: #444; + color: #ecf0f1; +} +body.dark-mode .reminder-table td { + border-bottom: 1px solid #555; +} +body.dark-mode .reminder-table td input, +body.dark-mode .reminder-table td select { + background: #3a3a3a; + color: #ecf0f1; + border: 1px solid #555; +} +body.dark-mode .reminder-table td button { + background: #e74c3c; +} +body.dark-mode .reminder-table td button:hover { + background: #c0392b; +} +body.dark-mode .action-btn { + background: #27ae60; +} +body.dark-mode .action-btn:hover { + box-shadow: 0 6px 16px rgba(39, 174, 96, 0.35); +} + +body.dark-mode .exam-message { + color: #bdc3c7; +} diff --git a/notification/styles/switch.css b/notification/styles/switch.css new file mode 100644 index 0000000..802bf42 --- /dev/null +++ b/notification/styles/switch.css @@ -0,0 +1,44 @@ +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .4s; + border-radius: 34px; +} + +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: #2196F3; +} + +input:checked + .slider:before { + transform: translateX(26px); +}