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>
120 lines
6.2 KiB
JavaScript
120 lines
6.2 KiB
JavaScript
window.XiaobaiPageModules.register("themes", ["themeLibraryView"], {
|
|
bind: bindThemeLibraryEvents,
|
|
enter: ["loadThemes"],
|
|
});
|
|
|
|
async function loadThemeLibrary(force = false) {
|
|
if (state.themeLoading) return;
|
|
state.themeLoading = true;
|
|
const button = document.querySelector("#themeRefreshButton");
|
|
button.disabled = true;
|
|
setText("themeDateLabel", "正在整理题材库");
|
|
try {
|
|
const query = new URLSearchParams({ trade_date: elements.tradeDate.value });
|
|
if (force) query.set("force", "1");
|
|
state.themeLibrary = await apiRequest(`/api/themes?${query}`);
|
|
renderThemeLibrary();
|
|
const available = (state.themeLibrary.items || []).some((item) => item.code === state.selectedThemeCode);
|
|
if (!available) state.selectedThemeCode = "";
|
|
const initialCode = state.selectedThemeCode || state.themeLibrary.items?.[0]?.code || "";
|
|
if (initialCode) await selectTheme(initialCode, true);
|
|
} catch (error) {
|
|
setText("themeDateLabel", error.message || "题材数据暂不可用");
|
|
renderEmptyState("themeDirectory", error.message || "题材数据加载失败");
|
|
showToast(error.message || "题材数据加载失败");
|
|
} finally {
|
|
state.themeLoading = false;
|
|
button.disabled = false;
|
|
}
|
|
}
|
|
|
|
function renderThemeLibrary() {
|
|
const payload = state.themeLibrary;
|
|
if (!payload) return;
|
|
const summary = payload.summary || {};
|
|
setText("themeDateLabel", `${payload.meta?.carried_forward ? "最近有效行情" : "行情日期"} ${payload.meta?.trade_date || "--"}`);
|
|
document.querySelector("#themeSummary").innerHTML = [
|
|
["收录题材", number(summary.theme_count), "个", ""],
|
|
["当日上涨", number(summary.up_count), "个", "up"],
|
|
["当日下跌", number(summary.down_count), "个", "down"],
|
|
["人气题材", number(summary.hot_count), "个", "warning"],
|
|
].map(([label, value, unit, tone]) => `<div><span>${label}</span><strong class="${tone}">${value}<small>${unit}</small></strong></div>`).join("");
|
|
renderThemeDirectory();
|
|
}
|
|
|
|
function renderThemeDirectory() {
|
|
let items = [...(state.themeLibrary?.items || [])];
|
|
if (state.themeQuery) {
|
|
items = items.filter((item) => `${item.code} ${item.name}`.toLocaleLowerCase("zh-CN").includes(state.themeQuery));
|
|
}
|
|
setText("themeResultCount", `${items.length} 个`);
|
|
document.querySelector("#themeDirectory").innerHTML = items.map((item, index) => {
|
|
const active = item.code === state.selectedThemeCode;
|
|
return `
|
|
<button type="button" class="theme-directory-item-v2 ${active ? "active" : ""}" data-theme-code="${escapeHtml(item.code)}" aria-pressed="${active}">
|
|
<span class="theme-rank-v2">${index + 1}</span>
|
|
<span class="theme-directory-copy-v2"><strong class="market-preview-trigger" data-market-preview-type="theme" data-market-preview-id="${escapeHtml(item.code)}" title="悬停预览题材行情">${escapeHtml(item.name)}</strong><small>${number(item.member_count)} 只成分${item.hot_rank ? ` · 人气第 ${number(item.hot_rank)}` : ""}</small></span>
|
|
<b class="${changeClass(item.change)}">${item.has_quote ? `${signed(item.change)}%` : "--"}</b>
|
|
</button>`;
|
|
}).join("") || emptyStateHtml("没有匹配的题材");
|
|
}
|
|
|
|
async function selectTheme(code, keepSelection = false) {
|
|
if (!code) return;
|
|
state.selectedThemeCode = code;
|
|
if (!keepSelection) renderThemeDirectory();
|
|
document.querySelector("#themeDetailEmpty").hidden = false;
|
|
document.querySelector("#themeDetailContent").hidden = true;
|
|
setText("themeDetailEmpty", "正在读取题材详情");
|
|
try {
|
|
const query = new URLSearchParams({ code, trade_date: elements.tradeDate.value });
|
|
state.themeDetail = await apiRequest(`/api/themes/detail?${query}`);
|
|
renderThemeDetail();
|
|
} catch (error) {
|
|
setText("themeDetailEmpty", error.message || "题材详情加载失败");
|
|
showToast(error.message || "题材详情加载失败");
|
|
}
|
|
}
|
|
|
|
function renderThemeDetail() {
|
|
const payload = state.themeDetail;
|
|
if (!payload) return;
|
|
const theme = payload.theme || {};
|
|
const summary = payload.summary || {};
|
|
document.querySelector("#themeDetailEmpty").hidden = true;
|
|
document.querySelector("#themeDetailContent").hidden = false;
|
|
setText("themeDetailName", theme.name || "--");
|
|
setText("themeDetailCode", `${theme.code || "--"} · ${payload.meta?.trade_date || "--"}`);
|
|
setText("themeDetailChange", `${signed(theme.change)}%`);
|
|
document.querySelector("#themeDetailChange").className = changeClass(theme.change);
|
|
document.querySelector("#themeDetailMetrics").innerHTML = [
|
|
["成分股", `${number(summary.member_count)} 只`, ""],
|
|
["有行情", `${number(summary.quoted_count)} 只`, ""],
|
|
["上涨", `${number(summary.up_count)} 只`, "up"],
|
|
["下跌", `${number(summary.down_count)} 只`, "down"],
|
|
["换手率", `${formatNumber(theme.turnover_rate, 2)}%`, ""],
|
|
].map(([label, value, tone]) => `<div><span>${label}</span><strong class="${tone}">${value}</strong></div>`).join("");
|
|
setText("themeMemberCount", `有行情 ${number(summary.quoted_count)} / ${number(summary.member_count)}`);
|
|
const body = document.querySelector("#themeMemberTableBody");
|
|
body.innerHTML = (payload.members || []).map((row, index) => `
|
|
<tr data-code="${escapeHtml(row.code)}"><td class="row-number muted">${index + 1}</td>
|
|
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
|
<td class="number num ${changeClass(row.change)}">${row.has_quote ? signed(row.change) : ""}</td>
|
|
<td class="number num">${row.has_quote ? formatNumber(row.price, 2) : ""}</td><td class="number num">${row.has_quote ? formatNumber(row.amount_billion, 2) : ""}</td></tr>`).join("");
|
|
bindStockRows(body);
|
|
renderThemeDirectory();
|
|
}
|
|
|
|
|
|
function bindThemeLibraryEvents() {
|
|
document.querySelector("#themeRefreshButton").addEventListener("click", () => loadThemeLibrary(true));
|
|
document.querySelector("#themeSearch").addEventListener("input", (event) => {
|
|
state.themeQuery = event.target.value.trim().toLocaleLowerCase("zh-CN");
|
|
renderThemeDirectory();
|
|
});
|
|
document.querySelector("#themeDirectory").addEventListener("click", (event) => {
|
|
const button = event.target.closest("[data-theme-code]");
|
|
if (button) selectTheme(button.dataset.themeCode);
|
|
});
|
|
}
|