Keep 1024-1439 broken/seal metrics in the detail panel, fold desktop admin actions into the ellipsis menu, and fit the 13-column limit pool at 1600 without hiding the data date. Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
498 lines
25 KiB
JavaScript
498 lines
25 KiB
JavaScript
window.XiaobaiPageModules.register("pools", [
|
||
"limitPool",
|
||
"brokenView",
|
||
"downView",
|
||
"yesterdayView",
|
||
"performanceView",
|
||
], { bind: bindPoolEvents });
|
||
|
||
function getVisibleStocks() {
|
||
if (!state.dashboard) return [];
|
||
let rows = [...(state.dashboard.limits || [])];
|
||
if (state.filter === "1") rows = rows.filter((row) => number(row.streak) === 1);
|
||
if (state.filter === "2") rows = rows.filter((row) => number(row.streak) === 2);
|
||
if (state.filter === "3") rows = rows.filter((row) => number(row.streak) >= 3);
|
||
if (state.query) {
|
||
rows = rows.filter((row) => {
|
||
const haystack = `${row.code} ${row.name} ${row.sector} ${row.reason}`.toLowerCase();
|
||
return haystack.includes(state.query);
|
||
});
|
||
}
|
||
return rows.sort((left, right) => compareRows(left, right));
|
||
}
|
||
|
||
function renderLimitTable() {
|
||
if (!state.dashboard) return;
|
||
const rows = getVisibleStocks();
|
||
const allRows = state.dashboard.limits || [];
|
||
const body = document.querySelector("#limitTableBody");
|
||
body.innerHTML = rows.map((row, index) => `
|
||
<tr data-code="${escapeHtml(row.code)}">
|
||
<td class="row-number muted">${index + 1}</td>
|
||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
||
<td class="number num"><span class="streak-pill${number(row.streak) >= 4 ? " high" : ""}">${streakLabel(row.streak)}</span></td>
|
||
<td class="number num up">${signed(row.change)}</td>
|
||
<td class="number num">${formatNumber(row.price, 2)}</td>
|
||
<td>${escapeHtml(row.sector || "其他")}</td>
|
||
<td class="number num muted">${escapeHtml(row.first_time || "")}</td>
|
||
<td class="number num muted">${escapeHtml(row.last_time || "")}</td>
|
||
<td class="number num">${limitOpenState(row)}</td>
|
||
<td class="number num">${formatNumber(row.turnover_rate, 2)}</td>
|
||
<td class="number num">${formatNumber(row.amount_billion, 2)}</td>
|
||
<td class="number num">${formatLimitSealAmount(row.seal_amount_million)}</td>
|
||
<td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
|
||
</tr>
|
||
`).join("");
|
||
bindStockRows(body);
|
||
setText("resultCount", `${rows.length} 只`);
|
||
setText("limitPoolSubtitle", `${allRows.length} 只 · 数据日期 ${displayCompactDate(state.dashboard.meta?.trade_date || elements.tradeDate.value)}`);
|
||
setText("limitAllCount", allRows.length);
|
||
setText("limitFirstCount", allRows.filter((row) => number(row.streak) === 1).length);
|
||
setText("limitSecondCount", allRows.filter((row) => number(row.streak) === 2).length);
|
||
setText("limitThreePlusCount", allRows.filter((row) => number(row.streak) >= 3).length);
|
||
document.querySelector("#emptyState").hidden = rows.length !== 0;
|
||
updateSortHeaders();
|
||
}
|
||
|
||
function limitOpenState(row) {
|
||
const openTimes = number(row.open_times);
|
||
const firstTime = String(row.first_time || "");
|
||
if (firstTime.startsWith("09:25") && openTimes === 0) return '<span class="pool-state-tag one-word">一字</span>';
|
||
if (openTimes >= 6) return `<span class="pool-state-tag broken">烂板×${openTimes}</span>`;
|
||
return String(openTimes);
|
||
}
|
||
|
||
function formatLimitSealAmount(value) {
|
||
const amount = number(value);
|
||
if (!amount) return "";
|
||
return Math.round(amount).toLocaleString("zh-CN");
|
||
}
|
||
|
||
function renderBrokenTable(rows) {
|
||
const visibleRows = getVisibleBrokenRows(rows);
|
||
setText("brokenCount", `${rows.length} 只`);
|
||
setText("brokenMeta", ` · 触及涨停后未能封住 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`);
|
||
const body = document.querySelector("#brokenTableBody");
|
||
body.innerHTML = visibleRows.map((row, index) => `
|
||
<tr data-code="${escapeHtml(row.code)}">
|
||
<td class="row-number muted">${index + 1}</td>
|
||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
||
<td class="number num ${changeClass(row.change)}" data-sort-value="${number(row.change)}">${signed(row.change)}</td>
|
||
<td class="number num broken-limit-gap" data-sort-value="${row.limitGap}">${formatNumber(row.limitGap, 2)}</td>
|
||
<td class="number num">${formatNumber(row.price, 2)}</td>
|
||
<td>${escapeHtml(row.sector || "其他")}</td>
|
||
<td class="number num muted">${escapeHtml(row.first_time || "")}</td>
|
||
<td class="number num" data-sort-value="${number(row.open_times)}">${brokenOpenState(row)}</td>
|
||
<td class="number num">${formatNumber(row.turnover_rate, 2)}</td>
|
||
<td class="number num">${formatNumber(row.amount_billion, 2)}</td>
|
||
<td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
|
||
</tr>
|
||
`).join("");
|
||
bindStockRows(body);
|
||
document.querySelector("#brokenEmptyState").hidden = visibleRows.length !== 0;
|
||
updateBrokenSortHeaders();
|
||
}
|
||
|
||
function getVisibleBrokenRows(rows = state.dashboard?.broken || []) {
|
||
let visibleRows = rows.map((row) => ({ ...row, limitGap: brokenLimitGap(row) }));
|
||
if (state.brokenQuery) {
|
||
visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.brokenQuery));
|
||
}
|
||
if (!state.brokenSortKey) return visibleRows;
|
||
return visibleRows.sort((left, right) => {
|
||
const result = number(left[state.brokenSortKey]) - number(right[state.brokenSortKey]);
|
||
return state.brokenSortDirection === "asc" ? result : -result;
|
||
});
|
||
}
|
||
|
||
function brokenLimitRate(row) {
|
||
const name = String(row.name || "").toUpperCase();
|
||
const code = String(row.code || "").replace(/\D/g, "");
|
||
if (name.includes("ST")) return 10;
|
||
if (/^(300|301|688|689)/.test(code)) return 20;
|
||
if (/^(4|8|92)/.test(code)) return 30;
|
||
return 10;
|
||
}
|
||
|
||
function brokenLimitGap(row) {
|
||
return Math.max(0, brokenLimitRate(row) - number(row.change));
|
||
}
|
||
|
||
function brokenOpenState(row) {
|
||
const openTimes = number(row.open_times);
|
||
return openTimes >= 6
|
||
? `<span class="broken-repeat-tag">反复炸 ×${openTimes}</span>`
|
||
: String(openTimes);
|
||
}
|
||
|
||
function changeBrokenSort(key) {
|
||
if (state.brokenSortKey === key) state.brokenSortDirection = state.brokenSortDirection === "asc" ? "desc" : "asc";
|
||
else {
|
||
state.brokenSortKey = key;
|
||
state.brokenSortDirection = "desc";
|
||
}
|
||
renderBrokenTable(state.dashboard?.broken || []);
|
||
}
|
||
|
||
function updateBrokenSortHeaders() {
|
||
document.querySelectorAll("#brokenTable th[data-broken-sort]").forEach((header) => {
|
||
header.classList.remove("sort-asc", "sort-desc", "sorted");
|
||
header.setAttribute("aria-sort", "none");
|
||
if (header.dataset.brokenSort === state.brokenSortKey) {
|
||
header.classList.add(state.brokenSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
|
||
header.setAttribute("aria-sort", state.brokenSortDirection === "asc" ? "ascending" : "descending");
|
||
}
|
||
const arrow = header.querySelector(".arr");
|
||
if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.brokenSortDirection === "asc" ? "▲" : "▼") : "↕";
|
||
});
|
||
}
|
||
|
||
function renderDownTable(rows) {
|
||
const visibleRows = getVisibleDownRows(rows);
|
||
setText("downCount", `${rows.length} 只`);
|
||
setText("downMeta", ` · 观察退潮、高位风险与亏钱效应 · 数据日期 ${displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value)}`);
|
||
renderDownSectorCluster(rows);
|
||
const body = document.querySelector("#downTableBody");
|
||
body.innerHTML = visibleRows.map((row, index) => `
|
||
<tr data-code="${escapeHtml(row.code)}">
|
||
<td class="row-number muted">${index + 1}</td>
|
||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
||
<td class="number num down" data-sort-value="${number(row.change)}">${signed(row.change)}</td>
|
||
<td class="number num">${formatNumber(row.price, 2)}</td>
|
||
<td>${escapeHtml(row.sector || "其他")}</td>
|
||
<td class="number num">${formatNumber(row.turnover_rate, 2)}</td>
|
||
<td class="number num">${formatNumber(row.amount_billion, 2)}</td>
|
||
<td class="number num">${number(row.streak) > 0 ? number(row.streak) : ""}</td>
|
||
<td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
|
||
</tr>
|
||
`).join("");
|
||
bindStockRows(body);
|
||
document.querySelector("#downEmptyState").hidden = visibleRows.length !== 0;
|
||
updateDownSortHeaders();
|
||
}
|
||
|
||
function getVisibleDownRows(rows = state.dashboard?.down_limits || []) {
|
||
let visibleRows = [...rows];
|
||
if (state.downQuery) {
|
||
visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.downQuery));
|
||
}
|
||
if (!state.downSortKey) return visibleRows;
|
||
return visibleRows.sort((left, right) => {
|
||
const result = number(left[state.downSortKey]) - number(right[state.downSortKey]);
|
||
return state.downSortDirection === "asc" ? result : -result;
|
||
});
|
||
}
|
||
|
||
function renderDownSectorCluster(rows) {
|
||
const counts = new Map();
|
||
rows.forEach((row) => {
|
||
const sector = String(row.sector || "其他").trim() || "其他";
|
||
if (sector === "其他") return;
|
||
counts.set(sector, (counts.get(sector) || 0) + 1);
|
||
});
|
||
const cluster = [...counts.entries()].sort((left, right) => right[1] - left[1])[0];
|
||
const element = document.querySelector("#downSectorCluster");
|
||
element.hidden = !cluster || cluster[1] < 2;
|
||
element.textContent = cluster && cluster[1] >= 2 ? `${cluster[0]}集中跌停 ×${cluster[1]}` : "";
|
||
}
|
||
|
||
function changeDownSort(key) {
|
||
if (state.downSortKey === key) state.downSortDirection = state.downSortDirection === "asc" ? "desc" : "asc";
|
||
else {
|
||
state.downSortKey = key;
|
||
state.downSortDirection = "asc";
|
||
}
|
||
renderDownTable(state.dashboard?.down_limits || []);
|
||
}
|
||
|
||
function updateDownSortHeaders() {
|
||
document.querySelectorAll("#downTable th[data-down-sort]").forEach((header) => {
|
||
header.classList.remove("sort-asc", "sort-desc", "sorted");
|
||
header.setAttribute("aria-sort", "none");
|
||
if (header.dataset.downSort === state.downSortKey) {
|
||
header.classList.add(state.downSortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
|
||
header.setAttribute("aria-sort", state.downSortDirection === "asc" ? "ascending" : "descending");
|
||
}
|
||
const arrow = header.querySelector(".arr");
|
||
if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.downSortDirection === "asc" ? "▲" : "▼") : "↕";
|
||
});
|
||
}
|
||
|
||
function renderYesterdayTable(rows) {
|
||
const visibleRows = getVisibleYesterdayRows(rows);
|
||
const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
|
||
const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
|
||
setText("yesterdayCount", `${rows.length} 只`);
|
||
setText("yesterdayMeta", ` · 昨日 ${previousDate} → 今日 ${currentDate}`);
|
||
renderYesterdaySummary(rows);
|
||
const body = document.querySelector("#yesterdayTableBody");
|
||
body.innerHTML = visibleRows.map((row, index) => `
|
||
<tr data-code="${escapeHtml(row.code)}">
|
||
<td class="row-number muted">${index + 1}</td>
|
||
<td><span class="stock-cell"><strong class="stock-name sname">${escapeHtml(row.name)}</strong><small class="stock-code scode">${escapeHtml(row.code)}</small></span></td>
|
||
<td class="number num">${number(row.prior_streak)}</td>
|
||
<td class="number num ${changeClass(row.current_change)}">${signed(row.current_change)}</td>
|
||
<td><span class="yesterday-outcome-tag ${yesterdayOutcomeClass(row.outcome)}">${escapeHtml(row.outcome)}</span></td>
|
||
<td class="number num">${number(row.current_streak) ? `<span class="yesterday-height-tag">${number(row.current_streak)}</span>` : ""}</td>
|
||
<td>${escapeHtml(row.sector || "其他")}</td>
|
||
<td class="pool-reason-cell" title="${escapeHtml(row.reason || "")}">${escapeHtml(row.reason || "")}</td>
|
||
</tr>
|
||
`).join("");
|
||
bindStockRows(body);
|
||
document.querySelector("#yesterdayEmptyState").hidden = visibleRows.length !== 0;
|
||
updateYesterdayControls();
|
||
}
|
||
|
||
function getVisibleYesterdayRows(rows = state.dashboard?.yesterday_limits || []) {
|
||
let visibleRows = rows.filter((row) => {
|
||
if (state.yesterdayFilter === "advance") return row.outcome === "晋级";
|
||
if (state.yesterdayFilter === "positive") return number(row.current_change) > 0;
|
||
if (state.yesterdayFilter === "fail") return row.outcome === "断板";
|
||
if (state.yesterdayFilter === "risk") return ["炸板", "跌停"].includes(row.outcome);
|
||
return true;
|
||
});
|
||
if (state.yesterdayQuery) {
|
||
visibleRows = visibleRows.filter((row) => `${row.code} ${row.name} ${row.sector}`.toLowerCase().includes(state.yesterdayQuery));
|
||
}
|
||
if (!state.yesterdaySortKey) return visibleRows;
|
||
return visibleRows.sort((left, right) => {
|
||
const result = number(left[state.yesterdaySortKey]) - number(right[state.yesterdaySortKey]);
|
||
return state.yesterdaySortDirection === "asc" ? result : -result;
|
||
});
|
||
}
|
||
|
||
function renderYesterdaySummary(rows) {
|
||
const total = rows.length;
|
||
const advance = rows.filter((row) => row.outcome === "晋级").length;
|
||
const positive = rows.filter((row) => number(row.current_change) > 0).length;
|
||
const fail = rows.filter((row) => row.outcome === "断板").length;
|
||
const risk = rows.filter((row) => ["炸板", "跌停"].includes(row.outcome)).length;
|
||
const rate = (value) => total ? value / total * 100 : 0;
|
||
setText("yesterdayAllCount", total);
|
||
setText("yesterdayAdvanceCount", advance);
|
||
setText("yesterdayAdvanceRate", `晋级率 ${formatNumber(rate(advance), 1)}%`);
|
||
setText("yesterdayPositiveCount", positive);
|
||
setText("yesterdayPositiveRate", `兑现率 ${formatNumber(rate(positive), 1)}%`);
|
||
setText("yesterdayFailCount", fail);
|
||
setText("yesterdayFailRate", `占 ${formatNumber(rate(fail), 1)}%`);
|
||
setText("yesterdayRiskCount", risk);
|
||
setText("yesterdayRiskRate", `亏钱效应 ${formatNumber(rate(risk), 1)}%`);
|
||
}
|
||
|
||
function yesterdayOutcomeClass(outcome) {
|
||
return { "晋级": "advance", "断板": "fail", "炸板": "broken", "跌停": "down" }[outcome] || "fail";
|
||
}
|
||
|
||
function changeYesterdaySort(key) {
|
||
if (state.yesterdaySortKey === key) state.yesterdaySortDirection = state.yesterdaySortDirection === "asc" ? "desc" : "asc";
|
||
else {
|
||
state.yesterdaySortKey = key;
|
||
state.yesterdaySortDirection = "desc";
|
||
}
|
||
renderYesterdayTable(state.dashboard?.yesterday_limits || []);
|
||
}
|
||
|
||
function updateYesterdayControls() {
|
||
document.querySelectorAll("[data-yesterday-filter]").forEach((button) => {
|
||
const active = button.dataset.yesterdayFilter === state.yesterdayFilter;
|
||
button.classList.toggle("active", active);
|
||
button.setAttribute("aria-pressed", String(active));
|
||
});
|
||
document.querySelectorAll("#yesterdayTable th[data-yesterday-sort]").forEach((header) => {
|
||
header.classList.remove("sort-asc", "sort-desc", "sorted");
|
||
header.setAttribute("aria-sort", "none");
|
||
if (header.dataset.yesterdaySort === state.yesterdaySortKey) {
|
||
header.classList.add(state.yesterdaySortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
|
||
header.setAttribute("aria-sort", state.yesterdaySortDirection === "asc" ? "ascending" : "descending");
|
||
}
|
||
const arrow = header.querySelector(".arr");
|
||
if (arrow) arrow.textContent = header.classList.contains("sorted") ? (state.yesterdaySortDirection === "asc" ? "▲" : "▼") : "↕";
|
||
});
|
||
}
|
||
|
||
function renderPerformance(rows) {
|
||
rows = normalizePerformanceRows(rows);
|
||
const currentDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate.value);
|
||
const previousDate = displayCompactDate(state.dashboard?.meta?.previous_trade_date || "");
|
||
setText("performanceDateRange", `昨日 ${previousDate} → 今日 ${currentDate}`);
|
||
document.querySelector("#performanceCards").innerHTML = rows.map((row) => `
|
||
<article class="performance-stage-card" title="收红 ${formatNumber(row.positive_rate, 1)}% · 平均涨幅 ${signed(row.average_change)}%"
|
||
aria-label="${escapeHtml(row.label)},晋级率 ${formatNumber(row.advance_rate, 1)}%,晋级 ${number(row.advanced)} 只,共 ${number(row.count)} 只,收红率 ${formatNumber(row.positive_rate, 1)}%,平均涨幅 ${signed(row.average_change)}%">
|
||
<div class="performance-stage-label"><span>${escapeHtml(row.label)} → 今日</span><i class="performance-status-tag ${performanceRateState(row.advance_rate).className}">${performanceRateState(row.advance_rate).label}</i></div>
|
||
<strong class="performance-stage-rate ${performanceRateState(row.advance_rate).className}">${formatNumber(row.advance_rate, 1)}%</strong>
|
||
<span class="performance-stage-count">晋级 ${number(row.advanced)} / 共 ${number(row.count)} 只</span>
|
||
<div class="performance-stage-track" aria-hidden="true"><i class="${performanceRateState(row.advance_rate).className}" style="width:${Math.max(number(row.advance_rate), number(row.advance_rate) > 0 ? 2 : 0)}%"></i></div>
|
||
</article>
|
||
`).join("") || '<div class="performance-empty-state">暂无昨日涨停统计</div>';
|
||
renderPerformanceConclusion(rows);
|
||
renderMarketBreadth(state.dashboard?.overview || {});
|
||
}
|
||
|
||
function normalizePerformanceRows(rows) {
|
||
const groups = new Map();
|
||
(rows || []).forEach((row) => {
|
||
const level = Math.max(1, number(row.level));
|
||
const displayLevel = Math.min(level, 5);
|
||
const group = groups.get(displayLevel) || {
|
||
level: displayLevel,
|
||
label: displayLevel === 1 ? "昨日首板" : displayLevel === 5 ? "昨日5板+" : `昨日${displayLevel}板`,
|
||
count: 0,
|
||
advanced: 0,
|
||
positive: 0,
|
||
changeTotal: 0,
|
||
};
|
||
const count = number(row.count);
|
||
group.count += count;
|
||
group.advanced += number(row.advanced);
|
||
group.positive += count * number(row.positive_rate) / 100;
|
||
group.changeTotal += count * number(row.average_change);
|
||
groups.set(displayLevel, group);
|
||
});
|
||
return [...groups.values()]
|
||
.sort((left, right) => right.level - left.level)
|
||
.map((group) => ({
|
||
level: group.level,
|
||
label: group.label,
|
||
count: group.count,
|
||
advanced: group.advanced,
|
||
advance_rate: group.count ? group.advanced / group.count * 100 : 0,
|
||
positive_rate: group.count ? group.positive / group.count * 100 : 0,
|
||
average_change: group.count ? group.changeTotal / group.count : 0,
|
||
}));
|
||
}
|
||
|
||
function performanceRateState(rate) {
|
||
const value = number(rate);
|
||
if (value === 0) return { label: "失效", className: "is-neutral" };
|
||
if (value < 20) return { label: "危险", className: "is-warning" };
|
||
return { label: "活跃", className: "is-active" };
|
||
}
|
||
|
||
function renderPerformanceConclusion(rows) {
|
||
const container = document.querySelector("#performanceConclusion");
|
||
if (!rows.length) {
|
||
container.innerHTML = '<div class="empty-state">暂无昨日梯队数据,暂不生成结论</div>';
|
||
return;
|
||
}
|
||
const sorted = [...rows].sort((left, right) => number(right.level) - number(left.level));
|
||
const highRows = sorted.filter((row) => number(row.level) >= 4);
|
||
const highAdvanced = highRows.reduce((total, row) => total + number(row.advanced), 0);
|
||
const highSamples = highRows.map((row) => escapeHtml(row.label)).join("、");
|
||
const strongest = [...rows].sort((left, right) => (
|
||
number(right.advance_rate) - number(left.advance_rate) || number(right.level) - number(left.level)
|
||
))[0];
|
||
const firstBoard = rows.find((row) => number(row.level) === 1);
|
||
const overview = state.dashboard?.overview || {};
|
||
const phase = overview.sentiment_phase || "观察";
|
||
const up = number(overview.up_count);
|
||
const down = number(overview.down_count);
|
||
const breadthRate = up + down > 0 ? up / (up + down) * 100 : 50;
|
||
const stance = breadthRate < 25 ? "宜守不宜攻" : breadthRate < 45 ? "控制仓位,聚焦核心" : "保持精选,跟随强势梯队";
|
||
const highText = highRows.length
|
||
? `高位晋级率<b class="${highAdvanced ? "up" : "is-neutral"}">${highAdvanced ? "仍有承接" : "全线失效"}</b>:${highSamples}${highAdvanced ? `共晋级 ${highAdvanced} 只` : "今日均未晋级"};`
|
||
: "高位梯队暂无昨日样本,空间信号仍待确认;";
|
||
const strongestText = strongest
|
||
? `<b>${escapeHtml(strongest.label)}</b>晋级率最高,为 <b class="up">${formatNumber(strongest.advance_rate, 1)}%</b>(${number(strongest.advanced)} 只晋级 / 共 ${number(strongest.count)} 只);`
|
||
: "暂无相对占优梯队;";
|
||
const firstBoardText = firstBoard
|
||
? `首板基数 ${number(firstBoard.count)} 只,晋级率 <b class="${performanceRateState(firstBoard.advance_rate).className}">${formatNumber(firstBoard.advance_rate, 1)}%</b>,低位接力${number(firstBoard.advance_rate) < 20 ? "胜率偏低" : "仍有活跃度"};`
|
||
: "首板梯队暂无有效样本;";
|
||
container.innerHTML = `
|
||
<div>· ${highText}</div>
|
||
<div>· ${strongestText}</div>
|
||
<div>· ${firstBoardText}</div>
|
||
<div>· 结论:<b>${stance}</b>,当前情绪周期「${escapeHtml(phase)}」。</div>
|
||
`;
|
||
}
|
||
|
||
|
||
function changeSort(key) {
|
||
if (state.sortKey === key) state.sortDirection = state.sortDirection === "asc" ? "desc" : "asc";
|
||
else {
|
||
state.sortKey = key;
|
||
state.sortDirection = ["name", "code", "sector", "first_time", "last_time"].includes(key) ? "asc" : "desc";
|
||
}
|
||
renderLimitTable();
|
||
}
|
||
|
||
function compareRows(left, right) {
|
||
const leftValue = left[state.sortKey] ?? "";
|
||
const rightValue = right[state.sortKey] ?? "";
|
||
let result = typeof leftValue === "number" || typeof rightValue === "number"
|
||
? number(leftValue) - number(rightValue)
|
||
: String(leftValue).localeCompare(String(rightValue), "zh-CN", { numeric: true });
|
||
if (result === 0 && state.sortKey !== "first_time") result = String(left.first_time || "").localeCompare(String(right.first_time || ""));
|
||
return state.sortDirection === "asc" ? result : -result;
|
||
}
|
||
|
||
function updateSortHeaders() {
|
||
document.querySelectorAll("#limitTable th[data-sort]").forEach((header) => {
|
||
header.classList.remove("sort-asc", "sort-desc", "sorted");
|
||
const active = header.dataset.sort === state.sortKey;
|
||
if (active) header.classList.add(state.sortDirection === "asc" ? "sort-asc" : "sort-desc", "sorted");
|
||
const arrow = header.querySelector(".arr");
|
||
if (arrow) arrow.textContent = active ? (state.sortDirection === "asc" ? "▲" : "▼") : "↕";
|
||
});
|
||
}
|
||
|
||
|
||
function bindPoolEvents() {
|
||
document.querySelector("#stockSearch").addEventListener("input", (event) => {
|
||
state.query = event.target.value.trim().toLowerCase();
|
||
renderLimitTable();
|
||
});
|
||
document.querySelectorAll("[data-table-search]").forEach((input) => {
|
||
input.addEventListener("input", () => {
|
||
const query = input.value.trim().toLowerCase();
|
||
const body = document.querySelector(`#${CSS.escape(input.dataset.tableSearch)}`);
|
||
body?.querySelectorAll("tr").forEach((row) => {
|
||
row.hidden = Boolean(query) && !row.textContent.toLowerCase().includes(query);
|
||
});
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll("[data-filter]").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
document.querySelectorAll("[data-filter]").forEach((item) => item.classList.remove("active"));
|
||
button.classList.add("active");
|
||
state.filter = button.dataset.filter;
|
||
renderLimitTable();
|
||
});
|
||
});
|
||
document.querySelectorAll("#limitTable th[data-sort]").forEach((header) => {
|
||
header.addEventListener("click", () => changeSort(header.dataset.sort));
|
||
});
|
||
document.querySelector("#brokenSearch").addEventListener("input", (event) => {
|
||
state.brokenQuery = event.target.value.trim().toLowerCase();
|
||
renderBrokenTable(state.dashboard?.broken || []);
|
||
});
|
||
document.querySelectorAll("#brokenTable th[data-broken-sort]").forEach((header) => {
|
||
header.addEventListener("click", () => changeBrokenSort(header.dataset.brokenSort));
|
||
});
|
||
document.querySelector("#downSearch").addEventListener("input", (event) => {
|
||
state.downQuery = event.target.value.trim().toLowerCase();
|
||
renderDownTable(state.dashboard?.down_limits || []);
|
||
});
|
||
document.querySelectorAll("#downTable th[data-down-sort]").forEach((header) => {
|
||
header.addEventListener("click", () => changeDownSort(header.dataset.downSort));
|
||
});
|
||
document.querySelector("#yesterdaySearch").addEventListener("input", (event) => {
|
||
state.yesterdayQuery = event.target.value.trim().toLowerCase();
|
||
renderYesterdayTable(state.dashboard?.yesterday_limits || []);
|
||
});
|
||
document.querySelectorAll("[data-yesterday-filter]").forEach((button) => {
|
||
button.addEventListener("click", () => {
|
||
state.yesterdayFilter = button.dataset.yesterdayFilter;
|
||
renderYesterdayTable(state.dashboard?.yesterday_limits || []);
|
||
});
|
||
});
|
||
document.querySelectorAll("#yesterdayTable th[data-yesterday-sort]").forEach((header) => {
|
||
header.addEventListener("click", () => changeYesterdaySort(header.dataset.yesterdaySort));
|
||
});
|
||
document.querySelector("#exportButton").addEventListener("click", exportStocks);
|
||
document.querySelector("#brokenExportButton").addEventListener("click", exportBroken);
|
||
document.querySelector("#downExportButton").addEventListener("click", exportDown);
|
||
document.querySelector("#yesterdayExportButton").addEventListener("click", exportYesterday);
|
||
document.querySelector("#reasonForm").addEventListener("submit", saveReasonOverride);
|
||
}
|