mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat(auth-files): add per-file enable/disable toggle
This commit is contained in:
@@ -4,6 +4,7 @@ interface ToggleSwitchProps {
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (value: boolean) => void;
|
onChange: (value: boolean) => void;
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
|
ariaLabel?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
labelPosition?: 'left' | 'right';
|
labelPosition?: 'left' | 'right';
|
||||||
}
|
}
|
||||||
@@ -12,6 +13,7 @@ export function ToggleSwitch({
|
|||||||
checked,
|
checked,
|
||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
|
ariaLabel,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
labelPosition = 'right'
|
labelPosition = 'right'
|
||||||
}: ToggleSwitchProps) {
|
}: ToggleSwitchProps) {
|
||||||
@@ -25,7 +27,13 @@ export function ToggleSwitch({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={className}>
|
<label className={className}>
|
||||||
<input type="checkbox" checked={checked} onChange={handleChange} disabled={disabled} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
/>
|
||||||
<span className="track">
|
<span className="track">
|
||||||
<span className="thumb" />
|
<span className="thumb" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -395,7 +395,10 @@
|
|||||||
"models_unsupported": "This feature is not supported in the current version",
|
"models_unsupported": "This feature is not supported in the current version",
|
||||||
"models_unsupported_desc": "Please update CLI Proxy API to the latest version and try again",
|
"models_unsupported_desc": "Please update CLI Proxy API to the latest version and try again",
|
||||||
"models_excluded_badge": "Excluded",
|
"models_excluded_badge": "Excluded",
|
||||||
"models_excluded_hint": "This model is excluded by OAuth"
|
"models_excluded_hint": "This model is excluded by OAuth",
|
||||||
|
"status_toggle_label": "Enabled",
|
||||||
|
"status_enabled_success": "\"{{name}}\" enabled",
|
||||||
|
"status_disabled_success": "\"{{name}}\" disabled"
|
||||||
},
|
},
|
||||||
"antigravity_quota": {
|
"antigravity_quota": {
|
||||||
"title": "Antigravity Quota",
|
"title": "Antigravity Quota",
|
||||||
|
|||||||
@@ -395,7 +395,10 @@
|
|||||||
"models_unsupported": "当前版本不支持此功能",
|
"models_unsupported": "当前版本不支持此功能",
|
||||||
"models_unsupported_desc": "请更新 CLI Proxy API 到最新版本后重试",
|
"models_unsupported_desc": "请更新 CLI Proxy API 到最新版本后重试",
|
||||||
"models_excluded_badge": "已排除",
|
"models_excluded_badge": "已排除",
|
||||||
"models_excluded_hint": "此模型已被 OAuth 排除"
|
"models_excluded_hint": "此模型已被 OAuth 排除",
|
||||||
|
"status_toggle_label": "启用",
|
||||||
|
"status_enabled_success": "已启用 \"{{name}}\"",
|
||||||
|
"status_disabled_success": "已停用 \"{{name}}\""
|
||||||
},
|
},
|
||||||
"antigravity_quota": {
|
"antigravity_quota": {
|
||||||
"title": "Antigravity 额度",
|
"title": "Antigravity 额度",
|
||||||
|
|||||||
@@ -601,10 +601,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: $spacing-xs;
|
gap: $spacing-xs;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: $spacing-sm;
|
padding-top: $spacing-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusToggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
.iconButton:global(.btn.btn-sm) {
|
.iconButton:global(.btn.btn-sm) {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ export function AuthFilesPage() {
|
|||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [deleting, setDeleting] = useState<string | null>(null);
|
const [deleting, setDeleting] = useState<string | null>(null);
|
||||||
const [deletingAll, setDeletingAll] = useState(false);
|
const [deletingAll, setDeletingAll] = useState(false);
|
||||||
|
const [statusUpdating, setStatusUpdating] = useState<Record<string, boolean>>({});
|
||||||
const [keyStats, setKeyStats] = useState<KeyStats>({ bySource: {}, byAuthIndex: {} });
|
const [keyStats, setKeyStats] = useState<KeyStats>({ bySource: {}, byAuthIndex: {} });
|
||||||
const [usageDetails, setUsageDetails] = useState<UsageDetail[]>([]);
|
const [usageDetails, setUsageDetails] = useState<UsageDetail[]>([]);
|
||||||
|
|
||||||
@@ -608,6 +609,42 @@ export function AuthFilesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStatusToggle = async (item: AuthFileItem, enabled: boolean) => {
|
||||||
|
const name = item.name;
|
||||||
|
const nextDisabled = !enabled;
|
||||||
|
const previousDisabled = item.disabled === true;
|
||||||
|
|
||||||
|
setStatusUpdating((prev) => ({ ...prev, [name]: true }));
|
||||||
|
// Optimistic update for snappy UI.
|
||||||
|
setFiles((prev) => prev.map((f) => (f.name === name ? { ...f, disabled: nextDisabled } : f)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await authFilesApi.setStatus(name, nextDisabled);
|
||||||
|
setFiles((prev) =>
|
||||||
|
prev.map((f) => (f.name === name ? { ...f, disabled: res.disabled } : f))
|
||||||
|
);
|
||||||
|
showNotification(
|
||||||
|
enabled
|
||||||
|
? t('auth_files.status_enabled_success', { name })
|
||||||
|
: t('auth_files.status_disabled_success', { name }),
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : '';
|
||||||
|
setFiles((prev) =>
|
||||||
|
prev.map((f) => (f.name === name ? { ...f, disabled: previousDisabled } : f))
|
||||||
|
);
|
||||||
|
showNotification(`${t('notification.update_failed')}: ${errorMessage}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setStatusUpdating((prev) => {
|
||||||
|
if (!prev[name]) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[name];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 显示详情弹窗
|
// 显示详情弹窗
|
||||||
const showDetails = (file: AuthFileItem) => {
|
const showDetails = (file: AuthFileItem) => {
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
@@ -1021,6 +1058,16 @@ export function AuthFilesPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!isRuntimeOnly && (
|
||||||
|
<div className={styles.statusToggle}>
|
||||||
|
<ToggleSwitch
|
||||||
|
ariaLabel={t('auth_files.status_toggle_label')}
|
||||||
|
checked={!item.disabled}
|
||||||
|
disabled={disableControls || statusUpdating[item.name] === true}
|
||||||
|
onChange={(value) => void handleStatusToggle(item, value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isRuntimeOnly && (
|
{isRuntimeOnly && (
|
||||||
<div className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</div>
|
<div className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { AuthFilesResponse } from '@/types/authFile';
|
|||||||
import type { OAuthModelMappingEntry } from '@/types';
|
import type { OAuthModelMappingEntry } from '@/types';
|
||||||
|
|
||||||
type StatusError = { status?: number };
|
type StatusError = { status?: number };
|
||||||
|
type AuthFileStatusResponse = { status: string; disabled: boolean };
|
||||||
|
|
||||||
const getStatusCode = (err: unknown): number | undefined => {
|
const getStatusCode = (err: unknown): number | undefined => {
|
||||||
if (!err || typeof err !== 'object') return undefined;
|
if (!err || typeof err !== 'object') return undefined;
|
||||||
@@ -17,7 +18,8 @@ const getStatusCode = (err: unknown): number | undefined => {
|
|||||||
const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]> => {
|
const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]> => {
|
||||||
if (!payload || typeof payload !== 'object') return {};
|
if (!payload || typeof payload !== 'object') return {};
|
||||||
|
|
||||||
const source = (payload as any)['oauth-excluded-models'] ?? (payload as any).items ?? payload;
|
const record = payload as Record<string, unknown>;
|
||||||
|
const source = record['oauth-excluded-models'] ?? record.items ?? payload;
|
||||||
if (!source || typeof source !== 'object') return {};
|
if (!source || typeof source !== 'object') return {};
|
||||||
|
|
||||||
const result: Record<string, string[]> = {};
|
const result: Record<string, string[]> = {};
|
||||||
@@ -54,10 +56,11 @@ const normalizeOauthExcludedModels = (payload: unknown): Record<string, string[]
|
|||||||
const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthModelMappingEntry[]> => {
|
const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthModelMappingEntry[]> => {
|
||||||
if (!payload || typeof payload !== 'object') return {};
|
if (!payload || typeof payload !== 'object') return {};
|
||||||
|
|
||||||
|
const record = payload as Record<string, unknown>;
|
||||||
const source =
|
const source =
|
||||||
(payload as any)['oauth-model-mappings'] ??
|
record['oauth-model-mappings'] ??
|
||||||
(payload as any)['oauth-model-alias'] ??
|
record['oauth-model-alias'] ??
|
||||||
(payload as any).items ??
|
record.items ??
|
||||||
payload;
|
payload;
|
||||||
if (!source || typeof source !== 'object') return {};
|
if (!source || typeof source !== 'object') return {};
|
||||||
|
|
||||||
@@ -70,16 +73,17 @@ const normalizeOauthModelMappings = (payload: unknown): Record<string, OAuthMode
|
|||||||
if (!key) return;
|
if (!key) return;
|
||||||
if (!Array.isArray(mappings)) return;
|
if (!Array.isArray(mappings)) return;
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const normalized = mappings
|
const normalized = mappings
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
if (!item || typeof item !== 'object') return null;
|
if (!item || typeof item !== 'object') return null;
|
||||||
const name = String((item as any).name ?? (item as any).id ?? (item as any).model ?? '').trim();
|
const entry = item as Record<string, unknown>;
|
||||||
const alias = String((item as any).alias ?? '').trim();
|
const name = String(entry.name ?? entry.id ?? entry.model ?? '').trim();
|
||||||
if (!name || !alias) return null;
|
const alias = String(entry.alias ?? '').trim();
|
||||||
const fork = (item as any).fork === true;
|
if (!name || !alias) return null;
|
||||||
return fork ? { name, alias, fork } : { name, alias };
|
const fork = entry.fork === true;
|
||||||
})
|
return fork ? { name, alias, fork } : { name, alias };
|
||||||
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter((entry) => {
|
.filter((entry) => {
|
||||||
const mapping = entry as OAuthModelMappingEntry;
|
const mapping = entry as OAuthModelMappingEntry;
|
||||||
@@ -103,6 +107,9 @@ const OAUTH_MODEL_MAPPINGS_LEGACY_ENDPOINT = '/oauth-model-alias';
|
|||||||
export const authFilesApi = {
|
export const authFilesApi = {
|
||||||
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
list: () => apiClient.get<AuthFilesResponse>('/auth-files'),
|
||||||
|
|
||||||
|
setStatus: (name: string, disabled: boolean) =>
|
||||||
|
apiClient.patch<AuthFileStatusResponse>('/auth-files/status', { name, disabled }),
|
||||||
|
|
||||||
upload: (file: File) => {
|
upload: (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file, file.name);
|
formData.append('file', file, file.name);
|
||||||
|
|||||||
Reference in New Issue
Block a user