Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion web/src/frontends/modern/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export async function bootstrap(): Promise<void> {
.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')
}
6 changes: 4 additions & 2 deletions web/src/frontends/modern/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthSession, 'hasCredential' | 'getPrincipalType'>,
Expand All @@ -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'
Expand Down Expand Up @@ -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' },
},
{
Expand Down
63 changes: 52 additions & 11 deletions web/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { getPreferredFrontend } from '@shared/frontend/preference'
import { getBrowserLocale } from '@shared/preferences/locale'
import './startup-recovery.css'

const startupRecoveryKey = 'gpt-load.startup-recovery'

async function bootstrap(): Promise<void> {
const frontend = await getPreferredFrontend()
Expand All @@ -9,33 +12,71 @@ async function bootstrap(): Promise<void> {
? await import('./frontends/classic/bootstrap')
: await import('./frontends/modern/bootstrap')
await module.bootstrap()
clearStartupRecovery()
}

function clearStartupRecovery(): void {
try {
window.sessionStorage.removeItem(startupRecoveryKey)
} catch {
// 存储不可用时,主页路径仍会限制自动恢复次数。
}
}

function showStartupFailure(): void {
function showStartupRecovery(): void {
const locale = getBrowserLocale()
const labels = {
'zh-CN': {
message: '无法加载界面,请重试。',
retry: '重新加载',
message: '页面暂时未能打开。',
retry: '重新打开主页',
},
'en-US': {
message: 'Unable to load the interface. Please retry.',
retry: 'Reload',
message: 'The page is temporarily unavailable.',
retry: 'Reload home',
},
'ja-JP': {
message: '画面を読み込めません。再試行してください。',
retry: '再読み込み',
message: '現在ページを開けません。',
retry: 'ホームを再読み込み',
},
}[getBrowserLocale()]
}[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', () => window.location.reload())
root.replaceChildren(message, 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 {
// 标记跨导航保留,避免资源持续不可用时循环刷新。
shouldReload = window.sessionStorage.getItem(startupRecoveryKey) !== '1'
if (shouldReload) window.sessionStorage.setItem(startupRecoveryKey, '1')
} catch {
// 无法保存标记时,只从其他页面跳到主页,主页失败不再刷新。
shouldReload = window.location.pathname !== '/'
}
if (shouldReload) window.location.replace('/')
else showStartupRecovery()
}

void bootstrap().catch(showStartupFailure)
void bootstrap().catch(recoverStartup)
64 changes: 64 additions & 0 deletions web/src/startup-recovery.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading