mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat(ui): implement custom AutocompleteInput and refactor model mapping UI
This commit is contained in:
@@ -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<string, string[]> = {};
|
||||
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<string, string[]> = {};
|
||||
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() {
|
||||
}
|
||||
>
|
||||
<div className={styles.providerField}>
|
||||
<Input
|
||||
<AutocompleteInput
|
||||
id="oauth-excluded-provider"
|
||||
list="oauth-excluded-provider-options"
|
||||
label={t('oauth_excluded.provider_label')}
|
||||
hint={t('oauth_excluded.provider_hint')}
|
||||
placeholder={t('oauth_excluded.provider_placeholder')}
|
||||
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 && (
|
||||
<div className={styles.providerTagList}>
|
||||
{providerOptions.map((provider) => {
|
||||
@@ -1945,20 +1968,15 @@ export function AuthFilesPage() {
|
||||
}
|
||||
>
|
||||
<div className={styles.providerField}>
|
||||
<Input
|
||||
<AutocompleteInput
|
||||
id="oauth-model-alias-provider"
|
||||
list="oauth-model-alias-provider-options"
|
||||
label={t('oauth_model_mappings.provider_label')}
|
||||
hint={t('oauth_model_mappings.provider_hint')}
|
||||
placeholder={t('oauth_model_mappings.provider_placeholder')}
|
||||
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 && (
|
||||
<div className={styles.providerTagList}>
|
||||
{providerOptions.map((provider) => {
|
||||
@@ -1980,9 +1998,8 @@ export function AuthFilesPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.providerField}>
|
||||
<Input
|
||||
<AutocompleteInput
|
||||
id="oauth-model-mapping-model-source"
|
||||
list="oauth-model-mapping-model-source-options"
|
||||
label={t('oauth_model_mappings.model_source_label')}
|
||||
hint={
|
||||
mappingModelsLoading
|
||||
@@ -1997,14 +2014,10 @@ export function AuthFilesPage() {
|
||||
}
|
||||
placeholder={t('oauth_model_mappings.model_source_placeholder')}
|
||||
value={mappingModelsFileName}
|
||||
onChange={(e) => setMappingModelsFileName(e.target.value)}
|
||||
onChange={(val) => setMappingModelsFileName(val)}
|
||||
disabled={savingMappings}
|
||||
options={modelSourceFileOptions}
|
||||
/>
|
||||
<datalist id="oauth-model-mapping-model-source-options">
|
||||
{modelSourceFileOptions.map((fileName) => (
|
||||
<option key={fileName} value={fileName} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>{t('oauth_model_mappings.mappings_label')}</label>
|
||||
@@ -2012,13 +2025,16 @@ export function AuthFilesPage() {
|
||||
{(mappingForm.mappings.length ? mappingForm.mappings : [buildEmptyMappingEntry()]).map(
|
||||
(entry, index) => (
|
||||
<div key={entry.id} className={styles.mappingRow}>
|
||||
<input
|
||||
className="input"
|
||||
<AutocompleteInput
|
||||
wrapperStyle={{ flex: 1, marginBottom: 0 }}
|
||||
placeholder={t('oauth_model_mappings.mapping_name_placeholder')}
|
||||
list={mappingModelsList.length ? 'oauth-model-mapping-model-options' : undefined}
|
||||
value={entry.name}
|
||||
onChange={(e) => 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,
|
||||
}))}
|
||||
/>
|
||||
<span className={styles.mappingSeparator}>→</span>
|
||||
<input
|
||||
@@ -2027,6 +2043,7 @@ export function AuthFilesPage() {
|
||||
value={entry.alias}
|
||||
onChange={(e) => updateMappingEntry(index, 'alias', e.target.value)}
|
||||
disabled={savingMappings}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<div className={styles.mappingFork}>
|
||||
<ToggleSwitch
|
||||
@@ -2060,13 +2077,6 @@ export function AuthFilesPage() {
|
||||
{t('oauth_model_mappings.add_mapping')}
|
||||
</Button>
|
||||
</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>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user