diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs index 276ebe4..120e959 100644 --- a/tests/strategy_switch_worker_validation.mjs +++ b/tests/strategy_switch_worker_validation.mjs @@ -10,6 +10,10 @@ const indexHtml = readFileSync(resolve(root, "web/strategy-switch-console/index. const renderPlatformsBody = indexHtml.match(/function renderPlatforms\(\) \{([\s\S]*?)\n \}/)?.[1] || ""; assert.ok(!renderPlatformsBody.includes("syncStrategyForAccount(")); assert.equal(indexHtml.includes(".innerHTML"), false); +assert.ok(indexHtml.includes('')); +assert.ok(indexHtml.includes('id="boot-screen"')); +assert.ok(indexHtml.includes('id="app-shell"')); +assert.match(indexHtml, /body\.app-loading \.shell\s*\{\s*display: none;/); const headers = __test.responseHeaders({ "Content-Type": "text/html; charset=utf-8" }); assert.equal(headers.get("X-Frame-Options"), "DENY"); diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html index 20a33f5..3154cd9 100644 --- a/web/strategy-switch-console/index.html +++ b/web/strategy-switch-console/index.html @@ -132,6 +132,69 @@ gap: 16px; } + body.app-loading .shell { + display: none; + } + + body:not(.app-loading) .boot-screen { + display: none; + } + + .boot-screen { + width: min(680px, calc(100vw - 36px)); + min-height: calc(100svh - 72px); + margin: 0 auto; + display: grid; + align-items: center; + padding: 34px 0; + } + + .boot-panel { + display: grid; + gap: 13px; + padding: 8px 0; + } + + .boot-kicker { + color: var(--accent); + font-size: 12px; + line-height: 1.2; + font-weight: 800; + text-transform: uppercase; + } + + .boot-panel h2 { + color: var(--ink); + font-size: clamp(28px, 5vw, 44px); + line-height: 1.04; + font-weight: 820; + } + + .boot-panel p { + color: var(--muted); + font-size: 14px; + line-height: 1.5; + max-width: 520px; + } + + .loading-track { + width: 100%; + height: 5px; + overflow: hidden; + border-radius: 999px; + background: #dde4ea; + } + + .loading-track::before { + content: ""; + display: block; + width: 42%; + height: 100%; + border-radius: inherit; + background: var(--accent); + animation: loading-sweep 1.05s ease-in-out infinite; + } + .pill, .btn { min-height: 36px; @@ -536,6 +599,12 @@ width: 100%; } + .boot-screen { + width: min(100% - 28px, 680px); + min-height: calc(100svh - 122px); + padding-top: 22px; + } + .shell { width: min(100% - 28px, 1160px); padding-top: 18px; @@ -580,10 +649,26 @@ transform: translateY(0); } } + + @keyframes loading-sweep { + from { + transform: translateX(-110%); + } + to { + transform: translateX(260%); + } + } + } + + @media (prefers-reduced-motion: reduce) { + .loading-track::before { + animation: none; + transform: translateX(0); + } } - +

策略切换

@@ -597,7 +682,16 @@

策略切换

-
+
+
+ 初始化控制台 +

读取策略配置

+

正在读取登录状态、账号配置和当前状态。

+ +
+
+ +
@@ -734,6 +828,13 @@

切换摘要

zh: { appTitle: "策略切换", appSubtitle: "选平台、目标账号和策略,一次执行完成切换。", + bootKicker: "初始化控制台", + bootTitle: "读取策略配置", + bootMessage: "正在读取登录状态、账号配置和当前状态。", + bootStrategy: "正在读取策略目录。", + bootSession: "正在验证登录状态。", + bootConfig: "正在读取账号配置和当前状态。", + bootPublic: "公开预览已就绪。", login: "登录", logout: "退出", signedInAs: "已登录 {login}", @@ -787,6 +888,13 @@

切换摘要

en: { appTitle: "Strategy Switch", appSubtitle: "Pick platform, target account, and strategy. One action switches everything.", + bootKicker: "Starting console", + bootTitle: "Loading strategy config", + bootMessage: "Reading session, account config, and current state.", + bootStrategy: "Reading strategy catalog.", + bootSession: "Checking sign-in status.", + bootConfig: "Reading account config and current state.", + bootPublic: "Public preview is ready.", login: "Sign in", logout: "Sign out", signedInAs: "Signed in as {login}", @@ -848,6 +956,8 @@

切换摘要

const state = { selected: "longbridge", lang: initialLang, + appReady: false, + bootMessageKey: "bootMessage", auth: { available: false, allowed: false, admin: false, login: null }, accountOptions: clone(defaultAccountOptions), currentStrategies: {}, @@ -1263,15 +1373,23 @@

切换摘要

note.classList.toggle("warning", state.auth.allowed && !loadingConfig && (!hasPrivateAccounts || !hasValidStrategy)); } + function renderAppVisibility() { + document.body.classList.toggle("app-loading", !state.appReady); + el("boot-message").textContent = t(state.bootMessageKey); + } + function render() { applyLanguage(); renderPlatforms(); renderControls(); renderSummary(); renderAuth(); + renderAppVisibility(); } async function refreshSession() { + state.bootMessageKey = "bootSession"; + render(); try { const response = await fetch("/api/session", { cache: "no-store" }); if (!response.ok) throw new Error("no backend"); @@ -1285,11 +1403,18 @@

切换摘要

} catch { state.auth = { available: false, allowed: false, admin: false, login: null }; } - render(); - await refreshConfig(); + if (state.auth.allowed) { + await refreshConfig(); + } else { + state.bootMessageKey = "bootPublic"; + state.appReady = true; + render(); + } } async function refreshStrategyProfiles() { + state.bootMessageKey = "bootStrategy"; + render(); try { const response = await fetch("/api/strategy-profiles", { cache: "no-store" }); if (!response.ok) throw new Error("no strategy profiles"); @@ -1306,6 +1431,7 @@

切换摘要

async function refreshConfig() { if (!state.auth.available || !state.auth.allowed) return; state.configSource = "loading"; + state.bootMessageKey = "bootConfig"; render(); try { const response = await fetch("/api/config", { cache: "no-store" }); @@ -1320,11 +1446,16 @@

切换摘要

ensureAccountSelection(platform); syncStrategyForAccount(platform); } - render(); + } else { + state.configSource = "default"; + state.currentStrategies = {}; } } catch { state.configSource = "default"; state.currentStrategies = {}; + } finally { + state.appReady = true; + render(); } } @@ -1454,8 +1585,12 @@

切换摘要

applyStrategyProfiles(defaultStrategyProfiles); for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform); render(); - refreshStrategyProfiles(); - refreshSession(); + boot(); + + async function boot() { + await refreshStrategyProfiles(); + await refreshSession(); + } diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js index e68cea5..d70542e 100644 --- a/web/strategy-switch-console/page_asset.js +++ b/web/strategy-switch-console/page_asset.js @@ -1,2 +1,2 @@ // Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand. -export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\n \n\n\n
\n
\n

策略切换

\n

选平台、目标账号和策略,一次执行完成切换。

\n
\n
\n \n \n \n \n
\n
\n\n
\n \n\n
\n
\n
\n 当前平台\n

LongBridge

\n
\n\n
\n \n\n \n\n
\n 模式\n
\n \n \n
\n
\n
\n\n
\n \n

登录后才可执行切换。

\n

\n
\n
\n\n \n
\n
\n\n \n\n\n"; +export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\n \n\n\n
\n
\n

策略切换

\n

选平台、目标账号和策略,一次执行完成切换。

\n
\n
\n \n \n \n \n
\n
\n\n
\n
\n 初始化控制台\n

读取策略配置

\n

正在读取登录状态、账号配置和当前状态。

\n
\n
\n
\n\n
\n \n\n
\n
\n
\n 当前平台\n

LongBridge

\n
\n\n
\n \n\n \n\n
\n 模式\n
\n \n \n
\n
\n
\n\n
\n \n

登录后才可执行切换。

\n

\n
\n
\n\n \n
\n
\n\n \n\n\n";