From 07cd95a2d75786f398cb4200b97c5ba70c76f54c Mon Sep 17 00:00:00 2001 From: hicocos Date: Tue, 25 Aug 2026 14:20:47 +0800 Subject: [PATCH] fix: keep storage probe feedback near its account Show connection-test success and failure inline in the account card instead of at the top of the settings page. --- .../src/components/pages/SettingsPage.tsx | 55 +++++++++++-------- .../pages/frontendInteractionPolish.test.ts | 12 +++- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/pages/SettingsPage.tsx b/frontend/src/components/pages/SettingsPage.tsx index 649585d..878f7a2 100644 --- a/frontend/src/components/pages/SettingsPage.tsx +++ b/frontend/src/components/pages/SettingsPage.tsx @@ -68,7 +68,21 @@ const SettingsRow = ({ icon: Icon, label, value, action, onClick, description }: ); -const StorageProbeStatus = ({ account, busy, onProbe }: { account: StorageAccount; busy: boolean; onProbe: () => void }) => { +interface ActionNoticeState { + title: string; + message: string; + tone: 'success' | 'error' | 'info'; +} +interface ActionDialogState { + mode: 'confirm' | 'prompt'; + title: string; + message: string; + inputType?: 'text' | 'password'; + resolve?: (value: boolean | string | null) => void; +} +interface ProbeFeedbackState { accountId: string; tone: 'success' | 'error'; message: string; sequence: number; } + +const StorageProbeStatus = ({ account, busy, feedback, onProbe }: { account: StorageAccount; busy: boolean; feedback: ProbeFeedbackState | null; onProbe: () => void }) => { const status = account.last_probe_status; const Icon = status === 'available' ? CheckCircle : status === 'failed' ? XCircle : CircleHelp; const label = status === 'available' ? '连接可用' : status === 'failed' ? '连接失败' : '尚未测试'; @@ -88,25 +102,12 @@ const StorageProbeStatus = ({ account, busy, onProbe }: { account: StorageAccoun {busy ? : } 测试连接 - {status === 'failed' && account.last_probe_error &&

{account.last_probe_error}

} + {feedback && {feedback.tone === 'success' ? : }{feedback.message}} + {!feedback && status === 'failed' && account.last_probe_error &&

{account.last_probe_error}

} ); }; -interface ActionDialogState { - mode: 'confirm' | 'prompt'; - title: string; - message: string; - inputType?: 'text' | 'password'; - resolve?: (value: boolean | string | null) => void; -} - -interface ActionNoticeState { - title: string; - message: string; - tone: 'success' | 'error' | 'info'; -} - const ActionNotice = ({ state, onClose }: { state: ActionNoticeState; onClose: () => void }) => ( (null); const [isSaving, setIsSaving] = useState(false); const [probingAccountId, setProbingAccountId] = useState(null); + const [probeFeedback, setProbeFeedback] = useState(null); const [showOneDriveForm, setShowOneDriveForm] = useState(false); // OneDrive Form State (for adding new account) @@ -322,16 +324,23 @@ export const SettingsPage = ({ storageStats, onSignedOut, onOpenTasksForAccount, return data; }; + useEffect(() => { + if (!probeFeedback) return; + const timer = window.setTimeout(() => setProbeFeedback(null), 4_000); + return () => window.clearTimeout(timer); + }, [probeFeedback?.sequence]); + const handleProbeAccount = async (account: StorageAccount) => { if (probingAccountId) return; setProbingAccountId(account.id); + setProbeFeedback(null); try { await fileApi.probeStorageAccount(account.id); await reloadStorageConfig(); - await showNotice(`“${account.name}”连接测试成功`); + setProbeFeedback(previous => ({ accountId: account.id, tone: 'success', message: '连接测试成功', sequence: (previous?.sequence ?? 0) + 1 })); } catch (error: any) { await reloadStorageConfig().catch(() => undefined); - await showNotice(error?.message || '存储账户连接测试失败', '连接测试失败'); + setProbeFeedback(previous => ({ accountId: account.id, tone: 'error', message: error?.message || '连接测试失败,请稍后重试', sequence: (previous?.sequence ?? 0) + 1 })); } finally { setProbingAccountId(null); } @@ -1234,7 +1243,7 @@ export const SettingsPage = ({ storageStats, onSignedOut, onOpenTasksForAccount,

{account.name || "未命名账户"}

{account.id}

- void handleProbeAccount(account)} /> + void handleProbeAccount(account)} />
@@ -1415,7 +1424,7 @@ export const SettingsPage = ({ storageStats, onSignedOut, onOpenTasksForAccount,

{account.name || "未命名账户"}

{account.id}

- void handleProbeAccount(account)} /> + void handleProbeAccount(account)} />
@@ -1600,7 +1609,7 @@ export const SettingsPage = ({ storageStats, onSignedOut, onOpenTasksForAccount,

{account.name || "未命名账户"}

{account.id}

- void handleProbeAccount(account)} /> + void handleProbeAccount(account)} />
@@ -1786,7 +1795,7 @@ export const SettingsPage = ({ storageStats, onSignedOut, onOpenTasksForAccount,

{account.name || "未命名账户"}

{account.id}

- void handleProbeAccount(account)} /> + void handleProbeAccount(account)} />
@@ -1994,7 +2003,7 @@ export const SettingsPage = ({ storageStats, onSignedOut, onOpenTasksForAccount,

{account.name || "未命名账户"}

{account.id}

- void handleProbeAccount(account)} /> + void handleProbeAccount(account)} />
diff --git a/frontend/src/components/pages/frontendInteractionPolish.test.ts b/frontend/src/components/pages/frontendInteractionPolish.test.ts index d31a4a3..b5b21b2 100644 --- a/frontend/src/components/pages/frontendInteractionPolish.test.ts +++ b/frontend/src/components/pages/frontendInteractionPolish.test.ts @@ -25,6 +25,16 @@ test('the global header owns the theme switch on every page', () => { assert.doesNotMatch(settings, /label=\{t\("settings\.general\.theme"\)\}/); }); +test('storage connection test feedback stays beside the account that triggered it', () => { + assert.match(settings, /interface ProbeFeedbackState \{ accountId: string; tone: 'success' \| 'error'; message: string; sequence: number; \}/); + assert.match(settings, /probeFeedback\?\.accountId === account\.id \? probeFeedback : null/); + assert.match(settings, /role="status" aria-live="polite"/); + assert.match(settings, /setProbeFeedback\(previous => \(\{ accountId: account\.id, tone: 'success', message: '连接测试成功', sequence: \(previous\?\.sequence \?\? 0\) \+ 1 \}\)\)/); + assert.match(settings, /tone: 'error'/); + assert.doesNotMatch(settings, /showNotice\(`“\$\{account\.name\}”连接测试成功`\)/); + assert.doesNotMatch(settings, /showNotice\(error\?\.message \|\| '存储账户连接测试失败', '连接测试失败'\)/); +}); + test('settings uses non-modal transient feedback for notices but keeps confirmations and prompts modal', () => { assert.match(settings, /interface ActionNoticeState/); assert.match(settings, /role="status"/); @@ -33,7 +43,7 @@ test('settings uses non-modal transient feedback for notices but keeps confirmat assert.match(settings, /setActionNotice\(\{ title, message, tone:[^\n]*\}\);/); assert.match(settings, /return Promise\.resolve\(\);/); assert.doesNotMatch(settings, /window\.location\.reload\(\)/); - assert.doesNotMatch(settings, /interface ActionNoticeState[\s\S]*resolve\?:/); + assert.doesNotMatch(settings, /interface ActionNoticeState\s*\{[^}]*resolve\?:/); assert.doesNotMatch(settings, /mode:\s*'notice'\s*\|\s*'confirm'\s*\|\s*'prompt'/); assert.doesNotMatch(settings, /state\.mode === 'notice' \? '知道了'/); });