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]) => `
${label}${value}${unit}
`).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 ` `; }).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]) => `
${label}${value}
`).join(""); setText("themeMemberCount", `有行情 ${number(summary.quoted_count)} / ${number(summary.member_count)}`); const body = document.querySelector("#themeMemberTableBody"); body.innerHTML = (payload.members || []).map((row, index) => ` ${index + 1} ${escapeHtml(row.name)}${escapeHtml(row.code)} ${row.has_quote ? signed(row.change) : ""} ${row.has_quote ? formatNumber(row.price, 2) : ""}${row.has_quote ? formatNumber(row.amount_billion, 2) : ""}`).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); }); }