工作台「账号周榜」入口改为「热点数据」多榜 tab 页:首页入口卡片/面包屑/路由标题同步改名;renderRanking 重构——RANK_TABS 四 tab(账号周榜/视频热榜/点赞榜/涨粉榜,与首页 rank-preview 一致),账号周榜 tab 保留完整列表(筛选/排序/分页/关注),其余榜暂占位(后端仅周榜 json,数据落盘后接入)

This commit is contained in:
maogeigei committed 2026-09-03 12:20:37 +08:00
1 parent b346c37b10
commit a1227600f7
3 files changed
+86 -43

No files matched your search

+6
View File
@@ -115,3 +115,9 @@
- **缓存目录数据清理(用户指令「去掉的赛道 现有数据也要删除」)**:删除全部已剔除赛道历史文件 12 个(颜值造型 3 + 情感 3 + 二次元 3 + 化妆美容 3),目录现仅保留 5 赛道×3 期 = 15 文件
- 验证:py_compile OK + 断言(保留=5 赛道,颜值造型/情感不在)+ ranking 接口实测 categories=5(美食/生活vlog/旅行/小剧场/亲子)dates=3 total=250 读取正常
- 教训:用户「情感 去掉」紧接「颜值造型 去掉」=连续精简指令,需完整执行不可只当上一条确认
## 工作台「账号周榜」入口 →「热点数据」多榜 tab 页
- 用户指令:「账号周榜入口改为热点数据,里面用 tab 区分数据列表,和首页一样」
- 改动:app.js 首页入口卡片 title 账号周榜→热点数据(desc 多榜切换)、CRUMB_TITLES.ranking + router titles.ranking→热点数据;data-pages.js renderRanking 重构为多 tab 页——RANK_TABS 四 tab(week 账号周榜/hot 视频热榜/likes 点赞榜/fans 涨粉榜,与首页 rank-preview 一致),week 显示完整周榜列表(筛选/排序/分页/关注,原逻辑保留),其余 tab 显示「建设中」占位(与首页行为一致,后端 RANKING_DIR 目前仅周榜 json,后续数据落盘即可接入)
- 验证:node --check 两文件通过;服务 8900 存活、新 js 已生效(grep 热点数据=3/1);带 ?v= 打开 #/ranking 页面
- 纯前端静态改动,server.js 未变无需重启服务
@@ -696,7 +696,7 @@ async function router() {
}
// 数据功能页
if (typeof window.DataPages !== 'undefined') {
const titles = { ranking: '账号周榜', accounts: '账号列表', account: '账号详情', video: '视频详情', rewrites: 'AI写脚本', reviews: 'AI脚本诊断' };
const titles = { ranking: '热点数据', accounts: '账号列表', account: '账号详情', video: '视频详情', rewrites: 'AI写脚本', reviews: 'AI脚本诊断' };
const pages = {
'ranking': () => window.DataPages.renderRanking(view),
'accounts': () => window.DataPages.renderAccounts(view),
@@ -502,18 +502,64 @@ async function confirmAiScript(accountName, btn) {
}
/* ================= 周榜 ================= */
/* ================= 热点数据(#/ranking:账号周榜为主,多榜 tab 与首页 rank-preview 一致) ================= */
async function renderRanking(view) {
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;
// tab 与首页 rank-tabs 一致:账号周榜有本地数据文件;视频热榜/点赞榜/涨粉榜暂占位(后端 RANKING_DIR 目前仅周榜 json)
const RANK_TABS = [
['week', '账号周榜'],
['hot', '视频热榜'],
['likes', '点赞榜'],
['fans', '涨粉榜'],
];
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 || '';
async function load() {
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>
const tabLabel = () => { const t = RANK_TABS.find(([k]) => k === state.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('')}
</div>`;
const toolbar = (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('')}
</select>
<button class="btn" id="rankPrev" title="上一期">◀</button>
<span class="pager-info" title="选择日期">${esc(state.date)}</span>
<button class="btn" id="rankNext" title="下一期">▶</button>
</div>` : ''}
</div>`;
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();
}));
}
function render() {
if (state.tab !== 'week') {
// 其余榜单与首页一致:建设中占位(无本地数据文件,数据获取技能落盘后即可接入)
view.innerHTML = `<div class="page">${toolbar(false)}${tabs()}<div class="rank-placeholder">「${esc(tabLabel())}」建设中,敬请期待</div></div>`;
bindShell(); return;
}
(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>
@@ -525,21 +571,9 @@ async function renderRanking(view) {
<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('');
view.innerHTML = `
view.innerHTML = `
<div class="page">
<div class="page-toolbar">
${toolbarBack()}
<span class="page-title">账号周榜</span>
<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>
${toolbar(true)}${tabs()}
<div class="table-wrap">
<table class="table table-rank">
<thead><tr>
@@ -548,26 +582,29 @@ async function renderRanking(view) {
<tbody>${rows || '<tr><td colspan="10" class="empty">暂无周榜数据</td></tr>'}</tbody>
</table>
</div>
${pagerHtml(d.total, d.page, state.pageSize, (p) => { state.page = p; load(); })}
${pagerHtml(d.total, d.page, state.pageSize, (p) => { state.page = p; render(); })}
</div>`;
bindBack(view);
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; load();
}));
view.querySelector('#rankCat').addEventListener('change', (e) => { state.category = e.target.value; state.page = 1; load(); });
view.querySelector('#rankPrev').addEventListener('click', () => { const i = state.dates.indexOf(state.date) - 1; if (i >= 0) { state.date = state.dates[i]; state.page = 1; load(); } });
view.querySelector('#rankNext').addEventListener('click', () => { const i = state.dates.indexOf(state.date) + 1; if (i < state.dates.length) { state.date = state.dates[i]; state.page = 1; load(); } });
bindPager(view, (p) => { state.page = p; load(); });
view.querySelectorAll('[data-import]').forEach((b) => b.addEventListener('click', () => {
const r = JSON.parse(b.dataset.import);
confirmSaveAccount(r.accountName, r.profileUrl || '', b);
}));
view._reload = load; // 静默自动刷新(保留筛选/翻页状态)
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();
}));
const rankCat = view.querySelector('#rankCat');
if (rankCat) rankCat.addEventListener('change', (e) => { state.category = e.target.value; state.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(); });
view.querySelectorAll('[data-import]').forEach((b) => b.addEventListener('click', () => {
const r = JSON.parse(b.dataset.import);
confirmSaveAccount(r.accountName, r.profileUrl || '', b);
}));
view._reload = render; // 静默自动刷新(保留 tab/筛选/翻页状态)
})();
}
await load();
render();
}
/* ================= 账号列表 ================= */