async function loadDashboard(force = false, background = false, showOverlay = true) { const requestedDate = elements.tradeDate.value; if (state.dashboardLoading && state.dashboardRequestDate === requestedDate) return; state.dashboardLoading = true; state.dashboardRequestDate = requestedDate; const requestSequence = ++state.dashboardRequestSequence; if (force) stockPreviewCache.clear(); if (!background && showOverlay) { setLoading(true, "正在加载市场数据"); setStatus("正在加载市场数据"); } else if (!background) { setStatus("正在刷新行情"); } try { const query = new URLSearchParams({ trade_date: elements.tradeDate.value }); if (force) query.set("force", "1"); const payload = await apiRequest(`/api/dashboard?${query}`); if ( requestSequence !== state.dashboardRequestSequence || requestedDate !== elements.tradeDate.value ) return; applyDashboard(payload, background); } catch (error) { if (background) { setStatus("实时刷新暂时中断,正在等待重试"); } else { showToast(error.message || "无法连接本地服务"); setStatus("加载失败"); } } finally { if (requestSequence === state.dashboardRequestSequence) { state.dashboardLoading = false; state.dashboardRequestDate = ""; if (!background && showOverlay) setLoading(false); updateDateButtons(); } } } async function startAdminRefresh() { const buttons = [document.querySelector("#syncButton"), document.querySelector("#adminRefreshButton")].filter(Boolean); buttons.forEach((button) => { button.disabled = true; }); const requestedDate = elements.tradeDate.value; setAdminRefreshStatus("running", `正在刷新 ${requestedDate} 的行情,请稍候…`, "loader-circle"); try { const payload = await apiRequest("/api/admin/refresh", "POST", { trade_date: requestedDate }); if (!payload.started || !payload.job_key) { setAdminRefreshStatus("warning", "已有刷新任务正在运行,请稍后再试。", "clock-3"); showToast(payload.message || "已有后台刷新任务正在运行"); return; } setStatus(`正在刷新 ${requestedDate} 的行情`); const job = await waitForAdminRefresh(payload.job_key); if (job.status === "failed") { const reason = job.message || job.error_code || "数据源未返回结果"; setAdminRefreshStatus("failure", `刷新失败:${reason}`, "circle-x"); setStatus("后台刷新失败"); showToast("后台刷新失败"); return; } const query = new URLSearchParams({ trade_date: requestedDate }); const dashboard = await apiRequest(`/api/dashboard?${query}`); applyDashboard(dashboard); const meta = dashboard.meta || {}; const actualDate = String(meta.trade_date || "").slice(0, 10); const requestedCompact = requestedDate.replaceAll("-", ""); const actualCompact = actualDate.replaceAll("-", ""); const updated = formatTimestamp(meta.updated_at); if (actualCompact !== requestedCompact || meta.carried_forward) { const reason = meta.notice ? `;${meta.notice}` : ""; setAdminRefreshStatus("warning", `刷新已完成,但没有获取到 ${requestedDate} 的最新行情;当前仍是 ${actualDate || "未知日期"}${reason}`, "triangle-alert"); showToast("刷新完成,但未获取到所选日期的最新行情"); } else if (meta.notice) { setAdminRefreshStatus("warning", `已刷新到 ${actualDate}(${updated}),但数据源提示:${meta.notice}`, "triangle-alert"); showToast(`已刷新到 ${actualDate},请留意数据源提示`); } else { setAdminRefreshStatus("success", `刷新成功:已获取 ${actualDate} 的最新行情,更新时间 ${updated}`, "circle-check"); showToast(`刷新成功:已获取 ${actualDate} 的最新行情`); } } catch (error) { const message = error.message || "后台刷新失败"; setAdminRefreshStatus("failure", `刷新失败:${message}`, "circle-x"); setStatus("后台刷新失败"); showToast(message); } finally { buttons.forEach((button) => { button.disabled = false; }); } } function setAdminRefreshStatus(tone, message, icon = "circle-dot") { const status = document.querySelector("#adminRefreshStatus"); if (!status) return; status.dataset.tone = tone; status.innerHTML = `${escapeHtml(message)}`; refreshIcons(); } async function waitForAdminRefresh(jobKey) { for (let attempt = 0; attempt < 120; attempt += 1) { const payload = await apiRequest("/api/admin/settings"); const job = (payload.data?.jobs || []).find((item) => item.idempotency_key === jobKey); if (job && ["success", "failed"].includes(job.status)) return job; await new Promise((resolve) => setTimeout(resolve, 1000)); } throw new Error("刷新等待超时,请稍后重试"); } function applyDashboard(payload, background = false) { state.dashboard = payload; const selectedDate = payload.meta.requested_date || payload.meta.trade_date; elements.tradeDate.value = selectedDate; document.querySelector("#qiObservationDate").value = selectedDate; document.querySelector("#journalDate").value = selectedDate; renderDashboard(); setStatus(`${dashboardSourceLabel(payload.meta)} · 数据已更新`); if (!background) { if (state.activeView === "dragonView") loadDragonTiger(); if (state.activeView === "screenerView") loadScreenerSetup(); if (state.activeView === "screenerTrackingView") loadScreenerTracking(true); if (state.activeView === "mentorView") loadMentorSetup(true); if (state.activeView === "heavenView") loadHeavenSetup(true); if (state.activeView === "sentimentCycleView") loadSentimentHistory(true); if (state.activeView === "rotationView") loadRotationHistory(true); if (state.activeView === "auctionView") loadAuctionCenter(true); if (state.activeView === "themeLibraryView") loadThemeLibrary(true); if (state.activeView === "popularityView") loadPopularity(true); } const requestedStock = new URLSearchParams(window.location.search).get("stock"); if (!state.initialStockOpened && /^\d{6}$/.test(requestedStock || "")) { state.initialStockOpened = true; openStock(requestedStock); } } function dashboardSourceLabel(meta = {}) { if (meta.realtime && !["closed", "after_hours"].includes(String(meta.market_status || ""))) return "盘中行情"; if (meta.carried_forward) return "最近收盘行情"; if (meta.market_status === "historical") return "历史行情"; return "收盘行情"; } function renderDashboard() { const { meta, overview, ladders, sectors } = state.dashboard; animateMetric("tapeUp", overview.up_count, (value) => Math.round(value)); animateMetric("tapeDown", overview.down_count, (value) => Math.round(value)); animateMetric("tapeLimit", overview.limit_up_count, (value) => `${Math.round(value)} 家`); animateMetric("tapeLimitDown", overview.limit_down_count, (value) => `${Math.round(value)} 家`); animateMetric("detailBroken", overview.broken_count, (value) => `${Math.round(value)} 家`); animateMetric("detailSealRate", overview.seal_rate, (value) => `${formatNumber(value, 1)}%`); animateMetric("tapeAmount", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`); animateMetric("limitUpMetric", overview.limit_up_count, (value) => `${Math.round(value)} 家`); animateMetric("limitDownMetric", overview.limit_down_count, (value) => `${Math.round(value)} 家`); animateMetric("brokenMetric", overview.broken_count, (value) => `${Math.round(value)} 家`); animateMetric("sealRateMetric", overview.seal_rate, (value) => `${formatNumber(value, 1)}%`); animateMetric("amountMetric", overview.amount_billion, (value) => `${formatNumber(value, 1)} 亿`); setText("dataDateMetric", dashboardDataTimestamp(meta)); animateMetric("sentimentScore", overview.sentiment_score, (value) => Math.round(value)); animateMetric("detailSentimentScore", overview.sentiment_score, (value) => Math.round(value)); const moodLabel = sentimentLabel(overview.sentiment_score); setText("sentimentText", moodLabel); setText("detailSentimentText", moodLabel); document.querySelectorAll("#sentimentText, #detailSentimentText").forEach((sentimentChip) => { sentimentChip.classList.toggle("is-hot", moodLabel === "情绪高涨"); sentimentChip.classList.toggle("is-strong", moodLabel === "情绪偏强"); sentimentChip.classList.toggle("is-cold", moodLabel === "情绪冰点" || moodLabel === "情绪偏弱"); }); const pageSubtitle = document.querySelector("#currentPageSubtitle"); const activePage = window.XiaobaiPages?.get(state.activeView); if (pageSubtitle) { const dateText = displayCompactDate(meta.trade_date); if (activePage?.id === "mentorView") { pageSubtitle.textContent = dateText === "--" ? "与不同交易思维模型持续对话 · 数据日期 --" : `与不同交易思维模型持续对话 · 数据日期 ${dateText}`; } else { const groupLabel = activePage?.group === "market" ? "市场复盘" : activePage?.group === "personal" ? "个人" : "智能工具"; pageSubtitle.textContent = dateText === "--" ? groupLabel : `${groupLabel} · ${dateText}`; } } updateSentimentGauge(overview.sentiment_score); setText("updatedAt", `${dashboardSourceLabel(meta)} · 更新 ${formatTimestamp(meta.updated_at)}`); renderLimitTable(); renderLadderMini(ladders || []); renderSectorMini(sectors || []); renderBrokenTable(state.dashboard.broken || []); renderDownTable(state.dashboard.down_limits || []); renderYesterdayTable(state.dashboard.yesterday_limits || []); renderPerformance(state.dashboard.limit_performance || []); renderLadderBoard(ladders || []); renderRotationMembers(); } function shiftDate(delta) { const current = parseLocalDate(elements.tradeDate.value); current.setDate(current.getDate() + delta); const next = localDateString(current); if (next > todayString()) return; elements.tradeDate.value = next; state.heavenManualData = null; document.querySelector("#qiObservationDate").value = next; loadDashboard(); } function updateDateButtons() { document.querySelector("#nextDate").disabled = elements.tradeDate.value >= todayString(); } function sentimentLabel(score) { const value = number(score); if (value >= 80) return "情绪高涨"; if (value >= 60) return "情绪偏强"; if (value >= 40) return "情绪中性"; if (value >= 20) return "情绪偏弱"; return "情绪冰点"; } function dashboardDataTimestamp(meta = {}) { const tradeDate = displayCompactDate(meta.trade_date); if (tradeDate === "--") return "--"; const intraday = tradeDate === todayString() && Boolean(meta.realtime) && !["closed", "after_hours"].includes(String(meta.market_status || "")); if (intraday) { const updated = new Date(meta.updated_at); if (!Number.isNaN(updated.getTime())) { const dateText = `${updated.getFullYear()}-${String(updated.getMonth() + 1).padStart(2, "0")}-${String(updated.getDate()).padStart(2, "0")}`; const timeText = updated.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }); return `${dateText} ${timeText}`; } } return `${tradeDate} 15:00`; } function updateSentimentGauge(rawScore) { const gauge = document.querySelector("#sentimentGauge"); if (!gauge) return; const score = clamp(rawScore, 0, 100); const previous = Number(gauge.dataset.score); gauge.dataset.score = String(score); gauge.style.setProperty("--score", score); if (!motionEnabled() || !Number.isFinite(previous) || Math.abs(previous - score) < 15) return; gauge.classList.remove("sentiment-pulse"); void gauge.offsetWidth; gauge.classList.add("sentiment-pulse"); gauge.addEventListener("animationend", () => gauge.classList.remove("sentiment-pulse"), { once: true }); } function bindDashboardEvents() { document.querySelector("#refreshButton").addEventListener("click", async (event) => { const button = event.currentTarget; button.disabled = true; try { await loadDashboard(false, false, false); } finally { button.disabled = false; } }); document.querySelector("#syncButton").addEventListener("click", startAdminRefresh); elements.tradeDate.addEventListener("change", () => { state.dashboardRequestSequence += 1; state.heavenRequestSequence += 1; state.heavenManualData = null; document.querySelector("#qiObservationDate").value = elements.tradeDate.value; loadDashboard(); }); document.querySelector("#prevDate").addEventListener("click", () => shiftDate(-1)); document.querySelector("#nextDate").addEventListener("click", () => shiftDate(1)); }