refactor: establish frontend page modules
This commit is contained in:
+66
-45
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user