From 00efe4a815215596e78508ea8a56056cbb63772b Mon Sep 17 00:00:00 2001 From: Elegy233 <82034747+wan0ge@users.noreply.github.com> Date: Sat, 15 Aug 2026 07:58:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=85=E7=90=86=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8C=89=E9=A1=B9=E5=8B=BE=E9=80=89=E6=B8=85?= =?UTF-8?q?=E7=90=86=20(#441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 清理缓存支持按项勾选清理 上游原版 handleClearCache() 不区分具体项,调用即一次性重置全部内存缓存(animes、episodeIds、episodeNum、lastSelectMap、searchCache、commentCache、requestHistory 及 Bangumi Data)。 本提交改为按项勾选清理: - apis/system-api.js:handleClearCache 接收可选请求体 { items: string[] },按 clearActions 映射逐项重置;请求记录(reqRecords)与今日计数(todayReqNum)归入「请求历史记录」项连带清理;未提供请求体或解析失败时回退为全清,兼容旧路由与测试的无参调用。未知键经 key in clearActions 过滤,无注入面。 - worker.js:POST /api/cache/clear 透传请求体。 - ui/template.js:清理缓存弹窗改为 8 项可勾选清单,含全选/全不选与已选计数。 - ui/js/systemsettings.js:新增 selectAllCacheItems、updateCacheClearCount;confirmClearCache 收集勾选项发送 {items},空选拦截。 - ui/css/components.css.js:新增缓存弹窗与通用复选框样式。 收藏(favoriteCache)按上游契约不在清理范围内,worker.test.js「清理缓存保留收藏」断言仍然成立。 * feat: 优化通用复选框样式与偏移设置交互 承接清理缓存弹窗的通用复选框,继续完善前端交互(均基于上游原版): - ui/css/components.css.js:通用复选框由 accent-color 改为 appearance 自绘,以 --theme-check-color 控制勾选色(浅色恒白、暗色恒黑,规避浅强调色在浅色模式得黑勾);以 body[data-theme] 前缀将特异性提升至 3002,压过主题规则 themes.css.js:191 对 .form-group input 背景与边框的重设(2002),使位于 .form-group 内的偏移百分比复选框与缓存复选框表现一致;补 padding:0 避免继承 .form-group input 内边距导致长方形。 - ui/css/themes.css.js:浅色与暗色模式分别定义 --theme-check-color。 - ui/js/systemsettings.js:偏移百分比复选框标签去除 white-space:nowrap,窄屏长文案可换行,复选框不被压缩。 * test: 新增 handleClearCache 按项清理测试 覆盖 handleClearCache 的全部关键路径: - 按项选清:单选一项只清该项,其余缓存保留 - 非法键过滤:不在 clearActions 的键被忽略,不触发任何清理动作 - requestHistory 项同时清空 reqRecords 并重置 todayReqNum 为 0 - episodeNum 重置为 10001 - 全清(无 req 或畸形 body 非数组)保留 favorites - 空数组 items 不清理任何项 - bangumiData 作为合法键被识别且独立清理 全部子测试在 node --test 下通过,整文件 36/36 通过。 * fix: 修复清理缓存原型键误放行并优化弹窗提示与计数样式 清理缓存功能:修复 handleClearCache 的原型键过滤缺陷,并优化弹窗提示与计数视觉。 - apis/system-api.js:handleClearCache 的 items 过滤由 'key in clearActions' 改为 Object.prototype.hasOwnProperty.call(clearActions, key),排除 __proto__/constructor 等原型链键误放行导致整次清理失败(success:false)。 - ui/template.js:提示文案改为「请勾选需要清理的缓存项:」,采用上游风格的全可见度清晰提示。 - ui/css/components.css.js:.cache-clear-hint 字号 13px 增至 16px、颜色由灰改为正文色,与全选/全不选按钮形成清晰层级;.cache-clear-count 增加淡中性全圆角状态标签底色,平衡左右视觉重量且刻意不像按钮。 - worker.test.js:新增原型键回归测试,固化上述修复。 * fix: 统一清理缓存项标签大小写并调整项顺序 清理缓存弹窗:统一缓存项标签写法并调整为预期顺序。 - ui/template.js:动画元数据缓存标签由「动画元数据缓存 (Bangumi Data)」改为「动画元数据缓存 (bangumiData)」,与另 7 项括号展示真实键名的写法(animes / episodeIds / episodeNum / lastSelectMap)统一,便于 UI 与代码键名对照。 - 清理项顺序调整为:搜索结果缓存、弹幕内容缓存、请求历史记录、动漫搜索缓存、动画元数据缓存、剧集ID缓存、剧集编号缓存、最后选择映射缓存。 - 各 checkbox 的 value 与 handleClearCache 的 clearActions 键名一致,无功能影响;仓库其余「Bangumi Data」均为数据源专有名词,不涉及此改动。 --- danmu_api/apis/system-api.js | 110 ++++++++++++++++---------- danmu_api/ui/css/components.css.js | 120 ++++++++++++++++++++++++++++ danmu_api/ui/css/themes.css.js | 2 + danmu_api/ui/js/systemsettings.js | 36 ++++++++- danmu_api/ui/template.js | 33 ++++---- danmu_api/worker.js | 2 +- danmu_api/worker.test.js | 121 +++++++++++++++++++++++++++++ 7 files changed, 364 insertions(+), 60 deletions(-) diff --git a/danmu_api/apis/system-api.js b/danmu_api/apis/system-api.js index 983bbe8..0846f36 100644 --- a/danmu_api/apis/system-api.js +++ b/danmu_api/apis/system-api.js @@ -169,39 +169,62 @@ export function handleClearLogs() { /** * 处理清理缓存的请求 - * @returns {Response} 表示操作成功的响应 + * @param {Request} [req] 可选请求体 { items: string[] },用于指定待清理项;未提供时清理全部已知项 + * @returns {Response} 表示操作结果的响应 */ -export async function handleClearCache() { - try { - // 清理 globals 中的缓存数据 - globals.animes = []; - globals.episodeIds = []; - globals.episodeNum = 10001; // 重置为初始值 - globals.lastSelectMap = new Map(); // 重新创建 Map 对象 - globals.reqRecords = []; // 清空请求记录 - globals.todayReqNum = 0; // 重置今日请求次数 - +export async function handleClearCache(req) { + // 各清理项对应的重置逻辑;请求记录与今日计数归入「请求历史记录」项 + const clearActions = { + animes: () => { globals.animes = []; }, + episodeIds: () => { globals.episodeIds = []; }, + episodeNum: () => { globals.episodeNum = 10001; }, // 重置为初始值 + lastSelectMap: () => { globals.lastSelectMap = new Map(); }, // 重新创建 Map 对象 // 清理搜索和弹幕缓存 - globals.searchCache = new Map(); - globals.commentCache = new Map(); - globals.requestHistory = new Map(); - - try { - // 清理 Bangumi-Data 内存与磁盘缓存 - clearBangumiDataCache(true); - - // 触发异步数据重载 - if (globals.useBangumiData) { - initBangumiData(globals.deployPlatform, false).catch(e => { - log("warn", `[system] [server] Bangumi-Data background reload failed: ${e.message}`); - }); + searchCache: () => { globals.searchCache = new Map(); }, + commentCache: () => { globals.commentCache = new Map(); }, + requestHistory: () => { + globals.requestHistory = new Map(); + globals.reqRecords = []; // 清空请求记录 + globals.todayReqNum = 0; // 重置今日请求次数 + }, + bangumiData: () => { + try { + clearBangumiDataCache(true); // 清理 Bangumi-Data 内存与磁盘缓存 + if (globals.useBangumiData) { + // 触发异步数据重载 + initBangumiData(globals.deployPlatform, false).catch(e => { + log("warn", `[system] [server] Bangumi-Data background reload failed: ${e.message}`); + }); + } + } catch (e) { + log("error", `[system] [server] Failed to clear Bangumi-Data cache: ${e.message}`); } - } catch (e) { - log("error", `[system] [server] Failed to clear Bangumi-Data cache: ${e.message}`); } - + }; + const allItems = Object.keys(clearActions); + + let effectiveItems; + if (req) { + let parsed = null; + try { + parsed = await req.json(); + } catch (e) { + parsed = null; + } + const items = parsed && Array.isArray(parsed.items) ? parsed.items : null; + // 仅保留自身属性键,排除 __proto__ 等原型链键,避免误放行导致整次清理失败 + effectiveItems = items ? items.filter(key => Object.prototype.hasOwnProperty.call(clearActions, key)) : allItems; + } else { + effectiveItems = allItems; + } + + try { + for (const key of effectiveItems) { + clearActions[key](); + } + log("info", `[system] [server] Memory cache cleared successfully`); - + // 同步清理本地缓存和Redis缓存 try { // 如果本地缓存有效,更新本地缓存 @@ -213,7 +236,7 @@ export async function handleClearCache() { } catch (localError) { log("warn", `[system] [server] Local cache may not be available: ${localError.message}`); } - + try { // 如果Redis有效,更新Redis缓存 if (globals.redisValid) { @@ -235,19 +258,22 @@ export async function handleClearCache() { } catch (redisError) { log("warn", `[system] [server] LocalRedis may not be available: ${redisError.message}`); } - - log("info", `[system] [server] All caches cleared successfully`); - return jsonResponse({ success: true, message: "Cache cleared successfully", clearedItems: { - animes: 0, - episodeIds: 0, - episodeNum: 10001, - lastSelectMap: 0, - searchCache: 0, - commentCache: 0, - requestHistory: 0, - reqRecords: 0, - todayReqNum: 0 - }}, 200); + + const clearedItems = {}; + for (const key of effectiveItems) { + if (key === "requestHistory") { + clearedItems.requestHistory = 0; + clearedItems.reqRecords = 0; + clearedItems.todayReqNum = 0; + } else if (key === "episodeNum") { + clearedItems.episodeNum = 10001; + } else { + clearedItems[key] = 0; + } + } + + log("info", `[system] [server] Selected caches cleared successfully`); + return jsonResponse({ success: true, message: "Cache cleared successfully", clearedItems }, 200); } catch (error) { log("error", `[system] [server] Cache clear failed: ${error.message}`); return jsonResponse({ success: false, message: `Cache clear failed: ${error.message}` }, 500); diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js index 795ccf1..aefa9b2 100644 --- a/danmu_api/ui/css/components.css.js +++ b/danmu_api/ui/css/components.css.js @@ -296,6 +296,17 @@ body[data-theme] .favorite-schedule-btn:disabled:hover { font-size: 12px; } +.btn-secondary { + background: var(--theme-panel-strong); + color: var(--theme-text); + border: 1px solid var(--theme-border); +} + +.btn-secondary:hover { + border-color: var(--theme-accent); + color: var(--theme-accent); +} + /* ============ 配置预览 ============ */ .preview-description { color: var(--theme-muted); @@ -959,6 +970,115 @@ body.modal-open { font-size: 13px; } +/* 通用复选框:appearance 自绘控制勾选色;body[data-theme] 前缀将特异性提至 3002,压过 .form-group input 的背景/边框(2002)与尺寸/内边距(1001) */ +body[data-theme] input[type="checkbox"].app-checkbox { + width: 16px; + height: 16px; + margin: 0; + padding: 0; + flex-shrink: 0; + vertical-align: middle; + cursor: pointer; + -webkit-appearance: none; + appearance: none; + position: relative; + background: var(--theme-panel-bg); + border: 1px solid var(--theme-muted); + border-radius: 3px; +} + +body[data-theme] input[type="checkbox"].app-checkbox:checked { + background: var(--theme-accent); + border-color: var(--theme-accent); +} + +body[data-theme] input[type="checkbox"].app-checkbox:checked::after { + content: ""; + position: absolute; + left: 5px; + top: 1px; + width: 4px; + height: 9px; + border: solid var(--theme-check-color); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +body[data-theme] input[type="checkbox"].app-checkbox:focus-visible { + outline: 2px solid var(--theme-accent-soft); + outline-offset: 1px; +} + +.cache-clear-hint { + margin: 0 0 12px; + font-size: 16px; + color: var(--theme-text); +} + +.cache-clear-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; +} + +.cache-clear-count { + font-size: 12px; + font-weight: 500; + color: var(--theme-muted); + background: color-mix(in srgb, var(--theme-muted) 10%, transparent); /* 状态标签底色,非按钮 */ + padding: 5px 10px; + border-radius: 999px; +} + +.cache-clear-actions { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.cache-clear-actions .btn { + flex-shrink: 0; + white-space: nowrap; + min-width: 64px; +} + +.cache-clear-options { + display: flex; + flex-direction: column; + margin: 0; + border: 1px solid var(--theme-border); + border-radius: var(--app-radius-card-sm); + overflow: hidden; + background: var(--theme-panel-bg); +} + +.cache-clear-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + cursor: pointer; + user-select: none; + padding: 9px 12px; + border-bottom: 1px solid var(--theme-border); +} + +.cache-clear-item:last-child { + border-bottom: none; +} + +.cache-clear-item:hover { + background: var(--theme-panel-strong); +} + +.cache-clear-note { + margin: 12px 0 0; + font-size: 12px; + color: var(--theme-muted); +} + .warning-box { background: rgba(240, 160, 96, 0.10); border-left: 3px solid #f0a060; diff --git a/danmu_api/ui/css/themes.css.js b/danmu_api/ui/css/themes.css.js index 83352fe..1d63d32 100644 --- a/danmu_api/ui/css/themes.css.js +++ b/danmu_api/ui/css/themes.css.js @@ -40,6 +40,7 @@ body { --theme-code-bg: #1a1b24; --theme-code-text: #e2e4ea; --theme-link: #8cb48c; + --theme-check-color: #ffffff; color-scheme: light; background: var(--theme-page-bg); color: var(--theme-text); @@ -112,6 +113,7 @@ body[data-color-scheme="dark"] { --theme-input-bg: rgba(255, 255, 255, 0.07); --theme-code-bg: #111118; --theme-code-text: #e2e4ea; + --theme-check-color: #000000; color-scheme: dark; } diff --git a/danmu_api/ui/js/systemsettings.js b/danmu_api/ui/js/systemsettings.js index a17b50d..875aa5e 100644 --- a/danmu_api/ui/js/systemsettings.js +++ b/danmu_api/ui/js/systemsettings.js @@ -309,9 +309,28 @@ async function importSystemConfigFile(file) { // 显示清理缓存确认模态框 function showClearCacheModal() { + selectAllCacheItems(true); // 每次打开恢复默认全选 document.getElementById('clear-cache-modal').classList.add('active'); } +// 批量勾选或取消勾选所有缓存项,并同步已选数量 +function selectAllCacheItems(checked) { + document.querySelectorAll('#clear-cache-modal input[name="cacheItem"]').forEach(cb => { + cb.checked = checked; + }); + updateCacheClearCount(); +} + +// 刷新清理缓存弹窗中已勾选项的数量统计 +function updateCacheClearCount() { + const all = document.querySelectorAll('#clear-cache-modal input[name="cacheItem"]'); + const checked = document.querySelectorAll('#clear-cache-modal input[name="cacheItem"]:checked'); + const countEl = document.getElementById('cache-clear-count'); + if (countEl) { + countEl.textContent = '已选 ' + checked.length + ' / ' + all.length; + } +} + // 隐藏清理缓存确认模态框 function hideClearCacheModal() { document.getElementById('clear-cache-modal').classList.remove('active'); @@ -319,6 +338,14 @@ function hideClearCacheModal() { // 确认清理缓存 async function confirmClearCache() { + // 收集勾选的缓存项 + const checkboxes = document.querySelectorAll('#clear-cache-modal input[name="cacheItem"]:checked'); + const items = Array.from(checkboxes).map(cb => cb.value); + if (items.length === 0) { + customAlert('请至少选择一项要清理的缓存'); + return; + } + // 检查部署平台配置 const configCheck = await checkDeployPlatformConfig(); if (!configCheck.success) { @@ -332,12 +359,13 @@ async function confirmClearCache() { addLog('开始清理缓存', 'info'); try { - // 调用真实的清理缓存API + // 调用真实的清理缓存API,附带勾选的待清理项 const response = await fetch(buildApiUrl('/api/cache/clear', true), { // 使用admin token method: 'POST', headers: { 'Content-Type': 'application/json' - } + }, + body: JSON.stringify({ items }) }); const result = await response.json(); @@ -973,9 +1001,9 @@ function renderValueInput(item) {