fix(MCN工作台): 修复刷新回首页(P0) + 周榜页全面对齐首页交互

- 根因: app.js 先于 data-pages.js 加载,boot 立即 router() 时 DataPages 未定义 → 数据页路由被当未知路由 → location.hash='#/' 回首页
- 修复: boot 改 DOMContentLoaded + 50ms 轮询等 DataPages 就绪再路由;router 兜底保护(数据页名+DataPages未定义不跳首页)
- 验证: browser-harness 真实验证(独立Chrome 9223调试实例) 直接访问/刷新/切换全部通过
- 首页周榜: 新增板块(前10+赛道日期筛选)、综合得分倒序排序、9列(新增粉丝/点赞/评论/分享+功能列)、导入按钮、去分页
- 完整周榜页(#/ranking): 对齐首页(赛道列前置、工具栏/分页右对齐、综合得分倒序+橙色、功能列居中、导入/查看按钮区分)
- 路由记忆: localStorage+cookie 双写,刷新后恢复上次页面
- 其他: AI写剧本→AI生成脚本、全部字体按比例放大+2(取整无小数)、外链新标签打开、btn-import/btn-view 样式
This commit is contained in:
maogeigei committed 2026-08-27 20:43:39 +08:00
1 parent 63e8a66f4f
commit 79ae403b7b
9 files changed
+325 -102

No files matched your search

@@ -36,7 +36,7 @@ const ICON_PATHS = {
// 功能图标亮色映射(不同功能不同亮色;未列出的保持 currentColor 中性)
const ICON_COLORS = {
'trending-up': '#e8590c', 'users': '#2f6fed', 'pen': '#8250df', 'flask': '#1a7f37',
'trending-up': '#e8590c', 'users': '#2f6fed', 'pen': '#ec4899', 'flask': '#0ea5e9',
'lightbulb': '#d99a2b', 'refresh': '#0f7f8f', 'chart': '#d6336c', 'wrench': '#5b6470',
};
@@ -166,8 +166,20 @@ function renderHome() {
<span class="data-card-desc">${c.desc}</span>
</button>`).join('')}
</div>
<div class="rank-preview">
<div class="recent-head">
<h3>抖音周榜</h3>
<div class="rank-tools">
<select id="rankCat" class="input-select rank-filter" title="赛道筛选"></select>
<button class="btn btn-sm" id="rankPrevDate" title="上一期">◀</button>
<span id="rankDateLabel" class="rank-date-label"></span>
<button class="btn btn-sm" id="rankNextDate" title="下一期">▶</button>
</div>
</div>
<div id="rankPreviewList" class="rank-preview-list"><span class="dim" style="padding:12px">加载中…</span></div>
</div>
<div class="recent-scripts">
<div class="recent-head"><h3>AI写剧本(最近)</h3><a class="link" href="#/rewrites">查看全部 →</a></div>
<div class="recent-head"><h3>AI生成脚本</h3><a class="link" href="#/rewrites">查看全部</a></div>
<div id="recentScriptsList" class="recent-list"><span class="dim">加载中…</span></div>
</div>
</div>`;
@@ -243,6 +255,53 @@ async function loadRecentScripts() {
}));
} catch (e) { el.innerHTML = '<span class="dim">' + esc(e.message) + '</span>'; }
}
/* ================= 首页 抖音周榜 预览(前10 + 赛道/日期筛选,无分页) ================= */
let _rankPreview = { page: 1, pageSize: 10, dates: [], categories: [], date: '', category: '' };
function renderRankTools() {
const catSel = $('#rankCat');
if (catSel) {
catSel.innerHTML = '<option value="">全部赛道</option>' + _rankPreview.categories.map((c) => `<option value="${esc(c)}" ${c === _rankPreview.category ? 'selected' : ''}>${esc(c)}</option>`).join('');
catSel.onchange = () => { _rankPreview.category = catSel.value; loadRankingPreview(); };
}
const dateLabel = $('#rankDateLabel');
const di = _rankPreview.dates.indexOf(_rankPreview.date);
if (dateLabel) dateLabel.textContent = _rankPreview.date ? _rankPreview.date.slice(5).replace('-', '-') : '';
const prevBtn = $('#rankPrevDate'), nextBtn = $('#rankNextDate');
if (prevBtn) { prevBtn.disabled = di <= 0; prevBtn.onclick = () => { if (di > 0) { _rankPreview.date = _rankPreview.dates[di - 1]; renderRankTools(); loadRankingPreview(); } }; }
if (nextBtn) { nextBtn.disabled = di < 0 || di >= _rankPreview.dates.length - 1; nextBtn.onclick = () => { if (di >= 0 && di < _rankPreview.dates.length - 1) { _rankPreview.date = _rankPreview.dates[di + 1]; renderRankTools(); loadRankingPreview(); } }; }
}
async function loadRankingPreview() {
const listEl = $('#rankPreviewList');
if (!listEl) return;
try {
if (!_rankPreview.dates.length) {
const m = await api('/api/dsh/ranking?page=1&pageSize=1');
_rankPreview.dates = m.dates || [];
_rankPreview.categories = m.categories || [];
_rankPreview.date = m.date || (_rankPreview.dates[0] || '');
_rankPreview.category = m.category || '';
renderRankTools();
}
const d = await api('/api/dsh/ranking?page=1&pageSize=' + _rankPreview.pageSize + '&date=' + encodeURIComponent(_rankPreview.date) + '&category=' + encodeURIComponent(_rankPreview.category));
const items = d.items || [];
const head = '<div class="rank-row rank-head"><span class="rk-no">排名</span><span class="rk-cat">赛道</span><span class="rk-name">账号</span><span class="rk-score">综合得分</span><span class="rk-num">新增粉丝</span><span class="rk-num">新增点赞</span><span class="rk-num">新增评论</span><span class="rk-num">新增分享</span><span class="rk-act">功能</span></div>';
listEl.innerHTML = items.length ? head + items.map((r) => `<div class="rank-row">
<span class="rk-no">${r.rank ?? '-'}</span>
<span class="rk-cat">${esc(r.category || '-')}</span>
<span class="rk-name">${r.inDb && r.dbId ? `<a class="link" href="#/account/${r.dbId}">${esc(r.accountName)}</a>` : (r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}">${esc(r.accountName)}</a>` : esc(r.accountName))}</span>
<span class="rk-score">${r.comprehensiveScore ?? '-'}</span>
<span class="rk-num">${esc(r.newFans ?? '-')}</span>
<span class="rk-num">${esc(r.newLikes ?? '-')}</span>
<span class="rk-num">${esc(r.newComments ?? '-')}</span>
<span class="rk-num">${esc(r.newShares ?? '-')}</span>
<span class="rk-act">${r.inDb && r.dbId ? `<a class="btn btn-sm btn-view" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm btn-import" data-rk-import data-rk-name="${esc(r.accountName)}" data-rk-url="${esc(r.profileUrl || '')}" title="导入并分析该账号">导入</button>`}</span>
</div>`).join('') : '<span class="dim" style="padding:12px">暂无周榜数据</span>';
listEl.querySelectorAll('[data-rk-import]').forEach((b) => b.addEventListener('click', () => {
firePrompt(`请帮我把抖音账号「${b.dataset.rkName}」导入并分析(账号信息 + 视频列表):${b.dataset.rkUrl || ''}`, '导入指令');
}));
} catch (e) { listEl.innerHTML = '<span class="dim" style="padding:12px">' + esc(e.message) + '</span>'; }
}
function openRecentScript(s) {
const title = s.title || s.video_title || 'AI 写剧本';
const mask = document.createElement('div'); mask.className = 'modal-mask';
@@ -334,7 +393,7 @@ function nodeHtml(n, depth) {
return `<div class="tree-node"><div class="tree-row" data-path="${esc(n.path)}" data-type="dir"><span class="caret">${open ? '▾' : '▸'}</span><span class="icon">${svgIcon(open ? 'folder-open' : 'folder', 15)}</span><span class="fname">${esc(n.name)}</span></div>${open && children.length ? `<div class="tree-children">${children.map((c) => nodeHtml(c, depth + 1)).join('')}</div>` : ''}</div>`;
}
const ext = (n.ext || '').slice(1);
return `<div class="tree-row" data-path="${esc(n.path)}" data-type="file"><span class="caret"></span><span class="icon">${svgIcon(EXT_ICON[ext] || 'file', 15)}</span><span class="fname ${'ext-' + (ext || 'txt')}">${esc(n.name)}</span><span style="margin-left:auto;font-size:11px;color:var(--dim)">${fmtSize(n.size)}</span></div>`;
return `<div class="tree-row" data-path="${esc(n.path)}" data-type="file"><span class="caret"></span><span class="icon">${svgIcon(EXT_ICON[ext] || 'file', 15)}</span><span class="fname ${'ext-' + (ext || 'txt')}">${esc(n.name)}</span><span style="margin-left:auto;font-size:13px;color:var(--dim)">${fmtSize(n.size)}</span></div>`;
}
function renderTree() {
@@ -374,7 +433,7 @@ async function openFile(p) {
function renderRootList() {
const list = $('#rootList');
const roots = fileState.roots;
list.innerHTML = (roots.extraRoots || []).map((p) => `<div class="root-item"><span class="rp">${esc(p)}</span><button class="rm" data-rm="${esc(p)}" title="移除">${svgIcon('x', 13)}</button></div>`).join('') || '<div class="dim" style="font-size:12px">暂无自定义目录</div>';
list.innerHTML = (roots.extraRoots || []).map((p) => `<div class="root-item"><span class="rp">${esc(p)}</span><button class="rm" data-rm="${esc(p)}" title="移除">${svgIcon('x', 13)}</button></div>`).join('') || '<div class="dim" style="font-size:14px">暂无自定义目录</div>';
list.querySelectorAll('.rm').forEach((btn) => btn.addEventListener('click', async () => {
const p = btn.dataset.rm;
roots.extraRoots = roots.extraRoots.filter((x) => x !== p);
@@ -416,6 +475,7 @@ async function router() {
updateCrumbs('');
view.innerHTML = renderHome();
view.querySelectorAll('.data-card').forEach((b) => b.addEventListener('click', () => { location.hash = b.dataset.hash; }));
loadRankingPreview();
loadRecentScripts();
return;
}
@@ -447,10 +507,19 @@ async function router() {
};
if (pages[name]) {
updateCrumbs(name, titles[name]);
await pages[name]();
try {
await pages[name]();
} catch (err) {
console.error('[router]', name, err);
view.innerHTML = '<div class="empty">页面渲染失败:' + esc(err.message) + '</div>';
}
return;
}
}
// 数据页脚本(data-pages.js)尚未加载完时,不要误跳首页——等待下次路由
if (name && ['ranking', 'accounts', 'account', 'video', 'rewrites', 'reviews'].includes(name) && typeof window.DataPages === 'undefined') {
return;
}
// 未知路由 → 首页
location.hash = '#/';
}
@@ -487,4 +556,40 @@ document.addEventListener('click', (e) => {
});
initIcons();
router();
/* ================= 路由记忆(刷新/面板重载后恢复到上次页面) =================
内置浏览器刷新 iframe 时可能丢失 URL hash → 回首页。
localStorage + cookie 双写(任一可用),hash 丢失时自动恢复。 */
function saveRoute() {
const h = location.hash || '#/';
try { localStorage.setItem('wb_route', h); } catch (e) {}
try { document.cookie = 'wb_route=' + encodeURIComponent(h) + '; path=/; max-age=86400'; } catch (e) {}
}
function readRoute() {
try { const v = localStorage.getItem('wb_route'); if (v) return v; } catch (e) {}
try {
const m = document.cookie.match(/(?:^|;\s*)wb_route=([^;]*)/);
if (m) return decodeURIComponent(m[1]);
} catch (e) {}
return null;
}
window.addEventListener('hashchange', saveRoute);
(function boot() {
const saved = readRoute();
if (saved && saved !== '#/' && saved !== '#' && (!location.hash || location.hash === '#/')) {
location.hash = saved; // 刷新丢 hash → 恢复上次页面
return;
}
saveRoute(); // 直接输入 URL/首次打开:立即记录当前路由,供下次刷新恢复
// 关键:data-pages.js 在 app.js 之后的 script 标签,下载/执行可能未完成。
// 须等其就绪(DataPages 存在)再路由,否则数据页会被当成未知路由。
function bootRouter() {
if (typeof window.DataPages === 'undefined') { setTimeout(bootRouter, 50); return; }
router();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootRouter);
} else {
bootRouter();
}
})();
@@ -22,15 +22,29 @@ function bindBack(container) {
container.querySelectorAll('[data-back]').forEach((b) => b.addEventListener('click', () => { location.hash = '#/'; }));
}
/* 生成「触发 AI」的 prompt(WorkBuddy 无 AI 桥接,降级为复制到剪贴板) */
/* 触发 AI 任务:提交 WorkBuddy Automation 一次性任务,WorkBuddy 自动执行,左侧会话栏可见 */
let _lastFireTs = 0;
async function firePrompt(prompt, label) {
const ok = await copyText(prompt);
showToast(ok ? `✅ 已复制${label || '任务指令'},请粘贴到对话框执行` : '❌ 复制失败,请手动复制');
const nowTs = Date.now();
if (nowTs - _lastFireTs < 5000) { showToast('⏳ 任务刚提交,请在左侧会话栏查看执行'); return false; }
_lastFireTs = nowTs;
try {
const d = await api('/api/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, name: label || '工作台任务' }),
});
showToast('✅ ' + (d.message || '任务已提交,请在左侧会话栏查看执行'));
return true;
} catch (e) {
showToast('❌ 任务提交失败: ' + e.message);
return false;
}
}
/* ================= 周榜 ================= */
async function renderRanking(view) {
let state = { date: '', category: '', sortBy: 'rank', order: 'asc', page: 1, pageSize: 20, dates: [], categories: [] };
let state = { date: '', category: '', sortBy: 'comprehensiveScore', order: 'desc', page: 1, pageSize: 20, dates: [], categories: [] };
const data = await api('/api/dsh/ranking?pageSize=20').catch((e) => ({ ok: false, error: e.message }));
if (!data.ok) { view.innerHTML = `<div class="page"><div class="page-toolbar">${toolbarBack()}<span class="page-title">抖音周榜</span></div><div class="empty">${esc(data.error || '加载失败')}</div></div>`; bindBack(view); return; }
state.dates = data.dates || []; state.categories = data.categories || []; state.date = data.date; state.category = data.category;
@@ -41,33 +55,35 @@ async function renderRanking(view) {
const sortable = (key, label) => `<th class="sortable" data-sort="${key}">${label}${state.sortBy === key ? (state.order === 'asc' ? ' ↑' : ' ↓') : ''}</th>`;
const rows = (d.items || []).map((r) => `<tr>
<td>${r.rank ?? '-'}</td>
<td>${r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}" title="在新标签打开账号主页">${esc(r.accountName)}</a>` : esc(r.accountName)}</td>
<td>${esc(r.category || '-')}</td>
<td>${fmt(r.comprehensiveScore)}</td>
<td>${r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}" title="在新标签打开账号主页">${esc(r.accountName)}</a>` : esc(r.accountName)}</td>
<td class="score-cell">${fmt(r.comprehensiveScore)}</td>
<td>${fmt(r.followers)}</td>
<td>${fmt(r.newFans)}</td>
<td>${fmt(r.newLikes)}</td>
<td>${fmt(r.newComments)}</td>
<td>${fmt(r.newShares)}</td>
<td>${r.inDb ? `<a class="link" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm" data-import='${esc(JSON.stringify(r))}'>导入</button>`}</td>
<td class="cell-act">${r.inDb ? `<a class="btn btn-sm btn-view" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm btn-import" data-import='${esc(JSON.stringify(r))}'>导入</button>`}</td>
</tr>`).join('');
view.innerHTML = `
<div class="page">
<div class="page-toolbar">
${toolbarBack()}
<span class="page-title">抖音周榜</span>
<select id="rankCat" class="input-select" title="赛道筛选">
<option value="">全部赛道</option>
${state.categories.map((c) => `<option value="${esc(c)}" ${c === state.category ? 'selected' : ''}>${esc(c)}</option>`).join('')}
</select>
<button class="btn" id="rankPrev" ${di <= 0 ? 'disabled' : ''}>◀</button>
<span class="pager-info" title="选择日期">${esc(state.date)}</span>
<button class="btn" id="rankNext" ${di < 0 || di >= state.dates.length - 1 ? 'disabled' : ''}>▶</button>
<div class="toolbar-right">
<select id="rankCat" class="input-select" title="赛道筛选">
<option value="">全部赛道</option>
${state.categories.map((c) => `<option value="${esc(c)}" ${c === state.category ? 'selected' : ''}>${esc(c)}</option>`).join('')}
</select>
<button class="btn" id="rankPrev" ${di <= 0 ? 'disabled' : ''}>◀</button>
<span class="pager-info" title="选择日期">${esc(state.date)}</span>
<button class="btn" id="rankNext" ${di < 0 || di >= state.dates.length - 1 ? 'disabled' : ''}>▶</button>
</div>
</div>
<div class="table-wrap">
<table class="table">
<thead><tr>
${sortable('rank', '排名')}<th>账号</th><th>赛道</th>${sortable('comprehensiveScore', '综合得分')}${sortable('followers', '粉丝')}${sortable('newFans', '新增粉丝')}${sortable('newLikes', '新增点赞')}${sortable('newComments', '新增评论')}${sortable('newShares', '新增分享')}<th>功能</th>
${sortable('rank', '排名')}<th>赛道</th><th>账号</th>${sortable('comprehensiveScore', '综合得分')}${sortable('followers', '粉丝')}${sortable('newFans', '新增粉丝')}${sortable('newLikes', '新增点赞')}${sortable('newComments', '新增评论')}${sortable('newShares', '新增分享')}<th class="th-act">功能</th>
</tr></thead>
<tbody>${rows || '<tr><td colspan="10" class="empty">暂无周榜数据</td></tr>'}</tbody>
</table>
@@ -114,7 +130,7 @@ async function renderAccounts(view) {
<td class="cell-content" title="${esc(a.bio || '')}">${esc((a.bio || '').slice(0, 50))}</td>
<td class="cell-content" title="${esc(a.content || '')}">${esc((a.content || '').slice(0, 50))}</td>
<td>${a.video_count ?? 0}</td>
<td><button class="btn btn-sm" data-create='${esc(a.account_name)}'>创作</button></td>
<td class="cell-act"><button class="btn btn-sm" data-create='${esc(a.account_name)}'>创作</button></td>
</tr>`).join('');
view.innerHTML = `
<div class="page">
@@ -180,7 +196,7 @@ async function renderAccountDetail(view, id) {
<td>${fmt(v.share_count)}</td>
<td>${fmtDuration(v.duration)}</td>
<td>${fmt(v.publish_time)}</td>
<td>${(v.has_source || v.has_analysis) ? `<button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>改写脚本</button>` : `<button class="btn btn-sm" data-analyze='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>解析</button>`}</td>
<td class="cell-act">${(v.has_source || v.has_analysis) ? `<button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>改写脚本</button>` : `<button class="btn btn-sm" data-analyze='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>解析</button>`}</td>
</tr>`).join('');
const tabBtns = [['info', '账号信息'], ['persona', '账号设定'], ['analysis', '数据分析']].map(([k, label]) => `<button class="tab ${tab === k ? 'tab-active' : ''}" data-tab="${k}">${label}</button>`).join('');
let panel = '';
@@ -279,7 +295,7 @@ async function renderRewrites(view) {
<td>${fmt(v.publish_time)}</td>
<td>${(v.has_source || v.has_analysis) ? '✅已解析' : '未解析'}</td>
<td>${v.rewrite_count > 0 ? '✅' + v.rewrite_count : '—'}</td>
<td><button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ id: v.id, name: v.account_name, title: v.video_title }))}'>改写</button></td>
<td class="cell-act"><button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ id: v.id, name: v.account_name, title: v.video_title }))}'>改写</button></td>
</tr>`).join('');
view.innerHTML = `
<div class="page">
@@ -405,7 +421,7 @@ async function renderReviews(view) {
<td class="cell-content">${row.title ? `<a class="link" data-open="${row.id}">${esc(row.title)}</a>` : '-'}</td>
<td>${fmtTime(row.created_time)}</td>
${dimsTd}
<td><button class="btn btn-sm" data-review-row="${row.id}" ${row.review_running ? 'disabled' : ''}>${row.review_running ? '复盘运行中' : '复盘'}</button></td>
<td class="cell-act"><button class="btn btn-sm" data-review-row="${row.id}" ${row.review_running ? 'disabled' : ''}>${row.review_running ? '复盘运行中' : '复盘'}</button></td>
</tr>`;
}).join('');
@@ -17,7 +17,7 @@ body {
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
font-size: 14px;
font-size: 16px;
}
/* ---- 顶栏 ---- */
@@ -31,13 +31,13 @@ body {
}
.brand { display: flex; align-items: center; gap: 8px; }
.logo { display: inline-flex; align-items: center; color: var(--primary); }
.title { font-size: 18px; font-weight: 700; }
.subtitle { font-size: 12px; color: var(--dim); }
.title { font-size: 21px; font-weight: 700; }
.subtitle { font-size: 14px; color: var(--dim); }
.tabs { display: flex; gap: 4px; margin-left: 20px; }
.tab {
border: none; background: none; padding: 8px 16px;
font-size: 14px; color: var(--dim); cursor: pointer;
font-size: 16px; color: var(--dim); cursor: pointer;
border-radius: 8px;
}
.tab:hover { background: var(--primary-soft); color: var(--primary); }
@@ -47,13 +47,13 @@ body {
.ghost {
border: 1px solid var(--border); background: var(--panel);
color: var(--text); padding: 6px 14px; border-radius: 8px;
cursor: pointer; font-size: 13px;
cursor: pointer; font-size: 15px;
display: inline-flex; align-items: center; gap: 6px;
}
.ghost:hover { border-color: var(--primary); color: var(--primary); }
.primary {
border: none; background: var(--primary); color: #fff;
padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px;
padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 15px;
}
/* ---- 主体 ---- */
@@ -72,27 +72,27 @@ main { height: calc(100vh - 54px); }
background: var(--panel); border: 1px solid var(--border);
border-radius: 12px; padding: 16px 18px; box-shadow: var(--shadow);
}
.func-card h3 { font-size: 16px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.func-card .desc { color: var(--dim); font-size: 13px; margin-bottom: 10px; }
.func-card h3 { font-size: 18px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.func-card .desc { color: var(--dim); font-size: 15px; margin-bottom: 10px; }
.func-card .qa { border-top: 1px dashed var(--border); padding-top: 8px; }
.qa-row { display: flex; gap: 8px; padding: 4px 0; font-size: 13px; }
.qa-row { display: flex; gap: 8px; padding: 4px 0; font-size: 15px; }
.qa-row .q { color: var(--accent); white-space: nowrap; }
.qa-row .a { color: var(--dim); }
.qa-row.copyable { cursor: pointer; border-radius: 6px; padding: 4px 6px; margin: 0 -6px; transition: background .15s; }
.qa-row.copyable:hover { background: var(--primary-soft); }
.qa-row.copyable:hover .q::after { content: ' 📋'; font-size: 12px; opacity: .7; }
.qa-row.copyable:hover .q::after { content: ' 📋'; font-size: 14px; opacity: .7; }
.modes {
max-width: 900px; margin: 28px auto 0;
background: var(--panel); border: 1px solid var(--border);
border-radius: 12px; padding: 16px 18px;
}
.modes h3 { font-size: 15px; margin-bottom: 10px; }
.modes h3 { font-size: 17px; margin-bottom: 10px; }
.mode-table { border-collapse: collapse; width: 100%; }
.mode-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 13px; }
.mode-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 15px; }
.mode-table tr:last-child td { border-bottom: none; }
.mode-name { font-weight: 600; width: 140px; display: flex; align-items: center; gap: 6px; }
.mode-hint { margin-top: 8px; font-size: 12px; color: var(--dim); }
.mode-hint { margin-top: 8px; font-size: 14px; color: var(--dim); }
/* ---- 产出浏览 ---- */
.files-layout { display: flex; height: 100%; gap: 0; }
@@ -104,11 +104,11 @@ main { height: calc(100vh - 54px); }
.sidebar-top { padding: 12px; display: flex; flex-direction: column; gap: 8px; border-bottom: 1px solid var(--border); }
.sidebar-top select, .sidebar-top input {
padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px;
font-size: 13px; width: 100%; background: #fff; color: var(--text);
font-size: 15px; width: 100%; background: #fff; color: var(--text);
}
.file-tree { flex: 1; overflow: auto; padding: 8px; }
.tree-empty { color: var(--dim); padding: 12px; font-size: 13px; }
.tree-node { font-size: 13px; }
.tree-empty { color: var(--dim); padding: 12px; font-size: 15px; }
.tree-node { font-size: 15px; }
.tree-row {
display: flex; align-items: center; gap: 6px;
padding: 4px 8px; border-radius: 6px; cursor: pointer; user-select: none;
@@ -116,7 +116,7 @@ main { height: calc(100vh - 54px); }
}
.tree-row:hover { background: var(--primary-soft); }
.tree-row.selected { background: var(--primary); color: #fff; }
.tree-row .caret { width: 14px; text-align: center; font-size: 10px; flex-shrink: 0; }
.tree-row .caret { width: 14px; text-align: center; font-size: 11px; flex-shrink: 0; }
.tree-row .icon { flex-shrink: 0; }
.tree-row .fname { overflow: hidden; text-overflow: ellipsis; }
.tree-children { margin-left: 18px; border-left: 1px dashed var(--border); padding-left: 4px; }
@@ -130,33 +130,33 @@ main { height: calc(100vh - 54px); }
height: 100%; display: flex; flex-direction: column; align-items: center;
justify-content: center; color: var(--dim); gap: 8px;
}
.preview-empty p { font-size: 15px; }
.preview-empty .dim { font-size: 12px; }
.preview-empty p { font-size: 17px; }
.preview-empty .dim { font-size: 14px; }
.preview { padding: 24px 32px; max-width: 900px; margin: 0 auto; }
.preview .file-head {
display: flex; align-items: center; gap: 10px;
border-bottom: 1px solid var(--border); padding-bottom: 10px; margin-bottom: 16px;
}
.preview .file-head .fpath { font-size: 12px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview .file-head .fmeta { font-size: 12px; color: var(--dim); white-space: nowrap; }
.preview .file-head .fpath { font-size: 14px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.preview .file-head .fmeta { font-size: 14px; color: var(--dim); white-space: nowrap; }
.btn-download {
border: 1px solid var(--primary); color: var(--primary); background: #fff;
padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;
padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 14px;
margin-left: auto; text-decoration: none;
}
.btn-download:hover { background: var(--primary-soft); }
/* ---- Markdown 渲染 ---- */
.md-body { line-height: 1.75; font-size: 14px; word-break: break-word; }
.md-body { line-height: 1.75; font-size: 16px; word-break: break-word; }
.md-body h1, .md-body h2, .md-body h3, .md-body h4 { margin: 18px 0 10px; line-height: 1.4; }
.md-body h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.md-body h2 { font-size: 18px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
.md-body h3 { font-size: 16px; }
.md-body h1 { font-size: 25px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.md-body h2 { font-size: 21px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
.md-body h3 { font-size: 18px; }
.md-body p { margin: 8px 0; }
.md-body strong { font-weight: 700; }
.md-body code {
background: #f0f2f5; border-radius: 4px; padding: 2px 5px;
font-family: Consolas, monospace; font-size: 13px;
font-family: Consolas, monospace; font-size: 15px;
}
.md-body pre {
background: #f6f8fa; border: 1px solid var(--border); border-radius: 8px;
@@ -168,7 +168,7 @@ main { height: calc(100vh - 54px); }
padding: 8px 14px; margin: 10px 0; border-radius: 0 8px 8px 0; color: #333;
}
.md-body table { border-collapse: collapse; margin: 10px 0; width: 100%; display: block; overflow-x: auto; }
.md-body th, .md-body td { border: 1px solid var(--border); padding: 6px 10px; font-size: 13px; }
.md-body th, .md-body td { border: 1px solid var(--border); padding: 6px 10px; font-size: 15px; }
.md-body th { background: #f6f8fa; font-weight: 600; }
.md-body ul, .md-body ol { margin: 8px 0 8px 22px; }
.md-body li { margin: 3px 0; }
@@ -178,7 +178,7 @@ main { height: calc(100vh - 54px); }
/* ---- JSON 预览 ---- */
.json-view pre {
background: #f6f8fa; border: 1px solid var(--border); border-radius: 8px;
padding: 14px; overflow: auto; font-size: 13px; line-height: 1.6;
padding: 14px; overflow: auto; font-size: 15px; line-height: 1.6;
font-family: Consolas, monospace;
}
@@ -193,21 +193,21 @@ main { height: calc(100vh - 54px); }
width: 520px; max-width: 92vw; box-shadow: 0 8px 30px rgba(0,0,0,.18);
}
.modal h3 { margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.modal .dim { color: var(--dim); font-size: 13px; margin-bottom: 12px; }
.modal code { background: #f0f2f5; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
.modal .dim { color: var(--dim); font-size: 15px; margin-bottom: 12px; }
.modal code { background: #f0f2f5; padding: 1px 5px; border-radius: 4px; font-size: 14px; }
.root-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.root-item {
display: flex; align-items: center; gap: 8px;
border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; font-size: 13px;
border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; font-size: 15px;
}
.root-item .rp { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.root-item .rm {
border: none; background: none; color: #cf222e; cursor: pointer; font-size: 16px; line-height: 1;
border: none; background: none; color: #cf222e; cursor: pointer; font-size: 18px; line-height: 1;
}
.root-item .rm:hover { opacity: .7; }
.root-add { display: flex; gap: 8px; }
.root-add input { flex: 1; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; }
.root-msg { margin-top: 8px; font-size: 12px; color: var(--accent); min-height: 16px; }
.root-add input { flex: 1; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 15px; }
.root-msg { margin-top: 8px; font-size: 14px; color: var(--accent); min-height: 16px; }
.modal-actions { text-align: right; margin-top: 14px; }
.dim { color: var(--dim); }
@@ -216,7 +216,7 @@ main { height: calc(100vh - 54px); }
.toast {
position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%) translateY(20px);
background: #24292f; color: #fff; padding: 10px 18px; border-radius: 8px;
font-size: 13px; opacity: 0; pointer-events: none; transition: all .25s;
font-size: 15px; opacity: 0; pointer-events: none; transition: all .25s;
max-width: 70vw; z-index: 200; box-shadow: 0 4px 16px rgba(0,0,0,.2);
word-break: break-all;
}
@@ -224,10 +224,10 @@ main { height: calc(100vh - 54px); }
/* ================= 面包屑 ================= */
.crumbs { display: flex; align-items: baseline; gap: 8px; margin-left: 12px; min-width: 0; }
.crumb-home { font-size: 18px; font-weight: 700; color: var(--text); cursor: pointer; text-decoration: none; }
.crumb-home { font-size: 21px; font-weight: 700; color: var(--text); cursor: pointer; text-decoration: none; }
a.crumb-home:hover { color: var(--primary); }
.crumb-sep { color: var(--dim); font-size: 14px; }
.crumb-current { color: var(--text); font-weight: 600; font-size: 14px; }
.crumb-sep { color: var(--dim); font-size: 16px; }
.crumb-current { color: var(--text); font-weight: 600; font-size: 16px; }
main#view { height: calc(100vh - 54px); overflow: auto; }
.view-empty { padding: 40px; text-align: center; color: var(--dim); }
@@ -238,26 +238,32 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
.data-card { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; cursor: pointer; text-align: left; box-shadow: var(--shadow); transition: all .15s; }
.data-card:hover { border-color: var(--primary); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(47,111,237,.15); }
.data-card-icon { color: var(--primary); display: inline-flex; }
.data-card-title { font-size: 15px; font-weight: 600; color: var(--text); }
.data-card-desc { font-size: 12px; color: var(--dim); }
.data-card-title { font-size: 17px; font-weight: 600; color: var(--text); }
.data-card-desc { font-size: 14px; color: var(--dim); }
/* ================= 功能页通用 ================= */
.page { padding: 20px 24px; max-width: 1440px; margin: 0 auto; }
.page-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
.page-title { font-size: 16px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; }
.input { border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 13px; min-width: 200px; background: #fff; color: var(--text); }
.input-select { border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; font-size: 13px; background: #fff; color: var(--text); }
.btn { border: 1px solid var(--border); background: var(--panel); color: var(--text); padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; }
.toolbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.page-title { font-size: 18px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; }
.input { border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 15px; min-width: 200px; background: #fff; color: var(--text); }
.input-select { border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; font-size: 15px; background: #fff; color: var(--text); }
.btn { border: 1px solid var(--border); background: var(--panel); color: var(--text); padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 15px; text-decoration: none; }
.btn:hover { border-color: var(--primary); color: var(--primary); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-sm { padding: 3px 10px; font-size: 12px; border-radius: 6px; }
.btn-sm { padding: 3px 10px; font-size: 14px; border-radius: 6px; }
.btn-primary { background: var(--primary); color: #fff; border-color: transparent; }
.btn-primary:hover { background: #2a5fd0; color: #fff; }
.empty { padding: 30px; text-align: center; color: var(--dim); font-size: 13px; }
/* 功能列按钮区分:导入=浅灰底深字,查看=白底蓝字 */
.btn-import { background: #f0f2f5; border-color: #e1e4e8; color: #495057; }
.btn-import:hover { background: #e4e7eb; border-color: #d0d4da; color: #343a40; }
.btn-view { background: #fff; border-color: var(--primary); color: var(--primary); }
.btn-view:hover { background: var(--primary-soft); color: var(--primary); }
.empty { padding: 30px; text-align: center; color: var(--dim); font-size: 15px; }
/* ================= 表格 ================= */
.table-wrap { overflow: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); }
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table { width: 100%; border-collapse: collapse; font-size: 15px; }
.table th { position: sticky; top: 0; background: #f6f8fa; text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); font-weight: 600; white-space: nowrap; z-index: 1; }
.table td { padding: 7px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.table tbody tr:hover { background: var(--primary-soft); }
@@ -267,15 +273,15 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
.link { color: var(--primary); cursor: pointer; text-decoration: none; }
.link:hover { text-decoration: underline; }
.link.strong { font-weight: 600; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 13px; }
.badge-signed { background: var(--primary-soft); color: var(--primary); }
.badge-ext { background: #fff4e5; color: var(--accent); }
.pager { display: flex; align-items: center; gap: 12px; padding: 12px 4px; }
.pager-total, .pager-info { color: var(--dim); font-size: 13px; }
.pager { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding: 12px 4px; }
.pager-total, .pager-info { color: var(--dim); font-size: 15px; }
/* ================= 数据页 tab ================= */
.tabs { display: flex; gap: 4px; margin-bottom: 14px; border-bottom: 1px solid var(--border); }
.tab { border: none; background: none; padding: 8px 16px; font-size: 14px; color: var(--dim); cursor: pointer; border-bottom: 2px solid transparent; }
.tab { border: none; background: none; padding: 8px 16px; font-size: 16px; color: var(--dim); cursor: pointer; border-bottom: 2px solid transparent; }
.tab:hover { color: var(--primary); }
.tab-active { color: var(--primary); font-weight: 600; border-bottom-color: var(--primary); }
@@ -284,11 +290,11 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
.info-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px 18px; }
.info-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.info-wide { grid-column: 1/-1; }
.info-label { font-size: 12px; color: var(--dim); }
.info-value { font-size: 13px; color: var(--text); word-break: break-word; white-space: pre-wrap; line-height: 1.6; }
.info-label { font-size: 14px; color: var(--dim); }
.info-value { font-size: 15px; color: var(--text); word-break: break-word; white-space: pre-wrap; line-height: 1.6; }
/* ================= 对比弹窗 ================= */
.modal-title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; font-size: 16px; font-weight: 600; }
.modal-title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; font-size: 18px; font-weight: 600; }
.rw-modal { width: min(1100px, 95vw); max-height: 90vh; overflow: auto; }
.rw-split { display: flex; gap: 14px; }
.rw-pane { flex: 1; min-width: 0; display: flex; flex-direction: column; }
@@ -300,35 +306,56 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
.sb-block { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; overflow: hidden; background: var(--panel); }
.sb-head { display: flex; align-items: center; gap: 10px; padding: 6px 12px; background: #f6f8fa; border-bottom: 1px solid var(--border); }
.sb-title { font-weight: 600; color: var(--primary); }
.sb-meta { margin-left: auto; font-size: 12px; color: var(--dim); }
.sb-meta { margin-left: auto; font-size: 14px; color: var(--dim); }
.sb-body { padding: 8px 12px; }
/* ================= 评分 ================= */
.score-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.score-name { width: 88px; font-size: 12px; color: var(--dim); white-space: nowrap; flex-shrink: 0; }
.score-name { width: 88px; font-size: 14px; color: var(--dim); white-space: nowrap; flex-shrink: 0; }
.score-bar { flex: 1; height: 6px; border-radius: 3px; background: #eef1f5; overflow: hidden; }
.score-fill { height: 100%; }
.score-val { width: 36px; text-align: right; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.score-val { width: 36px; text-align: right; font-size: 14px; font-weight: 600; flex-shrink: 0; }
.score-dual { display: flex; gap: 12px; margin-bottom: 12px; }
.score-box { flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; }
.score-box-title { font-weight: 600; margin-bottom: 8px; }
.review-issues { margin-bottom: 10px; }
.issue-row { display: flex; gap: 6px; font-size: 12px; margin-bottom: 4px; }
.review-summary { margin-bottom: 10px; font-size: 13px; }
.issue-row { display: flex; gap: 6px; font-size: 14px; margin-bottom: 4px; }
.review-summary { margin-bottom: 10px; font-size: 15px; }
/* ================= 首页 AI写剧本 快捷卡片 ================= */
.recent-scripts { margin: 4px 0 24px; }
.recent-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.recent-head h3 { font-size: 15px; }
.recent-head .link { font-size: 13px; }
.recent-head h3 { font-size: 17px; }
.recent-head .link { font-size: 13px; margin-left: auto; margin-right: 10px; }
.recent-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 14px; }
/* ================= 首页 抖音周榜 预览 ================= */
.rank-preview { margin: 4px 0 20px; }
.rank-tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }
.rank-filter { padding: 3px 6px; font-size: 14px; max-width: 110px; }
.rank-date-label { font-size: 14px; color: var(--dim); min-width: 62px; text-align: center; white-space: nowrap; }
.rank-preview-list { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.rank-row { display: grid; grid-template-columns: 34px 84px minmax(80px, 1fr) 52px repeat(4, 58px) 72px; align-items: center; gap: 6px; padding: 6px 10px; font-size: 14px; border-bottom: 1px solid var(--border); }
.rank-row:last-child { border-bottom: none; }
.rank-head { background: var(--panel); color: var(--dim); font-weight: 600; font-size: 13px; }
.rk-no { color: var(--dim); font-weight: 600; }
.rk-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rk-cat { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rk-score { text-align: right; font-weight: 600; color: var(--accent); }
.rk-num { text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rk-act { display: flex; align-items: center; justify-content: center; white-space: nowrap; }
.cell-act { text-align: center; white-space: nowrap; }
.table th.th-act { text-align: center; }
.table td.score-cell { color: var(--accent); font-weight: 600; }
.rank-pager { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 8px; }
.rank-page-info { font-size: 14px; color: var(--dim); }
.recent-card { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; cursor: pointer; text-align: left; transition: all .15s; min-height: 172px; }
.recent-card:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(47,111,237,.12); }
.recent-card-title { font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; display: flex; align-items: center; gap: 6px; }
.recent-card-title { font-size: 15px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; display: flex; align-items: center; gap: 6px; }
.recent-card-preview { width: 100%; display: flex; flex-direction: column; gap: 2px; }
.recent-preview-line { display: block; font-size: 12px; color: #57606a; line-height: 1.55; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; }
.recent-card-meta { font-size: 12px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; margin-top: auto; }
.recent-meta { font-size: 12px; color: var(--dim); margin-bottom: 12px; }
.recent-preview-line { display: block; font-size: 14px; color: #57606a; line-height: 1.55; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; }
.recent-card-meta { font-size: 14px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; margin-top: auto; }
.recent-meta { font-size: 14px; color: var(--dim); margin-bottom: 12px; }
/* ================= 扁平化向量图标 ================= */
.ic { display: inline-block; vertical-align: -0.18em; flex-shrink: 0; }