window.XiaobaiPageModules.register("auction", ["auctionView"], { bind: bindAuctionEvents, enter: ["loadAuction"], leave: ["clearAuction"], }); async function loadAuctionCenter(force = false) { if (state.auctionLoading) return; state.auctionLoading = true; const button = document.querySelector("#auctionRefreshButton"); button.disabled = true; setText("auctionDateLabel", "正在读取竞价数据"); try { const query = new URLSearchParams({ trade_date: elements.tradeDate.value }); if (force) query.set("force", "1"); state.auctionData = await apiRequest(`/api/auction?${query}`); renderAuctionCenter(); scheduleAuctionTransition(state.auctionData.meta || {}); } catch (error) { document.querySelector("#auctionSummary").innerHTML = ""; document.querySelector("#auctionThemeCarry").innerHTML = ""; document.querySelector("#auctionNewThemes").innerHTML = ""; document.querySelector("#auctionAmountTrend").innerHTML = ""; document.querySelector("#auctionAmountCompare").innerHTML = ""; document.querySelector("#auctionTableBody").innerHTML = ""; document.querySelector("#auctionEmpty").hidden = false; setText("auctionDateLabel", error.message || "竞价数据暂不可用"); showToast(error.message || "竞价数据加载失败"); } finally { state.auctionLoading = false; button.disabled = false; } } function renderAuctionCenter() { const payload = state.auctionData; if (!payload) return; const summary = payload.summary || {}; renderAuctionPhase(payload.meta || {}); setText( "auctionDateLabel", `${payload.meta?.carried_forward ? "最近有效竞价" : "竞价日期"} ${payload.meta?.trade_date || "--"}`, ); document.querySelector("#auctionSummary").innerHTML = [ ["竞价覆盖", `${formatNumber(summary.stock_count, 0)} 只`, ""], ["重点异动", `${formatNumber(summary.focus_count, 0)} 只`, "up"], ["竞价一字", `${formatNumber(summary.one_price_count, 0)} 只`, ""], ["竞价成交额", `${formatNumber(summary.amount_billion, 2)} 亿`, ""], ].map(([label, value, tone]) => `
${label}${value}
`).join(""); setText("auctionFocusCount", number(summary.focus_count)); setText("auctionAllCount", number(summary.candidate_count)); setText("auctionOnePriceCount", number(summary.one_price_count)); setText("auctionWatchlistCount", number(payload.watchlist_rows?.length)); renderAuctionInsights(payload); renderAuctionTable(); } function renderAuctionInsights(payload) { const themes = payload.themes || {}; const carry = themes.carry || []; const tone = { "强承接": "strong", "有承接": "steady", "分歧": "mixed", "承接弱": "weak" }; setText("auctionThemeBaseline", `基于 ${payload.candidate_meta?.baseline_date || "--"}`); document.querySelector("#auctionThemeCarry").innerHTML = carry.length ? carry.map((item) => `
${escapeHtml(item.name)} ${escapeHtml(item.leader || "--")} · 昨日 ${number(item.prior_limit_count)} 只涨停 ${escapeHtml(item.status)} ${item.median_change == null ? "暂无有效候选" : `${signed(item.median_change)}%`}中位
`).join("") : '
暂无昨日强势题材基线
'; const newThemes = themes.new_themes || []; document.querySelector("#auctionNewThemes").innerHTML = newThemes.length ? newThemes.map((item) => `${escapeHtml(item.name)} ${number(item.stock_count)}`).join("") : '尚未形成多股共振的新线索'; const history = payload.amount_history || []; const maximum = Math.max(...history.map((item) => number(item.amount_billion)), 1); const priorFive = history.slice(Math.max(0, history.length - 6), Math.max(0, history.length - 1)); const fiveDayAverage = priorFive.length ? priorFive.reduce((sum, item) => sum + number(item.amount_billion), 0) / priorFive.length : null; document.querySelector("#auctionAmountTrend").innerHTML = history.length ? history.map((item, index) => { const height = Math.max(8, number(item.amount_billion) / maximum * 100); const current = index === history.length - 1 ? " current" : ""; return `
${escapeHtml(String(item.trade_date || "").slice(5))}
`; }).join("") + (fiveDayAverage === null ? "" : `
5日均 ${formatNumber(fiveDayAverage, 1)}
`) : '
历史竞价量能尚未形成
'; setText("auctionAmountValue", `${formatNumber(payload.summary?.amount_billion, 2)} 亿`); const comparison = [ ["较昨日", payload.summary?.amount_change_previous], ["较5日均值", payload.summary?.amount_change_5d], ]; document.querySelector("#auctionAmountCompare").innerHTML = comparison.map(([label, value]) => ` ${label}${value == null ? "--" : `${signed(value)}%`} `).join(""); } function renderAuctionPhase(meta) { const phase = meta.phase || "archive"; const available = Boolean(meta.available); const copy = { pending: ["竞价尚未开始", "9:15 进入观察期,9:25 读取最终竞价结果。", "下一阶段 09:15"], observing: ["竞价观察期", "此阶段先观察盘前变化,系统将在 9:25 自动读取最终结果。", "09:25 定格"], selection: available ? ["竞价筛选窗口", "最终竞价结果已经定格,请在 9:30 前完成筛选。", "有效至 09:30"] : ["等待最终竞价", "9:25 数据尚未到达,系统正在自动重试。", "即将更新"], finalized: ["今日竞价已定格", "9:30 后停止更新,仅保留用于复盘、回测与智能选股。", "已冻结"], archive: ["历史竞价归档", "当前展示所选交易日的最终竞价结果。", "归档数据"], }[phase] || ["竞价状态", "当前竞价状态待确认。", "--"]; const notice = document.querySelector("#auctionPhaseNotice"); notice.dataset.phase = phase; setText("auctionPhaseTitle", copy[0]); setText("auctionPhaseDetail", copy[1]); setText("auctionPhaseTime", copy[2]); const refresh = document.querySelector("#auctionRefreshButton"); refresh.hidden = phase !== "selection"; refresh.disabled = state.auctionLoading; } function clearAuctionTimer() { if (state.auctionTimer) clearTimeout(state.auctionTimer); state.auctionTimer = null; } function scheduleAuctionTransition(meta) { clearAuctionTimer(); if (state.activeView !== "auctionView") return; let delay = 0; if (["selection", "finalized"].includes(meta.phase) && !meta.available) { delay = 10_000; } else if (meta.next_transition_at) { const transitionAt = new Date(meta.next_transition_at).getTime(); if (Number.isFinite(transitionAt)) delay = Math.max(800, transitionAt - Date.now() + 500); } if (!delay) return; state.auctionTimer = setTimeout(() => { state.auctionTimer = null; if (state.activeView === "auctionView") loadAuctionCenter(true); }, Math.min(delay, 2_147_000_000)); } function renderAuctionTable() { const rows = currentAuctionRows(); const columns = auctionColumns(); const head = document.querySelector("#auctionTableHead"); head.innerHTML = columns.map((column) => { const sorted = column.sortKey === state.auctionSortKey; const arrow = !column.sortKey ? "" : `${sorted ? (state.auctionSortDirection === "desc" ? "▼" : "▲") : "↕"}`; return `${column.label}${arrow}`; }).join(""); const body = document.querySelector("#auctionTableBody"); body.innerHTML = rows.map((row) => `${columns.map((column) => renderAuctionCell(row, column.key)).join("")}`).join(""); bindStockRows(body); const datasetCopy = { focus: ["重点异动", "优先查看市场核心与显著预期差"], onePrice: ["竞价一字", "竞价封于当日真实涨停价,不参与普通异动评分"], watchlist: ["我的自选", "仅展示当前账号关注标的的竞价反馈"], all: ["全部候选", "昨日涨停、炸板与热榜前20候选"], }[state.auctionDataset] || ["竞价异动", ""]; setText("auctionWorkspaceTitle", datasetCopy[0]); setText("auctionWorkspaceSubtitle", datasetCopy[1]); document.querySelector("#auctionExpectationControls").hidden = state.auctionDataset === "onePrice"; const empty = document.querySelector("#auctionEmpty"); const phase = state.auctionData?.meta?.phase || "archive"; empty.textContent = phase === "selection" && !state.auctionData?.meta?.available ? "正在等待 9:25 最终竞价数据" : state.auctionDataset === "watchlist" ? "当前账号还没有可观察的自选股" : state.auctionDataset === "onePrice" ? "当前没有竞价封于涨停价的股票" : "没有符合条件的竞价候选"; empty.hidden = rows.length > 0; } function currentAuctionRows() { const datasets = { focus: state.auctionData?.focus_rows || [], onePrice: state.auctionData?.one_price_rows || [], watchlist: state.auctionData?.watchlist_rows || [], all: state.auctionData?.rows || [], }; let rows = [...(datasets[state.auctionDataset] || [])]; const filter = state.auctionFilter; const labels = { above: "超预期", matched: "符合预期", below: "低于预期" }; if (labels[filter]) rows = rows.filter((item) => item.expectation === labels[filter]); if (state.auctionQuery) { rows = rows.filter((item) => `${item.code} ${item.name} ${item.sector}`.toLocaleLowerCase("zh-CN").includes(state.auctionQuery)); } const key = state.auctionSortKey; const direction = state.auctionSortDirection === "asc" ? 1 : -1; if (key) { rows.sort((left, right) => { const leftValue = left[key]; const rightValue = right[key]; if (leftValue == null && rightValue == null) return 0; if (leftValue == null) return 1; if (rightValue == null) return -1; const result = typeof leftValue === "number" || typeof rightValue === "number" ? number(leftValue) - number(rightValue) : String(leftValue).localeCompare(String(rightValue), "zh-CN", { numeric: true }); return result * direction; }); } return rows.slice(0, 300); } function auctionColumns() { const base = [ { key: "stock", label: "股票" }, { key: "context", label: "方向与来源" }, { key: "identity", label: "市场身份" }, ]; const metrics = [ { key: "score", label: "关注分", numeric: true, sortKey: "attention_score" }, { key: "expectation", label: "预期判断" }, { key: "change", label: "竞价涨幅(%)", numeric: true, sortKey: "change" }, { key: "amount", label: "竞价额(百万)", numeric: true, sortKey: "amount_million" }, { key: "volume", label: "量比", numeric: true, sortKey: "volume_ratio" }, ]; return state.auctionDataset === "onePrice" ? [...base, ...metrics.slice(2)] : [...base, ...metrics]; } function renderAuctionCell(row, key) { const unavailable = row.available === false; const onePrice = Boolean(row.is_one_price); const expectationTone = { "超预期": "above", "符合预期": "matched", "低于预期": "below" }; if (key === "stock") return `${escapeHtml(row.name)}${escapeHtml(row.code)}`; if (key === "context") return `${escapeHtml(row.sector || "其他")}${renderAuctionSources(row.source_label || (state.auctionDataset === "watchlist" ? "我的自选" : "全市场"))}`; if (key === "identity") return `${renderAuctionCoreTags(row.core_tags)}`; if (unavailable) return key === "expectation" ? '暂无竞价' : ``; if (key === "score") return `${onePrice ? "" : formatNumber(row.attention_score, 1)}`; if (key === "expectation") { const tag = onePrice ? '竞价一字' : `${escapeHtml(row.expectation || "符合预期")}`; return `${tag}`; } if (key === "change") return `${signed(row.change)}`; if (key === "amount") return `${formatNumber(row.amount_million, 2)}`; if (key === "volume") return `${formatNumber(row.volume_ratio, 2)}`; return ""; } function renderAuctionSources(value) { const sources = String(value || "").split(/[·、/]/).map((item) => item.trim()).filter(Boolean).slice(0, 3); return `${sources.map((source) => `${escapeHtml(source)}`).join("")}`; } function renderAuctionCoreTags(tags) { const values = Array.isArray(tags) ? tags : []; return values.length ? `${values.slice(0, 2).map((tag) => `${escapeHtml(tag)}`).join("")}` : ''; } function exportAuctionRows() { const rows = currentAuctionRows(); exportRows("集合竞价", rows, [ ["股票代码", "code"], ["股票名称", "name"], ["行业", "sector"], ["来源", "source_label"], ["市场身份", "core_tags"], ["关注分", "attention_score"], ["预期判断", "expectation"], ["竞价涨幅%", "change"], ["竞价额百万", "amount_million"], ["量比", "volume_ratio"], ]); } function bindAuctionEvents() { document.querySelector("#auctionRefreshButton").addEventListener("click", () => loadAuctionCenter(true)); document.querySelector("#auctionExportButton").addEventListener("click", exportAuctionRows); document.querySelector("#auctionSearch").addEventListener("input", (event) => { state.auctionQuery = event.target.value.trim().toLocaleLowerCase("zh-CN"); renderAuctionTable(); }); document.querySelectorAll("[data-auction-dataset]").forEach((button) => { button.addEventListener("click", () => { state.auctionDataset = button.dataset.auctionDataset || "focus"; state.auctionFilter = "all"; state.auctionSortKey = state.auctionDataset === "onePrice" ? "amount_million" : "attention_score"; state.auctionSortDirection = "desc"; document.querySelectorAll("[data-auction-dataset]").forEach((item) => { const active = item === button; item.classList.toggle("active", active); item.setAttribute("aria-selected", String(active)); }); document.querySelectorAll("[data-auction-filter]").forEach((item) => item.classList.toggle("active", item.dataset.auctionFilter === "all")); renderAuctionTable(); }); }); document.querySelectorAll("[data-auction-filter]").forEach((button) => { button.addEventListener("click", () => { state.auctionFilter = button.dataset.auctionFilter || "all"; document.querySelectorAll("[data-auction-filter]").forEach((item) => item.classList.toggle("active", item === button)); renderAuctionTable(); }); }); document.querySelector("#auctionTable").addEventListener("click", (event) => { const header = event.target.closest("th[data-auction-sort]"); if (!header) return; const key = header.dataset.auctionSort; if (state.auctionSortKey === key) state.auctionSortDirection = state.auctionSortDirection === "asc" ? "desc" : "asc"; else { state.auctionSortKey = key; state.auctionSortDirection = "desc"; } renderAuctionTable(); }); }