1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-02 09:19:23 +00:00
Classworks/src/utils/settings.js

681 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { v4 as uuidv4 } from "uuid";
// 请求通知权限
async function requestNotificationPermission() {
if (Notification && Notification.requestPermission) {
const permission = await Notification.requestPermission();
if (permission === "granted") {
console.log("通知权限已授予");
return true;
} else {
console.warn("通知权限被拒绝");
return false;
}
} else {
console.warn("浏览器不支持通知权限请求");
return false;
}
}
/**
* 请求持久性存储权限
* @returns {Promise<boolean>} 是否成功启用持久性存储
*/
async function requestPersistentStorage() {
try {
if (navigator.storage?.persist) {
return await navigator.storage.persist();
}
return false;
} catch (error) {
console.warn("请求持久性存储失败:", error);
return false;
}
}
/**
* 初始化存储权限
*/
async function initializeStorage() {
const notificationGranted = await requestNotificationPermission();
if (notificationGranted && SettingsManager.getSetting("storage.persistOnLoad")) {
const persisted = await requestPersistentStorage();
console.log(`持久性存储状态: ${persisted ? "已启用" : "未启用"}`);
}
}
// 在页面加载时初始化
if (typeof window !== 'undefined') {
window.addEventListener("load", initializeStorage);
}
/**
* 配置项定义
* @typedef {Object} SettingDefinition
* @property {string} type - 配置项类型 ('boolean' | 'number' | 'string')
* @property {any} default - 默认值
* @property {Function} [validate] - 可选的验证函数
* @property {string} [description] - 配置项描述
* @property {string} [legacyKey] - 旧版本localStorage键名(用于迁移)
* @property {boolean} [requireDeveloper] - 是否需要开发者选项启用
* @property {string} [icon] - 设置项的图标
*/
// 存储所有设置的localStorage键名
const SETTINGS_STORAGE_KEY = "Classworks_settings";
/**
* 生成UUID v4
* @returns {string} 生成的UUID字符串
*/
function generateUUID() {
return uuidv4();
}
// 新增: Classworks云端存储的默认设置
const classworksCloudDefaults = {
"server.domain": "https://kv.wuyuan.dev",
"server.siteKey": "",
};
/**
* 所有配置项的定义
* @type {Object.<string, SettingDefinition>}
*/
const settingsDefinitions = {
// 设备标识
"device.uuid": {
type: "string",
default: generateUUID(),
description: "设备唯一标识符",
icon: "mdi-identifier",
},
// 存储设置
"storage.persistOnLoad": {
type: "boolean",
default: true,
description: "是否在页面加载时自动请求持久性存储",
icon: "mdi-database-sync",
},
// 显示设置
"display.emptySubjectDisplay": {
type: "string",
default: "card", // 修改默认值为 'button'
validate: (value) => ["card", "button"].includes(value),
description: "空科目的显示方式",
icon: "mdi-card-outline",
},
"display.dynamicSort": {
type: "boolean",
default: true,
description: "是否启用动态排序",
icon: "mdi-sort-variant",
// 启用后会根据内容自动调整卡片顺序,提供更好的视觉体验
},
"display.showRandomButton": {
type: "boolean",
default: false,
description: "是否显示随机点人按钮",
icon: "mdi-shuffle-variant",
// 控制是否显示随机排序按钮,可用于随机调整卡片顺序
},
"display.showFullscreenButton": {
type: "boolean",
default: true,
description: "是否显示全屏按钮",
icon: "mdi-fullscreen",
// 控制是否显示进入全屏模式的按钮
},
"display.cardHoverEffect": {
type: "boolean",
default: true,
description: "是否启用卡片悬浮效果",
icon: "mdi-gesture-tap",
// 启用后鼠标悬停在卡片上时会显示视觉反馈效果
},
"display.enhancedTouchMode": {
type: "boolean",
default: true,
description: "是否启用增强触摸模式",
icon: "mdi-gesture-tap-button",
},
"display.showAntiScreenBurnCard": {
type: "boolean",
default: false,
description: "是否显示防烧屏忽悠卡片",
icon: "mdi-monitor-shimmer",
},
// 服务器设置(合并了数据提供者设置)
"server.domain": {
type: "string",
default: "",
validate: (value) => {
// 如果不是服务器模式或值为空,直接通过
if (!value) return true;
// 验证URL格式
try {
new URL(value);
return true;
} catch (e) {
console.error("域名格式无效:", e);
return false;
}
},
description: "后端服务器域名",
icon: "mdi-web",
// 设置后端服务器的域名,用于从远程服务器获取数据
},
"server.classNumber": {
type: "string",
default: "高三八班",
//validate: (value) => /^[A-Za-z0-9]*$/.test(value),
validate: (value) => /.*/.test(value),
description: "班级编号",
icon: "mdi-account-group",
// 设置班级标识,用于区分不同班级的数据
},
"server.siteKey": {
type: "string",
default: "",
description: "网站令牌",
icon: "mdi-key-chain",
// 用于后端验证请求的令牌,将作为请求头 x-site-key 发送
},
"server.provider": {
type: "string",
default: "kv-local",
validate: (value) => ["kv-local", "kv-server", "classworkscloud"].includes(value),
description: "数据提供者",
icon: "mdi-database",
// 选择数据存储方式:使用本地存储或远程服务器
},
// 刷新设置
"refresh.auto": {
type: "boolean",
default: false,
description: "是否启用自动刷新",
icon: "mdi-refresh-auto",
// 启用后将按设定的时间间隔自动刷新数据
},
"refresh.interval": {
type: "number",
default: 300,
validate: (value) => value >= 10 && value <= 3600,
description: "自动刷新间隔(秒)",
icon: "mdi-timer-outline",
// 设置自动刷新的时间间隔范围10-3600秒
},
// 字体设置
"font.size": {
type: "number",
default: 28,
validate: (value) => value >= 16 && value <= 100,
description: "字体大小",
icon: "mdi-format-size",
},
// 编辑设置
"edit.autoSave": {
type: "boolean",
default: true,
description: "是否启用自动保存",
icon: "mdi-content-save-outline",
// 启用后编辑内容时会自动保存更改,无需手动点击保存按钮
},
"edit.blockNonTodayAutoSave": {
// 添加新选项
type: "boolean",
default: true,
description: "禁止自动保存非当天数据",
icon: "mdi-calendar-lock",
// 启用后只有当天的数据会自动保存,防止意外修改历史数据
},
"edit.refreshBeforeEdit": {
type: "boolean",
default: true,
description: "编辑前是否自动刷新",
icon: "mdi-refresh",
// 启用后在开始编辑前会自动刷新数据,确保编辑的是最新内容
},
"edit.confirmNonTodaySave": {
// 添加新选项
type: "boolean",
default: true,
description: "保存非当天数据需确认",
icon: "mdi-calendar-alert",
},
// 开发者选项
"developer.enabled": {
type: "boolean",
default: false,
description: "是否启用开发者选项",
icon: "mdi-developer-board",
// 启用后可以访问高级开发者功能和设置项
},
"developer.showDebugConfig": {
type: "boolean",
default: false,
description: "是否显示调试配置",
icon: "mdi-bug-outline",
// 启用后在控制台显示详细的配置信息和设置变更日志
},
"developer.disableMessageLog": {
// 添加新的设置项
type: "boolean",
default: false,
description: "禁用消息日志记录",
requireDeveloper: true,
icon: "mdi-message-off-outline",
// 启用后将不再记录应用消息到日志,可减少内存占用
},
// 消息设置
"message.showSidebar": {
type: "boolean",
default: true,
description: "是否显示消息记录侧栏",
requireDeveloper: true, // 添加标记
icon: "mdi-message-text-outline",
// 控制是否显示消息历史记录侧栏,需要开发者模式
},
"message.maxActiveMessages": {
type: "number",
default: 5,
validate: (value) => value >= 1 && value <= 10,
description: "同时显示的最大消息数量",
requireDeveloper: true,
icon: "mdi-message-badge-outline",
// 控制界面上同时显示的最大消息数量范围1-10条
},
"message.timeout": {
type: "number",
default: 5000,
validate: (value) => value >= 1000 && value <= 30000,
description: "消息自动关闭时间(毫秒)",
requireDeveloper: true,
icon: "mdi-timer-sand",
// 设置消息自动消失的时间范围1000-30000毫秒
},
"message.saveHistory": {
type: "boolean",
default: true,
description: "是否保存消息历史记录",
requireDeveloper: true,
icon: "mdi-history",
// 启用后将保存消息历史记录,可在侧栏中查看
},
// 主题设置
"theme.mode": {
type: "string",
default: "dark",
validate: (value) => ["light", "dark"].includes(value),
description: "主题模式",
icon: "mdi-theme-light-dark",
// 设置应用的主题模式,可选亮色或暗色主题
},
// 随机点名设置
"randomPicker.enabled": {
type: "boolean",
default: true,
description: "是否启用随机点名功能",
icon: "mdi-account-question",
},
"randomPicker.animation": {
type: "boolean",
default: true,
description: "是否启用随机点名动画效果",
icon: "mdi-animation-play",
},
"randomPicker.defaultCount": {
type: "number",
default: 1,
validate: (value) => value >= 1 && value <= 10,
description: "默认抽取人数",
icon: "mdi-counter",
},
"randomPicker.excludeAbsent": {
type: "boolean",
default: true,
description: "是否排除请假学生",
icon: "mdi-account-off",
},
"randomPicker.excludeLate": {
type: "boolean",
default: false,
description: "是否排除迟到学生",
icon: "mdi-clock-alert",
},
"randomPicker.excludeExcluded": {
type: "boolean",
default: true,
description: "是否排除不参与学生",
icon: "mdi-account-cancel",
},
};
/**
* 设置管理器单例类
*/
class SettingsManagerClass {
constructor() {
this.settingsCache = null;
this.isInitialized = false;
}
/**
* 初始化设置管理器
*/
init() {
if (this.isInitialized) return;
this.loadSettings();
this.isInitialized = true;
}
/**
* 从localStorage加载所有设置
* @returns {Object} 所有设置的值
*/
loadSettings() {
try {
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(SETTINGS_STORAGE_KEY) : null;
if (stored) {
this.settingsCache = JSON.parse(stored);
} else {
// 首次使用或迁移旧数据
this.settingsCache = this.migrateFromLegacy();
}
} catch (error) {
console.error("加载设置失败:", error);
this.settingsCache = {};
}
// 确保所有设置项都有值(使用默认值填充)
for (const [key, definition] of Object.entries(settingsDefinitions)) {
if (!(key in this.settingsCache)) {
this.settingsCache[key] = definition.default;
}
}
return this.settingsCache;
}
/**
* 从旧版本的localStorage迁移数据
*/
migrateFromLegacy() {
if (typeof localStorage === 'undefined') return {};
const LEGACY_SETTINGS_KEY = "homeworkpage_settings";
const LEGACY_MESSAGE_KEY = "homeworkpage_messages";
// 尝试从旧版本的设置中迁移
const legacySettings = localStorage.getItem(LEGACY_SETTINGS_KEY);
if (legacySettings) {
try {
const settings = JSON.parse(legacySettings);
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
// 可选:删除旧的设置
localStorage.removeItem(LEGACY_SETTINGS_KEY);
return settings;
} catch (error) {
console.error("迁移旧设置失败:", error);
}
}
// 尝试从旧版本的message中迁移
const legacyMessages = localStorage.getItem(LEGACY_MESSAGE_KEY);
if (legacyMessages) {
try {
const messages = JSON.parse(legacyMessages);
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(messages));
// 可选删除旧的message
localStorage.removeItem(LEGACY_MESSAGE_KEY);
return messages; // 返回迁移后的消息
} catch (error) {
console.error("迁移旧消息失败:", error);
}
}
// 如果没有旧设置或迁移失败,返回空对象
return {};
}
/**
* 保存所有设置到localStorage
*/
saveSettings() {
if (typeof localStorage === 'undefined') return;
try {
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(this.settingsCache));
} catch (error) {
console.error("保存设置失败:", error);
}
}
/**
* 获取设置项的值
* @param {string} key - 设置项键名
* @returns {any} 设置项的值
*/
getSetting(key) {
if (!this.isInitialized) {
this.init();
}
const definition = settingsDefinitions[key];
if (!definition) {
console.warn(`未定义的设置项: ${key}`);
return null;
}
// 确保开发者相关设置正确处理
if (definition.requireDeveloper) {
const devEnabled = this.settingsCache["developer.enabled"];
if (!devEnabled) {
return definition.default;
}
}
// 检查是否使用Classworks云端存储并覆盖特定设置
if (this.settingsCache["server.provider"] === "classworkscloud") {
if (classworksCloudDefaults[key] !== undefined) {
return classworksCloudDefaults[key];
}
}
const value = this.settingsCache[key];
return value !== undefined ? value : definition.default;
}
/**
* 设置配置项的值
* @param {string} key - 设置项键名
* @param {any} value - 要设置的值
* @returns {boolean} 是否设置成功
*/
setSetting(key, value) {
if (!this.isInitialized) {
this.init();
}
const definition = settingsDefinitions[key];
if (!definition) {
console.warn(`未定义的设置项: ${key}`);
return false;
}
// 添加对开发者选项依赖的检查
if (definition.requireDeveloper && !this.settingsCache["developer.enabled"]) {
console.warn(`设置项 ${key} 需要启用开发者选项`);
return false;
}
try {
const oldValue = this.settingsCache[key];
// 类型转换
if (typeof value !== definition.type) {
value =
definition.type === "boolean"
? Boolean(value)
: definition.type === "number"
? Number(value)
: String(value);
}
// 验证
if (definition.validate && !definition.validate(value)) {
console.warn(`设置项 ${key} 的值无效`);
return false;
}
this.settingsCache[key] = value;
this.saveSettings();
this.logSettingsChange(key, oldValue, value);
// 为了保持向后兼容同时更新旧的localStorage键
const legacyKey = definition.legacyKey;
if (legacyKey && typeof localStorage !== 'undefined') {
localStorage.setItem(legacyKey, value.toString());
}
return true;
} catch (error) {
console.error(`设置配置项 ${key} 失败:`, error);
return false;
}
}
/**
* 记录设置变更
*/
logSettingsChange(key, oldValue, newValue) {
const shouldLog =
this.settingsCache["developer.enabled"] &&
this.settingsCache["developer.showDebugConfig"];
if (shouldLog) {
console.log(`[Settings] ${key}:`, {
old: oldValue,
new: newValue,
time: new Date().toLocaleTimeString(),
});
}
}
/**
* 重置指定设置项到默认值
* @param {string} key - 设置项键名
*/
resetSetting(key) {
if (!this.isInitialized) {
this.init();
}
const definition = settingsDefinitions[key];
if (!definition) {
console.warn(`未定义的设置项: ${key}`);
return;
}
this.settingsCache[key] = definition.default;
this.saveSettings();
}
/**
* 重置所有设置项到默认值
*/
resetAllSettings() {
this.settingsCache = {};
for (const [key, definition] of Object.entries(settingsDefinitions)) {
this.settingsCache[key] = definition.default;
}
this.saveSettings();
}
/**
* 监听设置变化
* @param {Function} callback - 当设置改变时调用的回调函数
* @returns {Function} 取消监听的函数
*/
watchSettings(callback) {
if (typeof window === 'undefined') return () => {};
const handler = (event) => {
if (event.key === SETTINGS_STORAGE_KEY) {
this.settingsCache = JSON.parse(event.newValue);
callback(this.settingsCache);
}
};
window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
}
/**
* 获取设置项的定义
* @param {string} key - 设置项键名
* @returns {SettingDefinition|null} 设置项的定义或null
*/
getSettingDefinition(key) {
return settingsDefinitions[key] || null;
}
/**
* 将当前配置导出为简单的键值对对象
* @returns {Object} 包含所有设置的键值对对象
*/
exportSettingsAsKeyValue() {
if (!this.isInitialized) {
this.init();
}
// 创建一个新对象,避免直接返回引用
const exportedSettings = {};
// 遍历所有设置项
for (const key in settingsDefinitions) {
// 获取当前值确保使用getSetting以应用所有规则如开发者选项依赖
exportedSettings[key] = this.getSetting(key);
}
return exportedSettings;
}
}
// 创建单例实例
const SettingsManager = new SettingsManagerClass();
// 在服务器端和客户端都能正常工作的初始化
if (typeof window !== 'undefined') {
SettingsManager.init();
}
// 为了向后兼容性,提供与原来相同的函数接口
const getSetting = (key) => SettingsManager.getSetting(key);
const setSetting = (key, value) => SettingsManager.setSetting(key, value);
const resetSetting = (key) => SettingsManager.resetSetting(key);
const resetAllSettings = () => SettingsManager.resetAllSettings();
const watchSettings = (callback) => SettingsManager.watchSettings(callback);
const getSettingDefinition = (key) => SettingsManager.getSettingDefinition(key);
const exportSettingsAsKeyValue = () => SettingsManager.exportSettingsAsKeyValue();
// 导出单例和直接方法
export {
settingsDefinitions,
SettingsManager,
getSetting,
setSetting,
resetSetting,
resetAllSettings,
watchSettings,
getSettingDefinition,
exportSettingsAsKeyValue
};