migration: preserve frontend shell pages and styles
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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 */
|
||||
Reference in New Issue
Block a user