mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat(ui): implement custom AutocompleteInput and refactor model mapping UI
This commit is contained in:
@@ -21,7 +21,7 @@ interface AmpcodeModalProps {
|
|||||||
|
|
||||||
export function AmpcodeModal({ isOpen, disableControls, onClose, onBusyChange }: AmpcodeModalProps) {
|
export function AmpcodeModal({ isOpen, disableControls, onClose, onBusyChange }: AmpcodeModalProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification, showConfirmation } = useNotificationStore();
|
||||||
const config = useConfigStore((state) => state.config);
|
const config = useConfigStore((state) => state.config);
|
||||||
const updateConfigValue = useConfigStore((state) => state.updateConfigValue);
|
const updateConfigValue = useConfigStore((state) => state.updateConfigValue);
|
||||||
const clearCache = useConfigStore((state) => state.clearCache);
|
const clearCache = useConfigStore((state) => state.clearCache);
|
||||||
@@ -81,32 +81,34 @@ export function AmpcodeModal({ isOpen, disableControls, onClose, onBusyChange }:
|
|||||||
}, [clearCache, config?.ampcode, isOpen, onBusyChange, t, updateConfigValue]);
|
}, [clearCache, config?.ampcode, isOpen, onBusyChange, t, updateConfigValue]);
|
||||||
|
|
||||||
const clearAmpcodeUpstreamApiKey = async () => {
|
const clearAmpcodeUpstreamApiKey = async () => {
|
||||||
if (!window.confirm(t('ai_providers.ampcode_clear_upstream_api_key_confirm'))) return;
|
showConfirmation({
|
||||||
setSaving(true);
|
title: t('ai_providers.ampcode_clear_upstream_api_key_title', { defaultValue: 'Clear Upstream API Key' }),
|
||||||
setError('');
|
message: t('ai_providers.ampcode_clear_upstream_api_key_confirm'),
|
||||||
try {
|
variant: 'danger',
|
||||||
await ampcodeApi.clearUpstreamApiKey();
|
confirmText: t('common.confirm'),
|
||||||
const previous = config?.ampcode ?? {};
|
onConfirm: async () => {
|
||||||
const next: AmpcodeConfig = { ...previous };
|
setSaving(true);
|
||||||
delete next.upstreamApiKey;
|
setError('');
|
||||||
updateConfigValue('ampcode', next);
|
try {
|
||||||
clearCache('ampcode');
|
await ampcodeApi.clearUpstreamApiKey();
|
||||||
showNotification(t('notification.ampcode_upstream_api_key_cleared'), 'success');
|
const previous = config?.ampcode ?? {};
|
||||||
} catch (err: unknown) {
|
const next: AmpcodeConfig = { ...previous };
|
||||||
const message = getErrorMessage(err);
|
delete next.upstreamApiKey;
|
||||||
setError(message);
|
updateConfigValue('ampcode', next);
|
||||||
showNotification(`${t('notification.update_failed')}: ${message}`, 'error');
|
clearCache('ampcode');
|
||||||
} finally {
|
showNotification(t('notification.ampcode_upstream_api_key_cleared'), 'success');
|
||||||
setSaving(false);
|
} catch (err: unknown) {
|
||||||
}
|
const message = getErrorMessage(err);
|
||||||
|
setError(message);
|
||||||
|
showNotification(`${t('notification.update_failed')}: ${message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveAmpcode = async () => {
|
const performSaveAmpcode = async () => {
|
||||||
if (!loaded && mappingsDirty) {
|
|
||||||
const confirmed = window.confirm(t('ai_providers.ampcode_mappings_overwrite_confirm'));
|
|
||||||
if (!confirmed) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
|
|||||||
175
src/components/ui/AutocompleteInput.tsx
Normal file
175
src/components/ui/AutocompleteInput.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent, type ReactNode } from 'react';
|
||||||
|
import { IconChevronDown } from './icons';
|
||||||
|
|
||||||
|
interface AutocompleteInputProps {
|
||||||
|
label?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => 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<HTMLDivElement>(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<HTMLInputElement>) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setIsOpen(true);
|
||||||
|
setHighlightedIndex(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (selectedValue: string) => {
|
||||||
|
onChange(selectedValue);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className={`form-group ${wrapperClassName}`} ref={containerRef} style={wrapperStyle}>
|
||||||
|
{label && <label htmlFor={id}>{label}</label>}
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
className={`input ${className}`.trim()}
|
||||||
|
value={value}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onFocus={() => setIsOpen(true)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
autoComplete="off"
|
||||||
|
style={{ paddingRight: 32 }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 8,
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
pointerEvents: disabled ? 'none' : 'auto',
|
||||||
|
cursor: 'pointer',
|
||||||
|
height: '100%'
|
||||||
|
}}
|
||||||
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
{rightElement}
|
||||||
|
<IconChevronDown size={16} style={{ opacity: 0.5, marginLeft: 4 }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && filteredOptions.length > 0 && !disabled && (
|
||||||
|
<div className="autocomplete-dropdown" style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 'calc(100% + 4px)',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: 'var(--bg-secondary)',
|
||||||
|
border: '1px solid var(--border-color)',
|
||||||
|
borderRadius: 'var(--radius-md)',
|
||||||
|
maxHeight: 200,
|
||||||
|
overflowY: 'auto',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'
|
||||||
|
}}>
|
||||||
|
{filteredOptions.map((opt, index) => (
|
||||||
|
<div
|
||||||
|
key={`${opt.value}-${index}`}
|
||||||
|
onClick={() => 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)}
|
||||||
|
>
|
||||||
|
<span style={{ fontWeight: 500 }}>{opt.value}</span>
|
||||||
|
{opt.label && opt.label !== opt.value && (
|
||||||
|
<span style={{ fontSize: '0.85em', color: 'var(--text-secondary)' }}>{opt.label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{hint && <div className="hint">{hint}</div>}
|
||||||
|
{error && <div className="error-box">{error}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ import styles from './AiProvidersPage.module.scss';
|
|||||||
|
|
||||||
export function AiProvidersPage() {
|
export function AiProvidersPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification, showConfirmation } = useNotificationStore();
|
||||||
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
|
|
||||||
@@ -180,18 +180,25 @@ export function AiProvidersPage() {
|
|||||||
const deleteGemini = async (index: number) => {
|
const deleteGemini = async (index: number) => {
|
||||||
const entry = geminiKeys[index];
|
const entry = geminiKeys[index];
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (!window.confirm(t('ai_providers.gemini_delete_confirm'))) return;
|
showConfirmation({
|
||||||
try {
|
title: t('ai_providers.gemini_delete_title', { defaultValue: 'Delete Gemini Key' }),
|
||||||
await providersApi.deleteGeminiKey(entry.apiKey);
|
message: t('ai_providers.gemini_delete_confirm'),
|
||||||
const next = geminiKeys.filter((_, idx) => idx !== index);
|
variant: 'danger',
|
||||||
setGeminiKeys(next);
|
confirmText: t('common.confirm'),
|
||||||
updateConfigValue('gemini-api-key', next);
|
onConfirm: async () => {
|
||||||
clearCache('gemini-api-key');
|
try {
|
||||||
showNotification(t('notification.gemini_key_deleted'), 'success');
|
await providersApi.deleteGeminiKey(entry.apiKey);
|
||||||
} catch (err: unknown) {
|
const next = geminiKeys.filter((_, idx) => idx !== index);
|
||||||
const message = getErrorMessage(err);
|
setGeminiKeys(next);
|
||||||
showNotification(`${t('notification.delete_failed')}: ${message}`, 'error');
|
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 (
|
const setConfigEnabled = async (
|
||||||
@@ -352,27 +359,34 @@ export function AiProvidersPage() {
|
|||||||
const source = type === 'codex' ? codexConfigs : claudeConfigs;
|
const source = type === 'codex' ? codexConfigs : claudeConfigs;
|
||||||
const entry = source[index];
|
const entry = source[index];
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (!window.confirm(t(`ai_providers.${type}_delete_confirm`))) return;
|
showConfirmation({
|
||||||
try {
|
title: t(`ai_providers.${type}_delete_title`, { defaultValue: `Delete ${type === 'codex' ? 'Codex' : 'Claude'} Config` }),
|
||||||
if (type === 'codex') {
|
message: t(`ai_providers.${type}_delete_confirm`),
|
||||||
await providersApi.deleteCodexConfig(entry.apiKey);
|
variant: 'danger',
|
||||||
const next = codexConfigs.filter((_, idx) => idx !== index);
|
confirmText: t('common.confirm'),
|
||||||
setCodexConfigs(next);
|
onConfirm: async () => {
|
||||||
updateConfigValue('codex-api-key', next);
|
try {
|
||||||
clearCache('codex-api-key');
|
if (type === 'codex') {
|
||||||
showNotification(t('notification.codex_config_deleted'), 'success');
|
await providersApi.deleteCodexConfig(entry.apiKey);
|
||||||
} else {
|
const next = codexConfigs.filter((_, idx) => idx !== index);
|
||||||
await providersApi.deleteClaudeConfig(entry.apiKey);
|
setCodexConfigs(next);
|
||||||
const next = claudeConfigs.filter((_, idx) => idx !== index);
|
updateConfigValue('codex-api-key', next);
|
||||||
setClaudeConfigs(next);
|
clearCache('codex-api-key');
|
||||||
updateConfigValue('claude-api-key', next);
|
showNotification(t('notification.codex_config_deleted'), 'success');
|
||||||
clearCache('claude-api-key');
|
} else {
|
||||||
showNotification(t('notification.claude_config_deleted'), 'success');
|
await providersApi.deleteClaudeConfig(entry.apiKey);
|
||||||
}
|
const next = claudeConfigs.filter((_, idx) => idx !== index);
|
||||||
} catch (err: unknown) {
|
setClaudeConfigs(next);
|
||||||
const message = getErrorMessage(err);
|
updateConfigValue('claude-api-key', next);
|
||||||
showNotification(`${t('notification.delete_failed')}: ${message}`, 'error');
|
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) => {
|
const saveVertex = async (form: VertexFormState, editIndex: number | null) => {
|
||||||
@@ -427,18 +441,25 @@ export function AiProvidersPage() {
|
|||||||
const deleteVertex = async (index: number) => {
|
const deleteVertex = async (index: number) => {
|
||||||
const entry = vertexConfigs[index];
|
const entry = vertexConfigs[index];
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (!window.confirm(t('ai_providers.vertex_delete_confirm'))) return;
|
showConfirmation({
|
||||||
try {
|
title: t('ai_providers.vertex_delete_title', { defaultValue: 'Delete Vertex Config' }),
|
||||||
await providersApi.deleteVertexConfig(entry.apiKey);
|
message: t('ai_providers.vertex_delete_confirm'),
|
||||||
const next = vertexConfigs.filter((_, idx) => idx !== index);
|
variant: 'danger',
|
||||||
setVertexConfigs(next);
|
confirmText: t('common.confirm'),
|
||||||
updateConfigValue('vertex-api-key', next);
|
onConfirm: async () => {
|
||||||
clearCache('vertex-api-key');
|
try {
|
||||||
showNotification(t('notification.vertex_config_deleted'), 'success');
|
await providersApi.deleteVertexConfig(entry.apiKey);
|
||||||
} catch (err: unknown) {
|
const next = vertexConfigs.filter((_, idx) => idx !== index);
|
||||||
const message = getErrorMessage(err);
|
setVertexConfigs(next);
|
||||||
showNotification(`${t('notification.delete_failed')}: ${message}`, 'error');
|
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) => {
|
const saveOpenai = async (form: OpenAIFormState, editIndex: number | null) => {
|
||||||
@@ -485,18 +506,25 @@ export function AiProvidersPage() {
|
|||||||
const deleteOpenai = async (index: number) => {
|
const deleteOpenai = async (index: number) => {
|
||||||
const entry = openaiProviders[index];
|
const entry = openaiProviders[index];
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
if (!window.confirm(t('ai_providers.openai_delete_confirm'))) return;
|
showConfirmation({
|
||||||
try {
|
title: t('ai_providers.openai_delete_title', { defaultValue: 'Delete OpenAI Provider' }),
|
||||||
await providersApi.deleteOpenAIProvider(entry.name);
|
message: t('ai_providers.openai_delete_confirm'),
|
||||||
const next = openaiProviders.filter((_, idx) => idx !== index);
|
variant: 'danger',
|
||||||
setOpenaiProviders(next);
|
confirmText: t('common.confirm'),
|
||||||
updateConfigValue('openai-compatibility', next);
|
onConfirm: async () => {
|
||||||
clearCache('openai-compatibility');
|
try {
|
||||||
showNotification(t('notification.openai_provider_deleted'), 'success');
|
await providersApi.deleteOpenAIProvider(entry.name);
|
||||||
} catch (err: unknown) {
|
const next = openaiProviders.filter((_, idx) => idx !== index);
|
||||||
const message = getErrorMessage(err);
|
setOpenaiProviders(next);
|
||||||
showNotification(`${t('notification.delete_failed')}: ${message}`, 'error');
|
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;
|
const geminiModalIndex = modal?.type === 'gemini' ? modal.index : null;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Card } from '@/components/ui/Card';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { AutocompleteInput } from '@/components/ui/AutocompleteInput';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { EmptyState } from '@/components/ui/EmptyState';
|
import { EmptyState } from '@/components/ui/EmptyState';
|
||||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||||
@@ -193,7 +194,7 @@ function resolveAuthFileStats(file: AuthFileItem, stats: KeyStats): KeyStatBucke
|
|||||||
|
|
||||||
export function AuthFilesPage() {
|
export function AuthFilesPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification, showConfirmation } = useNotificationStore();
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
||||||
|
|
||||||
@@ -689,18 +690,25 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
// 删除单个文件
|
// 删除单个文件
|
||||||
const handleDelete = async (name: string) => {
|
const handleDelete = async (name: string) => {
|
||||||
if (!window.confirm(`${t('auth_files.delete_confirm')} "${name}" ?`)) return;
|
showConfirmation({
|
||||||
setDeleting(name);
|
title: t('auth_files.delete_title', { defaultValue: 'Delete File' }),
|
||||||
try {
|
message: `${t('auth_files.delete_confirm')} "${name}" ?`,
|
||||||
await authFilesApi.deleteFile(name);
|
variant: 'danger',
|
||||||
showNotification(t('auth_files.delete_success'), 'success');
|
confirmText: t('common.confirm'),
|
||||||
setFiles((prev) => prev.filter((item) => item.name !== name));
|
onConfirm: async () => {
|
||||||
} catch (err: unknown) {
|
setDeleting(name);
|
||||||
const errorMessage = err instanceof Error ? err.message : '';
|
try {
|
||||||
showNotification(`${t('notification.delete_failed')}: ${errorMessage}`, 'error');
|
await authFilesApi.deleteFile(name);
|
||||||
} finally {
|
showNotification(t('auth_files.delete_success'), 'success');
|
||||||
setDeleting(null);
|
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_filtered_confirm', { type: typeLabel })
|
||||||
: t('auth_files.delete_all_confirm');
|
: 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);
|
if (filesToDelete.length === 0) {
|
||||||
try {
|
showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info');
|
||||||
if (!isFiltered) {
|
setDeletingAll(false);
|
||||||
// 删除全部
|
return;
|
||||||
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) {
|
let success = 0;
|
||||||
showNotification(t('auth_files.delete_filtered_none', { type: typeLabel }), 'info');
|
let failed = 0;
|
||||||
setDeletingAll(false);
|
const deletedNames: string[] = [];
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let success = 0;
|
for (const file of filesToDelete) {
|
||||||
let failed = 0;
|
try {
|
||||||
const deletedNames: string[] = [];
|
await authFilesApi.deleteFile(file.name);
|
||||||
|
success++;
|
||||||
|
deletedNames.push(file.name);
|
||||||
|
} catch {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const file of filesToDelete) {
|
setFiles((prev) => prev.filter((f) => !deletedNames.includes(f.name)));
|
||||||
try {
|
|
||||||
await authFilesApi.deleteFile(file.name);
|
if (failed === 0) {
|
||||||
success++;
|
showNotification(
|
||||||
deletedNames.push(file.name);
|
t('auth_files.delete_filtered_success', { count: success, type: typeLabel }),
|
||||||
} catch {
|
'success'
|
||||||
failed++;
|
);
|
||||||
|
} 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 deleteExcluded = async (provider: string) => {
|
||||||
const providerLabel = provider.trim() || provider;
|
const providerLabel = provider.trim() || provider;
|
||||||
if (!window.confirm(t('oauth_excluded.delete_confirm', { provider: providerLabel }))) return;
|
showConfirmation({
|
||||||
const providerKey = normalizeProviderKey(provider);
|
title: t('oauth_excluded.delete_title', { defaultValue: 'Delete Exclusion' }),
|
||||||
if (!providerKey) {
|
message: t('oauth_excluded.delete_confirm', { provider: providerLabel }),
|
||||||
showNotification(t('oauth_excluded.provider_required'), 'error');
|
variant: 'danger',
|
||||||
return;
|
confirmText: t('common.confirm'),
|
||||||
}
|
onConfirm: async () => {
|
||||||
try {
|
const providerKey = normalizeProviderKey(provider);
|
||||||
await authFilesApi.deleteOauthExcludedEntry(providerKey);
|
if (!providerKey) {
|
||||||
await loadExcluded();
|
showNotification(t('oauth_excluded.provider_required'), 'error');
|
||||||
showNotification(t('oauth_excluded.delete_success'), 'success');
|
return;
|
||||||
} catch (err: unknown) {
|
}
|
||||||
try {
|
try {
|
||||||
const current = await authFilesApi.getOauthExcludedModels();
|
await authFilesApi.deleteOauthExcludedEntry(providerKey);
|
||||||
const next: Record<string, string[]> = {};
|
await loadExcluded();
|
||||||
Object.entries(current).forEach(([key, models]) => {
|
showNotification(t('oauth_excluded.delete_success'), 'success');
|
||||||
if (normalizeProviderKey(key) === providerKey) return;
|
} catch (err: unknown) {
|
||||||
next[key] = models;
|
try {
|
||||||
});
|
const current = await authFilesApi.getOauthExcludedModels();
|
||||||
await authFilesApi.replaceOauthExcludedModels(next);
|
const next: Record<string, string[]> = {};
|
||||||
await loadExcluded();
|
Object.entries(current).forEach(([key, models]) => {
|
||||||
showNotification(t('oauth_excluded.delete_success'), 'success');
|
if (normalizeProviderKey(key) === providerKey) return;
|
||||||
} catch (fallbackErr: unknown) {
|
next[key] = models;
|
||||||
const errorMessage =
|
});
|
||||||
fallbackErr instanceof Error
|
await authFilesApi.replaceOauthExcludedModels(next);
|
||||||
? fallbackErr.message
|
await loadExcluded();
|
||||||
: err instanceof Error
|
showNotification(t('oauth_excluded.delete_success'), 'success');
|
||||||
? err.message
|
} catch (fallbackErr: unknown) {
|
||||||
: '';
|
const errorMessage =
|
||||||
showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error');
|
fallbackErr instanceof Error
|
||||||
}
|
? fallbackErr.message
|
||||||
}
|
: err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: '';
|
||||||
|
showNotification(`${t('oauth_excluded.delete_failed')}: ${errorMessage}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// OAuth 模型映射相关方法
|
// OAuth 模型映射相关方法
|
||||||
@@ -1218,15 +1239,22 @@ export function AuthFilesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteModelMappings = async (provider: string) => {
|
const deleteModelMappings = async (provider: string) => {
|
||||||
if (!window.confirm(t('oauth_model_mappings.delete_confirm', { provider }))) return;
|
showConfirmation({
|
||||||
try {
|
title: t('oauth_model_mappings.delete_title', { defaultValue: 'Delete Mappings' }),
|
||||||
await authFilesApi.deleteOauthModelMappings(provider);
|
message: t('oauth_model_mappings.delete_confirm', { provider }),
|
||||||
await loadModelMappings();
|
variant: 'danger',
|
||||||
showNotification(t('oauth_model_mappings.delete_success'), 'success');
|
confirmText: t('common.confirm'),
|
||||||
} catch (err: unknown) {
|
onConfirm: async () => {
|
||||||
const errorMessage = err instanceof Error ? err.message : '';
|
try {
|
||||||
showNotification(`${t('oauth_model_mappings.delete_failed')}: ${errorMessage}`, 'error');
|
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() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.providerField}>
|
<div className={styles.providerField}>
|
||||||
<Input
|
<AutocompleteInput
|
||||||
id="oauth-excluded-provider"
|
id="oauth-excluded-provider"
|
||||||
list="oauth-excluded-provider-options"
|
|
||||||
label={t('oauth_excluded.provider_label')}
|
label={t('oauth_excluded.provider_label')}
|
||||||
hint={t('oauth_excluded.provider_hint')}
|
hint={t('oauth_excluded.provider_hint')}
|
||||||
placeholder={t('oauth_excluded.provider_placeholder')}
|
placeholder={t('oauth_excluded.provider_placeholder')}
|
||||||
value={excludedForm.provider}
|
value={excludedForm.provider}
|
||||||
onChange={(e) => setExcludedForm((prev) => ({ ...prev, provider: e.target.value }))}
|
onChange={(val) => setExcludedForm((prev) => ({ ...prev, provider: val }))}
|
||||||
|
options={providerOptions}
|
||||||
/>
|
/>
|
||||||
<datalist id="oauth-excluded-provider-options">
|
|
||||||
{providerOptions.map((provider) => (
|
|
||||||
<option key={provider} value={provider} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
{providerOptions.length > 0 && (
|
{providerOptions.length > 0 && (
|
||||||
<div className={styles.providerTagList}>
|
<div className={styles.providerTagList}>
|
||||||
{providerOptions.map((provider) => {
|
{providerOptions.map((provider) => {
|
||||||
@@ -1945,20 +1968,15 @@ export function AuthFilesPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.providerField}>
|
<div className={styles.providerField}>
|
||||||
<Input
|
<AutocompleteInput
|
||||||
id="oauth-model-alias-provider"
|
id="oauth-model-alias-provider"
|
||||||
list="oauth-model-alias-provider-options"
|
|
||||||
label={t('oauth_model_mappings.provider_label')}
|
label={t('oauth_model_mappings.provider_label')}
|
||||||
hint={t('oauth_model_mappings.provider_hint')}
|
hint={t('oauth_model_mappings.provider_hint')}
|
||||||
placeholder={t('oauth_model_mappings.provider_placeholder')}
|
placeholder={t('oauth_model_mappings.provider_placeholder')}
|
||||||
value={mappingForm.provider}
|
value={mappingForm.provider}
|
||||||
onChange={(e) => setMappingForm((prev) => ({ ...prev, provider: e.target.value }))}
|
onChange={(val) => setMappingForm((prev) => ({ ...prev, provider: val }))}
|
||||||
|
options={providerOptions}
|
||||||
/>
|
/>
|
||||||
<datalist id="oauth-model-alias-provider-options">
|
|
||||||
{providerOptions.map((provider) => (
|
|
||||||
<option key={provider} value={provider} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
{providerOptions.length > 0 && (
|
{providerOptions.length > 0 && (
|
||||||
<div className={styles.providerTagList}>
|
<div className={styles.providerTagList}>
|
||||||
{providerOptions.map((provider) => {
|
{providerOptions.map((provider) => {
|
||||||
@@ -1980,9 +1998,8 @@ export function AuthFilesPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.providerField}>
|
<div className={styles.providerField}>
|
||||||
<Input
|
<AutocompleteInput
|
||||||
id="oauth-model-mapping-model-source"
|
id="oauth-model-mapping-model-source"
|
||||||
list="oauth-model-mapping-model-source-options"
|
|
||||||
label={t('oauth_model_mappings.model_source_label')}
|
label={t('oauth_model_mappings.model_source_label')}
|
||||||
hint={
|
hint={
|
||||||
mappingModelsLoading
|
mappingModelsLoading
|
||||||
@@ -1997,14 +2014,10 @@ export function AuthFilesPage() {
|
|||||||
}
|
}
|
||||||
placeholder={t('oauth_model_mappings.model_source_placeholder')}
|
placeholder={t('oauth_model_mappings.model_source_placeholder')}
|
||||||
value={mappingModelsFileName}
|
value={mappingModelsFileName}
|
||||||
onChange={(e) => setMappingModelsFileName(e.target.value)}
|
onChange={(val) => setMappingModelsFileName(val)}
|
||||||
disabled={savingMappings}
|
disabled={savingMappings}
|
||||||
|
options={modelSourceFileOptions}
|
||||||
/>
|
/>
|
||||||
<datalist id="oauth-model-mapping-model-source-options">
|
|
||||||
{modelSourceFileOptions.map((fileName) => (
|
|
||||||
<option key={fileName} value={fileName} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>{t('oauth_model_mappings.mappings_label')}</label>
|
<label>{t('oauth_model_mappings.mappings_label')}</label>
|
||||||
@@ -2012,13 +2025,16 @@ export function AuthFilesPage() {
|
|||||||
{(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map(
|
{(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map(
|
||||||
(entry, index) => (
|
(entry, index) => (
|
||||||
<div key={entry.id} className={styles.mappingRow}>
|
<div key={entry.id} className={styles.mappingRow}>
|
||||||
<input
|
<AutocompleteInput
|
||||||
className="input"
|
wrapperStyle={{ flex: 1, marginBottom: 0 }}
|
||||||
placeholder={t('oauth_model_mappings.mapping_name_placeholder')}
|
placeholder={t('oauth_model_mappings.mapping_name_placeholder')}
|
||||||
list={mappingModelsList.length ? 'oauth-model-mapping-model-options' : undefined}
|
|
||||||
value={entry.name}
|
value={entry.name}
|
||||||
onChange={(e) => updateMappingEntry(index, 'name', e.target.value)}
|
onChange={(val) => updateMappingEntry(index, 'name', val)}
|
||||||
disabled={savingMappings}
|
disabled={savingMappings}
|
||||||
|
options={mappingModelsList.map((m) => ({
|
||||||
|
value: m.id,
|
||||||
|
label: m.display_name && m.display_name !== m.id ? m.display_name : undefined,
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
<span className={styles.mappingSeparator}>→</span>
|
<span className={styles.mappingSeparator}>→</span>
|
||||||
<input
|
<input
|
||||||
@@ -2027,6 +2043,7 @@ export function AuthFilesPage() {
|
|||||||
value={entry.alias}
|
value={entry.alias}
|
||||||
onChange={(e) => updateMappingEntry(index, 'alias', e.target.value)}
|
onChange={(e) => updateMappingEntry(index, 'alias', e.target.value)}
|
||||||
disabled={savingMappings}
|
disabled={savingMappings}
|
||||||
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<div className={styles.mappingFork}>
|
<div className={styles.mappingFork}>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@@ -2060,13 +2077,6 @@ export function AuthFilesPage() {
|
|||||||
{t('oauth_model_mappings.add_mapping')}
|
{t('oauth_model_mappings.add_mapping')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<datalist id="oauth-model-mapping-model-options">
|
|
||||||
{mappingModelsList.map((model) => (
|
|
||||||
<option key={model.id} value={model.id}>
|
|
||||||
{model.display_name && model.display_name !== model.id ? model.display_name : null}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
<div className={styles.hint}>{t('oauth_model_mappings.mappings_hint')}</div>
|
<div className={styles.hint}>{t('oauth_model_mappings.mappings_hint')}</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ type TabType = 'logs' | 'errors';
|
|||||||
|
|
||||||
export function LogsPage() {
|
export function LogsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification, showConfirmation } = useNotificationStore();
|
||||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||||
const requestLogEnabled = useConfigStore((state) => state.config?.requestLog ?? false);
|
const requestLogEnabled = useConfigStore((state) => state.config?.requestLog ?? false);
|
||||||
|
|
||||||
@@ -478,19 +478,26 @@ export function LogsPage() {
|
|||||||
useHeaderRefresh(() => loadLogs(false));
|
useHeaderRefresh(() => loadLogs(false));
|
||||||
|
|
||||||
const clearLogs = async () => {
|
const clearLogs = async () => {
|
||||||
if (!window.confirm(t('logs.clear_confirm'))) return;
|
showConfirmation({
|
||||||
try {
|
title: t('logs.clear_confirm_title', { defaultValue: 'Clear Logs' }),
|
||||||
await logsApi.clearLogs();
|
message: t('logs.clear_confirm'),
|
||||||
setLogState({ buffer: [], visibleFrom: 0 });
|
variant: 'danger',
|
||||||
latestTimestampRef.current = 0;
|
confirmText: t('common.confirm'),
|
||||||
showNotification(t('logs.clear_success'), 'success');
|
onConfirm: async () => {
|
||||||
} catch (err: unknown) {
|
try {
|
||||||
const message = getErrorMessage(err);
|
await logsApi.clearLogs();
|
||||||
showNotification(
|
setLogState({ buffer: [], visibleFrom: 0 });
|
||||||
`${t('notification.delete_failed')}${message ? `: ${message}` : ''}`,
|
latestTimestampRef.current = 0;
|
||||||
'error'
|
showNotification(t('logs.clear_success'), 'success');
|
||||||
);
|
} catch (err: unknown) {
|
||||||
}
|
const message = getErrorMessage(err);
|
||||||
|
showNotification(
|
||||||
|
`${t('notification.delete_failed')}${message ? `: ${message}` : ''}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadLogs = () => {
|
const downloadLogs = () => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import styles from './SystemPage.module.scss';
|
|||||||
|
|
||||||
export function SystemPage() {
|
export function SystemPage() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { showNotification } = useNotificationStore();
|
const { showNotification, showConfirmation } = useNotificationStore();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const config = useConfigStore((state) => state.config);
|
const config = useConfigStore((state) => state.config);
|
||||||
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
||||||
@@ -106,12 +106,19 @@ export function SystemPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClearLoginStorage = () => {
|
const handleClearLoginStorage = () => {
|
||||||
if (!window.confirm(t('system_info.clear_login_confirm'))) return;
|
showConfirmation({
|
||||||
auth.logout();
|
title: t('system_info.clear_login_title', { defaultValue: 'Clear Login Storage' }),
|
||||||
if (typeof localStorage === 'undefined') return;
|
message: t('system_info.clear_login_confirm'),
|
||||||
const keysToRemove = [STORAGE_KEY_AUTH, 'isLoggedIn', 'apiBase', 'apiUrl', 'managementKey'];
|
variant: 'danger',
|
||||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
confirmText: t('common.confirm'),
|
||||||
showNotification(t('notification.login_storage_cleared'), 'success');
|
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(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user