diff --git a/src/web/components/SiteCreatedModal.test.tsx b/src/web/components/SiteCreatedModal.test.tsx index 875bdc3..97c172a 100644 --- a/src/web/components/SiteCreatedModal.test.tsx +++ b/src/web/components/SiteCreatedModal.test.tsx @@ -10,7 +10,7 @@ function collectText(node: ReactTestInstance): string { } describe('SiteCreatedModal', () => { - it('routes native dialog cancel events through onClose cleanup', async () => { + it('uses the shared centered modal shell and close button instead of a native dialog skin', async () => { const onChoice = vi.fn(); const onClose = vi.fn(); const root = create( @@ -21,15 +21,28 @@ describe('SiteCreatedModal', () => { />, ); - const dialog = root.root.findByType('dialog'); - expect(typeof dialog.props.onCancel).toBe('function'); + expect(root.root.findAllByType('dialog')).toHaveLength(0); + + const backdrop = root.root.find((node) => ( + typeof node.props.className === 'string' + && node.props.className.includes('modal-backdrop') + )); + const footer = root.root.find((node) => ( + typeof node.props.className === 'string' + && node.props.className.includes('modal-footer') + )); + const closeButton = root.root.find((node) => ( + node.type === 'button' + && node.props['aria-label'] === '关闭弹框' + )); + + expect(backdrop).toBeTruthy(); + expect(footer).toBeTruthy(); - const preventDefault = vi.fn(); await act(async () => { - dialog.props.onCancel({ preventDefault }); + closeButton.props.onClick(); }); - expect(preventDefault).toHaveBeenCalledTimes(1); expect(onClose).toHaveBeenCalledTimes(1); expect(onChoice).not.toHaveBeenCalled(); }); @@ -50,18 +63,43 @@ describe('SiteCreatedModal', () => { const choiceButtons = root.root.findAll((node) => ( node.type === 'button' && typeof node.props.onClick === 'function' + && node.props['aria-label'] !== '关闭弹框' && collectText(node) !== '稍后配置' )); - expect(choiceButtons.map((button) => collectText(button))).toEqual([ - '添加 API Key(推荐)', - '添加账号(用户名密码登录)', - ]); + expect(choiceButtons.map((button) => collectText(button))).toEqual( + expect.arrayContaining([ + '添加 API Key(推荐)', + '添加账号(用户名密码登录)', + ]), + ); + expect(choiceButtons.every((button) => !String(button.props.className || '').includes('btn-outline'))).toBe(true); + const sessionButton = choiceButtons.find((button) => collectText(button) === '添加账号(用户名密码登录)'); await act(async () => { - choiceButtons[1]!.props.onClick(); + sessionButton!.props.onClick(); }); expect(onChoice).toHaveBeenCalledWith('session'); }); + + it('uses the supplied session label for OAuth-style session actions', () => { + const root = create( + {}} + onClose={() => {}} + />, + ); + + const buttons = root.root.findAll((node) => ( + node.type === 'button' + && typeof node.props.onClick === 'function' + && node.props['aria-label'] !== '关闭弹框' + )); + + expect(buttons.some((button) => collectText(button) === '添加 OAuth 连接')).toBe(true); + }); }); diff --git a/src/web/components/SiteCreatedModal.tsx b/src/web/components/SiteCreatedModal.tsx index 0714b60..0c300e8 100644 --- a/src/web/components/SiteCreatedModal.tsx +++ b/src/web/components/SiteCreatedModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import CenteredModal from './CenteredModal.js'; import { getSiteInitializationPreset } from '../../shared/siteInitializationPresets.js'; type NextStepChoice = 'session' | 'apikey' | 'later'; @@ -7,6 +7,7 @@ type Props = { siteName: string; initializationPresetId?: string | null; initialSegment?: 'session' | 'apikey'; + sessionLabel?: string; onChoice: (choice: NextStepChoice) => void; onClose: () => void; }; @@ -15,122 +16,88 @@ export default function SiteCreatedModal({ siteName, initializationPresetId, initialSegment = 'session', + sessionLabel = '添加账号(用户名密码登录)', onChoice, onClose, }: Props) { - const dialogRef = useRef(null); const preset = getSiteInitializationPreset(initializationPresetId); const apiKeyFirst = initialSegment === 'apikey'; const helperText = preset?.description || (apiKeyFirst ? '该平台更适合直接通过 Base URL + API Key 接入,后续再补模型初始化。' : '接下来您可以继续补充登录连接或 API Key。'); - const actionButtons = apiKeyFirst - ? [ - { - choice: 'apikey' as const, - className: 'btn btn-primary btn-block', - label: '添加 API Key(推荐)', - }, - { - choice: 'session' as const, - className: 'btn btn-outline btn-block', - label: '添加账号(用户名密码登录)', - }, - ] - : [ - { - choice: 'session' as const, - className: 'btn btn-primary btn-block', - label: '添加账号(用户名密码登录)', - }, - { - choice: 'apikey' as const, - className: 'btn btn-outline btn-block', - label: '添加 API Key', - }, - ]; - - useEffect(() => { - const dialog = dialogRef.current; - if (!dialog) return; - - if (!dialog.open) { - dialog.showModal(); + const primaryAction = apiKeyFirst + ? { + choice: 'apikey' as const, + label: '添加 API Key(推荐)', } - - return () => { - if (dialog.open) { - dialog.close(); - } + : { + choice: 'session' as const, + label: sessionLabel, }; - }, []); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Escape') { - e.preventDefault(); - onChoice('later'); + const secondaryAction = apiKeyFirst + ? { + choice: 'session' as const, + label: sessionLabel, } - }; + : { + choice: 'apikey' as const, + label: '添加 API Key', + }; return ( - { - e.preventDefault(); - onClose(); - }} - onKeyDown={handleKeyDown} - onClick={(e) => { - if (e.target === dialogRef.current) { - onChoice('later'); - } - }} - > -
-

- 站点创建成功 -

-

- 站点 "{siteName}" 已添加成功。 -

- {preset && ( -
-
{preset.label}
-
- {helperText} -
-
- )} - {!preset && ( -

- {helperText} -

- )} - -
- {actionButtons.map((action) => ( - - ))} - + + + + )} + > +
+
站点已添加成功
+
+ 站点 "{siteName}" 已加入列表,您现在可以继续补充连接信息。
- -

- 提示:您可以随时在"站点管理"页面配置账号信息 -

-
+ + {preset ? ( +
+
{preset.label}
+
+ {helperText} +
+
+ ) : ( +

+ {helperText} +

+ )} + +

+ 提示:您可以随时在“站点管理”页面补充账号或 API Key。 +

+ ); } diff --git a/src/web/index.css b/src/web/index.css index 30c743d..a21b61d 100644 --- a/src/web/index.css +++ b/src/web/index.css @@ -3455,6 +3455,24 @@ textarea { flex-wrap: wrap; } +.site-created-summary, +.site-created-helper-text { + margin: 0; + font-size: 12px; + line-height: 1.7; +} + +.site-created-helper-text { + color: var(--color-text-muted); +} + +.site-created-note { + margin: 0; + font-size: 12px; + line-height: 1.6; + color: var(--color-text-muted); +} + .responsive-form-grid { display: grid; gap: 12px; diff --git a/src/web/pages/Sites.tsx b/src/web/pages/Sites.tsx index 46533e9..fc14731 100644 --- a/src/web/pages/Sites.tsx +++ b/src/web/pages/Sites.tsx @@ -93,6 +93,12 @@ function formatUsd(value?: number | null): string { return `$${(value || 0).toFixed(2)}`; } +function resolveSiteCreatedSessionLabel(platform?: string | null): string { + const normalized = String(platform || '').trim().toLowerCase(); + if (normalized === 'codex') return '添加 OAuth 连接'; + return '添加账号(用户名密码登录)'; +} + function formatSubscriptionDate(value?: string | null): string { if (!value) return ''; const parsed = Date.parse(value); @@ -977,6 +983,7 @@ export default function Sites() { getSiteInitializationPreset(createdSiteForChoice.initializationPresetId)?.initialSegment || resolveInitialConnectionSegment(createdSiteForChoice.platform) } + sessionLabel={resolveSiteCreatedSessionLabel(createdSiteForChoice.platform)} onChoice={handleSiteCreatedChoice} onClose={() => { setCreatedSiteForChoice(null); diff --git a/src/web/pages/sites.create-redirect.test.tsx b/src/web/pages/sites.create-redirect.test.tsx index 8f566c9..0e86786 100644 --- a/src/web/pages/sites.create-redirect.test.tsx +++ b/src/web/pages/sites.create-redirect.test.tsx @@ -106,26 +106,36 @@ async function createSiteAndClickModalChoice( }); await flushMicrotasks(); - // Find the modal dialog and click the appropriate button - const modalDialog = root.root.find((node) => node.type === 'dialog'); - const modalButtons = modalDialog.findAll((node) => node.type === 'button'); + // Find the created-site modal and click the appropriate button + const modalContent = root.root.find((node) => ( + typeof node.props.className === 'string' + && node.props.className.includes('modal-content') + && collectText(node).includes('站点创建成功') + )); + const modalButtons = modalContent.findAll((node) => ( + node.type === 'button' + && typeof node.props.onClick === 'function' + && node.props['aria-label'] !== '关闭弹框' + )); // Find the button based on choice let targetButton; if (choice === 'session') { - targetButton = modalButtons.find((btn) => collectText(btn).includes('添加账号(用户名密码登录)')); + const expectedSessionLabel = createdSite.platform === 'codex' + ? '添加 OAuth 连接' + : '添加账号(用户名密码登录)'; + targetButton = modalButtons.find((btn) => collectText(btn).includes(expectedSessionLabel)); } else if (choice === 'apikey') { targetButton = modalButtons.find((btn) => collectText(btn).includes('添加 API Key')); } else { targetButton = modalButtons.find((btn) => collectText(btn).includes('稍后配置')); } - if (targetButton) { - await act(async () => { - targetButton.props.onClick(); - }); - await flushMicrotasks(); - } + expect(targetButton).toBeTruthy(); + await act(async () => { + targetButton!.props.onClick(); + }); + await flushMicrotasks(); return JSON.stringify(root.toJSON()); } finally {