From e7f2932409af0bb3ddc409e23cc0821356c77d1c Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 31 Aug 2026 09:50:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=8B=AC=E7=AB=8B=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A3=80=E6=B5=8B=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/blank.vue | 2 +- src/locales/en-US.ts | 8 +- src/locales/zh-CN.ts | 8 +- src/locales/zh-TW.ts | 8 +- src/pages/__tests__/initialize.spec.ts | 21 +-- src/pages/__tests__/service-status.spec.ts | 54 ++++++ src/pages/initialize.vue | 24 +-- src/pages/service-status.vue | 205 +++++++++++++++++++++ src/router/__tests__/auth-guard.spec.ts | 25 ++- src/router/index.ts | 17 +- 10 files changed, 324 insertions(+), 48 deletions(-) create mode 100644 src/pages/__tests__/service-status.spec.ts create mode 100644 src/pages/service-status.vue diff --git a/src/layouts/blank.vue b/src/layouts/blank.vue index f19ddfd5..17006b3a 100644 --- a/src/layouts/blank.vue +++ b/src/layouts/blank.vue @@ -1,7 +1,7 @@ diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 3531d95e..07bbfa07 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -4594,7 +4594,6 @@ export default { title: 'Welcome to MoviePilot', subtitle: 'Create the single super administrator account before you start managing your media library.', checking: 'Checking instance status…', - statusRetrying: 'The service is starting. Retrying automatically…', accountSection: 'Administrator account', username: 'Super administrator username', usernamePlaceholder: 'For example: admin', @@ -4617,4 +4616,11 @@ export default { saved: 'Initialization complete. Sign in with your new account.', saveFailed: 'Initialization failed. Check the service and try again.', }, + serviceStatus: { + kicker: 'Service status', + title: 'MoviePilot is starting', + subtitle: 'Waiting for the backend service. Your account and existing data are not affected.', + checking: 'Checking the service connection…', + automatic: 'You will continue to sign-in or setup automatically once connected', + }, } diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 60eb33e5..a9822f9e 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -4521,7 +4521,6 @@ export default { title: '欢迎使用 MoviePilot', subtitle: '先创建唯一的超级管理员,再开始管理你的媒体库。整个过程只需一步。', checking: '正在检查实例状态…', - statusRetrying: '服务正在启动,稍后将自动重试…', accountSection: '管理员账号', username: '超级管理员用户名', usernamePlaceholder: '例如:admin', @@ -4544,4 +4543,11 @@ export default { saved: '初始化完成,请使用新账号登录', saveFailed: '初始化失败,请检查服务状态后重试', }, + serviceStatus: { + kicker: '服务状态', + title: 'MoviePilot 正在启动', + subtitle: '正在等待后端服务就绪,这不会影响你的账号和已有数据。', + checking: '正在检测服务连接…', + automatic: '连接成功后将自动进入登录或初始化界面', + }, } diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 9af65c96..ea21f2b4 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -4518,7 +4518,6 @@ export default { title: '歡迎使用 MoviePilot', subtitle: '先建立唯一的超級管理員,再開始管理你的媒體庫。整個過程只需一步。', checking: '正在檢查實例狀態…', - statusRetrying: '服務正在啟動,稍後將自動重試…', accountSection: '管理員帳號', username: '超級管理員使用者名稱', usernamePlaceholder: '例如:admin', @@ -4541,4 +4540,11 @@ export default { saved: '初始化完成,請使用新帳號登入', saveFailed: '初始化失敗,請檢查服務狀態後重試', }, + serviceStatus: { + kicker: '服務狀態', + title: 'MoviePilot 正在啟動', + subtitle: '正在等待後端服務就緒,這不會影響你的帳號和現有資料。', + checking: '正在偵測服務連線…', + automatic: '連線成功後將自動進入登入或初始化頁面', + }, } diff --git a/src/pages/__tests__/initialize.spec.ts b/src/pages/__tests__/initialize.spec.ts index ea9f5b8d..445d0c33 100644 --- a/src/pages/__tests__/initialize.spec.ts +++ b/src/pages/__tests__/initialize.spec.ts @@ -2,7 +2,7 @@ import InitializePage from '@/pages/initialize.vue' import { renderWithProviders } from '@tests/support/render' import { screen } from '@testing-library/vue' import { nextTick } from 'vue' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ api: { post: vi.fn() }, @@ -37,27 +37,16 @@ describe('initialization page', () => { mocks.router.replace.mockReset() }) - afterEach(() => { - vi.useRealTimers() - }) - - it('waits for the backend and retries before enabling the form', async () => { - vi.useFakeTimers() - mocks.getInitializationState.mockRejectedValueOnce(new Error('service starting')).mockResolvedValueOnce(false) + it('moves service availability checks to the dedicated status page', async () => { + mocks.getInitializationState.mockRejectedValue(new Error('service starting')) await renderWithProviders(InitializePage) await nextTick() await Promise.resolve() await nextTick() - expect(screen.getByText('服务正在启动,稍后将自动重试…')).toBeInTheDocument() + expect(mocks.getInitializationState).toHaveBeenCalledOnce() + expect(mocks.router.replace).toHaveBeenCalledWith('/service-status') expect(screen.getByRole('textbox', { name: '超级管理员用户名' })).toBeDisabled() - - await vi.advanceTimersByTimeAsync(1500) - await nextTick() - - expect(mocks.getInitializationState).toHaveBeenCalledTimes(2) - expect(screen.queryByText('服务正在启动,稍后将自动重试…')).not.toBeInTheDocument() - expect(screen.getByRole('textbox', { name: '超级管理员用户名' })).toBeEnabled() }) }) diff --git a/src/pages/__tests__/service-status.spec.ts b/src/pages/__tests__/service-status.spec.ts new file mode 100644 index 00000000..ba68f8db --- /dev/null +++ b/src/pages/__tests__/service-status.spec.ts @@ -0,0 +1,54 @@ +import ServiceStatusPage from '@/pages/service-status.vue' +import { renderWithProviders } from '@tests/support/render' +import { screen } from '@testing-library/vue' +import { nextTick } from 'vue' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + getInitializationState: vi.fn(), + router: { replace: vi.fn() }, +})) + +vi.mock('@/router', () => ({ + default: mocks.router, +})) + +vi.mock('@/utils/initialization', () => ({ + getInitializationState: mocks.getInitializationState, +})) + +describe('service status page', () => { + beforeEach(() => { + mocks.getInitializationState.mockReset() + mocks.router.replace.mockReset() + }) + + it('keeps checking until the initialized service becomes available', async () => { + vi.useFakeTimers() + mocks.getInitializationState.mockRejectedValueOnce(new Error('service starting')).mockResolvedValueOnce(true) + + await renderWithProviders(ServiceStatusPage) + await nextTick() + await Promise.resolve() + + expect(screen.getByRole('heading', { name: 'MoviePilot 正在启动' })).toBeInTheDocument() + expect(screen.getByText('正在等待后端服务就绪,这不会影响你的账号和已有数据。')).toBeInTheDocument() + expect(mocks.router.replace).not.toHaveBeenCalled() + + await vi.advanceTimersByTimeAsync(1500) + await nextTick() + + expect(mocks.getInitializationState).toHaveBeenCalledTimes(2) + expect(mocks.router.replace).toHaveBeenCalledWith('/login') + }) + + it('opens initialization only after the service confirms there is no user', async () => { + mocks.getInitializationState.mockResolvedValue(false) + + await renderWithProviders(ServiceStatusPage) + await nextTick() + await Promise.resolve() + + expect(mocks.router.replace).toHaveBeenCalledWith('/initialize') + }) +}) diff --git a/src/pages/initialize.vue b/src/pages/initialize.vue index 85f59e92..0ae7d826 100644 --- a/src/pages/initialize.vue +++ b/src/pages/initialize.vue @@ -27,13 +27,9 @@ const isPasswordVisible = ref(false) const isConfirmPasswordVisible = ref(false) const loading = ref(false) const checking = ref(true) -const statusRetrying = ref(false) const errorMessage = ref('') const apiKeyCopied = ref(false) const formRef = ref(null) -const INITIALIZATION_STATUS_RETRY_MS = 1500 -let statusRetryTimer: ReturnType | undefined -let disposed = false const form = ref({ username: '', @@ -44,9 +40,6 @@ const form = ref({ const currentTheme = computed(() => theme.global.name.value) const themeClass = computed(() => 'initialize-page--' + currentTheme.value) -const statusMessage = computed(() => - t(statusRetrying.value ? 'initialization.statusRetrying' : 'initialization.checking'), -) /** 使用浏览器密码学随机源生成一次性 API Key,避免把凭据交给第三方服务。 */ function generateApiKey(): string { @@ -80,23 +73,17 @@ function getErrorMessage(error: unknown): string { return getApiBusinessErrorMessage(error) || t('initialization.saveFailed') } -/** 持续确认实例状态;服务尚未就绪时保持表单锁定并自动重试。 */ +/** 进入表单前再次确认实例仍未初始化;服务不可达时交给独立状态页持续检测。 */ async function checkInitializationStatus() { try { const initialized = await getInitializationState(true) - if (disposed) return - statusRetrying.value = false if (initialized) { await router.replace('/login') return } checking.value = false } catch { - if (disposed) return - statusRetrying.value = true - statusRetryTimer = setTimeout(() => { - void checkInitializationStatus() - }, INITIALIZATION_STATUS_RETRY_MS) + await router.replace('/service-status') } } @@ -128,11 +115,6 @@ onMounted(() => { form.value.api_key = generateApiKey() void checkInitializationStatus() }) - -onBeforeUnmount(() => { - disposed = true - if (statusRetryTimer) clearTimeout(statusRetryTimer) -})