mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Compare commits
3 Commits
@@ -64,6 +64,8 @@ interface QuotaCardProps<TState extends QuotaStatusState> {
|
||||
cardIdleMessageKey?: string;
|
||||
cardClassName: string;
|
||||
defaultType: string;
|
||||
canRefresh?: boolean;
|
||||
onRefresh?: () => void;
|
||||
renderQuotaItems: (quota: TState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode;
|
||||
}
|
||||
|
||||
@@ -75,6 +77,8 @@ export function QuotaCard<TState extends QuotaStatusState>({
|
||||
cardIdleMessageKey,
|
||||
cardClassName,
|
||||
defaultType,
|
||||
canRefresh = false,
|
||||
onRefresh,
|
||||
renderQuotaItems
|
||||
}: QuotaCardProps<TState>) {
|
||||
const { t } = useTranslation();
|
||||
@@ -90,7 +94,7 @@ export function QuotaCard<TState extends QuotaStatusState>({
|
||||
quota?.errorStatus,
|
||||
quota?.error || t('common.unknown_error')
|
||||
);
|
||||
const idleMessageKey = cardIdleMessageKey ?? `${i18nPrefix}.idle`;
|
||||
const idleMessageKey = onRefresh ? `${i18nPrefix}.idle` : (cardIdleMessageKey ?? `${i18nPrefix}.idle`);
|
||||
|
||||
const getTypeLabel = (type: string): string => {
|
||||
const key = `auth_files.filter_${type}`;
|
||||
@@ -120,7 +124,18 @@ export function QuotaCard<TState extends QuotaStatusState>({
|
||||
{quotaStatus === 'loading' ? (
|
||||
<div className={styles.quotaMessage}>{t(`${i18nPrefix}.loading`)}</div>
|
||||
) : quotaStatus === 'idle' ? (
|
||||
<div className={styles.quotaMessage}>{t(idleMessageKey)}</div>
|
||||
onRefresh ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.quotaMessage} ${styles.quotaMessageAction}`}
|
||||
onClick={onRefresh}
|
||||
disabled={!canRefresh}
|
||||
>
|
||||
{t(idleMessageKey)}
|
||||
</button>
|
||||
) : (
|
||||
<div className={styles.quotaMessage}>{t(idleMessageKey)}</div>
|
||||
)
|
||||
) : quotaStatus === 'error' ? (
|
||||
<div className={styles.quotaError}>
|
||||
{t(`${i18nPrefix}.load_failed`, {
|
||||
|
||||
@@ -8,8 +8,9 @@ import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||
import { useQuotaStore, useThemeStore } from '@/stores';
|
||||
import { useNotificationStore, useQuotaStore, useThemeStore } from '@/stores';
|
||||
import type { AuthFileItem, ResolvedTheme } from '@/types';
|
||||
import { getStatusFromError } from '@/utils/quota';
|
||||
import { QuotaCard } from './QuotaCard';
|
||||
import type { QuotaStatusState } from './QuotaCard';
|
||||
import { useQuotaLoader } from './useQuotaLoader';
|
||||
@@ -105,6 +106,7 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
}: QuotaSectionProps<TState, TData>) {
|
||||
const { t } = useTranslation();
|
||||
const resolvedTheme: ResolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
||||
const showNotification = useNotificationStore((state) => state.showNotification);
|
||||
const setQuota = useQuotaStore((state) => state[config.storeSetter]) as QuotaSetter<
|
||||
Record<string, TState>
|
||||
>;
|
||||
@@ -202,6 +204,39 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
});
|
||||
}, [filteredFiles, loading, setQuota]);
|
||||
|
||||
const refreshQuotaForFile = useCallback(
|
||||
async (file: AuthFileItem) => {
|
||||
if (disabled || file.disabled) return;
|
||||
if (quota[file.name]?.status === 'loading') return;
|
||||
|
||||
setQuota((prev) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildLoadingState()
|
||||
}));
|
||||
|
||||
try {
|
||||
const data = await config.fetchQuota(file, t);
|
||||
setQuota((prev) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildSuccessState(data)
|
||||
}));
|
||||
showNotification(t('auth_files.quota_refresh_success', { name: file.name }), 'success');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('common.unknown_error');
|
||||
const status = getStatusFromError(err);
|
||||
setQuota((prev) => ({
|
||||
...prev,
|
||||
[file.name]: config.buildErrorState(message, status)
|
||||
}));
|
||||
showNotification(
|
||||
t('auth_files.quota_refresh_failed', { name: file.name, message }),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
},
|
||||
[config, disabled, quota, setQuota, showNotification, t]
|
||||
);
|
||||
|
||||
const titleNode = (
|
||||
<div className={styles.titleWrapper}>
|
||||
<span>{t(`${config.i18nPrefix}.title`)}</span>
|
||||
@@ -222,15 +257,21 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
<div className={styles.headerActions}>
|
||||
<div className={styles.viewModeToggle}>
|
||||
<Button
|
||||
variant={effectiveViewMode === 'paged' ? 'primary' : 'secondary'}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={`${styles.viewModeButton} ${
|
||||
effectiveViewMode === 'paged' ? styles.viewModeButtonActive : ''
|
||||
}`}
|
||||
onClick={() => setViewMode('paged')}
|
||||
>
|
||||
{t('auth_files.view_mode_paged')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={effectiveViewMode === 'all' ? 'primary' : 'secondary'}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={`${styles.viewModeButton} ${
|
||||
effectiveViewMode === 'all' ? styles.viewModeButtonActive : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (filteredFiles.length > MAX_SHOW_ALL_THRESHOLD) {
|
||||
setShowTooManyWarning(true);
|
||||
@@ -245,13 +286,15 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className={styles.refreshAllButton}
|
||||
onClick={handleRefresh}
|
||||
disabled={disabled || isRefreshing}
|
||||
loading={isRefreshing}
|
||||
title={t('quota_management.refresh_files_and_quota')}
|
||||
aria-label={t('quota_management.refresh_files_and_quota')}
|
||||
title={t('quota_management.refresh_all_credentials')}
|
||||
aria-label={t('quota_management.refresh_all_credentials')}
|
||||
>
|
||||
{!isRefreshing && <IconRefreshCw size={16} />}
|
||||
{t('quota_management.refresh_all_credentials')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@@ -274,6 +317,8 @@ export function QuotaSection<TState extends QuotaStatusState, TData>({
|
||||
cardIdleMessageKey={config.cardIdleMessageKey}
|
||||
cardClassName={config.cardClassName}
|
||||
defaultType={config.type}
|
||||
canRefresh={!disabled && !item.disabled}
|
||||
onRefresh={() => void refreshQuotaForFile(item)}
|
||||
renderQuotaItems={config.renderQuotaItems}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -84,6 +84,8 @@ type QuotaUpdater<T> = T | ((prev: T) => T);
|
||||
type QuotaType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi';
|
||||
|
||||
const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
|
||||
const QUOTA_PROGRESS_HIGH_THRESHOLD = 70;
|
||||
const QUOTA_PROGRESS_MEDIUM_THRESHOLD = 30;
|
||||
const geminiCliSupplementaryRequestIds = new Map<string, number>();
|
||||
const geminiCliSupplementaryCache = new Map<
|
||||
string,
|
||||
@@ -721,7 +723,11 @@ const renderAntigravityItems = (
|
||||
h('span', { className: styleMap.quotaReset }, resetLabel)
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, { percent, highThreshold: 60, mediumThreshold: 20 })
|
||||
h(QuotaProgressBar, {
|
||||
percent,
|
||||
highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
|
||||
mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -795,7 +801,11 @@ const renderCodexItems = (
|
||||
h('span', { className: styleMap.quotaReset }, window.resetLabel)
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, { percent: remaining, highThreshold: 80, mediumThreshold: 50 })
|
||||
h(QuotaProgressBar, {
|
||||
percent: remaining,
|
||||
highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
|
||||
mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -886,7 +896,11 @@ const renderGeminiCliItems = (
|
||||
h('span', { className: styleMap.quotaReset }, resetLabel)
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, { percent, highThreshold: 60, mediumThreshold: 20 })
|
||||
h(QuotaProgressBar, {
|
||||
percent,
|
||||
highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
|
||||
mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -1078,7 +1092,11 @@ const renderClaudeItems = (
|
||||
h('span', { className: styleMap.quotaReset }, window.resetLabel)
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, { percent: remaining, highThreshold: 80, mediumThreshold: 50 })
|
||||
h(QuotaProgressBar, {
|
||||
percent: remaining,
|
||||
highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
|
||||
mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -1293,7 +1311,11 @@ const renderKimiItems = (
|
||||
: null
|
||||
)
|
||||
),
|
||||
h(QuotaProgressBar, { percent: remaining, highThreshold: 60, mediumThreshold: 20 })
|
||||
h(QuotaProgressBar, {
|
||||
percent: remaining,
|
||||
highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
|
||||
mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1347,7 +1347,8 @@
|
||||
"description": "Monitor OAuth quota status for Antigravity, Codex, and Gemini CLI credentials.",
|
||||
"refresh_files": "Refresh auth files",
|
||||
"refresh_files_and_quota": "Refresh files & quota",
|
||||
"card_idle_hint": "Use the top \"Refresh files & quota\" button to fetch the latest quota data."
|
||||
"refresh_all_credentials": "Refresh all credentials",
|
||||
"card_idle_hint": "Use the top \"Refresh all credentials\" button to fetch the latest quota data."
|
||||
},
|
||||
"system_info": {
|
||||
"title": "Management Center Info",
|
||||
|
||||
@@ -1352,7 +1352,8 @@
|
||||
"description": "Следите за статусом квот OAuth для учётных данных Antigravity, Codex и Gemini CLI.",
|
||||
"refresh_files": "Обновить файлы авторизации",
|
||||
"refresh_files_and_quota": "Обновить файлы и квоты",
|
||||
"card_idle_hint": "Используйте кнопку «Обновить файлы и квоты» сверху, чтобы загрузить актуальные данные по квотам."
|
||||
"refresh_all_credentials": "Обновить все учётные данные",
|
||||
"card_idle_hint": "Используйте кнопку «Обновить все учётные данные» сверху, чтобы загрузить актуальные данные по квотам."
|
||||
},
|
||||
"system_info": {
|
||||
"title": "Информация о центре управления",
|
||||
|
||||
@@ -1347,7 +1347,8 @@
|
||||
"description": "集中查看 OAuth 额度与剩余情况",
|
||||
"refresh_files": "刷新认证文件",
|
||||
"refresh_files_and_quota": "刷新认证文件&额度",
|
||||
"card_idle_hint": "请使用顶部“刷新认证文件&额度”按钮获取最新额度。"
|
||||
"refresh_all_credentials": "刷新全部凭证",
|
||||
"card_idle_hint": "请使用顶部“刷新全部凭证”按钮获取最新额度。"
|
||||
},
|
||||
"system_info": {
|
||||
"title": "管理中心信息",
|
||||
|
||||
@@ -541,7 +541,7 @@
|
||||
}
|
||||
|
||||
.quotaBarFillMedium {
|
||||
background-color: var(--warning-color);
|
||||
background-color: var(--quota-medium-color, #e0aa14);
|
||||
}
|
||||
|
||||
.quotaBarFillLow {
|
||||
|
||||
@@ -31,14 +31,22 @@
|
||||
gap: $spacing-sm;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
|
||||
:global(.btn-sm) {
|
||||
:global(.btn.btn-sm) {
|
||||
line-height: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:global(svg) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.titleWrapper {
|
||||
@@ -146,9 +154,95 @@
|
||||
}
|
||||
|
||||
.viewModeToggle {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
gap: $spacing-xs;
|
||||
align-items: center;
|
||||
padding: 3px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 92%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||
|
||||
@include mobile {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.viewModeButton:global(.btn.btn-sm) {
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: transparent;
|
||||
background: color-mix(in srgb, var(--bg-hover) 72%, transparent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
}
|
||||
|
||||
.viewModeButtonActive:global(.btn.btn-sm) {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-contrast, #fff);
|
||||
box-shadow: 0 8px 18px -14px rgba(0, 0, 0, 0.45);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
color: var(--primary-contrast, #fff);
|
||||
}
|
||||
}
|
||||
|
||||
.refreshAllButton:global(.btn.btn-sm) {
|
||||
padding-inline: 14px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--primary-color) 12%, var(--bg-secondary));
|
||||
border-color: color-mix(in srgb, var(--primary-color) 22%, var(--border-color));
|
||||
color: var(--text-primary);
|
||||
|
||||
> span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--primary-color) 16%, var(--bg-secondary));
|
||||
border-color: color-mix(in srgb, var(--primary-color) 34%, var(--border-color));
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .viewModeToggle {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
:global([data-theme='dark']) .refreshAllButton:global(.btn.btn-sm) {
|
||||
background: color-mix(in srgb, var(--primary-color) 18%, var(--bg-secondary));
|
||||
border-color: color-mix(in srgb, var(--primary-color) 32%, var(--border-color));
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.headerActions {
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片渐变背景 — 基于 TYPE_COLORS light.bg 转 rgba
|
||||
@@ -243,7 +337,7 @@
|
||||
}
|
||||
|
||||
.quotaBarFillMedium {
|
||||
background-color: var(--warning-color);
|
||||
background-color: var(--quota-medium-color, #e0aa14);
|
||||
}
|
||||
|
||||
.quotaBarFillLow {
|
||||
@@ -283,6 +377,24 @@
|
||||
padding: $spacing-sm 0;
|
||||
}
|
||||
|
||||
.quotaMessageAction {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quotaError {
|
||||
font-size: 12px;
|
||||
color: var(--danger-color);
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
--primary-contrast: #ffffff;
|
||||
|
||||
--success-color: #10b981;
|
||||
--quota-medium-color: #e0aa14;
|
||||
--warning-color: #c65746; // 错误/警告色
|
||||
--error-color: #c65746;
|
||||
--danger-color: var(--error-color);
|
||||
@@ -88,6 +89,7 @@
|
||||
--primary-contrast: #ffffff;
|
||||
|
||||
--success-color: #10b981;
|
||||
--quota-medium-color: #e0aa14;
|
||||
--warning-color: #c65746;
|
||||
--error-color: #c65746;
|
||||
--danger-color: var(--error-color);
|
||||
@@ -145,6 +147,7 @@
|
||||
--primary-contrast: #ffffff;
|
||||
|
||||
--success-color: #10b981;
|
||||
--quota-medium-color: #ffd862;
|
||||
--warning-color: #c65746;
|
||||
--error-color: #c65746;
|
||||
--danger-color: var(--error-color);
|
||||
|
||||
Reference in New Issue
Block a user