window.XiaobaiPageModules.register("dragon_tiger", ["dragonView"], { bind: bindDragonTigerEvents, enter: ["loadDragonTiger"], }); function selectDragonViewMode(mode) { state.dragonViewMode = mode === "profiles" ? "profiles" : "daily"; document.querySelectorAll("[data-dragon-view-mode]").forEach((button) => { const active = button.dataset.dragonViewMode === state.dragonViewMode; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); if (state.dragonViewMode === "profiles") { document.querySelector("#dragonDailyContent").hidden = true; document.querySelector("#dragonEmptyState").hidden = true; document.querySelector("#dragonProfilesContent").hidden = false; if (state.hotMoneyProfiles) renderHotMoneyProfiles(); else loadHotMoneyProfiles(); } else { document.querySelector("#dragonProfilesContent").hidden = true; if (state.dragonTiger) renderDragonTiger(); else loadDragonTiger(); } } async function loadHotMoneyProfiles(force = false) { if (!force && state.hotMoneyProfiles) { renderHotMoneyProfiles(); return; } setStatus("正在加载游资档案"); try { const query = new URLSearchParams(); if (force) query.set("force", "1"); const suffix = query.size ? `?${query}` : ""; state.hotMoneyProfiles = await apiRequest(`/api/dragon-tiger/profiles${suffix}`); renderHotMoneyProfiles(); const count = number(state.hotMoneyProfiles.summary?.profile_count); setStatus(`游资档案已加载 · 共 ${count} 位`); } catch (error) { showToast(error.message || "游资档案加载失败"); setStatus("游资档案加载失败"); } } function renderHotMoneyProfiles() { const payload = state.hotMoneyProfiles; if (!payload) return; const profiles = payload.profiles || []; const summary = payload.summary || {}; const query = state.hotMoneyProfileQuery; const visible = profiles.filter((profile) => { if (!query) return true; return [profile.name, profile.description, ...(profile.organizations || [])] .join(" ") .toLocaleLowerCase("zh-CN") .includes(query); }); if (!visible.some((profile) => profile.id === state.selectedHotMoneyProfileId)) { state.selectedHotMoneyProfileId = visible[0]?.id || ""; } const selected = visible.find((profile) => profile.id === state.selectedHotMoneyProfileId) || null; setText("dragonDateLabel", `收录 ${number(summary.profile_count)} 位`); setText("hotMoneyProfileResultCount", query ? `${visible.length} / ${profiles.length} 位` : `${profiles.length} 位`); document.querySelector("#hotMoneyProfileSummary").innerHTML = [ ["收录游资", number(summary.profile_count)], ["已有简介", number(summary.described_count)], ["关联席位", number(summary.organization_count)], ].map(([label, value]) => `${label}${value}`).join(""); const list = document.querySelector("#hotMoneyProfileList"); list.innerHTML = visible.length ? visible.map((profile, index) => ` `).join("") : `
${profiles.length ? "没有符合条件的游资档案" : "游资名录暂不可用"}
`; const detail = document.querySelector("#hotMoneyProfileDetail"); if (!selected) { detail.innerHTML = `
${profiles.length ? "选择一位游资查看档案" : "暂无可展示的游资档案"}
`; } else { const organizations = selected.organizations || []; detail.innerHTML = `
${escapeHtml(selected.name.slice(0, 2))}
游资档案

${escapeHtml(selected.name)}

${organizations.length ? `关联 ${organizations.length} 个公开席位` : "暂无关联席位"}

人物简介

${escapeHtml(selected.description || "名录暂未收录该游资的公开简介。")}

关联营业部

${organizations.length} 个
${organizations.length ? organizations.map((organization) => ` ${escapeHtml(organization)} `).join("") : '

名录暂未收录关联营业部。

'}
${payload.meta?.notice ? `

${escapeHtml(payload.meta.notice)}

` : ""}`; } refreshIcons(); } async function loadDragonTiger(force = false) { const requestedDate = elements.tradeDate.value; if ( !force && ["success", "empty", "partial", "unavailable"].includes(state.dragonTiger?.meta?.status) && (state.dragonTiger?.meta?.requested_date || state.dragonTiger?.meta?.trade_date) === requestedDate ) { renderDragonTiger(); return; } setStatus("正在加载龙虎榜"); try { const query = new URLSearchParams({ trade_date: requestedDate }); if (force) query.set("force", "1"); const payload = await apiRequest(`/api/dragon-tiger?${query}`); state.dragonTiger = payload; renderDragonTiger(); const statusLabel = payload.meta.status === "error" ? "龙虎榜数据暂不可用" : payload.meta.status === "empty" ? "当日暂无公开游资明细" : payload.meta.status === "partial" ? "当日有龙虎榜,暂无命名游资明细" : payload.meta.status === "unavailable" ? "龙虎榜数据暂不可用" : "龙虎榜明细"; setStatus(`${statusLabel} · 龙虎榜已加载`); } catch (error) { showToast(error.message || "龙虎榜加载失败"); setStatus("龙虎榜加载失败"); } } function renderDragonTiger() { const payload = state.dragonTiger; if (!payload) return; const summary = payload.summary || {}; if (state.dragonViewMode === "daily") setText("dragonDateLabel", `数据日期 ${payload.meta.trade_date}`); const status = payload.meta?.status || "empty"; const hasRecognizedTraders = (payload.traders || []).some((item) => item.identity_type === "trader" && item.recognized !== false); const showEmptyState = !hasRecognizedTraders && !(payload.unclassified_seats || []).length && ["empty", "error", "unavailable"].includes(status); const dailyVisible = state.dragonViewMode === "daily"; document.querySelector("#dragonProfilesContent").hidden = dailyVisible; document.querySelector("#dragonEmptyState").hidden = !dailyVisible || !showEmptyState; document.querySelector("#dragonDailyContent").hidden = !dailyVisible || showEmptyState; if (showEmptyState) { const unavailable = ["error", "unavailable"].includes(status); setText("dragonEmptyTitle", unavailable ? "龙虎榜数据暂不可用" : `${payload.meta?.trade_date || "该交易日"} 暂无龙虎榜明细`); setText("dragonEmptyDescription", unavailable ? "当前数据暂未完成更新,可稍后重新检查或查看前一交易日。" : "龙虎榜明细通常在交易日盘后陆续披露,可稍后刷新或查看前一交易日。"); } document.querySelector("#dragonSummary").innerHTML = [ ["上榜游资", `${number(summary.trader_count)} 位`, ""], ["操作明细", `${number(summary.operation_count)} 条`, ""], ["席位净买入", formatMoneyMillion(summary.seat_net_buy_million), changeClass(summary.seat_net_buy_million)], ["活跃股票", `${number(summary.active_stock_count)} 只`, ""], ].map(([label, value, className]) => `
${label}${value}
`).join(""); renderDragonTraderList(); renderUnclassifiedSeats(); } function renderDragonTraderList() { const payload = state.dragonTiger; if (!payload) return; let traders = [...(payload.traders || [])].filter((item) => item.identity_type === "trader" && item.recognized !== false); if (state.dragonFilter === "buy") traders = traders.filter((item) => number(item.net_buy_million) > 0); if (state.dragonFilter === "sell") traders = traders.filter((item) => number(item.net_buy_million) < 0); if (state.dragonFilter === "unclassified") traders = []; if (state.dragonQuery) { traders = traders.filter((item) => { const searchable = [ item.name, ...(item.operations || []).flatMap((operation) => [operation.code, operation.name, operation.seat_name]), ].join(" ").toLowerCase(); return searchable.includes(state.dragonQuery); }); } const container = document.querySelector("#dragonTraderList"); let emptyMessage = "没有符合当前条件的游资操作"; if (!Array.isArray(payload.traders)) emptyMessage = "龙虎榜数据格式暂不可用,请稍后重试"; else if (["error", "unavailable"].includes(payload.meta?.status)) emptyMessage = "龙虎榜数据暂不可用,请稍后重试"; else if (payload.meta?.status === "empty") emptyMessage = "该交易日暂无游资每日明细"; else if (payload.meta?.status === "partial") emptyMessage = `当日有 ${number(payload.summary?.official_stock_count)} 只股票上榜,但暂无可识别的游资明细`; if (!traders.some((item) => item.id === state.selectedDragonTraderId)) { state.selectedDragonTraderId = traders[0]?.id || ""; } const cardMarkup = traders.map((trader, index) => { const description = trader.description || `${number(trader.stock_count)} 只股票,${number(trader.operation_count)} 笔操作`; return ` `; }).join(""); const hitZoneMarkup = traders.map((trader) => ` `).join(""); container.innerHTML = traders.length ? `${cardMarkup}
${hitZoneMarkup}
` : emptyStateHtml(state.dragonFilter === "unclassified" ? "待归类席位请在下方管理" : emptyMessage, { className: "dragon-empty" }); container.querySelectorAll("[data-dragon-card]").forEach((card) => { card.addEventListener("animationend", () => card.classList.remove("dealing"), { once: true }); }); container.querySelectorAll("[data-dragon-trader]").forEach((hitZone) => { const setHovered = (hovered) => { container.querySelector(`[data-dragon-card="${CSS.escape(hitZone.dataset.dragonTrader)}"]`)?.classList.toggle("hovered", hovered); }; hitZone.addEventListener("pointerenter", () => setHovered(true)); hitZone.addEventListener("pointerleave", () => setHovered(false)); hitZone.addEventListener("focus", () => setHovered(true)); hitZone.addEventListener("blur", () => setHovered(false)); hitZone.addEventListener("click", () => { state.selectedDragonTraderId = hitZone.dataset.dragonTrader; container.querySelectorAll("[data-dragon-card]").forEach((card) => { card.classList.toggle("selected", card.dataset.dragonCard === state.selectedDragonTraderId); }); container.querySelectorAll("[data-dragon-trader]").forEach((item) => { item.setAttribute("aria-pressed", String(item.dataset.dragonTrader === state.selectedDragonTraderId)); }); renderDragonTraderDetail(traders.find((item) => item.id === state.selectedDragonTraderId)); }); }); requestAnimationFrame(() => layoutDragonCards(container)); renderDragonTraderDetail(traders.find((item) => item.id === state.selectedDragonTraderId)); } function layoutDragonCards(container = document.querySelector("#dragonTraderList")) { if (!container) return; const cards = [...container.querySelectorAll(".dragon-trader-card")]; const hitZones = [...container.querySelectorAll(".dragon-card-hit-zone")]; if (!cards.length) return; const compact = window.innerWidth <= 720; const cardWidth = compact ? 148 : 176; const available = Math.max(cardWidth, container.clientWidth - (compact ? 30 : 72)); const spread = Math.min(available - cardWidth, compact ? 310 : 1050); const step = cards.length > 1 ? Math.min(cardWidth + 14, spread / (cards.length - 1)) : 0; const center = (cards.length - 1) / 2; container.style.setProperty("--dragon-card-width", `${cardWidth}px`); cards.forEach((card, index) => { const x = (index - center) * step; card.style.setProperty("--card-x", `${x.toFixed(2)}px`); card.style.setProperty("--card-rotation", "0deg"); card.style.setProperty("--card-y", "0px"); card.style.zIndex = String(index + 1); const hitZone = hitZones[index]; if (hitZone) { const zoneWidth = index === cards.length - 1 ? cardWidth : Math.max(18, step); hitZone.style.left = `calc(50% + ${(x - cardWidth / 2).toFixed(2)}px)`; hitZone.style.width = `${zoneWidth.toFixed(2)}px`; } }); } function renderDragonTraderDetail(trader) { const container = document.querySelector("#dragonTraderDetail"); if (!trader) { container.hidden = true; renderEmptyState(container, "选择一位游资查看操作明细", { className: "dragon-empty" }); return; } container.hidden = false; container.innerHTML = `
当日操作明细

${escapeHtml(trader.name)}

${escapeHtml(trader.description || "按当日公开龙虎榜席位汇总")}

买入
${formatMoneyMillion(trader.buy_million)}
卖出
${formatMoneyMillion(trader.sell_million)}
净额
${formatMoneyMillion(trader.net_buy_million)}
${(trader.operations || []).map((operation, index) => ` `).join("")}
序号股票方向涨幅(%)买入(百万)卖出(百万)净额(百万)关联席位标签 / 上榜原因
${index + 1} ${escapeHtml(operation.name)}${escapeHtml(operation.code)} ${escapeHtml(operation.direction)} ${operation.change == null ? "" : signed(operation.change)} ${operation.buy_million == null ? "" : formatNumber(operation.buy_million, 2)} ${operation.sell_million == null ? "" : formatNumber(operation.sell_million, 2)} ${operation.net_buy_million == null ? "" : signed(operation.net_buy_million)} ${escapeHtml(operation.seat_name)} ${escapeHtml(operation.tag && operation.tag !== "--" ? operation.tag : operation.reason && operation.reason !== "--" ? operation.reason : "")}
`; bindStockRows(container); markAutoSortableHeaders(container); } function renderUnclassifiedSeats() { const seats = state.dragonTiger?.unclassified_seats || []; const canManage = state.user?.role === "admin"; document.querySelector("#dragonUnclassifiedSection").hidden = !canManage || seats.length === 0; document.querySelector("#dragonUnclassifiedFilter").hidden = !canManage || seats.length === 0; if (!seats.length && state.dragonFilter === "unclassified") { state.dragonFilter = "all"; document.querySelectorAll("[data-dragon-filter]").forEach((button) => { button.classList.toggle("active", button.dataset.dragonFilter === "all"); }); renderDragonTraderList(); } setText("unclassifiedCount", `${seats.length} 个`); const list = document.querySelector("#unclassifiedSeatList"); list.innerHTML = seats.map((seat, index) => `
${escapeHtml(seat.seat_name)} ${number(seat.operation_count)} 笔 · ${number(seat.stock_count)} 股 ${formatMoneyMillion(seat.net_buy_million)}
`).join("") || emptyStateHtml("当前席位均已归类"); list.querySelectorAll(".unclassified-seat-row").forEach((form) => { form.addEventListener("submit", saveSeatAlias); }); } function dragonIdentityLabel(type) { return { trader: "游资", institution: "机构", channel: "通道", unclassified: "待归类" }[type] || "席位"; } async function saveSeatAlias(event) { event.preventDefault(); const form = event.currentTarget; const seat = state.dragonTiger?.unclassified_seats?.[number(form.dataset.unclassifiedIndex)]; const alias = form.querySelector("input").value.trim(); if (!seat || !alias) { showToast("请输入游资名"); return; } const button = form.querySelector("button"); button.disabled = true; try { await apiRequest("/api/seat-aliases", "POST", { seat_name: seat.seat_name, alias }); state.dragonTiger = null; await loadDragonTiger(); showToast(`已将席位归类为 ${alias}`); } catch (error) { showToast(error.message); button.disabled = false; } } function bindDragonTigerEvents() { document.querySelector("#dragonRefreshButton").addEventListener("click", () => { if (state.dragonViewMode === "profiles") loadHotMoneyProfiles(true); else loadDragonTiger(true); }); document.querySelector("#dragonEmptyRefreshButton").addEventListener("click", () => loadDragonTiger(true)); document.querySelector("#dragonPreviousButton").addEventListener("click", () => shiftDate(-1)); document.querySelector("#dragonExportButton").addEventListener("click", () => { if (state.dragonViewMode === "profiles") exportHotMoneyProfiles(); else exportDragonTiger(); }); document.querySelectorAll("[data-dragon-view-mode]").forEach((button) => { button.addEventListener("click", () => selectDragonViewMode(button.dataset.dragonViewMode)); }); document.querySelector("#dragonSearch").addEventListener("input", (event) => { state.dragonQuery = event.target.value.trim().toLowerCase(); renderDragonTraderList(); }); document.querySelectorAll("[data-dragon-filter]").forEach((button) => { button.addEventListener("click", () => { state.dragonFilter = button.dataset.dragonFilter; document.querySelectorAll("[data-dragon-filter]").forEach((item) => { item.classList.toggle("active", item === button); }); renderDragonTraderList(); }); }); document.querySelector("#hotMoneyProfileSearch").addEventListener("input", (event) => { state.hotMoneyProfileQuery = event.target.value.trim().toLocaleLowerCase("zh-CN"); renderHotMoneyProfiles(); }); document.querySelector("#hotMoneyProfileList").addEventListener("click", (event) => { const button = event.target.closest("[data-hot-money-profile]"); if (!button) return; state.selectedHotMoneyProfileId = button.dataset.hotMoneyProfile; renderHotMoneyProfiles(); }); window.addEventListener("resize", () => { if (state.activeView === "dragonView") layoutDragonCards(); }); }