Notification (#10)

* refactor: 史诗级重构拆分多年狮山代码

* style: 添加设置模态框淡出动画,优化关闭和保存设置的用户体验

* style: 添加错误提示系统,优化用户体验,处理设置和全屏操作中的异常

* feat: 添加主题切换功能,支持亮/暗色模式,优化用户界面

* chore: 测试配置

* feat: 更新亮色主题背景和透明度

* style: 滑块居右

* feat: 初步实现 全是Bug

* feat: 添加提醒队列功能,支持音频预加载和配置导出

* feat: 优化文件输入和按钮样式,提升用户交互体验

* feat: 添加时间广播功能,优化错误提示系统,移除不必要的设置选项

* feat: 移除不再使用的音频文件,更新课程结束时间,优化音频文件加载和选择功能
This commit is contained in:
MKStoler1024 2025-03-10 00:19:35 +08:00 committed by GitHub
parent b14587b9cc
commit 29bb709eae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1220 additions and 0 deletions

View File

@ -91,6 +91,7 @@
<ul>
<li><a href="./time/">电子钟表</a></li>
<li><a href="./exam/">考试看板</a></li>
<li><a href="./notification/">时间广播</a></li>
</ul>
<!-- 添加GitHub图标跳转项目地址 -->

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
notification/audio/end.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,10 @@
{
"上课铃": "audio/classstart.mp3",
"距离结束还有15分钟": "audio/15min_left.mp3",
"距离结束还有30分钟": "audio/30min_left.mp3",
"考试开始": "audio/start.mp3",
"考试结束": "audio/end.mp3",
"核对信息": "audio/hedui_noeng.mp3",
"检场": "audio/jinchang.mp3",
"展示科目袋": "audio/zhanshikemudai.mp3"
}

View File

@ -0,0 +1,62 @@
[
{
"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"
}
]

69
notification/index.html Normal file
View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>考试看板—广播适配</title>
<link rel="stylesheet" href="styles/style.css">
</head>
<body>
<div class="info-container">
<div class="info-content" id="infoMessage"></div>
</div>
<div class="error-container">
<div class="error-content" id="errorMessage"></div>
</div>
<div class="container">
<div class="control-bar">
<button class="control-btn" id="fullscreen-btn" onclick="toggleFullscreen()">全屏</button>
<button class="control-btn" id="exitFullscreenBtn" onclick="toggleFullscreen()" style="display: none;">退出全屏</button>
</div>
<div class="status-box">
<div class="status-label" id="statusLabel">正在初始化...</div>
<div class="time-display" id="timeDisplay">--:--</div>
<div id="timeDescription" style="text-align:center;color:#a4b0be;"></div>
</div>
<div class="settings-panel">
<h3>系统设置</h3>
<div class="button-group">
<label class="file-input-label">
<input type="file" id="importJson" accept=".json" multiple> 导入考试配置
</label>
<button class="action-btn" onclick="exportConfig()">导出配置</button>
</div>
<h3>提醒设置</h3>
<table class="reminder-table" id="reminderTable">
<tr>
<th>提醒条件</th>
<th>时间(分钟)</th>
<th>音频选择</th>
<th>操作</th>
</tr>
<tr>
<td colspan="4"><button class="action-btn" onclick="addReminder()">添加提醒</button></td>
</tr>
</table>
<button class="action-btn" onclick="saveConfig()">保存提醒设置</button>
</div>
<table class="schedule-table" id="scheduleTable">
<tr>
<th>科目</th>
<th>时间段</th>
<th>状态</th>
</tr>
</table>
</div>
<script src="scripts/errorSystem.js"></script>
<script src="scripts/audioController.js"></script>
<script src="scripts/courseSchedule.js"></script>
<script src="scripts/display.js"></script>
<script src="scripts/config.js"></script>
<script src="scripts/settings.js"></script>
<script src="scripts/reminderQueue.js"></script>
<script src="scripts/script.js"></script>
</body>
</html>

View File

@ -0,0 +1,109 @@
var audioController = (function() {
var audioPool = [];
var maxPoolSize = 3;
var soundFiles = {};
var audioSelectPopulated = false;
function init() {
fetch('audio_files.json')
.then(response => response.json())
.then(data => {
soundFiles = data;
Object.keys(soundFiles).forEach(function(type) {
for (var i = 0; i < 2; i++) {
createAudio(type);
}
});
if (!audioSelectPopulated) {
populateAudioSelect();
audioSelectPopulated = true;
}
removeInvalidAudioOptions();
})
.catch(e => errorSystem.show('音频文件加载失败: ' + e.message, 'error'));
}
function createAudio(type) {
var audio = document.createElement('audio');
audio.style.display = 'none';
audio.preload = 'auto';
audio.src = soundFiles[type];
var retryCount = 0;
function loadAudio() {
try {
audio.load();
} catch(e) {
if (retryCount++ < 3) {
setTimeout(loadAudio, 1000);
}
}
}
audio.addEventListener('error', function() {
if (retryCount++ < 3) {
setTimeout(loadAudio, 1000);
}
});
document.body.appendChild(audio);
loadAudio();
audioPool.push(audio);
return audio;
}
function play(type) {
try {
var audio = audioPool.find(function(a) { return a.paused; });
if (!audio) {
if (audioPool.length < maxPoolSize) {
audio = createAudio(type);
} else {
return errorSystem.show('系统繁忙,请稍后再试', 'error');
}
}
audio.src = soundFiles[type];
try {
audio.play();
} catch(e) {
errorSystem.show('播放失败: ' + e.message, 'error');
}
} catch(e) {
errorSystem.show('音频系统错误: ' + e.message, 'error');
}
}
function getAudioSrc(type) {
return soundFiles[type];
}
function populateAudioSelect() {
var selects = document.querySelectorAll('select[name="audioSelect"]');
selects.forEach(select => {
Object.keys(soundFiles).forEach(function(type) {
var option = document.createElement('option');
option.value = type;
option.textContent = type;
select.appendChild(option);
});
});
}
function removeInvalidAudioOptions() {
var selects = document.querySelectorAll('select[name="audioSelect"]');
selects.forEach(select => {
Array.from(select.options).forEach(option => {
if (!soundFiles[option.value]) {
option.remove();
}
});
});
}
return {
init: init,
play: play,
getAudioSrc: getAudioSrc,
populateAudioSelect: populateAudioSelect,
removeInvalidAudioOptions: removeInvalidAudioOptions
};
})();
audioController.init();

View File

@ -0,0 +1,63 @@
document.getElementById('importJson').addEventListener('change', function(event) {
var files = event.target.files;
if (files.length > 0) {
Array.from(files).forEach(file => {
var reader = new FileReader();
reader.onload = function(e) {
try {
var config = JSON.parse(e.target.result);
applyConfig(config);
} catch (err) {
errorSystem.show('导入配置失败: ' + err.message, 'error');
}
};
reader.readAsText(file);
});
}
});
function applyConfig(config) {
try {
if (config.examInfos) {
courseSchedule = config.examInfos.map(function(exam) {
return {
name: exam.name,
start: exam.start,
end: exam.end
};
});
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 config = {
examInfos: courseSchedule,
examName: document.title,
message: document.getElementById('statusLabel').textContent,
room: document.getElementById('timeDescription').textContent.replace('考场: ', ''),
reminders: JSON.parse(localStorage.getItem('reminders') || '[]')
};
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config));
var downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "exam_config.json");
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
}

View File

@ -0,0 +1,95 @@
var courseSchedule = [];
fetch('course_schedule.json')
.then(response => response.json())
.then(data => {
courseSchedule = data;
updateScheduleTable();
})
.catch(error => errorSystem.show('加载课程表失败: ' + error.message, 'error'));
function parseTime(timeStr) {
try {
return new Date(timeStr);
} catch (e) {
errorSystem.show('时间解析错误: ' + e.message, 'info', 'error');
return new Date();
}
}
function updateCourseStatus() {
try {
var now = new Date();
currentCourse = null;
for (var i = 0; i < courseSchedule.length; i++) {
var course = courseSchedule[i],
start = parseTime(course.start),
end = parseTime(course.end);
if (end < start) end.setDate(end.getDate() + 1);
if (now >= start && now <= end) {
currentCourse = course;
break;
}
}
if (currentCourse !== lastCourse) {
handleStatusChange();
lastCourse = currentCourse;
}
} catch (e) {
errorSystem.show('课程状态更新失败: ' + e.message, 'error');
}
}
function handleStatusChange() {
// 处理状态变化的逻辑
console.log('课程状态已更改:', currentCourse);
}
function getNextCourse() {
try {
var now = new Date();
for (var i = 0; i < courseSchedule.length; i++) {
var start = parseTime(courseSchedule[i].start);
if (start > now) return courseSchedule[i];
}
return null;
} catch (e) {
errorSystem.show('获取下一节课失败: ' + e.message, 'error');
return null;
}
}
function updateScheduleTable() {
try {
var now = new Date(),
table = document.getElementById('scheduleTable'),
rows = table.querySelectorAll('tr:not(:first-child)');
rows.forEach(row => row.remove()); // 清空现有行
courseSchedule.forEach(function(course) {
var row = table.insertRow(-1);
row.innerHTML = '<td>' + course.name + '</td>' +
'<td>' + formatDateTime(course.start) + ' - ' + formatDateTime(course.end) + '</td>' +
'<td></td>';
});
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 = '';
if (now >= start && now <= end) {
row.className = 'current-class';
row.cells[2].textContent = '进行中';
} else if (now < start) {
row.className = 'future-class';
row.cells[2].textContent = '即将开始';
} else {
row.className = 'past-class';
row.cells[2].textContent = '已结束';
}
}
} catch (e) {
errorSystem.show('课程表更新失败: ' + e.message, 'error');
}
}

View File

@ -0,0 +1,53 @@
function updateDisplay() {
try {
var now = new Date(),
timeDisplay = document.getElementById('timeDisplay'),
statusLabel = document.getElementById('statusLabel'),
timeDesc = document.getElementById('timeDescription');
if (currentCourse) {
var endTime = parseTime(currentCourse.end),
remain = endTime - now;
statusLabel.textContent = currentCourse.name + ' 进行中';
timeDisplay.textContent = formatTime(remain);
timeDesc.textContent = '剩余时间';
} else {
statusLabel.textContent = '休息中';
var nextCourse = getNextCourse();
if (nextCourse) {
var startTime = parseTime(nextCourse.start),
remain = startTime - now;
timeDisplay.textContent = formatTime(remain);
timeDesc.textContent = '距离 ' + nextCourse.name;
} else {
timeDisplay.textContent = '00:00';
timeDesc.textContent = '今日课程已结束';
}
}
updateScheduleTable();
} catch (e) {
errorSystem.show('界面更新失败: ' + e.message, 'error');
}
}
function formatTime(ms) {
try {
if (ms < 0) return '00:00';
var totalSeconds = Math.floor(ms / 1000),
minutes = Math.floor(totalSeconds / 60),
seconds = totalSeconds % 60;
return (minutes < 10 ? '0' : '') + minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
} catch (e) {
return '--:--';
}
}
function formatDateTime(dateTimeStr) {
var date = new Date(dateTimeStr);
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}

View File

@ -0,0 +1,17 @@
var errorSystem = {
show: function(message, type = 'error') {
try {
var container = document.querySelector(type === 'info' ? '.info-container' : '.error-container');
var content = document.getElementById(type === 'info' ? 'infoMessage' : 'errorMessage');
content.textContent = message;
container.style.display = 'flex';
setTimeout(() => this.hide(type), 5000);
} catch(e) {
console.error('错误提示系统异常:', e);
}
},
hide: function(type = 'error') {
var container = document.querySelector(type === 'info' ? '.info-container' : '.error-container');
if (container) container.style.display = 'none';
}
};

View File

@ -0,0 +1,45 @@
var reminderQueue = (function() {
var queue = [];
var audioCache = {};
function addReminder(reminder) {
queue.push(reminder);
queue.sort(function(a, b) {
return a.time - b.time;
});
preloadAudio(reminder.audio);
}
function processQueue() {
var now = Date.now();
while (queue.length > 0 && queue[0].time <= now) {
var reminder = queue.shift();
executeReminder(reminder);
}
setTimeout(processQueue, 1000);
}
function executeReminder(reminder) {
if (audioCache[reminder.audio]) {
audioCache[reminder.audio].play();
} else if (audioController.getAudioSrc(reminder.audio)) {
audioController.play(reminder.audio);
} else {
errorSystem.show('音频文件不存在: ' + reminder.audio, 'error');
}
}
function preloadAudio(audioType) {
if (!audioCache[audioType] && audioController.getAudioSrc(audioType)) {
var audio = new Audio(audioController.getAudioSrc(audioType));
audioCache[audioType] = audio;
}
}
return {
addReminder: addReminder,
processQueue: processQueue
};
})();
reminderQueue.processQueue();

View File

@ -0,0 +1,265 @@
// 全局状态变量
var isFullscreen = false, currentCourse = null, lastCourse = null, timer = null, lastUpdate = Date.now();
// 新增:安全更新循环函数
function safeUpdate() {
try {
var now = Date.now();
updateCourseStatus();
updateDisplay();
var nextTick = Math.max(1000 - (Date.now() - now), 50);
timer = setTimeout(safeUpdate, nextTick);
lastUpdate = now;
} catch (e) {
errorSystem.show('更新循环错误: ' + e.message, 'error');
}
}
// 修改:全屏切换函数
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 => {
if (isFullscreen) {
element.style.fontSize = '10vw';
} else {
element.style.fontSize = '';
}
});
var countdownElement = document.getElementById('timeDisplay');
if (isFullscreen) {
countdownElement.style.fontSize = '20vw';
} else {
countdownElement.style.fontSize = '';
}
}
function addReminder() {
var table = document.getElementById('reminderTable');
var row = table.insertRow(table.rows.length - 1);
row.draggable = true;
row.innerHTML = `
<td>
<select>
<option value="beforeStart">当距离考试开始时间还有</option>
<option value="beforeEnd">当距离考试结束时间还有</option>
<option value="afterEnd">当考试结束后</option>
<option value="start">当考试开始时</option>
<option value="end">当考试结束时</option>
</select>
</td>
<td><input type="number" placeholder="分钟" disabled></td>
<td>
<select name="audioSelect"></select>
</td>
<td><button onclick="removeReminder(this)">删除</button></td>
<td class="drag-handle"></td>
`;
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) {
var row = button.parentNode.parentNode;
row.parentNode.removeChild(row);
}
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 });
}
}
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) {
var now = Date.now();
reminders.forEach(function(reminder) {
var reminderTime;
if (currentCourse) {
switch (reminder.condition) {
case 'beforeStart':
reminderTime = new Date(currentCourse.start).getTime() - reminder.time * 60000;
break;
case 'beforeEnd':
reminderTime = new Date(currentCourse.end).getTime() - reminder.time * 60000;
break;
case 'afterEnd':
reminderTime = new Date(currentCourse.end).getTime() + reminder.time * 60000;
break;
case 'start':
reminderTime = new Date(currentCourse.start).getTime();
break;
case 'end':
reminderTime = new Date(currentCourse.end).getTime();
break;
default:
console.error('未知的提醒条件:', reminder.condition);
return;
}
} else {
var nextCourse = getNextCourse();
if (nextCourse) {
switch (reminder.condition) {
case 'beforeStart':
reminderTime = new Date(nextCourse.start).getTime() - reminder.time * 60000;
break;
case 'start':
reminderTime = new Date(nextCourse.start).getTime();
break;
default:
console.error('未知的提醒条件:', reminder.condition);
return;
}
} else {
errorSystem.show('当前没有课程信息', 'info');
return;
}
}
if (reminderTime > now) {
reminderQueue.addReminder({ time: reminderTime, condition: reminder.condition, audio: reminder.audio });
}
});
}
// 修改:系统初始化函数
function init() {
try {
var table = document.getElementById('scheduleTable');
courseSchedule.forEach(function(course) {
var row = table.insertRow(-1);
row.innerHTML = '<td>' + course.name + '</td>' +
'<td>' + formatDateTime(course.start) + ' - ' + formatDateTime(course.end) + '</td>' +
'<td></td>';
});
// 初始化设置复选框
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 = `
<td>
<select>
<option value="beforeStart" ${reminder.condition === 'beforeStart' ? 'selected' : ''}>当距离考试开始时间还有</option>
<option value="beforeEnd" ${reminder.condition === 'beforeEnd' ? 'selected' : ''}>当距离考试结束时间还有</option>
<option value="afterEnd" ${reminder.condition === 'afterEnd' ? 'selected' : ''}>当考试结束后</option>
<option value="start" ${reminder.condition === 'start' ? 'selected' : ''}>当考试开始时</option>
<option value="end" ${reminder.condition === 'end' ? 'selected' : ''}>当考试结束时</option>
</select>
</td>
<td><input type="number" value="${reminder.time}" placeholder="${reminder.condition === 'start' || reminder.condition === 'end' ? '-' : '分钟'}" ${reminder.condition === 'start' || reminder.condition === 'end' ? 'disabled' : ''}></td>
<td>
<select name="audioSelect">
<option value="classStart" ${reminder.audio === 'classStart' ? 'selected' : ''}>考试开始铃声</option>
<option value="classEnd" ${reminder.audio === 'classEnd' ? 'selected' : ''}>考试结束铃声</option>
</select>
</td>
<td><button onclick="removeReminder(this)">删除</button></td>
<td class="drag-handle"></td>
`;
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();
// 移除或修改音频权限激活代码(用原音频系统无需特殊激活)
document.body.onclick = null;
// 加载设置从Cookies
loadSettingsFromCookies();
// 加载提醒到队列
loadRemindersToQueue(reminders);
} catch (e) {
errorSystem.show('系统初始化失败: ' + e.message);
}
}
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');
});
}

View File

@ -0,0 +1,55 @@
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 });
}
}
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 = `
<td>
<select>
<option value="beforeStart" ${reminder.condition === 'beforeStart' ? 'selected' : ''}>当距离考试开始时间还有</option>
<option value="beforeEnd" ${reminder.condition === 'beforeEnd' ? 'selected' : ''}>当距离考试结束时间还有</option>
<option value="afterEnd" ${reminder.condition === 'afterEnd' ? 'selected' : ''}>当考试结束后</option>
<option value="start" ${reminder.condition === 'start' ? 'selected' : ''}>当考试开始时</option>
<option value="end" ${reminder.condition === 'end' ? 'selected' : ''}>当考试结束时</option>
</select>
</td>
<td><input type="number" value="${reminder.time}" placeholder="${reminder.condition === 'start' || reminder.condition === 'end' ? '-' : '分钟'}" ${reminder.condition === 'start' || reminder.condition === 'end' ? 'disabled' : ''}></td>
<td>
<select>
<option value="classStart" ${reminder.audio === 'classStart' ? 'selected' : ''}>考试开始铃声</option>
<option value="classEnd" ${reminder.audio === 'classEnd' ? 'selected' : ''}>考试结束铃声</option>
</select>
</td>
<td><button onclick="removeReminder(this)">删除</button></td>
`;
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' ? '-' : '分钟';
});
});
}
});
}

View File

@ -0,0 +1,376 @@
/* 基础样式 */
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;
}
/* 状态显示框 */
.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;
}
/* 课程表格 */
.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;
}
/* 全屏模式 */
.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;
}
/* 控制按钮 */
.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);
}
/* 设置面板 */
.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;
}
/* 信息提示 */
.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;
}
/* 提醒表格 */
.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;
}
/* 文件输入标签 */
.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;
}
/* 动作按钮 */
.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);
}