110 lines
9.7 KiB
JavaScript
110 lines
9.7 KiB
JavaScript
window.XiaobaiPageModules.register("ladder", ["ladderView"], { bind: bindLadderEvents });
|
||
|
||
function renderLadderMini(ladders) {
|
||
const container = document.querySelector("#ladderMini");
|
||
const highest = ladders.length ? Math.max(...ladders.map((item) => number(item.level))) : 0;
|
||
setText("maxHeight", highest ? `最高 ${highest} 板` : "暂无");
|
||
container.innerHTML = ladders.slice(0, 5).map((group) => {
|
||
const allNames = group.stocks.map((stock) => stock.name).filter(Boolean);
|
||
const visibleNames = allNames.slice(0, 3).join("、");
|
||
const suffix = allNames.length > 3 ? ` <em>等 ${number(group.count)} 只</em>` : "";
|
||
return `<div class="pool-side-group">
|
||
<div><strong>${escapeHtml(group.label)}</strong><small>${number(group.count)} 只</small></div>
|
||
<p title="${escapeHtml(allNames.join("、"))}">${escapeHtml(visibleNames || "--")}${suffix}</p>
|
||
</div>`;
|
||
}).join("") || emptyStateHtml("暂无梯队数据");
|
||
}
|
||
|
||
function renderSectorMini(sectors) {
|
||
document.querySelector("#sectorMini").innerHTML = sectors.slice(0, 7).map((sector) => `
|
||
<div class="pool-hot-row"><strong title="${escapeHtml(sector.name)}">${escapeHtml(sector.name)}</strong><span>${number(sector.count)}</span></div>
|
||
`).join("") || emptyStateHtml("暂无板块数据");
|
||
}
|
||
|
||
function renderLadderBoard(ladders) {
|
||
const container = document.querySelector("#ladderBoard");
|
||
const insights = document.querySelector("#ladderInsights");
|
||
const ordered = [...ladders].sort((left, right) => number(right.level) - number(left.level));
|
||
const maxLevel = ordered.length ? Math.max(...ordered.map((group) => number(group.level))) : 0;
|
||
const topVisibleLevel = Math.max(5, maxLevel);
|
||
const groupMap = new Map(ordered.map((group) => [number(group.level), group]));
|
||
const displayGroups = Array.from({ length: topVisibleLevel }, (_, index) => {
|
||
const level = topVisibleLevel - index;
|
||
return groupMap.get(level) || { level, label: level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}板`, count: 0, stocks: [] };
|
||
});
|
||
const total = ordered.reduce((sum, group) => sum + number(group.count), 0);
|
||
const spaceStocks = ordered.find((group) => number(group.level) === maxLevel)?.stocks || [];
|
||
const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
|
||
const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
|
||
setText("ladderDateRange", `数据日期 ${currentDate}`);
|
||
container.innerHTML = displayGroups.map((group) => {
|
||
const level = number(group.level);
|
||
const limit = level === 1 || level === 2 ? 8 : 99;
|
||
const expanded = state.expandedLadderLevels.has(level);
|
||
const groupStocks = [...(group.stocks || [])].sort((left, right) => {
|
||
if (state.ladderSortMode === "open") {
|
||
return number(left.open_times) - number(right.open_times)
|
||
|| String(left.first_time || "99:99:99").localeCompare(String(right.first_time || "99:99:99"));
|
||
}
|
||
return String(left.first_time || "99:99:99").localeCompare(String(right.first_time || "99:99:99"));
|
||
});
|
||
const stocks = expanded ? groupStocks : groupStocks.slice(0, limit);
|
||
const remaining = Math.max(0, groupStocks.length - stocks.length);
|
||
const label = group.label || (level === 1 ? "首板" : level === 5 && maxLevel < 5 ? "5板+" : `${level}板`);
|
||
const color = { 1: "#2563eb", 2: "#16a34a", 3: "#d97706", 4: "#e04536" }[level] || "#9ca3af";
|
||
return `
|
||
<section class="market-ladder-tier ${number(group.count) ? "" : "is-gap"}" data-ladder-level-card="${level}">
|
||
<div class="market-ladder-label" style="--tier-color:${color}"><div class="market-ladder-level"><span class="market-ladder-dot"></span>${escapeHtml(label)}</div><div class="market-ladder-count">${number(group.count)} 只</div>${number(group.count) && level > 1 ? `<div class="market-ladder-rate">${escapeHtml(label)} · <b>${formatNumber(number(group.count) / Math.max(number(groupMap.get(level - 1)?.count), 1) * 100, 1)}%</b></div>` : ""}</div>
|
||
<div class="market-ladder-stocks">${stocks.length ? stocks.map((stock) => {
|
||
const onePrice = String(stock.first_time || "").startsWith("09:25") && number(stock.open_times) === 0;
|
||
const broken = number(stock.open_times) >= 6;
|
||
const amount = number(stock.seal_amount_million) ? `封单 ${formatNumber(stock.seal_amount_million, 0)} 万` : `成交 ${formatNumber(stock.amount_billion, 1)} 亿`;
|
||
return `<button type="button" class="market-ladder-stock" data-code="${escapeHtml(stock.code)}" aria-label="查看 ${escapeHtml(stock.name)} ${escapeHtml(stock.code)}详情">
|
||
<span class="market-ladder-stock-first"><strong>${escapeHtml(stock.name)}</strong><small class="stock-code">${escapeHtml(stock.code)}</small><span class="market-ladder-tags">${onePrice ? '<em class="market-ladder-tag one-price">一字</em>' : ""}${broken ? `<em class="market-ladder-tag broken">烂板×${number(stock.open_times)}</em>` : ""}</span></span>
|
||
<span class="market-ladder-stock-second"><b>${escapeHtml(stock.sector || stock.reason || "其他")}</b><small>${stock.first_time && stock.first_time !== "--" ? escapeHtml(stock.first_time) : "时间待校正"}</small><small>${amount}</small></span>
|
||
</button>`;
|
||
}).join("") : `<div class="market-ladder-gap-note">${level >= maxLevel ? `断层 · ${escapeHtml(label)}及以上空缺` : "该层暂时空缺"}</div>`}${groupStocks.length > limit ? `<button class="market-ladder-more" type="button" data-ladder-level="${level}">${expanded ? "收起" : `展开剩余 ${remaining} 只`}<i data-lucide="chevron-${expanded ? "up" : "down"}"></i></button>` : ""}</div>
|
||
</section>`;
|
||
}).join("");
|
||
const structureRows = displayGroups.filter((group) => number(group.count) || number(group.level) <= maxLevel + 1);
|
||
const maxCount = Math.max(1, ...structureRows.map((group) => number(group.count)));
|
||
const rateRows = (state.dashboard?.limit_performance || []).map((row) => ({
|
||
label: `${row.label || (number(row.level) === 1 ? "昨日首板" : `昨日${number(row.level)}板`)} → 今日`,
|
||
value: clamp(number(row.advance_rate), 0, 100),
|
||
}));
|
||
const previousMax = Math.max(0, ...(state.dashboard?.yesterday_limits || []).map((row) => number(row.prior_streak)));
|
||
const spaceChange = previousMax && maxLevel < previousMax ? `较昨日 ${previousMax} 板 ↓ 空间压缩` : previousMax && maxLevel > previousMax ? `较昨日 ${previousMax} 板 ↑ 高度抬升` : "高度与昨日接近";
|
||
const spaceNote = maxLevel >= 5 ? "高位梯队仍有辨识度,重点观察承接而非单看高度。" : maxLevel >= 3 ? "空间位于中段,梯队延续性比绝对高度更重要。" : "高度受到压缩,先观察首板向二板的结构修复。";
|
||
const strongestGroup = structureRows.reduce((best, group) => number(group.count) > number(best?.count) ? group : best, structureRows[0]);
|
||
insights.innerHTML = `
|
||
<section class="market-ladder-insight-card market-ladder-apex-card"><header><h3>空间板</h3><span>市场高度</span></header><div class="market-ladder-apex"><div><strong>${maxLevel ? `${maxLevel} 板` : "--"}</strong><em>${escapeHtml(spaceChange)}</em></div><p>${spaceStocks.length ? spaceStocks.map((stock) => `<b>${escapeHtml(stock.name)}</b>(${escapeHtml(stock.sector || "其他")})`).join(" · ") : "暂无空间板"}</p></div><p>${spaceNote}</p></section>
|
||
<section class="market-ladder-insight-card"><header><h3>梯队结构</h3><span>完整度</span></header><div class="market-ladder-pyramid">${structureRows.map((group) => `<div class="market-ladder-pyramid-row ${number(group.count) ? "" : "is-gap"}"><span>${escapeHtml(group.label || `${number(group.level)}板`)}</span><i><b style="width:${Math.max(number(group.count) ? 8 : 100, number(group.count) / maxCount * 100)}%"></b></i><strong>${number(group.count) ? `${number(group.count)} 只` : "断层"}</strong></div>`).join("")}</div><p>断层越少,梯队从低位向高位传导越连贯。当前腰部为 <b>${escapeHtml(strongestGroup?.label || "--")}</b>。</p></section>
|
||
<section class="market-ladder-insight-card"><header><h3>晋级率参考</h3><span>昨日梯队 → 今日</span></header><div class="market-ladder-rate-list">${rateRows.length ? rateRows.map((row) => `<div><span>${escapeHtml(row.label)}</span><i><b class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}" style="width:${Math.max(row.value, row.value > 0 ? 2 : 0)}%"></b></i><strong class="${row.value === 0 ? "is-zero" : row.value < 20 ? "is-low" : ""}">${formatNumber(row.value, 1)}%</strong></div>`).join("") : '<div class="empty-state">暂无可比梯队</div>'}</div><small class="market-ladder-source">数据来自“涨停表现”页 · 昨日梯队样本</small></section>`;
|
||
container.querySelectorAll("[data-ladder-level]").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
const level = number(button.dataset.ladderLevel);
|
||
if (state.expandedLadderLevels.has(level)) state.expandedLadderLevels.delete(level);
|
||
else state.expandedLadderLevels.add(level);
|
||
renderLadderBoard(state.dashboard?.ladders || []);
|
||
});
|
||
});
|
||
bindStockRows(container);
|
||
refreshIcons();
|
||
}
|
||
|
||
|
||
function bindLadderEvents() {
|
||
document.querySelectorAll("[data-ladder-sort]").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
state.ladderSortMode = button.dataset.ladderSort === "open" ? "open" : "time";
|
||
document.querySelectorAll("[data-ladder-sort]").forEach((item) => {
|
||
const active = item === button;
|
||
item.classList.toggle("active", active);
|
||
item.setAttribute("aria-pressed", String(active));
|
||
});
|
||
renderLadderBoard(state.dashboard?.ladders || []);
|
||
});
|
||
});
|
||
document.querySelector("#ladderExportButton").addEventListener("click", exportLadder);
|
||
}
|