diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 9d7d9f6..cdcafb7 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -2057,7 +2057,8 @@ export async function matchAnime(url, req, clientIp) { "type": resAnime.type, "typeDescription": resAnime.typeDescription, "shift": 0, - "imageUrl": resAnime.imageUrl + "imageUrl": resAnime.imageUrl, + "url": resEpisode.url || "" }) ] } diff --git a/danmu_api/models/dandan-model.js b/danmu_api/models/dandan-model.js index 1cf053a..85a696b 100644 --- a/danmu_api/models/dandan-model.js +++ b/danmu_api/models/dandan-model.js @@ -82,7 +82,7 @@ class Link { // ===================== export class AnimeMatch { constructor({ episodeId = 10001, animeId = 111, animeTitle = "", episodeTitle = "", - type = "", typeDescription = "", shift = 1, imageUrl = "" } = {}) { + type = "", typeDescription = "", shift = 1, imageUrl = "", url = "" } = {}) { // ---- 类型检查 ---- validateType(episodeId, "number"); validateType(animeId, "number"); @@ -92,9 +92,10 @@ export class AnimeMatch { validateType(typeDescription, "string"); validateType(shift, "number"); validateType(imageUrl, "string"); + validateType(url, "string"); // 直接解构并赋值给 this - Object.assign(this, { episodeId, animeId, animeTitle, episodeTitle, type, typeDescription, shift, imageUrl }); + Object.assign(this, { episodeId, animeId, animeTitle, episodeTitle, type, typeDescription, shift, imageUrl, url }); } // ---- 静态方法:从 JSON 创建 User 对象 ---- diff --git a/danmu_api/ui/README.md b/danmu_api/ui/README.md index 6630036..82c26c7 100644 --- a/danmu_api/ui/README.md +++ b/danmu_api/ui/README.md @@ -82,7 +82,7 @@ UI 系统需要通过在 URL 中添加 TOKEN 来访问,以确保安全性: - 手动匹配测试:搜索动漫 → 选择番剧 → 选择剧集 → 获取弹幕 - 弹幕统计:弹幕数、时长、高能时刻、平均密度、匹配时长、类型分布 - 弹幕热力图:按时间段可视化弹幕密度分布 -- 弹幕列表:支持全部/滚动/顶部/底部过滤,显示颜色和类型标签,懒加载每次100条 +- 弹幕列表:支持全部/滚动/顶部/底部过滤,显示颜色和类型标签,首次加载及每次加载更多500条 - 导出功能:支持导出JSON和XML格式弹幕文件 - 时长处理:优先请求 `/api/v2/comment/:commentId?format=json&duration=true` 返回的 `videoDuration`,没有真实时长时再回退到尾部裁剪算法 - 视图导航优化:搜索结果/剧集详情/弹幕详情逐级切换,带返回按钮和加载动画 diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js index 7610797..b334d8e 100644 --- a/danmu_api/ui/css/components.css.js +++ b/danmu_api/ui/css/components.css.js @@ -1832,6 +1832,25 @@ body.modal-open { .danmu-filter-tabs::-webkit-scrollbar { display: none; } +.danmu-list-tools { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.danmu-search { + flex: 1 1 260px; + min-width: 0; + max-width: 420px; +} + +.danmu-search-status { + color: var(--theme-muted); + font-size: 12px; + white-space: nowrap; +} + .danmu-filter-tab { flex: 0 0 auto; display: inline-flex; @@ -2034,6 +2053,109 @@ body.modal-open { display: flex; gap: 6px; margin-left: auto; + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; +} + +.danmu-export-select { + background-color: var(--theme-panel-strong); + color: var(--theme-text); + border: 1px solid var(--theme-border); + border-radius: var(--app-radius-btn); + padding: 5px 12px; + font-size: 12px; + line-height: 1.4; + max-width: 220px; + cursor: pointer; +} + +.danmu-export-select:hover { + border-color: var(--theme-accent); +} + +.danmu-export-select:focus-visible { + outline: 2px solid var(--theme-accent); + outline-offset: 2px; +} + +.danmu-source-url { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + margin-bottom: 14px; + padding: 9px 10px; + border: 1px solid var(--theme-border); + border-radius: var(--app-radius-card-sm); + background: var(--theme-panel-bg); +} + +.danmu-source-url-label { + color: var(--theme-muted); + font-size: 12px; + white-space: nowrap; +} + +.danmu-source-url-value { + min-width: 0; + overflow: hidden; + color: var(--theme-text); + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; + user-select: text; +} + +.danmu-source-url-actions { + display: flex; + gap: 6px; +} + +.danmu-source-url-actions .btn { + flex: 0 0 auto; + white-space: nowrap; +} + +@media (max-width: 768px) { + .danmu-list-tools { + flex-wrap: wrap; + } + + .preview-search.danmu-search { + order: 0; + flex: 1 1 100%; + width: 100%; + max-width: none; + } + + .danmu-search-status { + order: 1; + margin-left: auto; + } + + .danmu-export-btns { + flex: 1 1 0; + min-width: 0; + flex-wrap: nowrap; + } + + .danmu-export-select { + flex: 1 1 160px; + max-width: none; + min-width: 0; + } + + .danmu-export-btns .btn { + flex: 0 0 auto; + white-space: nowrap; + } + + .danmu-result-toolbar .btn-back { + flex: 0 0 auto; + white-space: nowrap; + } } /* ============ 颜色池配置 ============ */ diff --git a/danmu_api/ui/js/apitest.js b/danmu_api/ui/js/apitest.js index a1e23d1..4bba619 100644 --- a/danmu_api/ui/js/apitest.js +++ b/danmu_api/ui/js/apitest.js @@ -64,10 +64,12 @@ let danmuTestState = { filteredComments: [], currentFilter: 'all', displayedCount: 0, - pageSize: 100, + pageSize: 500, currentEpisodeId: null, currentTitle: '', currentDuration: 0, + currentSourceUrl: '', + currentSearchQuery: '', currentCallTrace: null, currentManualSearchCallTraceBase: null, currentManualEpisodeCallTraceBase: null, @@ -586,6 +588,51 @@ function inlineJsString(value) { return escapeHtml(JSON.stringify(String(value || ''))); } +function getDanmuSourceUrl(value) { + const sourceUrl = String(value || '').trim(); + if (!sourceUrl) return ''; + try { + const parsedUrl = new URL(sourceUrl); + return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:' ? sourceUrl : ''; + } catch (error) { + return ''; + } +} + +function renderDanmuSourceUrl() { + const sourceUrl = getDanmuSourceUrl(danmuTestState.currentSourceUrl); + if (!sourceUrl) return ''; + const escapedUrl = escapeHtml(sourceUrl); + return '
'; +} + +function copyDanmuSourceUrl(button) { + const sourceUrl = getDanmuSourceUrl(danmuTestState.currentSourceUrl); + if (!sourceUrl) return; + + const done = function() { + const originalText = button.textContent; + button.textContent = '已复制'; + setTimeout(function() { button.textContent = originalText; }, 1500); + addLog('来源地址已复制', 'success'); + }; + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(sourceUrl).then(done).catch(function() { + fallbackCopy(sourceUrl, done); + }); + } else { + fallbackCopy(sourceUrl, done); + } +} + function createDanmuCallTrace(mode, inputText) { return { mode: mode || 'manual', @@ -1111,7 +1158,7 @@ async function autoMatchTest() { }); addLog('自动匹配命中: ' + title + ' (共' + data.matches.length + '个结果,取第1个)', 'success'); setDanmuFlowButtonLoading(btn, flowRequestId, false); - fetchDanmuForTest(best.episodeId, title, 'auto', trace); + fetchDanmuForTest(best.episodeId, title, 'auto', trace, best.url); return; } else { finishDanmuCallEmpty(trace, matchStartedAt, { @@ -1314,7 +1361,7 @@ function displayManualEpisodeList(animeTitle, episodes) { const title = String(animeTitle || '') + ' 第' + episodeNumber + '集'; html += '无弹幕数据
'; + const emptyText = danmuTestState.currentSearchQuery.trim() ? '未找到匹配的弹幕' : '无弹幕数据'; + container.innerHTML = html || '' + emptyText + '
'; } else { container.insertAdjacentHTML('beforeend', html); }