Files
xiaobaifupan/app/frontend/pages/popularity/page.js
T
ad08d309c6 feat: apply B-147 visual tokens to remaining market data pages
Unify ladder, themes, rotation, auction, dragon-tiger and popularity with shared tokens, left-aligned first columns, and table density.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-20 04:34:36 +08:00

101 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.XiaobaiPageModules.register("popularity", ["popularityView"], {
bind: bindPopularityEvents,
enter: ["loadPopularity"],
});
async function loadPopularity(force = false) {
if (state.popularityLoading) return;
state.popularityLoading = true;
const button = document.querySelector("#popularityRefreshButton");
button.disabled = true;
setText("popularityDateLabel", "正在读取人气榜");
try {
const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
if (force) query.set("force", "1");
state.popularityData = await apiRequest(`/api/popularity?${query}`);
renderPopularity();
} catch (error) {
setText("popularityDateLabel", error.message || "人气榜暂不可用");
document.querySelector("#popularityTableBody").innerHTML = "";
document.querySelector("#popularityEmpty").hidden = false;
showToast(error.message || "人气榜加载失败");
} finally {
state.popularityLoading = false;
button.disabled = false;
}
}
function renderPopularity() {
const payload = state.popularityData;
if (!payload) return;
const summary = payload.summary || {};
setText("popularityDateLabel", `${payload.meta?.carried_forward ? "最近有效榜单" : "榜单日期"} ${payload.meta?.trade_date || "--"}`);
const topNames = (rows) => (rows || []).slice(0, 3).map((item) => item.name).filter(Boolean).join(" · ") || "--";
document.querySelector("#popularitySummary").innerHTML = [
["同花顺热度 Top3", topNames(payload.ths), `共 ${number(summary.ths_count)} 只上榜`],
["东方财富热度 Top3", topNames(payload.dc), `共 ${number(summary.dc_count)} 只上榜`],
["双榜共识", `${number(summary.dual_count)} 只`, "同时进入两榜,共识度更高"],
].map(([label, value, detail], index) => `<article class="${index === 2 ? "consensus" : ""}"><span>${label}</span><strong>${escapeHtml(value)}</strong><small>${escapeHtml(detail)}</small></article>`).join("");
renderPopularityTable();
}
function renderPopularityTable() {
const source = state.popularitySource;
let rows = [...(state.popularityData?.[source] || [])];
if (state.popularityQuery) {
rows = rows.filter((item) => `${item.code} ${item.name} ${(item.concepts || []).join(" ")}`.toLocaleLowerCase("zh-CN").includes(state.popularityQuery));
}
const combined = source === "combined";
const sourceName = source === "ths" ? "同花顺" : source === "dc" ? "东方财富" : "双榜综合";
setText("popularityTableTitle", `${sourceName}榜`);
setText("popularityTableNote", combined ? "按双榜排名综合排序 · 已隐藏重复的榜单状态" : "按榜单名次排序 · 状态显示是否同时进入另一榜");
const headers = [
["排名", "row-number"], ["股票", ""], ["最新价(元)", "number num"], ["涨跌幅(%", "number num"],
...(source !== "dc" ? [["同花顺", "number num"]] : []),
...(source !== "ths" ? [["东方财富", "number num"]] : []),
["排名变化", "number num"], ["热门概念", ""], ...(!combined ? [["榜单状态", ""]] : []),
];
document.querySelector("#popularityTableHead").innerHTML = headers.map(([label, className]) => `<th scope="col" class="${className}">${label}</th>`).join("");
const body = document.querySelector("#popularityTableBody");
body.innerHTML = rows.map((row, index) => {
const thsRank = source === "ths" ? row.rank : row.ths_rank;
const dcRank = source === "dc" ? row.rank : row.dc_rank;
const move = row.rank_change;
const movement = move === null || move === undefined ? "新" : number(move) > 0 ? `↑${number(move)}` : number(move) < 0 ? `↓${Math.abs(number(move))}` : "持平";
return `<tr data-code="${escapeHtml(row.code)}">
<td class="row-number popularity-rank-v2"><b>${index + 1}</b>${index < 3 ? '<span>热</span>' : ""}</td>
<td><div class="popularity-stock-v2"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><span class="stock-code scode">${escapeHtml(row.code)}</span></div></td>
<td class="number num">${row.price == null ? "" : formatNumber(row.price, 2)}</td>
<td class="number num ${row.change == null ? "" : changeClass(row.change)}">${row.change == null ? "" : signed(row.change)}</td>
${source !== "dc" ? `<td class="number num popularity-list-rank-v2">${thsRank ? number(thsRank) : ""}</td>` : ""}
${source !== "ths" ? `<td class="number num popularity-list-rank-v2">${dcRank ? number(dcRank) : ""}</td>` : ""}
<td class="number num popularity-movement-v2 ${number(move) > 0 ? "up" : number(move) < 0 ? "down" : ""}">${movement}</td>
<td class="popularity-concepts-v2" title="${escapeHtml((row.concepts || []).join("、"))}">${escapeHtml((row.concepts || []).slice(0, 3).join("、"))}</td>
${!combined ? `<td><span class="popularity-source-tag-v2 ${row.dual_source ? "dual" : ""}">${row.dual_source ? "双榜共识" : "单榜入选"}</span></td>` : ""}
</tr>`;
}).join("");
bindStockRows(body);
markAutoSortableHeaders(body.closest("table"));
document.querySelector("#popularityEmpty").hidden = rows.length > 0;
}
function bindPopularityEvents() {
document.querySelector("#popularityRefreshButton").addEventListener("click", () => loadPopularity(true));
document.querySelector("#popularitySearch").addEventListener("input", (event) => {
state.popularityQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
renderPopularityTable();
});
document.querySelectorAll("[data-popularity-source]").forEach((button) => {
button.addEventListener("click", () => {
state.popularitySource = button.dataset.popularitySource || "combined";
document.querySelectorAll("[data-popularity-source]").forEach((item) => {
const active = item === button;
item.classList.toggle("active", active);
item.setAttribute("aria-selected", String(active));
});
renderPopularityTable();
});
});
}