From 42a613432d4cae627cc36ed4c90c5f2e037a1747 Mon Sep 17 00:00:00 2001 From: tbphp Date: Mon, 5 Oct 2026 09:25:22 +0800 Subject: [PATCH 1/2] fix(web): Recover startup failures at home --- web/src/frontends/modern/bootstrap.ts | 7 +++- web/src/frontends/modern/router.ts | 6 ++-- web/src/main.ts | 46 +++++++++++---------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/web/src/frontends/modern/bootstrap.ts b/web/src/frontends/modern/bootstrap.ts index c48e94e01..7bb69f5b2 100644 --- a/web/src/frontends/modern/bootstrap.ts +++ b/web/src/frontends/modern/bootstrap.ts @@ -47,6 +47,11 @@ export async function bootstrap(): Promise { .use(VueQueryPlugin, { queryClient }) .use(router) app.onUnmount(preferences.dispose) - await router.isReady() + try { + await router.isReady() + } catch (error) { + console.error('Failed to load the initial page; opening home.', error) + await router.replace({ name: 'modern-home' }) + } app.mount('#app') } diff --git a/web/src/frontends/modern/router.ts b/web/src/frontends/modern/router.ts index 07a2e5c77..22906b7fd 100644 --- a/web/src/frontends/modern/router.ts +++ b/web/src/frontends/modern/router.ts @@ -4,6 +4,8 @@ import { createRouter, createWebHistory, type RouterHistory } from 'vue-router' import { navigationItems, pagePath } from './app/navigation' import { loginLocation } from './app/redirect' import type { AuthSession } from './features/auth/auth-session' +import LoginView from './features/auth/LoginView.vue' +import HomeView from './features/home/HomeView.vue' export function createModernRouter( session: Pick, @@ -21,7 +23,7 @@ export function createModernRouter( item.id === 'proxies' ? () => import('./features/proxies/ProxiesView.vue') : item.id === 'home' - ? () => import('./features/home/HomeView.vue') + ? HomeView : item.id === 'settings' ? () => import('./features/settings/SettingsView.vue') : item.id === 'groups' @@ -81,7 +83,7 @@ export function createModernRouter( { path: pagePath('login'), name: 'modern-login', - component: () => import('./features/auth/LoginView.vue'), + component: LoginView, meta: { titleKey: 'auth.title' }, }, { diff --git a/web/src/main.ts b/web/src/main.ts index 324cff23d..31d453fca 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,5 +1,6 @@ import { getPreferredFrontend } from '@shared/frontend/preference' -import { getBrowserLocale } from '@shared/preferences/locale' + +const startupRecoveryKey = 'gpt-load.startup-recovery' async function bootstrap(): Promise { const frontend = await getPreferredFrontend() @@ -9,33 +10,24 @@ async function bootstrap(): Promise { ? await import('./frontends/classic/bootstrap') : await import('./frontends/modern/bootstrap') await module.bootstrap() + try { + window.sessionStorage.removeItem(startupRecoveryKey) + } catch { + // 恢复标记不可用时,不影响已启动的界面。 + } } -function showStartupFailure(): void { - const labels = { - 'zh-CN': { - message: '无法加载界面,请重试。', - retry: '重新加载', - }, - 'en-US': { - message: 'Unable to load the interface. Please retry.', - retry: 'Reload', - }, - 'ja-JP': { - message: '画面を読み込めません。再試行してください。', - retry: '再読み込み', - }, - }[getBrowserLocale()] - const root = document.getElementById('app') - if (!root) return - const message = document.createElement('p') - message.setAttribute('role', 'alert') - message.textContent = labels.message - const retry = document.createElement('button') - retry.type = 'button' - retry.textContent = labels.retry - retry.addEventListener('click', () => window.location.reload()) - root.replaceChildren(message, retry) +function recoverStartup(error: unknown): void { + console.error('Failed to start the interface.', error) + try { + // 标记跨导航保留;只有成功启动才清除,避免资源持续不可用时循环刷新。 + if (window.sessionStorage.getItem(startupRecoveryKey) === '1') return + window.sessionStorage.setItem(startupRecoveryKey, '1') + } catch { + // 无法保存标记时,只从其他页面跳到主页,主页失败不再刷新。 + if (window.location.pathname === '/') return + } + window.location.replace('/') } -void bootstrap().catch(showStartupFailure) +void bootstrap().catch(recoverStartup) From 12525775de87b716fae2699e1c22b503838f361b Mon Sep 17 00:00:00 2001 From: tbphp Date: Mon, 5 Oct 2026 09:40:44 +0800 Subject: [PATCH 2/2] fix(web): Handle repeated startup failures --- web/src/main.ts | 61 ++++++++++++++++++++++++++++++---- web/src/startup-recovery.css | 64 ++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 web/src/startup-recovery.css diff --git a/web/src/main.ts b/web/src/main.ts index 31d453fca..34d5e2554 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,4 +1,6 @@ import { getPreferredFrontend } from '@shared/frontend/preference' +import { getBrowserLocale } from '@shared/preferences/locale' +import './startup-recovery.css' const startupRecoveryKey = 'gpt-load.startup-recovery' @@ -10,24 +12,71 @@ async function bootstrap(): Promise { ? await import('./frontends/classic/bootstrap') : await import('./frontends/modern/bootstrap') await module.bootstrap() + clearStartupRecovery() +} + +function clearStartupRecovery(): void { try { window.sessionStorage.removeItem(startupRecoveryKey) } catch { - // 恢复标记不可用时,不影响已启动的界面。 + // 存储不可用时,主页路径仍会限制自动恢复次数。 } } +function showStartupRecovery(): void { + const locale = getBrowserLocale() + const labels = { + 'zh-CN': { + message: '页面暂时未能打开。', + retry: '重新打开主页', + }, + 'en-US': { + message: 'The page is temporarily unavailable.', + retry: 'Reload home', + }, + 'ja-JP': { + message: '現在ページを開けません。', + retry: 'ホームを再読み込み', + }, + }[locale] + const root = document.getElementById('app') + if (!root) return + document.documentElement.lang = locale + const main = document.createElement('main') + main.className = 'startup-recovery' + const card = document.createElement('section') + card.className = 'startup-recovery-card' + const title = document.createElement('h1') + title.textContent = 'GPT-Load' + const message = document.createElement('p') + message.setAttribute('role', 'alert') + message.textContent = labels.message + const retry = document.createElement('button') + retry.type = 'button' + retry.textContent = labels.retry + retry.addEventListener('click', () => { + clearStartupRecovery() + window.location.replace('/') + }) + card.append(title, message, retry) + main.append(card) + root.replaceChildren(main) + retry.focus() +} + function recoverStartup(error: unknown): void { console.error('Failed to start the interface.', error) + let shouldReload: boolean try { - // 标记跨导航保留;只有成功启动才清除,避免资源持续不可用时循环刷新。 - if (window.sessionStorage.getItem(startupRecoveryKey) === '1') return - window.sessionStorage.setItem(startupRecoveryKey, '1') + // 标记跨导航保留,避免资源持续不可用时循环刷新。 + shouldReload = window.sessionStorage.getItem(startupRecoveryKey) !== '1' + if (shouldReload) window.sessionStorage.setItem(startupRecoveryKey, '1') } catch { // 无法保存标记时,只从其他页面跳到主页,主页失败不再刷新。 - if (window.location.pathname === '/') return + shouldReload = window.location.pathname !== '/' } - window.location.replace('/') + if (shouldReload) window.location.replace('/') + else showStartupRecovery() } void bootstrap().catch(recoverStartup) diff --git a/web/src/startup-recovery.css b/web/src/startup-recovery.css new file mode 100644 index 000000000..4c7917433 --- /dev/null +++ b/web/src/startup-recovery.css @@ -0,0 +1,64 @@ +/* 入口恢复界面独立于两套业务前端,所有样式仅作用于恢复容器。 */ +.startup-recovery { + position: fixed; + inset: 0; + display: grid; + overflow: auto; + box-sizing: border-box; + padding: 24px; + place-items: center; + color-scheme: light dark; + background: Canvas; + color: CanvasText; + font: + 16px / 1.5 system-ui, + sans-serif; +} + +html[data-theme='light'] .startup-recovery { + color-scheme: light; +} + +html[data-theme='dark'] .startup-recovery { + color-scheme: dark; +} + +.startup-recovery-card { + display: grid; + box-sizing: border-box; + width: min(100%, 360px); + gap: 16px; + border: 1px solid color-mix(in srgb, CanvasText 15%, Canvas); + border-radius: 16px; + padding: 32px; + text-align: center; +} + +.startup-recovery h1 { + max-width: none; + margin: 0; + font: + 600 24px / 1.3 system-ui, + sans-serif; + letter-spacing: normal; +} + +.startup-recovery p { + margin: 0; + color: GrayText; +} + +.startup-recovery button { + min-height: 44px; + border: 1px solid GrayText; + border-radius: 8px; + background: ButtonFace; + color: ButtonText; + font: inherit; + cursor: pointer; +} + +.startup-recovery button:focus-visible { + outline: 2px solid Highlight; + outline-offset: 4px; +}