function meaningfulNumber(value) { return value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value)); } async function openStock(code, fallback = null) { closeStockPreview(); const pools = [state.dashboard?.limits || [], state.dashboard?.broken || [], state.dashboard?.down_limits || []]; const row = pools.flat().find((item) => String(item.code) === String(code)) || fallback || { code, name: "--", sector: "其他" }; state.activeStock = row; state.stockDetail = null; state.stockDetailIntraday = null; state.stockDetailChartMode = "daily"; const requestSequence = ++state.stockDetailRequestSequence; syncDetailChartButtons("stock", "daily"); setText("detailCode", row.code); setText("detailName", row.name); setText("detailPrice", formatNumber(row.price, 2)); setText("detailChange", `${signed(row.change)}%`); const changeElement = document.querySelector("#detailChange"); changeElement.className = changeClass(row.change); setText("detailStreak", row.status === "涨停" ? streakLabel(row.streak) : row.status || "--"); setText("detailReason", row.reason || "--"); setText("detailSector", row.sector || "其他"); setText("detailFirst", row.first_time || "--"); setText("detailLast", row.last_time || "--"); setText("detailOpen", `${number(row.open_times)} 次`); setText("detailTurnover", `${formatNumber(row.turnover_rate, 2)}%`); setText("detailAmount", `${formatNumber(row.amount_billion, 2)} 亿`); setText("detailSeal", `${formatNumber(row.seal_amount_million, 0)} 万`); setText("chartSource", "正在加载行情"); setText("flowNet", "--"); setText("flowLarge", "--"); setText("flowMedium", "--"); setText("flowSmall", "--"); document.querySelector("#reasonInput").value = row.reason || ""; document.querySelector("#stockNoteContent").value = ""; document.querySelector("#stockNotePlan").value = ""; renderEmptyState("stockNotes", "正在加载笔记"); updateWatchButton(); openModalDialog(elements.stockDialog); clearPriceChart("正在加载日 K 数据"); try { const query = new URLSearchParams({ trade_date: elements.tradeDate.value }); const payload = await apiRequest(`/api/stock/${encodeURIComponent(code)}?${query}`); if (requestSequence !== state.stockDetailRequestSequence) return; state.stockDetail = payload; const stock = payload.stock || {}; state.activeStock = { ...row, name: stock.name || row.name, sector: stock.industry || row.sector }; setText("detailName", stock.name || row.name); setText("detailPrice", formatNumber(stock.price || row.price, 2)); setText("detailChange", `${signed(stock.change ?? row.change)}%`); renderMoneyflow(payload.moneyflow || {}); renderStockNotes(payload.notes || []); updateWatchButton(); if (state.stockDetailChartMode === "daily") { setText("chartSource", `日 K 行情 · ${payload.prices.length} 个交易日`); requestAnimationFrame(() => drawPriceChart(payload.prices || [])); } } catch (error) { if (requestSequence !== state.stockDetailRequestSequence) return; setText("chartSource", "行情加载失败"); if (state.stockDetailChartMode === "daily") clearPriceChart(error.message || "行情加载失败"); showToast(error.message || "个股详情加载失败"); } } async function selectStockDetailChart(mode) { const selected = mode === "intraday" ? "intraday" : "daily"; state.stockDetailChartMode = selected; syncDetailChartButtons("stock", selected); if (selected === "daily") { const prices = state.stockDetail?.prices || []; setText("chartSource", prices.length ? `日 K 行情 · ${prices.length} 个交易日` : "正在加载行情"); if (prices.length) requestAnimationFrame(() => drawPriceChart(prices)); else clearPriceChart("正在加载日 K 数据"); return; } if (state.stockDetailIntraday) { renderStockDetailIntraday(state.stockDetailIntraday); return; } const code = String(state.activeStock?.code || ""); if (!/^\d{6}$/.test(code)) return; const requestSequence = state.stockDetailRequestSequence; setText("chartSource", "正在加载分时"); clearPriceChart("正在加载分时数据"); try { const params = new URLSearchParams({ type: "stock", id: code }); const payload = await apiRequest(`/api/chart/intraday?${params}`); if (requestSequence !== state.stockDetailRequestSequence) return; state.stockDetailIntraday = payload; if (state.stockDetailChartMode === "intraday") renderStockDetailIntraday(payload); } catch (error) { if (requestSequence !== state.stockDetailRequestSequence || state.stockDetailChartMode !== "intraday") return; setText("chartSource", "分时暂不可用"); clearPriceChart(error.message || "分时行情暂不可用"); } } function renderStockDetailIntraday(payload) { const points = payload.points || []; if (!points.length) { setText("chartSource", "分时暂不可用"); clearPriceChart("分时行情暂不可用"); return; } setText("chartSource", `分时 · ${payload.meta?.trade_date || "--"}`); requestAnimationFrame(() => { if (state.stockDetailChartMode !== "intraday") return; drawIntradayCanvas(elements.priceChart, points, [], payload.meta?.previous_close); }); } function openActiveStockInHeaven() { const code = state.activeStock?.code; if (!/^\d{6}$/.test(String(code || ""))) return; elements.stockDialog.close(); state.heavenPanel = "trend"; state.heavenManualData = null; const input = document.querySelector("#heavenStockInput"); input.value = code; openView("heavenView"); selectHeavenPanel("trend", true); }