174 lines
9.2 KiB
JavaScript
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 */
|