diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js index 0a6edc6..967afaf 100644 --- a/danmu_api/ui/css/components.css.js +++ b/danmu_api/ui/css/components.css.js @@ -1079,7 +1079,7 @@ export const componentsCssContent = /* css */ ` flex-direction: column; align-items: stretch; } - + .record-method, .record-interface, .record-ip { @@ -1087,4 +1087,367 @@ export const componentsCssContent = /* css */ ` box-sizing: border-box; } } + +/* ===================== */ +/* 弹幕测试相关样式 */ +/* ===================== */ + +/* 顶级标签页 */ +.api-top-tabs { + display: flex; + gap: 0; + margin-bottom: 20px; + border-bottom: 2px solid #e0e0e0; +} + +.api-top-tab { + padding: 10px 24px; + background: none; + border: none; + border-bottom: 3px solid transparent; + cursor: pointer; + font-size: 15px; + font-weight: 500; + color: #666; + transition: all 0.3s; + margin-bottom: -2px; +} + +.api-top-tab:hover { + color: #667eea; +} + +.api-top-tab.active { + color: #667eea; + border-bottom-color: #667eea; + font-weight: bold; +} + +.api-tab-content { + display: none; +} + +.api-tab-content.active { + display: block; +} + +/* 弹幕测试子标签 */ +.danmu-test-tabs { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.danmu-test-tab { + padding: 8px 20px; + background: #f0f0f0; + border: 2px solid transparent; + border-radius: 20px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.3s; + color: #555; +} + +.danmu-test-tab:hover { + background: #e0e0e0; +} + +.danmu-test-tab.active { + background: #667eea; + color: white; + border-color: #667eea; +} + +.danmu-test-panel { + display: none; +} + +.danmu-test-panel.active { + display: block; +} + +/* 弹幕统计卡片 */ +.danmu-stats { + margin-bottom: 15px; +} + +.danmu-stats-title { + font-size: 16px; + font-weight: bold; + color: #667eea; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid #eee; +} + +.danmu-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 12px; +} + +.danmu-stat-card { + background: #f8f9fa; + border-radius: 10px; + padding: 15px; + text-align: center; + border: 1px solid #eee; + transition: transform 0.2s; +} + +.danmu-stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} + +.stat-value { + font-size: 20px; + font-weight: bold; + color: #333; + margin-bottom: 4px; + word-break: break-all; +} + +.stat-label { + font-size: 12px; + color: #999; +} + +/* 弹幕热力图 */ +.danmu-heatmap-container { + margin-bottom: 15px; + background: #f8f9fa; + border-radius: 10px; + padding: 15px; +} + +.danmu-heatmap-container h3 { + margin: 0 0 10px; + font-size: 15px; +} + +.heatmap-bars { + display: flex; + align-items: flex-end; + gap: 2px; + height: 100px; + padding: 0 2px; +} + +.heatmap-bar { + flex: 1; + min-width: 3px; + border-radius: 2px 2px 0 0; + transition: opacity 0.2s; + cursor: pointer; +} + +.heatmap-bar:hover { + opacity: 0.8; +} + +.heatmap-axis { + display: flex; + justify-content: space-between; + margin-top: 6px; + font-size: 11px; + color: #999; +} + +/* 弹幕过滤标签 */ +.danmu-filter-tabs { + display: flex; + gap: 8px; + margin-bottom: 12px; +} + +.danmu-filter-tab { + padding: 6px 16px; + background: #f0f0f0; + border: 1px solid #ddd; + border-radius: 15px; + cursor: pointer; + font-size: 13px; + transition: all 0.2s; + color: #555; +} + +.danmu-filter-tab:hover { + background: #e0e0e0; +} + +.danmu-filter-tab.active { + background: #667eea; + color: white; + border-color: #667eea; +} + +/* 弹幕列表 */ +.danmu-list-area { + background: #f8f9fa; + border-radius: 10px; + padding: 15px; +} + +.danmu-list-area h3 { + margin: 0 0 10px; + font-size: 15px; +} + +.danmu-list { + max-height: 500px; + overflow-y: auto; + border: 1px solid #eee; + border-radius: 8px; + background: white; +} + +.danmu-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-bottom: 1px solid #f0f0f0; + font-size: 13px; + transition: background 0.15s; +} + +.danmu-item:last-child { + border-bottom: none; +} + +.danmu-item:hover { + background: #f8f9ff; +} + +.danmu-time { + color: #999; + font-family: 'Courier New', monospace; + font-size: 12px; + min-width: 50px; + flex-shrink: 0; +} + +.danmu-color-dot { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; + border: 1px solid rgba(0,0,0,0.1); +} + +.danmu-mode-tag { + font-size: 11px; + padding: 2px 6px; + border-radius: 8px; + flex-shrink: 0; + font-weight: 500; +} + +.danmu-mode-scroll { + background: #e3f2fd; + color: #1976d2; +} + +.danmu-mode-top { + background: #fce4ec; + color: #c62828; +} + +.danmu-mode-bottom { + background: #e8f5e9; + color: #2e7d32; +} + +.danmu-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: #333; +} + +.danmu-load-more { + margin-top: 10px; +} + +/* 返回按钮 */ +.btn-back { + padding: 8px 16px; + background: #f0f0f0; + border: 1px solid #ddd; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + color: #555; + transition: all 0.2s; + margin-bottom: 15px; + display: inline-block; +} + +.btn-back:hover { + background: #e0e0e0; + color: #333; +} + +/* 弹幕加载动画 */ +.danmu-loading { + text-align: center; + padding: 60px 20px; +} + +.danmu-loading .loading-spinner { + margin: 0 auto 15px; +} + +.danmu-loading .loading-text { + color: #666; + font-size: 15px; +} + +/* 弹幕结果工具栏(返回+导出同行) */ +.danmu-result-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + flex-wrap: wrap; + gap: 10px; +} + +.danmu-result-toolbar .btn-back { + margin-bottom: 0; +} + +.danmu-export-btns { + display: flex; + gap: 8px; + margin-left: auto; +} + +/* 弹幕测试移动端适配 */ +@media (max-width: 768px) { + .danmu-stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .api-top-tab { + padding: 8px 16px; + font-size: 14px; + } + + .danmu-test-tabs { + flex-wrap: wrap; + } + + .stat-value { + font-size: 16px; + } + + .heatmap-bars { + height: 70px; + } +} `; diff --git a/danmu_api/ui/js/apitest.js b/danmu_api/ui/js/apitest.js index e79ec51..15bf6ea 100644 --- a/danmu_api/ui/js/apitest.js +++ b/danmu_api/ui/js/apitest.js @@ -57,15 +57,35 @@ const apiConfigs = { } }; +// 弹幕测试全局状态 +let danmuTestState = { + allComments: [], + filteredComments: [], + currentFilter: 'all', + displayedCount: 0, + pageSize: 100, + currentEpisodeId: null, + currentTitle: '' +}; + // 初始化接口调试界面 function initApiTestInterface() { - // 为API选择下拉框添加回车事件监听 const apiSelect = document.getElementById('api-select'); if (apiSelect) { apiSelect.addEventListener('keypress', function(event) { - if (event.key === 'Enter') { - loadApiParams(); - } + if (event.key === 'Enter') { loadApiParams(); } + }); + } + const autoInput = document.getElementById('auto-match-filename'); + if (autoInput) { + autoInput.addEventListener('keypress', function(event) { + if (event.key === 'Enter') { autoMatchTest(); } + }); + } + const manualInput = document.getElementById('manual-search-keyword'); + if (manualInput) { + manualInput.addEventListener('keypress', function(event) { + if (event.key === 'Enter') { manualSearchAnime(); } }); } } @@ -344,4 +364,522 @@ function testApi() { sendButton.disabled = false; }); } + +// ===================== +// 标签页切换 +// ===================== +function switchApiTopTab(tab, event) { + document.querySelectorAll('.api-top-tab').forEach(btn => btn.classList.remove('active')); + document.querySelectorAll('.api-tab-content').forEach(el => el.classList.remove('active')); + event.target.classList.add('active'); + if (tab === 'debug') { + document.getElementById('api-debug-content').classList.add('active'); + } else { + document.getElementById('danmu-test-content').classList.add('active'); + } +} + +function switchDanmuTestTab(tab, event) { + document.querySelectorAll('.danmu-test-tab').forEach(btn => btn.classList.remove('active')); + document.querySelectorAll('.danmu-test-panel').forEach(el => el.classList.remove('active')); + event.target.classList.add('active'); + if (tab === 'auto') { + document.getElementById('auto-match-panel').classList.add('active'); + } else { + document.getElementById('manual-match-panel').classList.add('active'); + } +} + +// ===================== +// 工具函数 +// ===================== +function decColorToHex(dec) { + const n = parseInt(dec) || 16777215; + return '#' + n.toString(16).padStart(6, '0'); +} + +function parseDanmuMode(p) { + const parts = p.split(','); + return parseInt(parts[1]) || 1; +} + +function parseDanmuColor(p) { + const parts = p.split(','); + return parseInt(parts[2]) || 16777215; +} + +function parseDanmuTime(p) { + return parseFloat(p.split(',')[0]) || 0; +} + +function getModeLabel(mode) { + switch (mode) { + case 4: return '底部'; + case 5: return '顶部'; + default: return '滚动'; + } +} + +function formatDuration(seconds) { + const s = Math.floor(seconds); + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + if (h > 0) return h + ':' + String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0'); + return m + ':' + String(sec).padStart(2,'0'); +} + +// 用P99分位数计算有效时长,剔除异常时间点 +function getEffectiveDuration(comments) { + const times = comments.map(c => c.t !== undefined ? c.t : parseDanmuTime(c.p)).sort((a, b) => a - b); + if (times.length === 0) return 0; + const idx = Math.floor(times.length * 0.99); + return times[Math.min(idx, times.length - 1)]; +} + +function setBtnLoading(btn, loading) { + if (!btn) return; + if (loading) { + btn._origText = btn.innerHTML; + btn.innerHTML = ''; + btn.disabled = true; + } else { + btn.innerHTML = btn._origText || '按钮'; + btn.disabled = false; + } +} + +// 通用:显示指定面板,隐藏同级其他面板 +function showDanmuView(showIds, hideIds) { + hideIds.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); + showIds.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'block'; }); +} + +// 生成返回按钮HTML +function backBtnHtml(text, onclick) { + return ''; +} + +// ===================== +// 自动匹配测试 +// ===================== +async function autoMatchTest() { + const fileName = document.getElementById('auto-match-filename').value.trim(); + if (!fileName) { customAlert('请输入文件名'); return; } + + const btn = document.getElementById('auto-match-btn'); + setBtnLoading(btn, true); + document.getElementById('danmu-result-area').style.display = 'none'; + addLog('自动匹配测试: ' + fileName, 'info'); + + try { + const resp = await fetch(buildApiUrl('/api/v2/match'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fileName: fileName }) + }); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const data = await resp.json(); + + if (data.isMatched && data.matches && data.matches.length > 0) { + const best = data.matches[0]; + const title = (best.animeTitle || '') + ' ' + (best.episodeTitle || ''); + addLog('自动匹配命中: ' + title + ' (共' + data.matches.length + '个结果,取第1个)', 'success'); + setBtnLoading(btn, false); + fetchDanmuForTest(best.episodeId, title, 'auto'); + return; + } else { + customAlert('未匹配到任何结果'); + addLog('自动匹配无结果', 'warn'); + } + } catch (e) { + customAlert('匹配失败: ' + e.message); + addLog('自动匹配失败: ' + e.message, 'error'); + } finally { + setBtnLoading(btn, false); + } +} + +// ===================== +// 手动匹配测试 +// ===================== +async function manualSearchAnime() { + const keyword = document.getElementById('manual-search-keyword').value.trim(); + if (!keyword) { customAlert('请输入搜索关键字'); return; } + + const btn = document.getElementById('manual-search-btn'); + setBtnLoading(btn, true); + document.getElementById('manual-anime-list').style.display = 'none'; + document.getElementById('manual-episode-list').style.display = 'none'; + document.getElementById('danmu-result-area').style.display = 'none'; + addLog('手动搜索: ' + keyword, 'info'); + + try { + const resp = await fetch(buildApiUrl('/api/v2/search/anime?keyword=' + encodeURIComponent(keyword))); + if (!resp.ok) throw new Error('HTTP ' + resp.status); + const data = await resp.json(); + + if (data.success && data.animes && data.animes.length > 0) { + displayManualAnimeList(data.animes); + addLog('搜索到 ' + data.animes.length + ' 个动漫', 'success'); + } else { + customAlert('未找到相关动漫'); + addLog('搜索无结果', 'warn'); + } + } catch (e) { + customAlert('搜索失败: ' + e.message); + addLog('搜索失败: ' + e.message, 'error'); + } finally { + setBtnLoading(btn, false); + } +} + +function displayManualAnimeList(animes) { + const container = document.getElementById('manual-anime-list'); + let html = '
无数据
'; return; } + + const barCount = Math.min(60, Math.max(20, Math.ceil(maxTime / 30))); + const segLen = maxTime / barCount; + const segs = new Array(barCount).fill(0); + comments.forEach(c => { + const t = c.t !== undefined ? c.t : parseDanmuTime(c.p); + if (t > maxTime) return; // 跳过异常时间点 + const idx = Math.min(Math.floor(t / segLen), barCount - 1); + segs[idx]++; + }); + + const maxSeg = Math.max(...segs, 1); + let html = ''; + html += '无弹幕数据
'; + } else { + container.insertAdjacentHTML('beforeend', html); + } + danmuTestState.displayedCount = end; + + // 控制加载更多按钮 + const loadMoreBtn = document.getElementById('danmu-load-more'); + if (end < list.length) { + loadMoreBtn.style.display = 'block'; + loadMoreBtn.textContent = '加载更多 (' + end + '/' + list.length + ')'; + } else { + loadMoreBtn.style.display = 'none'; + } +} + +function loadMoreDanmu() { + renderDanmuList(); +} `; diff --git a/danmu_api/ui/template.js b/danmu_api/ui/template.js index d387554..89e7536 100644 --- a/danmu_api/ui/template.js +++ b/danmu_api/ui/template.js @@ -97,30 +97,68 @@ export const HTML_TEMPLATE = /* html */ `