From e6c292c463392b2ea3c567256f26665bb937aa32 Mon Sep 17 00:00:00 2001 From: Elegy233 <82034747+wan0ge@users.noreply.github.com> Date: Mon, 25 May 2026 10:05:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=20`/api/cache/an?= =?UTF-8?q?imes`=20API=20=E6=8E=A5=E5=8F=A3=E8=8E=B7=E5=8F=96=E6=9C=80?= =?UTF-8?q?=E8=BF=91=E7=9A=84=20animes=20=E7=BC=93=E5=AD=98=EF=BC=8C?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=88=B0CUSTOM=5FMERGE=5FRULES=E4=B8=8EDANMU?= =?UTF-8?q?=5FOFFSET=E9=85=8D=E7=BD=AE=E9=9D=A2=E6=9D=BF=E4=B8=AD=E8=BE=85?= =?UTF-8?q?=E5=8A=A9=E5=A1=AB=E5=86=99=20(#318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(api): 新增处理获取最近animes缓存列表请求函数 * feat(api): 新增 `/api/cache/animes API` 接口 * feat(model): 新增 `mergedChildren` 与 `isHiddenChild` 参数 * feat(utils): 记录合并层级 * feat(ui): 新增最近数据与`animes`缓存面板样式 * feat(ui): 新增最近数据与`animes`缓存面板功能 * feat(md): 新增GET `/api/cache/animes`说明 * fix(ui): 修复合并结果由于长英文字符串导致前端网格布被撑破的 UI 错位问题 远古问题 * fix(ui): 调整换行逻辑提升可读性 * feat(ui): 给MERGE_SOURCE_PAIRS也添加最新数据面板,供合并参考 Add button to fetch and display recent data in the UI. --- README.md | 3 +- danmu_api/apis/system-api.js | 48 ++++ danmu_api/models/dandan-model.js | 7 +- danmu_api/ui/css/components.css.js | 204 ++++++++++++++++ danmu_api/ui/js/systemsettings.js | 376 ++++++++++++++++++++++++++++- danmu_api/utils/merge-util.js | 19 ++ danmu_api/worker.js | 7 +- 7 files changed, 658 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7f5111d..832c37c 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,10 @@ LogVar 弹幕 API 服务器 - `GET /api/v2/comment/:commentId?format=json&duration=true`:获取指定弹幕评论;当 `duration=true` 且返回 JSON 时,会额外附带 `videoDuration` 字段,优先返回源站时长,拿不到时返回 `0`。 - `GET /api/v2/comment?url=${videoUrl}&format=json`:通过视频URL直接获取弹幕(兼容第三方弹幕服务器格式)。 - `POST /api/v2/segmentcomment?format=json`:通过comment接口返回体中的Segment类JSON数据获取单独一个分片的弹幕数据。 - - `GET /api/logs`:获取最近的日志(最多 500 行,格式为 `[时间戳] 级别: 消息`)。 - `GET /api/v2/fongmi/danmaku?name={name}&episode={episode}`:FengMi影视api。 - `GET /danmaku/api/v2/fongmi/danmaku?name={name}&episode={episode}`:兼容FengMi影视api短路径。 + - `GET /api/logs`:获取最近的日志(最多 500 行,格式为 `[时间戳] 级别: 消息`)。 + - `GET /api/cache/animes`:获取最近的 animes 缓存。 - **弹幕格式输出**:支持 JSON 和 XML 两种格式输出,通过以下方式配置: - 环境变量:`DANMU_OUTPUT_FORMAT=json|xml`(默认:json) - 查询参数:`?format=xml` 或 `?format=json`(优先级最高) diff --git a/danmu_api/apis/system-api.js b/danmu_api/apis/system-api.js index b2de912..179e1ca 100644 --- a/danmu_api/apis/system-api.js +++ b/danmu_api/apis/system-api.js @@ -251,3 +251,51 @@ export function handleReqRecords() { return jsonResponse({ records, todayReqNum }, 200); } +/** + * 处理获取最近 animes 缓存列表的请求 + * @returns {Response} 包含格式化后番剧及子源集数的 JSON 响应 + */ +export function handleCacheAnimes() { + try { + const localAnimes = [...(globals.animes || [])].reverse(); + + // 1. 预构建一个全局映射字典,提升查找效率 + const fullAnimeMap = new Map(); + localAnimes.forEach(a => { + if (a?.source && a?.animeId) { + fullAnimeMap.set(`${a.source}_${a.animeId}`, a); + } + }); + + // 2. 视图层数据适配 + const formattedData = localAnimes + .filter(anime => !anime.isHiddenChild) + .map(anime => { + // 兼容实体类与普通对象 + const animeJson = typeof anime.toJson === 'function' ? anime.toJson() : { ...anime }; + const { links = [], mergedChildren = [], ...rest } = animeJson; + + return { + ...rest, + episodes: rest.episodeCount || rest.episodes || 1, + links, + mergedChildren: mergedChildren.map(child => { + const fullChild = fullAnimeMap.get(`${child.source}_${child.animeId}`); + const fullChildJson = typeof fullChild?.toJson === 'function' ? fullChild.toJson() : fullChild; + const fullLinks = (fullChildJson?.links?.length > 0) ? fullChildJson.links : (child.links || []); + + return { + ...child, + episodes: child.episodeCount || child.episodes || 1, + links: fullLinks + }; + }) + }; + }); + + return jsonResponse({ success: true, data: formattedData }, 200); + } catch (error) { + log("error", `[server] Fetch cache animes failed: ${error.message}`); + return jsonResponse({ success: false, message: `获取缓存失败: ${error.message}` }, 500); + } +} diff --git a/danmu_api/models/dandan-model.js b/danmu_api/models/dandan-model.js index 9727ed7..c81a43e 100644 --- a/danmu_api/models/dandan-model.js +++ b/danmu_api/models/dandan-model.js @@ -6,7 +6,8 @@ import { validateType } from "../utils/common-util.js"; export class Anime { constructor({ animeId = 111, bangumiId = "", animeTitle = "", type = "", typeDescription = "", imageUrl = "", startDate = "", episodeCount = 1, - rating = 0, isFavorited = true, source = "", links = [] } = {}) { + rating = 0, isFavorited = true, source = "", links = [], + mergedChildren = [], isHiddenChild = false } = {}) { // ---- 类型检查 ---- validateType(animeId, "number"); validateType(bangumiId, "string"); @@ -20,13 +21,15 @@ export class Anime { validateType(isFavorited, "boolean"); validateType(source, "string"); validateType(links, "array"); + validateType(mergedChildren, "array"); + validateType(isHiddenChild, "boolean"); // 将 links 转换为 Link 实例数组 this.links = links.map(linkData => Link.fromJson(linkData)); // 直接解构并赋值给 this Object.assign(this, { animeId, bangumiId, animeTitle, type, typeDescription, imageUrl, startDate, - episodeCount, rating, isFavorited, source }); + episodeCount, rating, isFavorited, source, mergedChildren, isHiddenChild }); } // ---- 静态方法:从 JSON 创建 Anime 对象 ---- diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js index f978a4d..5662092 100644 --- a/danmu_api/ui/css/components.css.js +++ b/danmu_api/ui/css/components.css.js @@ -691,6 +691,13 @@ export const componentsCssContent = /* css */ ` .anime-title { margin: 8px 0 5px; font-size: 12px; + word-break: normal; /* 保持正常的单词换行规则,优先在空格处断句以保证可读性 */ + overflow-wrap: break-word; /* 当长字符串(如多源合并后缀)超出容器时,允许在其内部强制换行 */ + display: -webkit-box; /* 启用 WebKit 特有的弹性伸缩盒模型 */ + -webkit-line-clamp: 5; /* 限制文本块最多显示的行数为5行 */ + -webkit-box-orient: vertical; /* 设置弹性盒子的子元素垂直排列 */ + overflow: hidden; /* 隐藏超出容器设定高度范围的文本内容 */ + text-overflow: ellipsis; /* 文本溢出时使用省略号(...)进行截断展示 */ } .episode-list-container { @@ -1726,4 +1733,201 @@ export const componentsCssContent = /* css */ ` gap: 8px; justify-content: flex-end; } + +/* 最近数据与animes缓存面板样式 */ + +/* 面板容器 */ +.recent-data-panel { + display: none; + max-height: 350px; + overflow-y: auto; +} + +.anime-cache-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.anime-cache-card { + background: #fff; + border-radius: 6px; + border: 1px solid #eaeaea; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); + overflow: hidden; +} + +.anime-cache-card-body { + display: flex; + gap: 12px; + align-items: center; + padding: 10px; +} + +.anime-cache-cover { + width: 44px; + height: 60px; + border-radius: 4px; + background-color: #e2e3e5; + background-size: cover; + background-position: center; + flex-shrink: 0; + box-shadow: 0 1px 4px rgba(0,0,0,0.15); +} + +.anime-cache-info, +.anime-cache-child-info { + flex: 1; + min-width: 0; +} + +.anime-cache-title { + font-weight: 600; + font-size: 13px; + line-height: 1.4; + margin-bottom: 4px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-all; +} + +.anime-cache-meta { + color: #888; + font-size: 11px; +} + +.anime-cache-actions, +.anime-cache-child-actions { + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.anime-cache-actions { gap: 6px; } + +.btn-xs { + padding: 2px 6px; + font-size: 11px; +} + +/* 卡片底部标签切换栏(Tab 风格) */ +.anime-cache-footer { + display: flex; + gap: 8px; + padding: 8px 10px; + background: #fdfdfd; + border-top: 1px solid #f0f0f0; +} + +.cache-badge { + font-size: 11px; + padding: 4px 10px; + border-radius: 12px; + cursor: pointer; + user-select: none; + display: inline-flex; + align-items: center; + font-weight: 500; + transition: all 0.2s; +} + +.cache-badge.badge-sources { background: #e3f2fd; color: #0d47a1; border: 1px solid #bbdefb; } +.cache-badge.badge-episodes { background: #e8f5e9; color: #1b5e20; border: 1px solid #c8e6c9; } +.cache-badge.active { filter: brightness(0.9); box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); } + +/* 折叠列表容器 */ +.merged-children-container, +.episodes-list-container { + display: none; + flex-direction: column; + gap: 6px; + padding: 8px 10px; + background: #fafafa; + border-top: 1px solid #f0f0f0; +} + +.episodes-list-container { + max-height: 200px; + overflow-y: auto; +} + +/* 子节点卡片 */ +.anime-cache-child-item { + display: flex; + flex-direction: column; + background: #fff; + padding: 8px; + border-radius: 4px; + border: 1px dashed #ccc; +} + +.anime-cache-child-main { + display: flex; + gap: 10px; + align-items: center; + width: 100%; +} + +.anime-cache-child-cover { + width: 33px; + height: 45px; + border-radius: 3px; + background-color: #e2e3e5; + background-size: cover; + background-position: center; + flex-shrink: 0; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +.anime-cache-child-title { + font-weight: 500; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.anime-cache-child-actions { gap: 4px; } + +/* 合并映射详情 */ +.child-mapping-toggle { + font-size: 10px; + color: #666; + cursor: pointer; + margin-top: 8px; + padding-top: 6px; + border-top: 1px dashed #eee; + text-align: center; +} + +.child-mapping-toggle:hover { + color: #0066cc; +} + +.child-mapping-container { + display: none; + flex-direction: column; + gap: 4px; + margin-top: 6px; + background: #fdfdfd; + padding: 6px; + border-radius: 4px; + border: 1px solid #f0f0f0; + max-height: 150px; + overflow-y: auto; +} + +.mapping-row { + font-size: 10px; + display: flex; + gap: 6px; + align-items: flex-start; +} + +.mapping-status { flex-shrink: 0; font-weight: bold; } +.mapping-status.success { color: #28a745; } +.mapping-status.warning { color: #d39e00; } +.mapping-text { color: #555; word-break: break-all; } `; diff --git a/danmu_api/ui/js/systemsettings.js b/danmu_api/ui/js/systemsettings.js index 14a8f7c..184035e 100644 --- a/danmu_api/ui/js/systemsettings.js +++ b/danmu_api/ui/js/systemsettings.js @@ -522,6 +522,17 @@ function renderValueInput(item) { }).join('')} + + \${currentKey === 'MERGE_SOURCE_PAIRS' ? \` +