热点三榜数据接入定稿:全品类单文件(不传赛道一次调用)+ 三子目录落盘

- fetch_week_ranks.py 重写:28 赛道循环→全品类不传 type 每榜每期 1 次调用;hotContentRank source 修正为生产名「抖音每日点赞飙升榜」(GitHub 后缀版返回空);API 单次上限 TOP50(limit/page/offset 实测均无效)固化防再探测;成本铁律「批量付费调用前须先经用户确认」
- 落盘契约:抖音榜单/ 三子目录(账号周榜/视频热榜/点赞榜),hot/likes 全品类单文件/期 抖音周榜_{视频热榜|点赞榜}_{周一}.json;旧 28 赛道 56 文件归档 _archive_旧28赛道结构_20260903
- dsh-data.js:RANK_SUB 子目录映射 + rankFilePath + listRankFiles(子目录优先+根目录旧平铺回退) + collectCategories(week=文件名赛道、hot/likes=条目 category 并集)+ getRanking 赛道筛选(week 文件名级/hot likes 条目级)
- 前端三榜渲染(data-pages.js/app.js/style.css 承接前段)+ 文档同步(主 SKILL.md 落盘表/抓取器说明段)+ dsh 副本 diff=0 + 记忆日志
This commit is contained in:
maogeigei committed 2026-09-03 15:00:01 +08:00
1 parent 2fc8b7b3f6
commit 250d51c8c2
10 files changed
+553 -116

No files matched your search

@@ -442,9 +442,12 @@ 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: '' };
/* ================= 首页 榜单预览(账号周榜/视频热榜/点赞榜 三榜;前10 + 赛道/日期筛选,无分页/排序) ================= */
let _rankPreview = { board: 'week', pageSize: 10, dates: [], categories: [], date: '', category: '', _metaBoard: '' };
function renderRankTools() {
const has = _rankPreview.dates.length > 0;
const tools = $('#rankTools');
if (tools) tools.hidden = !has;
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('');
@@ -457,37 +460,97 @@ function renderRankTools() {
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(); } }; }
}
// 首页各榜列模板:head → 表头行;row(r) → 数据行。wk-num 数值(万显示);标题/作者吃剩余宽
const _PREVIEW_COLS = {
week: {
gridCls: '',
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>`,
row: (r) => `<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">${fmt(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>`,
},
hot: {
// likesRank 周窗口作品榜
gridCls: ' rk-video',
head: `<span class="rk-no">排名</span><span class="rk-cat">赛道</span><span class="rk-name">作品标题</span><span class="rk-author">作者</span><span class="rk-num">点赞</span><span class="rk-num">评论</span><span class="rk-num">分享</span><span class="rk-num">收藏</span>`,
row: (r) => `<span class="rk-no">${r.rank ?? '-'}</span>
<span class="rk-cat">${esc(r.category || '-')}</span>
<span class="rk-name" title="${esc(String(r.title || '').replace(/"/g, '&quot;'))}">${r.workUrl ? `<a class="title-plain" data-ext="${esc(r.workUrl)}" title="打开作品页">${esc(r.title || '-')}</a>` : esc(r.title || '-')}</span>
<span class="rk-author" title="${esc(String(r.accountName || '').replace(/"/g, '&quot;'))}">${esc(r.accountName || '-')}</span>
<span class="rk-num">${fmtWan(r.likeCount)}</span>
<span class="rk-num">${fmtWan(r.commentCount)}</span>
<span class="rk-num">${fmtWan(r.shareCount)}</span>
<span class="rk-num">${fmtWan(r.collectCount)}</span>`,
},
likes: {
// hotContentRank 七日点赞飙升
gridCls: ' rk-video',
head: `<span class="rk-no">排名</span><span class="rk-cat">赛道</span><span class="rk-name">作品标题</span><span class="rk-author">作者</span><span class="rk-num">新增点赞</span><span class="rk-num">新增收藏</span><span class="rk-num">新增评论</span><span class="rk-num">新增分享</span>`,
row: (r) => `<span class="rk-no">${r.rank ?? '-'}</span>
<span class="rk-cat">${esc(r.category || '-')}</span>
<span class="rk-name" title="${esc(String(r.aweme_desc || '').replace(/"/g, '&quot;'))}">${r.share_url ? `<a class="title-plain" data-ext="${esc(r.share_url)}" title="打开作品页">${esc(r.aweme_desc || '-')}</a>` : esc(r.aweme_desc || '-')}</span>
<span class="rk-author" title="${esc(String(r.user_nickname || '').replace(/"/g, '&quot;'))}">${esc(r.user_nickname || '-')}</span>
<span class="rk-num">${fmtWan(r.add_digg_count)}</span>
<span class="rk-num">${fmtWan(r.add_collect_count)}</span>
<span class="rk-num">${fmtWan(r.add_comment_count)}</span>
<span class="rk-num">${fmtWan(r.add_share_count)}</span>`,
},
};
async function loadRankingPreview() {
const listEl = $('#rankPreviewList');
if (!listEl) return;
const ph = $('#rankPlaceholder');
try {
if (!_rankPreview.dates.length) {
const m = await api('/api/dsh/ranking?page=1&pageSize=1');
const board = _rankPreview.board;
// meta 按榜独立拉取(board 变化或首次进入)
if (_rankPreview._metaBoard !== board) {
const m = await api('/api/dsh/ranking?board=' + board + '&page=1&pageSize=1');
_rankPreview.dates = m.dates || [];
_rankPreview.categories = m.categories || [];
_rankPreview.date = m.date || (_rankPreview.dates[0] || '');
_rankPreview.category = m.category || '';
_rankPreview._metaBoard = board;
renderRankTools();
}
const d = await api('/api/dsh/ranking?page=1&pageSize=' + _rankPreview.pageSize + '&date=' + encodeURIComponent(_rankPreview.date) + '&category=' + encodeURIComponent(_rankPreview.category));
if (!_rankPreview.dates.length) {
listEl.hidden = true;
if (ph) { ph.hidden = false; ph.textContent = '「' + ({ week: '账号周榜', hot: '视频热榜', likes: '点赞榜' }[board] || '') + '」暂无本地数据文件,先运行 mcn-data-insight 抓取技能落盘'; }
return;
}
if (ph) ph.hidden = true;
listEl.hidden = false;
const d = await api('/api/dsh/ranking?board=' + board + '&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>';
const col = _PREVIEW_COLS[board];
listEl.innerHTML = items.length ? `<div class="rank-row rank-head${col.gridCls}">${col.head}</div>` + items.map((r) => `<div class="rank-row${col.gridCls}">${col.row(r)}</div>`).join('') : '<span class="dim" style="padding:12px">暂无周榜数据</span>';
listEl.querySelectorAll('[data-rk-import]').forEach((b) => b.addEventListener('click', () => {
confirmSaveAccount(b.dataset.rkName, b.dataset.rkUrl || '', b);
}));
} catch (e) { listEl.innerHTML = '<span class="dim" style="padding:12px">' + esc(e.message) + '</span>'; }
}
function setRankBoard(board, activeEl) {
if (_rankPreview.board === board && activeEl) return;
_rankPreview.board = board;
// meta 缓存按 board 失效 → 强制重拉该榜 dates/categories
_rankPreview._metaBoard = '';
_rankPreview.date = ''; _rankPreview.category = '';
if (activeEl) {
const root = activeEl.closest('.rank-tabs');
if (root) root.querySelectorAll('.rank-tab').forEach((x) => x.classList.toggle('active', x === activeEl));
}
const listEl = $('#rankPreviewList');
if (listEl) { listEl.hidden = true; listEl.innerHTML = '<span class="dim" style="padding:12px">加载中…</span>'; }
const ph = $('#rankPlaceholder');
if (ph) ph.hidden = true;
renderRankTools();
loadRankingPreview();
}
function openRecentScript(s) {
const title = cleanTitle(s.title || s.video_title) || 'AI 写剧本';
const mask = document.createElement('div'); mask.className = 'modal-mask';
@@ -661,15 +724,9 @@ async function router() {
updateCrumbs('');
view.innerHTML = renderHome();
view.querySelectorAll('.data-card').forEach((b) => b.addEventListener('click', () => { location.hash = b.dataset.hash; }));
// 周榜 tab 切换:账号周榜显示列表+工具条,其余 2 个新榜先占位
// 榜单 tab 切换:week/hot/likes 各自按 board 拉数据渲染(meta 与数据均带 board 参数)
view.querySelectorAll('.rank-tab').forEach((t) => t.addEventListener('click', () => {
view.querySelectorAll('.rank-tab').forEach((x) => x.classList.toggle('active', x === t));
const week = t.dataset.rankTab === 'week';
view.querySelector('#rankTools').hidden = !week;
view.querySelector('#rankPreviewList').hidden = !week;
const ph = view.querySelector('#rankPlaceholder');
ph.hidden = week;
if (!week) ph.textContent = `「${t.textContent.trim()}」建设中,敬请期待`;
setRankBoard(t.dataset.rankTab, t);
}));
loadHomeStats();
loadRankingPreview();
@@ -501,101 +501,156 @@ async function confirmAiScript(accountName, btn) {
inputEl.focus();
}
/* ================= 周榜 ================= */
/* ================= 热点数据(#/ranking:账号周榜为主,多榜 tab 与首页 rank-preview 一致) ================= */
/* ================= 热点数据(#/ranking:账号周榜 / 视频热榜 / 点赞榜 三榜,board 参数驱动,与首页 rank-preview 同构) ================= */
async function renderRanking(view) {
// tab 与首页 rank-tabs 一致:账号周榜有本地数据文件;视频热榜/点赞榜暂占位(后端 RANKING_DIR 目前仅周榜 json)
// 数据源:RANKING_DIR 下周榜族 JSON(fetch_week_ranks.py 落盘)——账号周榜 抖音周榜_{赛道}_{周一}.json,
// 视频热榜 抖音周榜_视频热榜_{赛道}_{周一}.json,点赞榜 抖音周榜_点赞榜_{赛道}_{周一}.json;后端按 board 识别
const RANK_TABS = [
['week', '账号周榜'],
['hot', '视频热榜'],
['likes', '点赞榜'],
];
let state = { tab: 'week', date: '', category: '', sortBy: 'comprehensiveScore', order: 'desc', page: 1, pageSize: 20, dates: [], categories: [] };
const meta = await api('/api/dsh/ranking?pageSize=20').catch((e) => ({ ok: false, error: e.message }));
if (!meta.ok) {
view.innerHTML = `<div class="page"><div class="page-toolbar">${toolbarBack()}<span class="page-title">热点数据</span></div><div class="empty">${esc(meta.error || '加载失败')}</div></div>`;
bindBack(view); return;
}
state.dates = meta.dates || []; state.categories = meta.categories || []; state.date = meta.date || (state.dates[0] || ''); state.category = meta.category || '';
const tabLabel = () => { const t = RANK_TABS.find(([k]) => k === state.tab); return t ? t[1] : ''; };
const mkState = () => ({ date: '', category: '', sortBy: '', order: '', page: 1, pageSize: 20, dates: [], categories: [] });
// 每榜独立状态(切 tab 保留各自筛选/翻页/日期)
const st = { week: mkState(), hot: mkState(), likes: mkState() };
let tab = 'week';
const tabLabel = () => { const t = RANK_TABS.find(([k]) => k === tab); return t ? t[1] : ''; };
const tabs = () => `
<div class="rank-tabs" role="tablist">
${RANK_TABS.map(([k, label]) => `<button class="rank-tab${k === state.tab ? ' active' : ''}" data-rank-tab="${k}" role="tab">${label}</button>`).join('')}
${RANK_TABS.map(([k, label]) => `<button class="rank-tab${k === tab ? ' active' : ''}" data-rank-tab="${k}" role="tab">${label}</button>`).join('')}
</div>`;
const toolbar = (withFilters) => `
const toolbar = (s, withFilters) => `
<div class="page-toolbar">
${toolbarBack()}
<span class="page-title">热点数据</span>
${withFilters ? `<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('')}
${s.categories.map((c) => `<option value="${esc(c)}" ${c === s.category ? 'selected' : ''}>${esc(c)}</option>`).join('')}
</select>
<button class="btn" id="rankPrev" title="上一期">◀</button>
<span class="pager-info" title="选择日期">${esc(state.date)}</span>
<span class="pager-info" title="选择日期">${esc(s.date)}</span>
<button class="btn" id="rankNext" title="下一期">▶</button>
</div>` : ''}
</div>`;
// 标题列统一截断(作品标题很长):悬停 title 看全文
const titleTd = (text, url) => `<span class="rk-tl" title="${esc(String(text || '').replace(/"/g, '&quot;'))}">${url ? `<a class="link" data-ext="${esc(url)}">${esc(text)}</a>` : esc(text)}</span>`;
// 数值列(过万转万显示,右对齐)
const numTd = (v) => `<td class="num">${fmtWan(v)}</td>`;
// 可排序数值字段(表头窄列宽 + 点击排序)——非数值列不参与排序
const SORT_NUM = new Set(['rank', 'comprehensiveScore', 'followers', 'newFans', 'newLikes', 'newComments', 'newShares', 'likeCount', 'commentCount', 'shareCount', 'collectCount', 'add_digg_count', 'add_collect_count', 'add_comment_count', 'add_share_count']);
// 三榜列模板:head(s, sortable) → 表头;row(r) → 行单元格
const COLS = {
week: {
span: 10, empty: '暂无周榜数据', defKey: 'comprehensiveScore',
head: (s, srt) => `${srt('rank', '排名', true)}<th>赛道</th><th>账号</th>${srt('comprehensiveScore', '综合得分')}${srt('followers', '粉丝')}${srt('newFans', '新增粉丝')}${srt('newLikes', '新增点赞')}${srt('newComments', '新增评论')}${srt('newShares', '新增分享')}<th class="th-act">功能</th>`,
row: (r) => `<tr>
<td>${r.rank ?? '-'}</td>
<td>${esc(r.category || '-')}</td>
<td>${r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}" title="在新标签打开账号主页">${esc(r.accountName)}</a>` : esc(r.accountName)}</td>
<td class="num score-cell">${fmt(r.comprehensiveScore)}</td>
<td class="num">${fmt(r.followers)}</td>
<td class="num">${fmt(r.newFans)}</td>
<td class="num">${fmt(r.newLikes)}</td>
<td class="num">${fmt(r.newComments)}</td>
<td class="num">${fmt(r.newShares)}</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>`,
},
hot: {
// likesRank 周窗口作品榜:标题/作者/赛道/点赞/评论/分享/收藏/发布时间
span: 9, empty: '暂无视频热榜数据', defKey: 'likeCount',
head: (s, srt) => `${srt('rank', '排名', true)}<th>作品标题</th><th>作者</th><th>赛道</th>${srt('likeCount', '点赞')}${srt('commentCount', '评论')}${srt('shareCount', '分享')}${srt('collectCount', '收藏')}<th>发布时间</th>`,
row: (r) => `<tr>
<td>${r.rank ?? '-'}</td>
<td class="cell-tl">${titleTd(r.title || '-', r.workUrl)}</td>
<td>${esc(r.accountName || '-')}</td>
<td>${esc(r.category || '-')}</td>
${numTd(r.likeCount)}${numTd(r.commentCount)}${numTd(r.shareCount)}${numTd(r.collectCount)}
<td class="num">${fmtTime(r.publishTime)}</td>
</tr>`,
},
likes: {
// hotContentRank 七日点赞飙升:作品/作者/赛道/新增点赞/收藏/评论/分享/发布时间
span: 9, empty: '暂无点赞榜数据', defKey: 'add_digg_count',
head: (s, srt) => `${srt('rank', '排名', true)}<th>作品标题</th><th>作者</th><th>赛道</th>${srt('add_digg_count', '新增点赞')}${srt('add_collect_count', '新增收藏')}${srt('add_comment_count', '新增评论')}${srt('add_share_count', '新增分享')}<th>发布时间</th>`,
row: (r) => `<tr>
<td>${r.rank ?? '-'}</td>
<td class="cell-tl">${titleTd(r.aweme_desc || '-', r.share_url)}</td>
<td>${esc(r.user_nickname || '-')}</td>
<td>${esc(r.category || '-')}</td>
${numTd(r.add_digg_count)}${numTd(r.add_collect_count)}${numTd(r.add_comment_count)}${numTd(r.add_share_count)}
<td class="num">${fmtTime(r.create_time_str)}</td>
</tr>`,
},
};
function bindShell() {
bindBack(view);
view.querySelectorAll('.rank-tab').forEach((t) => t.addEventListener('click', () => {
const k = t.dataset.rankTab;
if (k === state.tab) return;
state.tab = k; state.page = 1; render();
if (k === tab) return;
tab = k; render();
}));
}
// meta 每榜独立拉取并缓存到 st:dates/categories 因榜而异(hot/likes 全赛道28;week 仅抓取的 5 赛道)
const metas = {};
async function loadMeta() {
if (metas[tab]) return metas[tab];
const m = await api('/api/dsh/ranking?board=' + tab + '&pageSize=20').catch((e) => ({ ok: false, error: e.message }));
if (!m.ok) { view.innerHTML = `<div class="page"><div class="page-toolbar">${toolbarBack()}<span class="page-title">热点数据</span></div><div class="empty">${esc(m.error || '加载失败')}</div></div>`; bindBack(view); return null; }
metas[tab] = m;
const s = st[tab];
s.dates = m.dates || []; s.categories = m.categories || [];
if (!s.date) { s.date = m.date || (s.dates[0] || ''); s.category = m.category || ''; }
return m;
}
function render() {
if (state.tab !== 'week') {
// 其余榜单与首页一致:建设中占位(无本地数据文件,数据获取技能落盘后即可接入)
view.innerHTML = `<div class="page">${toolbar(false)}${tabs()}<div class="rank-placeholder">「${esc(tabLabel())}」建设中,敬请期待</div></div>`;
bindShell(); return;
}
const s = st[tab];
const col = COLS[tab];
const apiUrl = '/api/dsh/ranking?board=' + tab + '&date=' + encodeURIComponent(s.date) + '&category=' + encodeURIComponent(s.category) + '&sortBy=' + encodeURIComponent(s.sortBy) + '&order=' + s.order + '&page=' + s.page + '&pageSize=' + s.pageSize;
// 非 week 榜若本 tab 从未拉过 meta → 先拉(含首次进入),期间显示 loading
(async () => {
const d = await api('/api/dsh/ranking?date=' + encodeURIComponent(state.date) + '&category=' + encodeURIComponent(state.category) + '&sortBy=' + state.sortBy + '&order=' + state.order + '&page=' + state.page + '&pageSize=' + state.pageSize);
const di = state.dates.indexOf(state.date);
const WIDE = new Set(['comprehensiveScore', 'followers', 'newFans', 'newLikes', 'newComments', 'newShares']); // 数值列(窄+右对齐)
const sortable = (key, label) => `<th class="sortable${WIDE.has(key) ? ' th-wide' : ''}" data-sort="${key}">${label}${state.sortBy === key ? (state.order === 'asc' ? ' ↑' : ' ↓') : ''}</th>`;
const rows = (d.items || []).map((r) => `<tr>
<td>${r.rank ?? '-'}</td>
<td>${esc(r.category || '-')}</td>
<td>${r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}" title="在新标签打开账号主页">${esc(r.accountName)}</a>` : esc(r.accountName)}</td>
<td class="num score-cell">${fmt(r.comprehensiveScore)}</td>
<td class="num">${fmt(r.followers)}</td>
<td class="num">${fmt(r.newFans)}</td>
<td class="num">${fmt(r.newLikes)}</td>
<td class="num">${fmt(r.newComments)}</td>
<td class="num">${fmt(r.newShares)}</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('');
const m = await loadMeta();
if (!m) return;
if (!s.dates.length) {
view.innerHTML = `<div class="page">${toolbar(s, false)}${tabs()}<div class="empty">「${esc(tabLabel())}」暂无本地数据文件,先运行 mcn-data-insight 抓取技能落盘</div></div>`;
bindShell(); return;
}
const d = await api(apiUrl).catch((e) => ({ ok: false, error: e.message }));
if (!d.ok) { view.innerHTML = `<div class="page">${toolbar(s, true)}${tabs()}<div class="empty">${esc(d.error || '加载失败')}</div></div>`; bindShell(); return; }
const di = s.dates.indexOf(s.date);
const curKey = s.sortBy || col.defKey;
const srt = (key, label) => `<th class="sortable${SORT_NUM.has(key) ? ' th-wide' : ''}" data-sort="${key}">${label}${curKey === key ? (s.order === 'asc' ? ' ↑' : ' ↓') : ''}</th>`;
const rows = (d.items || []).map((r) => col.row(r)).join('');
view.innerHTML = `
<div class="page">
${toolbar(true)}${tabs()}
${toolbar(s, true)}${tabs()}
<div class="table-wrap">
<table class="table table-rank">
<thead><tr>
${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 class="table table-rank${tab === 'week' ? '' : ' rank-video'}">
<thead><tr>${col.head(s, srt)}</tr></thead>
<tbody>${rows || `<tr><td colspan="${col.span}" class="empty">${col.empty}</td></tr>`}</tbody>
</table>
</div>
${pagerHtml(d.total, d.page, state.pageSize, (p) => { state.page = p; render(); })}
${pagerHtml(d.total, d.page, s.pageSize, (p) => { s.page = p; render(); })}
</div>`;
bindShell();
view.querySelectorAll('.sortable').forEach((th) => th.addEventListener('click', () => {
const k = th.dataset.sort;
if (state.sortBy === k) state.order = state.order === 'asc' ? 'desc' : 'asc';
else { state.sortBy = k; state.order = k === 'rank' ? 'asc' : 'desc'; }
state.page = 1; render();
if (s.sortBy === k) s.order = s.order === 'asc' ? 'desc' : 'asc';
else { s.sortBy = k; s.order = k === 'rank' ? 'asc' : 'desc'; }
s.page = 1; render();
}));
const rankCat = view.querySelector('#rankCat');
if (rankCat) rankCat.addEventListener('change', (e) => { state.category = e.target.value; state.page = 1; render(); });
if (rankCat) rankCat.addEventListener('change', (e) => { s.category = e.target.value; s.page = 1; render(); });
const prev = view.querySelector('#rankPrev'), next = view.querySelector('#rankNext');
if (prev) { prev.disabled = di <= 0; prev.addEventListener('click', () => { const i = state.dates.indexOf(state.date) - 1; if (i >= 0) { state.date = state.dates[i]; state.page = 1; render(); } }); }
if (next) { next.disabled = di < 0 || di >= state.dates.length - 1; next.addEventListener('click', () => { const i = state.dates.indexOf(state.date) + 1; if (i < state.dates.length) { state.date = state.dates[i]; state.page = 1; render(); } }); }
bindPager(view, (p) => { state.page = p; render(); });
if (prev) { prev.disabled = di <= 0; prev.addEventListener('click', () => { const i = s.dates.indexOf(s.date) - 1; if (i >= 0) { s.date = s.dates[i]; s.page = 1; render(); } }); }
if (next) { next.disabled = di < 0 || di >= s.dates.length - 1; next.addEventListener('click', () => { const i = s.dates.indexOf(s.date) + 1; if (i < s.dates.length) { s.date = s.dates[i]; s.page = 1; render(); } }); }
bindPager(view, (p) => { s.page = p; render(); });
view.querySelectorAll('[data-import]').forEach((b) => b.addEventListener('click', () => {
const r = JSON.parse(b.dataset.import);
confirmSaveAccount(r.accountName, r.profileUrl || '', b);
@@ -454,6 +454,14 @@ main#view { height: calc(100vh - 55px); overflow: auto; } /* 55px=topbar 实际
.table.table-rank th:nth-child(1) { width: 40px; } /* 排名 */
.table.table-rank th:nth-child(2) { width: 90px; } /* 赛道 */
.table.table-rank th:nth-child(10) { width: 85px; } /* 功能 */
/* 视频热榜/点赞榜(rank-video):第2列=作品标题(长标题吃宽),第3作者/第4赛道/第9发布时间 */
.table.table-rank.rank-video th:nth-child(1) { width: 44px; }
.table.table-rank.rank-video th:nth-child(2) { width: 30%; } /* 作品标题:占比列宽,截断省略 */
.table.table-rank.rank-video th:nth-child(3) { width: 120px; } /* 作者 */
.table.table-rank.rank-video th:nth-child(4) { width: 88px; } /* 赛道 */
.table.table-rank.rank-video th:nth-child(9) { width: 140px; } /* 发布时间 */
.table.table-rank.rank-video td.cell-tl { max-width: 0; } /* 标题单元格允许被压缩(配合占位列宽省略) */
.table.table-rank.rank-video .rk-tl { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.link { color: #4a90e2; cursor: pointer; text-decoration: none; }
.link:hover { text-decoration: underline; }
.link.strong { font-weight: 600; }
@@ -585,6 +593,9 @@ td.td-title { max-width: 240px; }
.rank-row { display: grid; grid-template-columns: 34px 84px minmax(80px, 1fr) 76px repeat(4, 76px) 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; }
/* 首页 视频热榜/点赞榜 预览(8 列:排名/赛道/作品标题/作者/4 数值)——标题与作者吃剩余宽 */
.rank-row.rk-video { grid-template-columns: 34px 76px minmax(140px, 1fr) minmax(80px, 1.1fr) repeat(4, 74px); }
.rank-row.rk-video .rk-author { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dim); }
.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; }