From 268b92c59b584f3b70a810762c85423d4c7f40b7 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 24 Jan 2026 15:55:31 +0800 Subject: [PATCH] feat(ui): implement custom AutocompleteInput and refactor model mapping UI --- .../providers/AmpcodeSection/AmpcodeModal.tsx | 67 +++-- src/components/ui/AutocompleteInput.tsx | 175 +++++++++++ src/pages/AiProvidersPage.tsx | 144 +++++---- src/pages/AuthFilesPage.tsx | 282 +++++++++--------- src/pages/LogsPage.tsx | 35 ++- src/pages/SystemPage.tsx | 21 +- 6 files changed, 484 insertions(+), 240 deletions(-) create mode 100644 src/components/ui/AutocompleteInput.tsx diff --git a/src/components/providers/AmpcodeSection/AmpcodeModal.tsx b/src/components/providers/AmpcodeSection/AmpcodeModal.tsx index 3b201e5..876a272 100644 --- a/src/components/providers/AmpcodeSection/AmpcodeModal.tsx +++ b/src/components/providers/AmpcodeSection/AmpcodeModal.tsx @@ -21,7 +21,7 @@ interface AmpcodeModalProps { export function AmpcodeModal({ isOpen, disableControls, onClose, onBusyChange }: AmpcodeModalProps) { const { t } = useTranslation(); - const { showNotification } = useNotificationStore(); + const { showNotification, showConfirmation } = useNotificationStore(); const config = useConfigStore((state) => state.config); const updateConfigValue = useConfigStore((state) => state.updateConfigValue); const clearCache = useConfigStore((state) => state.clearCache); @@ -81,32 +81,34 @@ export function AmpcodeModal({ isOpen, disableControls, onClose, onBusyChange }: }, [clearCache, config?.ampcode, isOpen, onBusyChange, t, updateConfigValue]); const clearAmpcodeUpstreamApiKey = async () => { - if (!window.confirm(t('ai_providers.ampcode_clear_upstream_api_key_confirm'))) return; - setSaving(true); - setError(''); - try { - await ampcodeApi.clearUpstreamApiKey(); - const previous = config?.ampcode ?? {}; - const next: AmpcodeConfig = { ...previous }; - delete next.upstreamApiKey; - updateConfigValue('ampcode', next); - clearCache('ampcode'); - showNotification(t('notification.ampcode_upstream_api_key_cleared'), 'success'); - } catch (err: unknown) { - const message = getErrorMessage(err); - setError(message); - showNotification(`${t('notification.update_failed')}: ${message}`, 'error'); - } finally { - setSaving(false); - } + showConfirmation({ + title: t('ai_providers.ampcode_clear_upstream_api_key_title', { defaultValue: 'Clear Upstream API Key' }), + message: t('ai_providers.ampcode_clear_upstream_api_key_confirm'), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + setSaving(true); + setError(''); + try { + await ampcodeApi.clearUpstreamApiKey(); + const previous = config?.ampcode ?? {}; + const next: AmpcodeConfig = { ...previous }; + delete next.upstreamApiKey; + updateConfigValue('ampcode', next); + clearCache('ampcode'); + showNotification(t('notification.ampcode_upstream_api_key_cleared'), 'success'); + } catch (err: unknown) { + const message = getErrorMessage(err); + setError(message); + showNotification(`${t('notification.update_failed')}: ${message}`, 'error'); + } finally { + setSaving(false); + } + }, + }); }; - const saveAmpcode = async () => { - if (!loaded && mappingsDirty) { - const confirmed = window.confirm(t('ai_providers.ampcode_mappings_overwrite_confirm')); - if (!confirmed) return; - } - + const performSaveAmpcode = async () => { setSaving(true); setError(''); try { @@ -173,6 +175,21 @@ export function AmpcodeModal({ isOpen, disableControls, onClose, onBusyChange }: } }; + const saveAmpcode = async () => { + if (!loaded && mappingsDirty) { + showConfirmation({ + title: t('ai_providers.ampcode_mappings_overwrite_title', { defaultValue: 'Overwrite Mappings' }), + message: t('ai_providers.ampcode_mappings_overwrite_confirm'), + variant: 'secondary', // Not dangerous, just a warning + confirmText: t('common.confirm'), + onConfirm: performSaveAmpcode, + }); + return; + } + + await performSaveAmpcode(); + }; + return ( void; + options: string[] | { value: string; label?: string }[]; + placeholder?: string; + disabled?: boolean; + hint?: string; + error?: string; + className?: string; + wrapperClassName?: string; + wrapperStyle?: React.CSSProperties; + id?: string; + rightElement?: ReactNode; +} + +export function AutocompleteInput({ + label, + value, + onChange, + options, + placeholder, + disabled, + hint, + error, + className = '', + wrapperClassName = '', + wrapperStyle, + id, + rightElement +}: AutocompleteInputProps) { + const [isOpen, setIsOpen] = useState(false); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + const containerRef = useRef(null); + + const normalizedOptions = options.map(opt => + typeof opt === 'string' ? { value: opt, label: opt } : { value: opt.value, label: opt.label || opt.value } + ); + + const filteredOptions = normalizedOptions.filter(opt => { + const v = value.toLowerCase(); + return opt.value.toLowerCase().includes(v) || (opt.label && opt.label.toLowerCase().includes(v)); + }); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleInputChange = (e: ChangeEvent) => { + onChange(e.target.value); + setIsOpen(true); + setHighlightedIndex(-1); + }; + + const handleSelect = (selectedValue: string) => { + onChange(selectedValue); + setIsOpen(false); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (disabled) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + if (!isOpen) { + setIsOpen(true); + return; + } + setHighlightedIndex(prev => + prev < filteredOptions.length - 1 ? prev + 1 : prev + ); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setHighlightedIndex(prev => prev > 0 ? prev - 1 : 0); + } else if (e.key === 'Enter') { + if (isOpen && highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) { + e.preventDefault(); + handleSelect(filteredOptions[highlightedIndex].value); + } else if (isOpen) { + e.preventDefault(); + setIsOpen(false); + } + } else if (e.key === 'Escape') { + setIsOpen(false); + } else if (e.key === 'Tab') { + setIsOpen(false); + } + }; + + return ( +
+ {label && } +
+ setIsOpen(true)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + disabled={disabled} + autoComplete="off" + style={{ paddingRight: 32 }} + /> +
!disabled && setIsOpen(!isOpen)} + > + {rightElement} + +
+ + {isOpen && filteredOptions.length > 0 && !disabled && ( +
+ {filteredOptions.map((opt, index) => ( +
handleSelect(opt.value)} + style={{ + padding: '8px 12px', + cursor: 'pointer', + backgroundColor: index === highlightedIndex ? 'var(--bg-tertiary)' : 'transparent', + color: 'var(--text-primary)', + display: 'flex', + flexDirection: 'column', + fontSize: '0.9rem' + }} + onMouseEnter={() => setHighlightedIndex(index)} + > + {opt.value} + {opt.label && opt.label !== opt.value && ( + {opt.label} + )} +
+ ))} +
+ )} +
+ {hint &&
{hint}
} + {error &&
{error}
} +
+ ); +} diff --git a/src/pages/AiProvidersPage.tsx b/src/pages/AiProvidersPage.tsx index d437b9b..9b24f62 100644 --- a/src/pages/AiProvidersPage.tsx +++ b/src/pages/AiProvidersPage.tsx @@ -28,7 +28,7 @@ import styles from './AiProvidersPage.module.scss'; export function AiProvidersPage() { const { t } = useTranslation(); - const { showNotification } = useNotificationStore(); + const { showNotification, showConfirmation } = useNotificationStore(); const resolvedTheme = useThemeStore((state) => state.resolvedTheme); const connectionStatus = useAuthStore((state) => state.connectionStatus); @@ -180,18 +180,25 @@ export function AiProvidersPage() { const deleteGemini = async (index: number) => { const entry = geminiKeys[index]; if (!entry) return; - if (!window.confirm(t('ai_providers.gemini_delete_confirm'))) return; - try { - await providersApi.deleteGeminiKey(entry.apiKey); - const next = geminiKeys.filter((_, idx) => idx !== index); - setGeminiKeys(next); - updateConfigValue('gemini-api-key', next); - clearCache('gemini-api-key'); - showNotification(t('notification.gemini_key_deleted'), 'success'); - } catch (err: unknown) { - const message = getErrorMessage(err); - showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); - } + showConfirmation({ + title: t('ai_providers.gemini_delete_title', { defaultValue: 'Delete Gemini Key' }), + message: t('ai_providers.gemini_delete_confirm'), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + try { + await providersApi.deleteGeminiKey(entry.apiKey); + const next = geminiKeys.filter((_, idx) => idx !== index); + setGeminiKeys(next); + updateConfigValue('gemini-api-key', next); + clearCache('gemini-api-key'); + showNotification(t('notification.gemini_key_deleted'), 'success'); + } catch (err: unknown) { + const message = getErrorMessage(err); + showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); + } + }, + }); }; const setConfigEnabled = async ( @@ -352,27 +359,34 @@ export function AiProvidersPage() { const source = type === 'codex' ? codexConfigs : claudeConfigs; const entry = source[index]; if (!entry) return; - if (!window.confirm(t(`ai_providers.${type}_delete_confirm`))) return; - try { - if (type === 'codex') { - await providersApi.deleteCodexConfig(entry.apiKey); - const next = codexConfigs.filter((_, idx) => idx !== index); - setCodexConfigs(next); - updateConfigValue('codex-api-key', next); - clearCache('codex-api-key'); - showNotification(t('notification.codex_config_deleted'), 'success'); - } else { - await providersApi.deleteClaudeConfig(entry.apiKey); - const next = claudeConfigs.filter((_, idx) => idx !== index); - setClaudeConfigs(next); - updateConfigValue('claude-api-key', next); - clearCache('claude-api-key'); - showNotification(t('notification.claude_config_deleted'), 'success'); - } - } catch (err: unknown) { - const message = getErrorMessage(err); - showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); - } + showConfirmation({ + title: t(`ai_providers.${type}_delete_title`, { defaultValue: `Delete ${type === 'codex' ? 'Codex' : 'Claude'} Config` }), + message: t(`ai_providers.${type}_delete_confirm`), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + try { + if (type === 'codex') { + await providersApi.deleteCodexConfig(entry.apiKey); + const next = codexConfigs.filter((_, idx) => idx !== index); + setCodexConfigs(next); + updateConfigValue('codex-api-key', next); + clearCache('codex-api-key'); + showNotification(t('notification.codex_config_deleted'), 'success'); + } else { + await providersApi.deleteClaudeConfig(entry.apiKey); + const next = claudeConfigs.filter((_, idx) => idx !== index); + setClaudeConfigs(next); + updateConfigValue('claude-api-key', next); + clearCache('claude-api-key'); + showNotification(t('notification.claude_config_deleted'), 'success'); + } + } catch (err: unknown) { + const message = getErrorMessage(err); + showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); + } + }, + }); }; const saveVertex = async (form: VertexFormState, editIndex: number | null) => { @@ -427,18 +441,25 @@ export function AiProvidersPage() { const deleteVertex = async (index: number) => { const entry = vertexConfigs[index]; if (!entry) return; - if (!window.confirm(t('ai_providers.vertex_delete_confirm'))) return; - try { - await providersApi.deleteVertexConfig(entry.apiKey); - const next = vertexConfigs.filter((_, idx) => idx !== index); - setVertexConfigs(next); - updateConfigValue('vertex-api-key', next); - clearCache('vertex-api-key'); - showNotification(t('notification.vertex_config_deleted'), 'success'); - } catch (err: unknown) { - const message = getErrorMessage(err); - showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); - } + showConfirmation({ + title: t('ai_providers.vertex_delete_title', { defaultValue: 'Delete Vertex Config' }), + message: t('ai_providers.vertex_delete_confirm'), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + try { + await providersApi.deleteVertexConfig(entry.apiKey); + const next = vertexConfigs.filter((_, idx) => idx !== index); + setVertexConfigs(next); + updateConfigValue('vertex-api-key', next); + clearCache('vertex-api-key'); + showNotification(t('notification.vertex_config_deleted'), 'success'); + } catch (err: unknown) { + const message = getErrorMessage(err); + showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); + } + }, + }); }; const saveOpenai = async (form: OpenAIFormState, editIndex: number | null) => { @@ -485,18 +506,25 @@ export function AiProvidersPage() { const deleteOpenai = async (index: number) => { const entry = openaiProviders[index]; if (!entry) return; - if (!window.confirm(t('ai_providers.openai_delete_confirm'))) return; - try { - await providersApi.deleteOpenAIProvider(entry.name); - const next = openaiProviders.filter((_, idx) => idx !== index); - setOpenaiProviders(next); - updateConfigValue('openai-compatibility', next); - clearCache('openai-compatibility'); - showNotification(t('notification.openai_provider_deleted'), 'success'); - } catch (err: unknown) { - const message = getErrorMessage(err); - showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); - } + showConfirmation({ + title: t('ai_providers.openai_delete_title', { defaultValue: 'Delete OpenAI Provider' }), + message: t('ai_providers.openai_delete_confirm'), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + try { + await providersApi.deleteOpenAIProvider(entry.name); + const next = openaiProviders.filter((_, idx) => idx !== index); + setOpenaiProviders(next); + updateConfigValue('openai-compatibility', next); + clearCache('openai-compatibility'); + showNotification(t('notification.openai_provider_deleted'), 'success'); + } catch (err: unknown) { + const message = getErrorMessage(err); + showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); + } + }, + }); }; const geminiModalIndex = modal?.type === 'gemini' ? modal.index : null; diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 75491d0..d3d3ad2 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -6,6 +6,7 @@ import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { Input } from '@/components/ui/Input'; +import { AutocompleteInput } from '@/components/ui/AutocompleteInput'; import { Modal } from '@/components/ui/Modal'; import { EmptyState } from '@/components/ui/EmptyState'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; @@ -193,7 +194,7 @@ function resolveAuthFileStats(file: AuthFileItem, stats: KeyStats): KeyStatBucke export function AuthFilesPage() { const { t } = useTranslation(); - const { showNotification } = useNotificationStore(); + const { showNotification, showConfirmation } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme); @@ -689,18 +690,25 @@ export function AuthFilesPage() { // 删除单个文件 const handleDelete = async (name: string) => { - if (!window.confirm(`${t('auth_files.delete_confirm')} "${name}" ?`)) return; - setDeleting(name); - try { - await authFilesApi.deleteFile(name); - showNotification(t('auth_files.delete_success'), 'success'); - setFiles((prev) => prev.filter((item) => item.name !== name)); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); - } finally { - setDeleting(null); - } + showConfirmation({ + title: t('auth_files.delete_title', { defaultValue: 'Delete File' }), + message: `${t('auth_files.delete_confirm')} "${name}" ?`, + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + setDeleting(name); + try { + await authFilesApi.deleteFile(name); + showNotification(t('auth_files.delete_success'), 'success'); + setFiles((prev) => prev.filter((item) => item.name !== name)); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); + } finally { + setDeleting(null); + } + }, + }); }; // 删除全部(根据筛选类型) @@ -711,60 +719,66 @@ export function AuthFilesPage() { ? t('auth_files.delete_filtered_confirm', { type: typeLabel }) : t('auth_files.delete_all_confirm'); - if (!window.confirm(confirmMessage)) return; + showConfirmation({ + title: t('auth_files.delete_all_title', { defaultValue: 'Delete All Files' }), + message: confirmMessage, + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + setDeletingAll(true); + try { + if (!isFiltered) { + // 删除全部 + await authFilesApi.deleteAll(); + showNotification(t('auth_files.delete_all_success'), 'success'); + setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file))); + } else { + // 删除筛选类型的文件 + const filesToDelete = files.filter((f) => f.type === filter && !isRuntimeOnlyAuthFile(f)); - setDeletingAll(true); - try { - if (!isFiltered) { - // 删除全部 - await authFilesApi.deleteAll(); - showNotification(t('auth_files.delete_all_success'), 'success'); - setFiles((prev) => prev.filter((file) => isRuntimeOnlyAuthFile(file))); - } else { - // 删除筛选类型的文件 - const filesToDelete = files.filter((f) => f.type === filter && !isRuntimeOnlyAuthFile(f)); + if (filesToDelete.length === 0) { + showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info'); + setDeletingAll(false); + return; + } - if (filesToDelete.length === 0) { - showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info'); - setDeletingAll(false); - return; - } + let success = 0; + let failed = 0; + const deletedNames: string[] = []; - let success = 0; - let failed = 0; - const deletedNames: string[] = []; + for (const file of filesToDelete) { + try { + await authFilesApi.deleteFile(file.name); + success++; + deletedNames.push(file.name); + } catch { + failed++; + } + } - for (const file of filesToDelete) { - try { - await authFilesApi.deleteFile(file.name); - success++; - deletedNames.push(file.name); - } catch { - failed++; + setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name))); + + if (failed === 0) { + showNotification( + t('auth_files.delete_filtered_success', { count: success, type: typeLabel }), + 'success' + ); + } else { + showNotification( + t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }), + 'warning' + ); + } + setFilter('all'); } + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); + } finally { + setDeletingAll(false); } - - setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name))); - - if (failed === 0) { - showNotification( - t('auth_files.delete_filtered_success', { count: success, type: typeLabel }), - 'success' - ); - } else { - showNotification( - t('auth_files.delete_filtered_partial', { success, failed, type: typeLabel }), - 'warning' - ); - } - setFilter('all'); - } - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error'); - } finally { - setDeletingAll(false); - } + }, + }); }; // 下载文件 @@ -1067,37 +1081,44 @@ export function AuthFilesPage() { const deleteExcluded = async (provider: string) => { const providerLabel = provider.trim() || provider; - if (!window.confirm(t('oauth_excluded.delete_confirm', { provider: providerLabel }))) return; - const providerKey = normalizeProviderKey(provider); - if (!providerKey) { - showNotification(t('oauth_excluded.provider_required'), 'error'); - return; - } - try { - await authFilesApi.deleteOauthExcludedEntry(providerKey); - await loadExcluded(); - showNotification(t('oauth_excluded.delete_success'), 'success'); - } catch (err: unknown) { - try { - const current = await authFilesApi.getOauthExcludedModels(); - const next: Record = {}; - Object.entries(current).forEach(([key, models]) => { - if (normalizeProviderKey(key) === providerKey) return; - next[key] = models; - }); - await authFilesApi.replaceOauthExcludedModels(next); - await loadExcluded(); - showNotification(t('oauth_excluded.delete_success'), 'success'); - } catch (fallbackErr: unknown) { - const errorMessage = - fallbackErr instanceof Error - ? fallbackErr.message - : err instanceof Error - ? err.message - : ''; - showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); - } - } + showConfirmation({ + title: t('oauth_excluded.delete_title', { defaultValue: 'Delete Exclusion' }), + message: t('oauth_excluded.delete_confirm', { provider: providerLabel }), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + const providerKey = normalizeProviderKey(provider); + if (!providerKey) { + showNotification(t('oauth_excluded.provider_required'), 'error'); + return; + } + try { + await authFilesApi.deleteOauthExcludedEntry(providerKey); + await loadExcluded(); + showNotification(t('oauth_excluded.delete_success'), 'success'); + } catch (err: unknown) { + try { + const current = await authFilesApi.getOauthExcludedModels(); + const next: Record = {}; + Object.entries(current).forEach(([key, models]) => { + if (normalizeProviderKey(key) === providerKey) return; + next[key] = models; + }); + await authFilesApi.replaceOauthExcludedModels(next); + await loadExcluded(); + showNotification(t('oauth_excluded.delete_success'), 'success'); + } catch (fallbackErr: unknown) { + const errorMessage = + fallbackErr instanceof Error + ? fallbackErr.message + : err instanceof Error + ? err.message + : ''; + showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error'); + } + } + }, + }); }; // OAuth 模型映射相关方法 @@ -1218,15 +1239,22 @@ export function AuthFilesPage() { }; const deleteModelMappings = async (provider: string) => { - if (!window.confirm(t('oauth_model_mappings.delete_confirm', { provider }))) return; - try { - await authFilesApi.deleteOauthModelMappings(provider); - await loadModelMappings(); - showNotification(t('oauth_model_mappings.delete_success'), 'success'); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : ''; - showNotification(`${t('oauth_model_mappings.delete_failed')}: ${errorMessage}`, 'error'); - } + showConfirmation({ + title: t('oauth_model_mappings.delete_title', { defaultValue: 'Delete Mappings' }), + message: t('oauth_model_mappings.delete_confirm', { provider }), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + try { + await authFilesApi.deleteOauthModelMappings(provider); + await loadModelMappings(); + showNotification(t('oauth_model_mappings.delete_success'), 'success'); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : ''; + showNotification(`${t('oauth_model_mappings.delete_failed')}: ${errorMessage}`, 'error'); + } + }, + }); }; // 渲染标签筛选器 @@ -1877,20 +1905,15 @@ export function AuthFilesPage() { } >
- setExcludedForm((prev) => ({ ...prev, provider: e.target.value }))} + onChange={(val) => setExcludedForm((prev) => ({ ...prev, provider: val }))} + options={providerOptions} /> - - {providerOptions.map((provider) => ( - {providerOptions.length > 0 && (
{providerOptions.map((provider) => { @@ -1945,20 +1968,15 @@ export function AuthFilesPage() { } >
- setMappingForm((prev) => ({ ...prev, provider: e.target.value }))} + onChange={(val) => setMappingForm((prev) => ({ ...prev, provider: val }))} + options={providerOptions} /> - - {providerOptions.map((provider) => ( - {providerOptions.length > 0 && (
{providerOptions.map((provider) => { @@ -1980,9 +1998,8 @@ export function AuthFilesPage() { )}
- setMappingModelsFileName(e.target.value)} + onChange={(val) => setMappingModelsFileName(val)} disabled={savingMappings} + options={modelSourceFileOptions} /> - - {modelSourceFileOptions.map((fileName) => ( -
@@ -2012,13 +2025,16 @@ export function AuthFilesPage() { {(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map( (entry, index) => (
- updateMappingEntry(index, 'name', e.target.value)} + onChange={(val) => updateMappingEntry(index, 'name', val)} disabled={savingMappings} + options={mappingModelsList.map((m) => ({ + value: m.id, + label: m.display_name && m.display_name !== m.id ? m.display_name : undefined, + }))} /> updateMappingEntry(index, 'alias', e.target.value)} disabled={savingMappings} + style={{ flex: 1 }} />
- - {mappingModelsList.map((model) => ( - - ))} -
{t('oauth_model_mappings.mappings_hint')}
diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index 7e58d65..2953ee9 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -371,7 +371,7 @@ type TabType = 'logs' | 'errors'; export function LogsPage() { const { t } = useTranslation(); - const { showNotification } = useNotificationStore(); + const { showNotification, showConfirmation } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const requestLogEnabled = useConfigStore((state) => state.config?.requestLog ?? false); @@ -478,19 +478,26 @@ export function LogsPage() { useHeaderRefresh(() => loadLogs(false)); const clearLogs = async () => { - if (!window.confirm(t('logs.clear_confirm'))) return; - try { - await logsApi.clearLogs(); - setLogState({ buffer: [], visibleFrom: 0 }); - latestTimestampRef.current = 0; - showNotification(t('logs.clear_success'), 'success'); - } catch (err: unknown) { - const message = getErrorMessage(err); - showNotification( - `${t('notification.delete_failed')}${message ? `: ${message}` : ''}`, - 'error' - ); - } + showConfirmation({ + title: t('logs.clear_confirm_title', { defaultValue: 'Clear Logs' }), + message: t('logs.clear_confirm'), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: async () => { + try { + await logsApi.clearLogs(); + setLogState({ buffer: [], visibleFrom: 0 }); + latestTimestampRef.current = 0; + showNotification(t('logs.clear_success'), 'success'); + } catch (err: unknown) { + const message = getErrorMessage(err); + showNotification( + `${t('notification.delete_failed')}${message ? `: ${message}` : ''}`, + 'error' + ); + } + }, + }); }; const downloadLogs = () => { diff --git a/src/pages/SystemPage.tsx b/src/pages/SystemPage.tsx index 445b34b..d601c45 100644 --- a/src/pages/SystemPage.tsx +++ b/src/pages/SystemPage.tsx @@ -11,7 +11,7 @@ import styles from './SystemPage.module.scss'; export function SystemPage() { const { t, i18n } = useTranslation(); - const { showNotification } = useNotificationStore(); + const { showNotification, showConfirmation } = useNotificationStore(); const auth = useAuthStore(); const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); @@ -106,12 +106,19 @@ export function SystemPage() { }; const handleClearLoginStorage = () => { - if (!window.confirm(t('system_info.clear_login_confirm'))) return; - auth.logout(); - if (typeof localStorage === 'undefined') return; - const keysToRemove = [STORAGE_KEY_AUTH, 'isLoggedIn', 'apiBase', 'apiUrl', 'managementKey']; - keysToRemove.forEach((key) => localStorage.removeItem(key)); - showNotification(t('notification.login_storage_cleared'), 'success'); + showConfirmation({ + title: t('system_info.clear_login_title', { defaultValue: 'Clear Login Storage' }), + message: t('system_info.clear_login_confirm'), + variant: 'danger', + confirmText: t('common.confirm'), + onConfirm: () => { + auth.logout(); + if (typeof localStorage === 'undefined') return; + const keysToRemove = [STORAGE_KEY_AUTH, 'isLoggedIn', 'apiBase', 'apiUrl', 'managementKey']; + keysToRemove.forEach((key) => localStorage.removeItem(key)); + showNotification(t('notification.login_storage_cleared'), 'success'); + }, + }); }; useEffect(() => {