refactor: streamline frontend language and shared UI utilities
This commit is contained in:
+35
-81
@@ -1,3 +1,15 @@
|
||||
const {
|
||||
clamp,
|
||||
displayCompactDate,
|
||||
escapeHtml,
|
||||
formatNumber,
|
||||
formatTimestamp,
|
||||
localDateString,
|
||||
number,
|
||||
parseLocalDate,
|
||||
todayString,
|
||||
} = window.XiaobaiUI;
|
||||
|
||||
const state = {
|
||||
user: null,
|
||||
csrfToken: "",
|
||||
@@ -598,11 +610,7 @@ function applyDashboard(payload, background = false) {
|
||||
document.querySelector("#qiObservationDate").value = selectedDate;
|
||||
document.querySelector("#journalDate").value = selectedDate;
|
||||
renderDashboard();
|
||||
const source = payload.meta.source === "tushare" ? "Tushare" : "演示数据";
|
||||
const cacheText = payload.meta.realtime
|
||||
? "rt_k 实时行情"
|
||||
: payload.meta.cached ? "SQLite 缓存" : "已写入 SQLite";
|
||||
setStatus(`${source} · ${cacheText}`);
|
||||
setStatus(`${dashboardSourceLabel(payload.meta)} · 数据已更新`);
|
||||
if (!background) {
|
||||
if (state.activeView === "dragonView") loadDragonTiger();
|
||||
if (state.activeView === "screenerView") loadScreenerSetup();
|
||||
@@ -619,10 +627,10 @@ function applyDashboard(payload, background = false) {
|
||||
}
|
||||
|
||||
function dashboardSourceLabel(meta = {}) {
|
||||
if (meta.source === "demo") return "演示数据";
|
||||
if (meta.realtime && !["closed", "after_hours"].includes(String(meta.market_status || ""))) return "Tushare 实时行情";
|
||||
if (meta.source === "tushare") return "Tushare 盘后行情";
|
||||
return "本地行情";
|
||||
if (meta.realtime && !["closed", "after_hours"].includes(String(meta.market_status || ""))) return "盘中行情";
|
||||
if (meta.carried_forward) return "最近收盘行情";
|
||||
if (meta.market_status === "historical") return "历史行情";
|
||||
return "收盘行情";
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
@@ -1140,7 +1148,7 @@ async function loadDragonTiger(force = false) {
|
||||
const requestedDate = elements.tradeDate.value;
|
||||
if (
|
||||
!force
|
||||
&& ["success", "demo"].includes(state.dragonTiger?.meta?.status)
|
||||
&& ["success", "empty", "partial", "unavailable"].includes(state.dragonTiger?.meta?.status)
|
||||
&& (state.dragonTiger?.meta?.requested_date || state.dragonTiger?.meta?.trade_date) === requestedDate
|
||||
) {
|
||||
renderDragonTiger();
|
||||
@@ -1159,7 +1167,7 @@ async function loadDragonTiger(force = false) {
|
||||
? "当日暂无公开游资明细"
|
||||
: payload.meta.status === "partial"
|
||||
? "当日有龙虎榜,暂无命名游资明细"
|
||||
: payload.meta.source === "tushare" ? "龙虎榜明细" : "演示数据";
|
||||
: payload.meta.status === "unavailable" ? "龙虎榜数据暂不可用" : "龙虎榜明细";
|
||||
setStatus(`${statusLabel} · 龙虎榜已加载`);
|
||||
} catch (error) {
|
||||
showToast(error.message || "龙虎榜加载失败");
|
||||
@@ -1202,8 +1210,8 @@ function renderDragonTraderList() {
|
||||
|
||||
const container = document.querySelector("#dragonTraderList");
|
||||
let emptyMessage = "没有符合当前条件的游资操作";
|
||||
if (!Array.isArray(payload.traders)) emptyMessage = "后端服务尚未重启,当前接口仍是旧版龙虎榜结构";
|
||||
else if (payload.meta?.status === "error") emptyMessage = "游资接口调用失败,请查看上方提示并检查 Tushare 积分权限";
|
||||
if (!Array.isArray(payload.traders)) emptyMessage = "龙虎榜数据格式暂不可用,请稍后重试";
|
||||
else if (["error", "unavailable"].includes(payload.meta?.status)) emptyMessage = "龙虎榜数据暂不可用,请稍后重试";
|
||||
else if (payload.meta?.status === "empty") emptyMessage = "该交易日暂无游资每日明细";
|
||||
else if (payload.meta?.status === "partial") emptyMessage = `当日有 ${number(payload.summary?.official_stock_count)} 只股票上榜,但暂无可识别的游资明细`;
|
||||
if (!traders.some((item) => item.id === state.selectedDragonTraderId)) {
|
||||
@@ -1600,8 +1608,7 @@ function renderScreenerSetup() {
|
||||
? `${displayCompactDate(setup.factor_data.start_date)} 至 ${displayCompactDate(setup.factor_data.end_date)}`
|
||||
: "尚未达到 21 个交易日");
|
||||
setText("factorTaskStatus", setup.factor_data.ready ? `已就绪 · ${number(setup.factor_data.date_count)} 日` : "需要同步");
|
||||
const compilerText = setup.llm.configured ? "模型编译就绪" : "本地编译就绪";
|
||||
setText("compilerStatus", compilerText);
|
||||
setText("compilerStatus", "策略生成已就绪");
|
||||
setText(
|
||||
"screenerRunStatus",
|
||||
state.screenerResult ? `已有结果 · ${(state.screenerResult.candidates || []).length} 只` : "等待执行",
|
||||
@@ -1721,10 +1728,7 @@ async function compileStrategy() {
|
||||
document.querySelector("#strategyNameInput").value = strategy.name;
|
||||
document.querySelector("#strategyDescriptionInput").value = strategy.description;
|
||||
document.querySelector("#formulaEditor").value = JSON.stringify(strategy.formula, null, 2);
|
||||
setText(
|
||||
"compilerStatus",
|
||||
strategy.compiler === "local" ? "本地编译完成" : "模型编译完成",
|
||||
);
|
||||
setText("compilerStatus", "策略生成完成");
|
||||
if (strategy.notice) showToast(strategy.notice);
|
||||
setStatus("选股策略已编译");
|
||||
} catch (error) {
|
||||
@@ -1923,11 +1927,10 @@ async function sendMentorQuestion(event) {
|
||||
question,
|
||||
history,
|
||||
});
|
||||
const modelRole = payload.compiler === "fallback" ? "辅助模型" : "主模型";
|
||||
state.mentorMessages.push({
|
||||
role: "assistant",
|
||||
content: payload.answer,
|
||||
meta: `${displayCompactDate(payload.data_trade_date)} · ${modelRole} ${payload.model} · ${number(payload.latency_ms)}ms`,
|
||||
meta: `${displayCompactDate(payload.data_trade_date)} · 回答完成`,
|
||||
});
|
||||
if (payload.notice) showMentorNotice(payload.notice);
|
||||
setStatus("问师回答完成");
|
||||
@@ -2756,12 +2759,13 @@ function renderSectorPhaseOverrides(items) {
|
||||
<div class="sector-phase-override-row">
|
||||
<span class="phase-symbol phase-${phaseClass(item.element)}">${escapeHtml(item.element)}</span>
|
||||
<strong>${escapeHtml(item.name)}</strong>
|
||||
${canManage ? `<button class="icon-button" type="button" data-sector-phase-delete="${escapeHtml(item.name)}" title="删除手动归类" aria-label="删除 ${escapeHtml(item.name)} 的手动归类">×</button>` : ""}
|
||||
${canManage ? `<button class="icon-button" type="button" data-sector-phase-delete="${escapeHtml(item.name)}" title="删除手动归类" aria-label="删除 ${escapeHtml(item.name)} 的手动归类"><i data-lucide="trash-2"></i></button>` : ""}
|
||||
</div>
|
||||
`).join("") : '<p class="sector-phase-empty">暂无手动归类</p>';
|
||||
container.querySelectorAll("[data-sector-phase-delete]").forEach((button) => {
|
||||
button.addEventListener("click", () => deleteSectorPhaseOverride(button.dataset.sectorPhaseDelete));
|
||||
});
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
async function saveSectorPhaseOverride(event) {
|
||||
@@ -2933,10 +2937,9 @@ async function interpretHeaven(mode) {
|
||||
if (mode === "trend" && state.heavenManualData) payload.manual_data = state.heavenManualData;
|
||||
if (mode === "heart") payload.lines = state.heartLines;
|
||||
const result = await apiRequest("/api/heaven/interpret", "POST", payload);
|
||||
const modelRole = result.compiler === "fallback" ? "辅助模型" : "主模型";
|
||||
state.heavenInterpretations[mode] = {
|
||||
answer: result.answer,
|
||||
meta: `${modelRole} ${result.model} · ${number(result.latency_ms)}ms`,
|
||||
meta: mode === "trend" ? "观势已成" : mode === "fortune" ? "观气已成" : "解卦已成",
|
||||
};
|
||||
if (result.notice) showHeavenNotice(result.notice);
|
||||
if (mode === "heart") {
|
||||
@@ -3525,7 +3528,7 @@ function renderScreenerResult() {
|
||||
setText(
|
||||
"screenerDisclaimer",
|
||||
meta.realtime
|
||||
? `rt_k 实时截面 · 历史因子截至 ${displayCompactDate(meta.history_cutoff)} · ${result.disclaimer}`
|
||||
? `盘中行情 · 历史样本截至 ${displayCompactDate(meta.history_cutoff)} · ${result.disclaimer}`
|
||||
: `盘后数据 ${displayCompactDate(meta.trade_date)} · ${result.disclaimer}`,
|
||||
);
|
||||
document.querySelector("#screenerEmpty").hidden = candidates.length > 0;
|
||||
@@ -4083,9 +4086,7 @@ function renderStockPreview(payload) {
|
||||
setText("stockPreviewDate", payload.meta?.trade_date || elements.tradeDate.value);
|
||||
document.querySelector("#stockPreviewLoading").hidden = true;
|
||||
const intradayAvailable = (payload.intraday || []).length > 0;
|
||||
const source = payload.meta?.realtime
|
||||
? "Tushare 实时行情"
|
||||
: payload.meta?.source === "tushare" ? "Tushare 日K" : "演示日K";
|
||||
const source = payload.meta?.realtime ? "盘中行情" : "日 K 行情";
|
||||
setText(
|
||||
"stockPreviewSource",
|
||||
intradayAvailable ? `${source} · 1分钟` : `${source} · 分时不可用`,
|
||||
@@ -4478,7 +4479,7 @@ async function openStock(code, fallback = null) {
|
||||
setText("detailName", stock.name || row.name);
|
||||
setText("detailPrice", formatNumber(stock.price || row.price, 2));
|
||||
setText("detailChange", `${signed(stock.change ?? row.change)}%`);
|
||||
setText("chartSource", `${payload.meta.source === "tushare" ? "Tushare" : "演示"} · ${payload.prices.length} 个交易日`);
|
||||
setText("chartSource", `日 K 行情 · ${payload.prices.length} 个交易日`);
|
||||
renderMoneyflow(payload.moneyflow || {});
|
||||
renderStockNotes(payload.notes || []);
|
||||
updateWatchButton();
|
||||
@@ -4705,8 +4706,8 @@ async function openSettings(panel = "profile") {
|
||||
updateAccountIdentityBadges(membership);
|
||||
applyMembershipAccess();
|
||||
}
|
||||
const sourceLabel = membership.active && access.platform_configured ? "平台会员模型" : "尚未开通会员算力";
|
||||
status.textContent = `公共行情${payload.configured ? "已就绪" : "使用演示数据"} · ${sourceLabel}`;
|
||||
const sourceLabel = membership.active && access.platform_configured ? "智能功能已就绪" : "智能功能尚未开通";
|
||||
status.textContent = `公共行情${payload.configured ? "已就绪" : "等待管理员配置"} · ${sourceLabel}`;
|
||||
status.classList.toggle("connected", Boolean(payload.configured));
|
||||
setText("membershipBadge", membership.subscribed ? "会员有效" : membership.is_admin ? "管理员权限" : "普通用户");
|
||||
setText("membershipStateValue", membership.subscribed ? "已开通" : membership.is_admin ? "管理员可用" : "未开通");
|
||||
@@ -4717,11 +4718,11 @@ async function openSettings(panel = "profile") {
|
||||
? `${membership.plan || "会员"}${membership.expires_at ? ` · 有效至 ${membershipDateDisplay(membership.expires_at, true)}` : " · 长期有效"}`
|
||||
: membership.is_admin
|
||||
? "管理员拥有智能功能管理权限,但不会因此显示为已开通会员。"
|
||||
: "开通会员后可使用智能选股、问师、问天及平台 LLM 算力。");
|
||||
setText("membershipQuotaHint", `会员默认每日 LLM 用量 ${number(access.daily_limit)} 次,由管理员统一设置。`);
|
||||
: "开通会员后可使用智能选股、问师、问天等智能功能。");
|
||||
setText("membershipQuotaHint", `会员默认每日智能分析额度 ${number(access.daily_limit)} 次,由管理员统一设置。`);
|
||||
setText("membershipUsage", membership.active
|
||||
? `今日已用 ${number(access.used_today)} 次`
|
||||
: "今日 LLM 用量:--");
|
||||
: "今日智能分析:--");
|
||||
setText("membershipUsageSummary", membership.active ? `${number(access.used_today)} / ${number(access.daily_limit)}` : "--");
|
||||
setText("membershipRemainingUsage", membership.is_admin ? "不限" : membership.active ? `${number(access.remaining_calls)} 次` : "--");
|
||||
const birth = payload.birth_profile || {};
|
||||
@@ -5142,16 +5143,6 @@ function signed(value) {
|
||||
return `${parsed > 0 ? "+" : ""}${formatNumber(parsed, 2)}`;
|
||||
}
|
||||
|
||||
function formatNumber(value, digits = 0) {
|
||||
return new Intl.NumberFormat("zh-CN", { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(number(value));
|
||||
}
|
||||
|
||||
function formatTimestamp(value) {
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return "--";
|
||||
return parsed.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
function dashboardDataTimestamp(meta = {}) {
|
||||
const tradeDate = displayCompactDate(meta.trade_date);
|
||||
if (tradeDate === "--") return "--";
|
||||
@@ -5174,12 +5165,6 @@ function formatMoneyMillion(value) {
|
||||
return `${sign}${formatNumber(parsed * 100, 0)} 万`;
|
||||
}
|
||||
|
||||
function displayCompactDate(value) {
|
||||
const text = String(value || "").replaceAll("-", "");
|
||||
if (text.length !== 8) return value || "--";
|
||||
return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
async function apiRequest(url, method = "GET", body = null, requestOptions = {}) {
|
||||
const options = { method, headers: {}, signal: requestOptions.signal };
|
||||
if (method !== "GET" && state.csrfToken) {
|
||||
@@ -5427,34 +5412,3 @@ function animateRows(container) {
|
||||
function waitForMotion(duration) {
|
||||
return new Promise((resolve) => setTimeout(resolve, motionEnabled() ? duration : 0));
|
||||
}
|
||||
|
||||
function number(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function clamp(value, minimum, maximum) {
|
||||
return Math.min(maximum, Math.max(minimum, number(value)));
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "").replace(/[&<>"']/g, (character) => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
||||
})[character]);
|
||||
}
|
||||
|
||||
function todayString() {
|
||||
return localDateString(new Date());
|
||||
}
|
||||
|
||||
function localDateString(value) {
|
||||
const year = value.getFullYear();
|
||||
const month = String(value.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(value.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function parseLocalDate(value) {
|
||||
const [year, month, day] = value.split("-").map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user