Files
xiaobaifupan/app/frontend/pages/rotation/page.js
T

174 lines
9.2 KiB
JavaScript

window.XiaobaiPageModules.register("rotation", ["rotationView"], {
enter: ["loadRotation"],
});
/* PRESERVATION-SOURCE-BEGIN app.js:1958-2124 */
async function loadRotationHistory(force = false) {
if (!state.dashboard || state.rotationLoading) return;
const key = `${elements.tradeDate.value}:9`;
if (!force && state.rotationHistoryKey === key && state.rotationHistory) {
renderRotationHistory();
return;
}
state.rotationLoading = true;
const container = document.querySelector("#rotationHistory");
renderEmptyState(container, "正在读取轮动历史");
try {
const query = new URLSearchParams({
trade_date: elements.tradeDate.value,
});
state.rotationHistory = await apiRequest(`/api/rotation/history?${query}`);
state.rotationHistoryKey = key;
renderRotationHistory();
} catch (error) {
renderEmptyState(container, error.message || "轮动历史加载失败");
showToast(error.message || "轮动历史加载失败");
} finally {
state.rotationLoading = false;
}
}
function renderRotationHistory() {
const rows = state.rotationHistory?.rows || [];
const selected = state.rotationSelectedSector;
const container = document.querySelector("#rotationHistory");
const tracker = document.querySelector("#rotationTracker");
if (!rows.length) {
renderEmptyState(container, "尚无连续交易日的板块数据");
setText("rotationHistoryRange", "暂无轮动历史");
tracker.hidden = true;
return;
}
const chronological = [...rows]
.sort((left, right) => String(left.trade_date).localeCompare(String(right.trade_date)))
.slice(-9);
const displayRows = state.rotationOrder === "latest" ? [...chronological].reverse() : chronological;
document.querySelectorAll("[data-rotation-order]").forEach((button) => {
button.classList.toggle("active", button.dataset.rotationOrder === state.rotationOrder);
});
setText(
"rotationHistoryRange",
`最近 ${chronological.length} 个交易日 · ${displayCompactDate(chronological[0].trade_date)}${displayCompactDate(chronological[chronological.length - 1].trade_date)} · ${state.rotationOrder === "latest" ? "由近到远,左侧为最新交易日" : "由远到近,右侧为最新交易日"}`,
);
setText("rotationSelectionHint", selected ? `已联动高亮 ${selected}` : "点击任意板块追踪其连续性");
if (selected) {
const sequence = displayRows.map((day) => {
const sector = (day.sectors || []).find((item) => item.name === selected);
return { tradeDate: day.trade_date, sector };
});
const appearances = sequence.filter((item) => item.sector);
const bestRank = appearances.length ? Math.min(...appearances.map((item) => number(item.sector.rank))) : 0;
tracker.hidden = false;
const continuity = appearances.length >= 3 ? "主线候选" : appearances.length === 1 ? "单日异动,持续性待验证" : "间断活跃";
tracker.innerHTML = `
<div class="rotation-tracker-copy"><strong>${escapeHtml(selected)}</strong><span>近 9 日在榜 <b>${appearances.length}</b> 天 · 最高排名 <b>#${bestRank || "--"}</b> · ${continuity}</span></div>
<div class="rotation-tracker-spark" aria-label="${escapeHtml(selected)}九日强度轨迹">
${sequence.map((item) => item.sector
? `<span style="--spark-height:${Math.max(18, clamp(number(item.sector.strength), 0, 100))}%" title="${escapeHtml(displayCompactDate(item.tradeDate))} · 第 ${number(item.sector.rank)} 名 · 强度 ${formatNumber(item.sector.strength, 0)}"><i></i><small>#${number(item.sector.rank)}</small></span>`
: `<span class="missing" title="${escapeHtml(displayCompactDate(item.tradeDate))} · 未上榜"><i></i><small>--</small></span>`).join("")}
</div>
<button class="rotation-track-cancel" type="button">取消追踪</button>`;
tracker.querySelector(".rotation-track-cancel").addEventListener("click", () => {
state.rotationSelectedSector = "";
state.rotationSelectedDate = "";
renderRotationHistory();
loadRotationMembers("");
});
} else {
tracker.hidden = true;
tracker.innerHTML = "";
}
container.classList.toggle("tracking", Boolean(selected));
const latestTradeDate = chronological[chronological.length - 1].trade_date;
container.innerHTML = displayRows.map((day) => {
const hasSelected = selected && (day.sectors || []).some((sector) => sector.name === selected);
return `
<article class="rotation-day ${selected ? "has-selection" : ""} ${hasSelected ? "selected-day" : ""} ${day.trade_date === latestTradeDate ? "latest-day" : ""}">
<header><time>${escapeHtml(displayCompactDate(day.trade_date).slice(5))}</time><span>${(day.sectors || []).length} 个热点</span></header>
<div class="rotation-day-sectors">${(day.sectors || []).map((sector) => {
const strength = clamp(number(sector.strength), 0, 100);
const heatClass = strength >= 90 ? "heat-strong" : strength >= 70 ? "heat-warm" : "heat-mild";
return `
<button type="button" class="rotation-sector-chip ${heatClass} ${selected === sector.name ? "selected" : ""}" data-rotation-sector="${escapeHtml(sector.name)}" data-rotation-date="${escapeHtml(day.trade_date)}">
<span class="rotation-rank rank-${Math.min(number(sector.rank), 4)}">${number(sector.rank)}</span><strong>${escapeHtml(sector.name)}</strong><small><b>${number(sector.count)}</b> 家 · ${formatNumber(sector.strength, 0)}</small>
<span class="rotation-cell-tooltip">${escapeHtml(displayCompactDate(day.trade_date).slice(5))} · 第 ${number(sector.rank)} 名 · 涨停 ${number(sector.count)} 家 · 强度 ${formatNumber(sector.strength, 0)}</span>
</button>`;
}).join("")}</div>
</article>`;
}).join("");
container.querySelectorAll("[data-rotation-sector]").forEach((button) => {
button.addEventListener("click", () => {
const clickedSector = button.dataset.rotationSector;
const clickedDate = button.dataset.rotationDate;
const isSameSelection = clickedSector === state.rotationSelectedSector
&& clickedDate === state.rotationSelectedDate;
state.rotationSelectedSector = isSameSelection ? "" : clickedSector;
state.rotationSelectedDate = isSameSelection ? "" : clickedDate;
renderRotationHistory();
loadRotationMembers(state.rotationSelectedSector);
});
});
}
async function loadRotationMembers(sector, force = false) {
if (!sector) {
state.rotationMembers = null;
state.rotationMembersKey = "";
renderRotationMembers();
return;
}
const memberDate = state.rotationSelectedDate || elements.tradeDate.value;
const key = `${memberDate}:${sector}`;
if (!force && state.rotationMembersKey === key && state.rotationMembers) {
renderRotationMembers();
return;
}
state.rotationMembersLoading = true;
renderRotationMembers();
try {
const query = new URLSearchParams({ trade_date: memberDate, sector });
state.rotationMembers = await apiRequest(`/api/rotation/members?${query}`);
state.rotationMembersKey = key;
} catch (error) {
state.rotationMembers = { error: error.message || "成分股加载失败", rows: [] };
state.rotationMembersKey = key;
} finally {
state.rotationMembersLoading = false;
renderRotationMembers();
}
}
function renderRotationMembers() {
const body = document.querySelector("#rotationTableBody");
const empty = document.querySelector("#rotationMembersEmpty");
if (state.rotationMembersLoading) {
body.innerHTML = "";
empty.textContent = `正在核验${state.rotationSelectedSector}成分股`;
empty.hidden = false;
return;
}
const payload = state.rotationMembers;
const rows = payload?.rows || [];
if (!state.rotationSelectedSector || !payload || payload.error || !rows.length) {
body.innerHTML = "";
empty.textContent = payload?.error || (state.rotationSelectedSector ? "该板块暂无可用成分行情" : "点击上方任意板块查看成分股");
empty.hidden = false;
setText("rotationDetailTitle", "板块成分股");
setText("rotationDetailMeta", state.rotationSelectedSector || "--");
return;
}
empty.hidden = true;
setText("rotationDetailTitle", `${payload.meta?.sector_name || state.rotationSelectedSector}成分股`);
setText("rotationDetailMeta", `${displayCompactDate(payload.meta?.trade_date)} · ${number(payload.meta?.quoted_count)} / ${number(payload.meta?.member_count)} 只`);
body.innerHTML = rows.map((row, index) => `
<tr data-code="${escapeHtml(row.code)}"><td class="number num muted">${index + 1}</td><td class="stock-code">${escapeHtml(row.code)}</td><td class="stock-name">${escapeHtml(row.name)}</td>
<td class="number num ${row.quoted ? changeClass(row.change) : "muted"}" data-sort-value="${row.quoted ? number(row.change) : -999}">${row.quoted ? signed(row.change) : ""}</td>
<td class="number num">${row.quoted ? formatNumber(row.open, 2) : ""}</td><td class="number num">${row.quoted ? formatNumber(row.close, 2) : ""}</td>
<td class="number num" data-sort-value="${number(row.amount_billion)}">${row.quoted ? formatNumber(row.amount_billion, 2) : ""}</td><td>${row.quoted ? "正常交易" : "当日无行情"}</td></tr>
`).join("");
animateRows(body);
bindStockRows(body);
}
/* PRESERVATION-SOURCE-END app.js:1958-2124 */