refactor: establish frontend page modules

This commit is contained in:
leefer
2026-07-29 22:15:36 +08:00
parent ed2b47fe90
commit b284531dfa
19 changed files with 350 additions and 50 deletions
+66 -45
View File
@@ -10,6 +10,11 @@ const {
todayString,
} = window.XiaobaiUI;
const {
emptyStateHtml,
renderEmptyState,
} = window.XiaobaiComponents;
const HEART_BREATH_INHALE_MS = 3_000;
const HEART_BREATH_HOLD_MS = 2_000;
const HEART_BREATH_EXHALE_MS = 4_000;
@@ -214,6 +219,37 @@ const applicationShell = window.XiaobaiShell.create({
onNavigationSync: () => toggleAccountDropdown(false),
});
const pageModules = window.XiaobaiPageModules.create({
pages: window.XiaobaiPages,
actions: {
closeTransientUi: () => closeStockPreview(),
applyAccess: () => applyMembershipAccess(),
clearAuction: () => clearAuctionTimer(),
stopHeaven: () => {
stopQiFieldCanvas();
stopHeartDust();
cancelHeavenPerformance();
},
loadSentiment: () => loadSentimentHistory(),
loadRotation: () => loadRotationHistory(),
loadAuction: () => loadAuctionCenter(),
loadThemes: () => loadThemeLibrary(),
loadPopularity: () => loadPopularity(),
loadDragonTiger: () => loadDragonTiger(),
loadReview: () => loadReviewWorkspace(),
loadScreener: () => {
if (hasMemberAccess() && state.dashboard) loadScreenerSetup();
},
loadMentor: () => {
if (hasMemberAccess()) loadMentorSetup();
},
loadHeaven: () => {
if (!hasMemberAccess()) return;
loadHeavenSetup(false, "", document.querySelector("#heavenStockInput").value.trim());
},
},
});
const elements = {
tradeDate: document.querySelector("#tradeDate"),
loading: document.querySelector("#loadingOverlay"),
@@ -1928,7 +1964,7 @@ async function loadRotationHistory(force = false) {
}
state.rotationLoading = true;
const container = document.querySelector("#rotationHistory");
container.innerHTML = '<div class="empty-state">正在读取轮动历史</div>';
renderEmptyState(container, "正在读取轮动历史");
try {
const query = new URLSearchParams({
trade_date: elements.tradeDate.value,
@@ -1937,7 +1973,7 @@ async function loadRotationHistory(force = false) {
state.rotationHistoryKey = key;
renderRotationHistory();
} catch (error) {
container.innerHTML = `<div class="empty-state">${escapeHtml(error.message || "轮动历史加载失败")}</div>`;
renderEmptyState(container, error.message || "轮动历史加载失败");
showToast(error.message || "轮动历史加载失败");
} finally {
state.rotationLoading = false;
@@ -1950,7 +1986,7 @@ function renderRotationHistory() {
const container = document.querySelector("#rotationHistory");
const tracker = document.querySelector("#rotationTracker");
if (!rows.length) {
container.innerHTML = '<div class="empty-state">尚无连续交易日的板块数据</div>';
renderEmptyState(container, "尚无连续交易日的板块数据");
setText("rotationHistoryRange", "暂无轮动历史");
tracker.hidden = true;
return;
@@ -2098,13 +2134,13 @@ function renderLadderMini(ladders) {
<div><strong>${escapeHtml(group.label)}</strong><small>${number(group.count)} </small></div>
<p title="${escapeHtml(allNames.join(""))}">${escapeHtml(visibleNames || "--")}${suffix}</p>
</div>`;
}).join("") || '<div class="empty-state">暂无梯队数据</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("") || '<div class="empty-state">暂无板块数据</div>';
`).join("") || emptyStateHtml("暂无板块数据");
}
function renderLadderBoard(ladders) {
@@ -2460,7 +2496,7 @@ async function loadThemeLibrary(force = false) {
if (initialCode) await selectTheme(initialCode, true);
} catch (error) {
setText("themeDateLabel", error.message || "题材数据暂不可用");
document.querySelector("#themeDirectory").innerHTML = `<div class="empty-state">${escapeHtml(error.message || "题材数据加载失败")}</div>`;
renderEmptyState("themeDirectory", error.message || "题材数据加载失败");
showToast(error.message || "题材数据加载失败");
} finally {
state.themeLoading = false;
@@ -2496,7 +2532,7 @@ function renderThemeDirectory() {
<span class="theme-directory-copy-v2"><strong class="market-preview-trigger" data-market-preview-type="theme" data-market-preview-id="${escapeHtml(item.code)}" title="悬停预览题材行情">${escapeHtml(item.name)}</strong><small>${number(item.member_count)} ${item.hot_rank ? ` · ${number(item.hot_rank)}` : ""}</small></span>
<b class="${changeClass(item.change)}">${item.has_quote ? `${signed(item.change)}%` : "--"}</b>
</button>`;
}).join("") || '<div class="empty-state">没有匹配的题材</div>';
}).join("") || emptyStateHtml("没有匹配的题材");
}
async function selectTheme(code, keepSelection = false) {
@@ -2847,7 +2883,7 @@ function renderDragonTraderList() {
`).join("");
container.innerHTML = traders.length
? `${cardMarkup}<div class="dragon-card-hit-layer">${hitZoneMarkup}</div>`
: `<div class="empty-state dragon-empty">${escapeHtml(state.dragonFilter === "unclassified" ? "待归类席位请在下方管理" : emptyMessage)}</div>`;
: emptyStateHtml(state.dragonFilter === "unclassified" ? "待归类席位请在下方管理" : emptyMessage, { className: "dragon-empty" });
container.querySelectorAll("[data-dragon-card]").forEach((card) => {
card.addEventListener("animationend", () => card.classList.remove("dealing"), { once: true });
});
@@ -2905,7 +2941,7 @@ function renderDragonTraderDetail(trader) {
const container = document.querySelector("#dragonTraderDetail");
if (!trader) {
container.hidden = true;
container.innerHTML = '<div class="empty-state dragon-empty">选择一位游资查看操作明细</div>';
renderEmptyState(container, "选择一位游资查看操作明细", { className: "dragon-empty" });
return;
}
container.hidden = false;
@@ -2958,7 +2994,7 @@ function renderUnclassifiedSeats() {
<input type="text" maxlength="50" placeholder="输入游资名" aria-label="${escapeHtml(seat.seat_name)}的游资名" required>
<button class="button" type="submit">归类</button>
</form>
`).join("") || '<div class="empty-state">当前席位均已归类</div>';
`).join("") || emptyStateHtml("当前席位均已归类");
list.querySelectorAll(".unclassified-seat-row").forEach((form) => {
form.addEventListener("submit", saveSeatAlias);
});
@@ -3411,7 +3447,7 @@ function renderNotesHistory(notes, container, compact) {
<div class="note-block"><strong>计划</strong><p>${escapeHtml(note.plan || "--")}</p></div>
<button class="table-action down" type="button" data-note-delete="${number(note.id)}">删除</button>
</article>
`).join("") || '<div class="empty-state">暂无复盘记录</div>';
`).join("") || emptyStateHtml("暂无复盘记录");
container.querySelectorAll("[data-note-delete]").forEach((button) => {
button.addEventListener("click", () => deleteNote(number(button.dataset.noteDelete), compact));
});
@@ -3801,7 +3837,7 @@ function renderCuratedStrategyLibrary() {
<span class="curated-card-head"><i class="curated-strategy-rank">${String(rank).padStart(2, "0")}</i><span><strong>${escapeHtml(strategy.name)}</strong><small>${escapeHtml(school)} · ${escapeHtml(meta.category || "")}</small></span><em class="curated-card-result ${runState.className}">${escapeHtml(runState.label)}</em></span>
<span class="curated-card-tags"><em>${escapeHtml(meta.quality || "--")}</em><em>${escapeHtml(meta.frequency || "--")}</em><em> ${escapeHtml(meta.risk || "--")}</em></span>
</article>`;
}).join("") : '<div class="empty-state">没有符合条件的策略</div>';
}).join("") : emptyStateHtml("没有符合条件的策略");
renderCuratedStrategyDetail();
}
@@ -4115,7 +4151,7 @@ function renderStrategyList() {
<strong>${escapeHtml(strategy.name)}</strong><span>${escapeHtml(strategy.description || "--")}</span>
<small>${strategy.regimes.map((item) => regimeLabel(item)).join(" / ")}</small>
</button>
`).join("") || '<div class="empty-state">暂无已保存的自定义公式</div>';
`).join("") || emptyStateHtml("暂无已保存的自定义公式");
list.querySelectorAll("[data-strategy-id]").forEach((button) => {
button.addEventListener("click", () => {
state.customStrategyDraft = state.screenerSetup.strategies.find((item) => item.id === number(button.dataset.strategyId));
@@ -5244,7 +5280,7 @@ function renderHeavenLineChecks(chart) {
status.textContent = checks.length ? `${passedCount}/6 通过${manualCount ? ` · ${manualCount} 爻含补录` : ""}` : "等待载入";
status.className = passedCount === 6 ? (manualCount ? "is-manual" : "is-passed") : "is-failed";
if (!checks.length) {
container.innerHTML = '<div class="empty-state">载入股票后查看六爻数据状态</div>';
renderEmptyState(container, "载入股票后查看六爻数据状态");
return;
}
const lineValueLabel = { 6: "老阴 · 动", 7: "少阳 · 静", 8: "少阴 · 静", 9: "老阳 · 动" };
@@ -5881,7 +5917,7 @@ function selectHeavenReadingTab(tab) {
async function loadHeavenReadingHistory(mode) {
const list = document.querySelector("#heavenReadingHistoryList");
list.innerHTML = '<div class="empty-state">正在读取历史记录</div>';
renderEmptyState(list, "正在读取历史记录");
try {
const query = new URLSearchParams({ mode, limit: "100" });
const payload = await apiRequest(`/api/heaven/readings?${query}`);
@@ -5891,7 +5927,7 @@ async function loadHeavenReadingHistory(mode) {
}
renderHeavenReadingHistory();
} catch (error) {
list.innerHTML = `<div class="empty-state">${escapeHtml(error.message || "历史记录加载失败")}</div>`;
renderEmptyState(list, error.message || "历史记录加载失败");
}
}
@@ -5947,7 +5983,7 @@ function renderHeavenReadingHistory() {
<small>${escapeHtml(item.subject_detail || displayCompactDate(item.context_date))}</small>
<time>${formatTimestamp(item.created_at)}</time>
</button>
`).join("") || '<div class="empty-state">暂无历史解读</div>';
`).join("") || emptyStateHtml("暂无历史解读");
const selected = items.find((item) => number(item.id) === state.heavenReadingSelectedId);
const detail = document.querySelector("#heavenReadingHistoryDetail");
detail.innerHTML = selected ? `
@@ -5955,7 +5991,7 @@ function renderHeavenReadingHistory() {
<p>${escapeHtml(selected.subject_detail || displayCompactDate(selected.context_date))}</p>
<div class="heaven-reading-answer">${formatMentorAnswer(selected.answer || "")}</div>
<footer><button class="button" type="button" data-delete-heaven-reading="${number(selected.id)}"><i data-lucide="trash-2"></i><span></span></button></footer>
` : '<div class="empty-state">选择一条记录查看完整解读</div>';
` : emptyStateHtml("选择一条记录查看完整解读");
refreshIcons();
}
@@ -7798,7 +7834,7 @@ function renderAlerts() {
<button class="icon-button" type="button" data-alert-action="delete" data-alert-id="${number(item.id)}" title="删除提醒" aria-label="删除提醒"><i data-lucide="trash-2"></i></button>
</div>
</article>`;
}).join("") || '<div class="empty-state">暂无提醒</div>';
}).join("") || emptyStateHtml("暂无提醒");
bindStockRows(container);
refreshIcons();
}
@@ -7911,7 +7947,7 @@ function renderAssistantMessages() {
<div class="assistant-message-content">${message.role === "assistant" ? (message.content ? formatMentorAnswer(message.content) : '<span class="assistant-thinking">正在整理复盘数据</span>') : escapeHtml(message.content)}</div>
${message.streaming ? '<span class="assistant-stream-caret" aria-hidden="true"></span>' : ""}
</article>
`).join("") || '<div class="empty-state">可以从市场、策略或自己的交易记录开始复盘</div>';
`).join("") || emptyStateHtml("可以从市场、策略或自己的交易记录开始复盘");
updateAssistantControls();
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
}
@@ -8075,7 +8111,7 @@ async function openEntityDetail(item) {
setText("entityDetailType", item.type_label || "--");
setText("entityDetailDate", "正在加载行情");
document.querySelector("#entityDetailChange").className = "";
document.querySelector("#entityDetailMetrics").innerHTML = '<div class="empty-state">正在加载交易数据</div>';
renderEmptyState("entityDetailMetrics", "正在加载交易数据");
openModalDialog(elements.entityDetailDialog);
clearEntityDetailChart("正在加载日 K 数据");
try {
@@ -8098,7 +8134,7 @@ async function openEntityDetail(item) {
} catch (error) {
if (requestSequence !== state.entityDetailRequestSequence) return;
setText("entityDetailDate", "行情加载失败");
document.querySelector("#entityDetailMetrics").innerHTML = `<div class="empty-state">${escapeHtml(error.message || "交易数据加载失败")}</div>`;
renderEmptyState("entityDetailMetrics", error.message || "交易数据加载失败");
if (state.entityDetailChartMode === "daily") clearEntityDetailChart(error.message || "行情加载失败");
showToast(error.message || "详情加载失败");
}
@@ -8166,7 +8202,7 @@ function syncDetailChartButtons(scope, mode) {
function renderEntityDetailMetrics(metrics) {
const container = document.querySelector("#entityDetailMetrics");
if (!metrics.length) {
container.innerHTML = '<div class="empty-state">暂无交易数据</div>';
renderEmptyState(container, "暂无交易数据");
return;
}
container.innerHTML = metrics.map((metric) => {
@@ -8299,7 +8335,7 @@ async function openStock(code, fallback = null) {
document.querySelector("#reasonInput").value = row.reason || "";
document.querySelector("#stockNoteContent").value = "";
document.querySelector("#stockNotePlan").value = "";
document.querySelector("#stockNotes").innerHTML = '<div class="empty-state">正在加载笔记</div>';
renderEmptyState("stockNotes", "正在加载笔记");
updateWatchButton();
openModalDialog(elements.stockDialog);
clearPriceChart("正在加载日 K 数据");
@@ -8421,26 +8457,11 @@ function applyMembershipAccess() {
}
function openView(viewId, updateHash = true) {
if (!applicationShell.page(viewId)) return;
closeStockPreview();
if (viewId !== "auctionView") clearAuctionTimer();
if (viewId !== "heavenView") {
stopQiFieldCanvas();
stopHeartDust();
cancelHeavenPerformance();
}
if (!applicationShell.page(viewId) || !pageModules.has(viewId)) return;
const previousView = state.activeView;
pageModules.beforeMount(viewId, previousView);
if (!applicationShell.mount(viewId, { updateUrl: updateHash })) return;
applyMembershipAccess();
if (viewId === "dragonView") loadDragonTiger();
if (viewId === "reviewWorkspaceView") loadReviewWorkspace();
if (viewId === "screenerView" && hasMemberAccess() && state.dashboard) loadScreenerSetup();
if (viewId === "mentorView" && hasMemberAccess()) loadMentorSetup();
if (viewId === "heavenView" && hasMemberAccess()) loadHeavenSetup(false, "", document.querySelector("#heavenStockInput").value.trim());
if (viewId === "sentimentCycleView") loadSentimentHistory();
if (viewId === "rotationView") loadRotationHistory();
if (viewId === "auctionView") loadAuctionCenter();
if (viewId === "themeLibraryView") loadThemeLibrary();
if (viewId === "popularityView") loadPopularity();
pageModules.afterMount(viewId, previousView);
}
function initializeAutoTableSorting() {
@@ -8697,7 +8718,7 @@ function renderModelPool(models, primaryId = "", fallbackId = "") {
</div>
<div class="model-test-row"><button class="button" type="button" data-test-model>测试连接</button><span class="model-test-status" aria-live="polite"></span><button class="icon-button model-delete-button" type="button" data-delete-model aria-label="" title=""><i data-lucide="trash-2"></i></button></div>
</article>
`).join("") || '<div class="empty-state">模型池为空,请先添加模型</div>';
`).join("") || emptyStateHtml("模型池为空,请先添加模型");
updateModelRoleOptions(primaryId, fallbackId);
container.querySelectorAll("[data-test-model]").forEach((button) => button.addEventListener("click", () => testPlatformModel(button.closest("[data-model-id]"))));
container.querySelectorAll("[data-delete-model]").forEach((button) => button.addEventListener("click", () => deletePlatformModel(button.closest("[data-model-id]"))));
@@ -8777,7 +8798,7 @@ function renderAdminUsers(users) {
<button class="button" type="submit">应用</button>
</form>
</article>`;
}).join("") || '<div class="empty-state">暂无注册用户</div>';
}).join("") || emptyStateHtml("暂无注册用户");
container.querySelectorAll(".membership-form").forEach((form) => form.addEventListener("submit", saveMembership));
}