mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Merge pull request #149 from RGBadmin/feat/auth-priority-sort
feat(auth-files): priority display, sorting & credential notes
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
getTypeColor,
|
||||
getTypeLabel,
|
||||
isRuntimeOnlyAuthFile,
|
||||
parsePriorityValue,
|
||||
resolveAuthFileStats,
|
||||
type QuotaProviderType,
|
||||
type ResolvedTheme,
|
||||
@@ -110,6 +111,9 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
const hasStatusWarning =
|
||||
Boolean(rawStatusMessage) && !HEALTHY_STATUS_MESSAGES.has(rawStatusMessage.toLowerCase());
|
||||
|
||||
const priorityValue = parsePriorityValue(file.priority ?? file['priority']);
|
||||
const noteValue = typeof file.note === 'string' ? file.note.trim() : '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.fileCard} ${providerCardClass} ${selected ? styles.fileCardSelected : ''} ${file.disabled ? styles.fileCardDisabled : ''}`}
|
||||
@@ -147,8 +151,20 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
<span>
|
||||
{t('auth_files.file_modified')}: {formatModified(file)}
|
||||
</span>
|
||||
{priorityValue !== undefined && (
|
||||
<span className={styles.priorityBadge}>
|
||||
{t('auth_files.priority_display')}: <span className={styles.priorityValue}>{priorityValue}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{noteValue && (
|
||||
<div className={styles.noteText} title={noteValue}>
|
||||
<span className={styles.noteLabel}>{t('auth_files.note_display')}: </span>
|
||||
{noteValue}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rawStatusMessage && hasStatusWarning && (
|
||||
<div className={styles.healthStatusMessage} title={rawStatusMessage}>
|
||||
{rawStatusMessage}
|
||||
|
||||
@@ -114,6 +114,14 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('disableCooling', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth_files.note_label')}
|
||||
value={editor.note}
|
||||
placeholder={t('auth_files.note_placeholder')}
|
||||
hint={t('auth_files.note_hint')}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('note', e.target.value)}
|
||||
/>
|
||||
{editor.isCodexFile && (
|
||||
<div className="form-group">
|
||||
<label>{t('ai_providers.codex_websockets_label')}</label>
|
||||
|
||||
@@ -18,7 +18,8 @@ export type PrefixProxyEditorField =
|
||||
| 'priority'
|
||||
| 'excludedModelsText'
|
||||
| 'disableCooling'
|
||||
| 'websocket';
|
||||
| 'websocket'
|
||||
| 'note';
|
||||
|
||||
export type PrefixProxyEditorFieldValue = string | boolean;
|
||||
|
||||
@@ -37,6 +38,7 @@ export type PrefixProxyEditorState = {
|
||||
excludedModelsText: string;
|
||||
disableCooling: string;
|
||||
websocket: boolean;
|
||||
note: string;
|
||||
};
|
||||
|
||||
export type UseAuthFilesPrefixProxyEditorOptions = {
|
||||
@@ -93,6 +95,13 @@ const buildPrefixProxyUpdatedText = (editor: PrefixProxyEditorState | null): str
|
||||
next.websocket = editor.websocket;
|
||||
}
|
||||
|
||||
const noteValue = editor.note.trim();
|
||||
if (noteValue) {
|
||||
next.note = noteValue;
|
||||
} else if ('note' in next) {
|
||||
delete next.note;
|
||||
}
|
||||
|
||||
return JSON.stringify(next);
|
||||
};
|
||||
|
||||
@@ -146,6 +155,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
excludedModelsText: '',
|
||||
disableCooling: '',
|
||||
websocket: false,
|
||||
note: '',
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -195,6 +205,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
const excludedModels = normalizeExcludedModels(json.excluded_models);
|
||||
const disableCoolingValue = parseDisableCoolingValue(json.disable_cooling);
|
||||
const websocketValue = parseDisableCoolingValue(json.websocket);
|
||||
const note = typeof json.note === 'string' ? json.note : '';
|
||||
|
||||
setPrefixProxyEditor((prev) => {
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
@@ -211,6 +222,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
disableCooling:
|
||||
disableCoolingValue === undefined ? '' : disableCoolingValue ? 'true' : 'false',
|
||||
websocket: websocketValue ?? false,
|
||||
note,
|
||||
error: null,
|
||||
};
|
||||
});
|
||||
@@ -235,6 +247,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
if (field === 'priority') return { ...prev, priority: String(value) };
|
||||
if (field === 'excludedModelsText') return { ...prev, excludedModelsText: String(value) };
|
||||
if (field === 'disableCooling') return { ...prev, disableCooling: String(value) };
|
||||
if (field === 'note') return { ...prev, note: String(value) };
|
||||
return { ...prev, websocket: Boolean(value) };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -495,6 +495,11 @@
|
||||
"search_placeholder": "Filter by name, type, or provider",
|
||||
"problem_filter_label": "Problem Filter",
|
||||
"problem_filter_only": "Only show problematic credentials",
|
||||
"sort_label": "Sort",
|
||||
"sort_default": "Default",
|
||||
"sort_az": "A-Z Name",
|
||||
"sort_priority": "Priority",
|
||||
"priority_display": "Priority",
|
||||
"page_size_label": "Per page",
|
||||
"page_size_unit": "items",
|
||||
"view_mode_paged": "Paged",
|
||||
@@ -564,6 +569,10 @@
|
||||
"disable_cooling_label": "Disable cooling (disable_cooling)",
|
||||
"disable_cooling_placeholder": "e.g. true / false / 1 / 0",
|
||||
"disable_cooling_hint": "Supports booleans, numeric 0/non-0, and strings like true/false/1/0; unparseable values are ignored.",
|
||||
"note_label": "Note",
|
||||
"note_placeholder": "Enter a note, e.g.: John's account",
|
||||
"note_hint": "Optional. Used to describe the purpose or owner of this credential; leave empty to omit.",
|
||||
"note_display": "Note",
|
||||
"prefix_proxy_invalid_json": "This auth file is not a JSON object, so fields cannot be edited.",
|
||||
"prefix_proxy_saved_success": "Updated auth file \"{{name}}\" successfully",
|
||||
"quota_refresh_success": "Quota refreshed for \"{{name}}\"",
|
||||
|
||||
@@ -495,6 +495,11 @@
|
||||
"search_placeholder": "Фильтр по имени, типу или провайдеру",
|
||||
"problem_filter_label": "Фильтр проблем",
|
||||
"problem_filter_only": "Показывать только проблемные учётные данные",
|
||||
"sort_label": "Сортировка",
|
||||
"sort_default": "По умолчанию",
|
||||
"sort_az": "A-Z Имя",
|
||||
"sort_priority": "Приоритет",
|
||||
"priority_display": "Приоритет",
|
||||
"page_size_label": "На странице",
|
||||
"page_size_unit": "элементов",
|
||||
"view_mode_paged": "Постранично",
|
||||
@@ -564,6 +569,10 @@
|
||||
"disable_cooling_label": "Отключение охлаждения (disable_cooling)",
|
||||
"disable_cooling_placeholder": "например: true / false / 1 / 0",
|
||||
"disable_cooling_hint": "Поддерживает boolean, числа 0/не 0 и строки true/false/1/0; непарсируемые значения игнорируются.",
|
||||
"note_label": "Заметка (note)",
|
||||
"note_placeholder": "Введите заметку, например: аккаунт Ивана",
|
||||
"note_hint": "Необязательно. Используется для описания назначения или владельца учётных данных; оставьте пустым, чтобы не записывать.",
|
||||
"note_display": "Заметка",
|
||||
"prefix_proxy_invalid_json": "Этот файл авторизации не является JSON-объектом, поэтому поля нельзя редактировать.",
|
||||
"prefix_proxy_saved_success": "Файл авторизации \"{{name}}\" успешно обновлён",
|
||||
"card_tools_title": "Инструменты",
|
||||
|
||||
@@ -495,6 +495,11 @@
|
||||
"search_placeholder": "输入名称、类型或提供方关键字",
|
||||
"problem_filter_label": "问题筛选",
|
||||
"problem_filter_only": "仅显示有问题凭证",
|
||||
"sort_label": "排序",
|
||||
"sort_default": "默认",
|
||||
"sort_az": "A-Z 名称",
|
||||
"sort_priority": "优先级",
|
||||
"priority_display": "优先级",
|
||||
"page_size_label": "单页数量",
|
||||
"page_size_unit": "个/页",
|
||||
"view_mode_paged": "按页显示",
|
||||
@@ -564,6 +569,10 @@
|
||||
"disable_cooling_label": "禁用冷却(disable_cooling)",
|
||||
"disable_cooling_placeholder": "例如: true / false / 1 / 0",
|
||||
"disable_cooling_hint": "支持布尔值、0/非0 数字或字符串 true/false/1/0;无法解析时忽略。",
|
||||
"note_label": "备注(note)",
|
||||
"note_placeholder": "输入备注信息,例如:张三的账号",
|
||||
"note_hint": "可选,用于标记凭证用途或归属;留空则不写入。",
|
||||
"note_display": "备注",
|
||||
"prefix_proxy_invalid_json": "该认证文件不是 JSON 对象,无法编辑字段。",
|
||||
"prefix_proxy_saved_success": "已更新认证文件 \"{{name}}\"",
|
||||
"quota_refresh_success": "已刷新 \"{{name}}\" 的额度",
|
||||
|
||||
@@ -593,6 +593,53 @@
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.priorityBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
.priorityValue {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
}
|
||||
|
||||
.noteText {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
|
||||
.noteLabel {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.sortSelect {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
height: 38px;
|
||||
box-sizing: border-box;
|
||||
min-width: 140px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.healthStatusMessage {
|
||||
font-size: 12px;
|
||||
color: var(--warning-text);
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
hasAuthFileStatusMessage,
|
||||
isRuntimeOnlyAuthFile,
|
||||
normalizeProviderKey,
|
||||
parsePriorityValue,
|
||||
type QuotaProviderType,
|
||||
type ResolvedTheme,
|
||||
} from '@/features/authFiles/constants';
|
||||
@@ -74,6 +75,7 @@ export function AuthFilesPage() {
|
||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<AuthFileItem | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'diagram' | 'list'>('list');
|
||||
const [sortMode, setSortMode] = useState<'default' | 'az' | 'priority'>('default');
|
||||
const [batchActionBarVisible, setBatchActionBarVisible] = useState(false);
|
||||
const floatingBatchActionsRef = useRef<HTMLDivElement>(null);
|
||||
const batchActionAnimationRef = useRef<AnimationPlaybackControlsWithThen | null>(null);
|
||||
@@ -281,10 +283,25 @@ export function AuthFilesPage() {
|
||||
});
|
||||
}, [filesMatchingProblemFilter, filter, search]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
const sorted = useMemo(() => {
|
||||
if (sortMode === 'default') return filtered;
|
||||
const copy = [...filtered];
|
||||
if (sortMode === 'az') {
|
||||
copy.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} else if (sortMode === 'priority') {
|
||||
copy.sort((a, b) => {
|
||||
const pa = parsePriorityValue(a.priority ?? a['priority']) ?? 0;
|
||||
const pb = parsePriorityValue(b.priority ?? b['priority']) ?? 0;
|
||||
return pb - pa; // 高优先级排前面
|
||||
});
|
||||
}
|
||||
return copy;
|
||||
}, [filtered, sortMode]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
const start = (currentPage - 1) * pageSize;
|
||||
const pageItems = filtered.slice(start, start + pageSize);
|
||||
const pageItems = sorted.slice(start, start + pageSize);
|
||||
const selectablePageItems = useMemo(
|
||||
() => pageItems.filter((file) => !isRuntimeOnlyAuthFile(file)),
|
||||
[pageItems]
|
||||
@@ -559,6 +576,21 @@ export function AuthFilesPage() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterItem}>
|
||||
<label>{t('auth_files.sort_label')}</label>
|
||||
<select
|
||||
className={styles.sortSelect}
|
||||
value={sortMode}
|
||||
onChange={(e) => {
|
||||
setSortMode(e.target.value as 'default' | 'az' | 'priority');
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="default">{t('auth_files.sort_default')}</option>
|
||||
<option value="az">{t('auth_files.sort_az')}</option>
|
||||
<option value="priority">{t('auth_files.sort_priority')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`${styles.filterItem} ${styles.filterToggleItem}`}>
|
||||
<label>{t('auth_files.problem_filter_label')}</label>
|
||||
<div className={styles.filterToggle}>
|
||||
@@ -615,7 +647,7 @@ export function AuthFilesPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && filtered.length > pageSize && (
|
||||
{!loading && sorted.length > pageSize && (
|
||||
<div className={styles.pagination}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -629,7 +661,7 @@ export function AuthFilesPage() {
|
||||
{t('auth_files.pagination_info', {
|
||||
current: currentPage,
|
||||
total: totalPages,
|
||||
count: filtered.length,
|
||||
count: sorted.length,
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user