mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
/**
|
|
* Generic quota card component.
|
|
*/
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
import type { ReactElement, ReactNode } from 'react';
|
|
import type { TFunction } from 'i18next';
|
|
import type { AuthFileItem, ResolvedTheme, ThemeColors } from '@/types';
|
|
import { TYPE_COLORS } from '@/utils/quota';
|
|
import styles from '@/pages/QuotaPage.module.scss';
|
|
|
|
type QuotaStatus = 'idle' | 'loading' | 'success' | 'error';
|
|
|
|
export interface QuotaStatusState {
|
|
status: QuotaStatus;
|
|
error?: string;
|
|
errorStatus?: number;
|
|
}
|
|
|
|
export interface QuotaProgressBarProps {
|
|
percent: number | null;
|
|
highThreshold: number;
|
|
mediumThreshold: number;
|
|
}
|
|
|
|
export function QuotaProgressBar({
|
|
percent,
|
|
highThreshold,
|
|
mediumThreshold
|
|
}: QuotaProgressBarProps) {
|
|
const clamp = (value: number, min: number, max: number) =>
|
|
Math.min(max, Math.max(min, value));
|
|
const normalized = percent === null ? null : clamp(percent, 0, 100);
|
|
const fillClass =
|
|
normalized === null
|
|
? styles.quotaBarFillMedium
|
|
: normalized >= highThreshold
|
|
? styles.quotaBarFillHigh
|
|
: normalized >= mediumThreshold
|
|
? styles.quotaBarFillMedium
|
|
: styles.quotaBarFillLow;
|
|
const widthPercent = Math.round(normalized ?? 0);
|
|
|
|
return (
|
|
<div className={styles.quotaBar}>
|
|
<div
|
|
className={`${styles.quotaBarFill} ${fillClass}`}
|
|
style={{ width: `${widthPercent}%` }}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export interface QuotaRenderHelpers {
|
|
styles: typeof styles;
|
|
QuotaProgressBar: (props: QuotaProgressBarProps) => ReactElement;
|
|
}
|
|
|
|
interface QuotaCardProps<TState extends QuotaStatusState> {
|
|
item: AuthFileItem;
|
|
quota?: TState;
|
|
resolvedTheme: ResolvedTheme;
|
|
i18nPrefix: string;
|
|
cardClassName: string;
|
|
defaultType: string;
|
|
renderQuotaItems: (quota: TState, t: TFunction, helpers: QuotaRenderHelpers) => ReactNode;
|
|
}
|
|
|
|
export function QuotaCard<TState extends QuotaStatusState>({
|
|
item,
|
|
quota,
|
|
resolvedTheme,
|
|
i18nPrefix,
|
|
cardClassName,
|
|
defaultType,
|
|
renderQuotaItems
|
|
}: QuotaCardProps<TState>) {
|
|
const { t } = useTranslation();
|
|
|
|
const displayType = item.type || item.provider || defaultType;
|
|
const typeColorSet = TYPE_COLORS[displayType] || TYPE_COLORS.unknown;
|
|
const typeColor: ThemeColors =
|
|
resolvedTheme === 'dark' && typeColorSet.dark ? typeColorSet.dark : typeColorSet.light;
|
|
|
|
const quotaStatus = quota?.status ?? 'idle';
|
|
const quotaErrorMessage = resolveQuotaErrorMessage(
|
|
t,
|
|
quota?.errorStatus,
|
|
quota?.error || t('common.unknown_error')
|
|
);
|
|
|
|
const getTypeLabel = (type: string): string => {
|
|
const key = `auth_files.filter_${type}`;
|
|
const translated = t(key);
|
|
if (translated !== key) return translated;
|
|
if (type.toLowerCase() === 'iflow') return 'iFlow';
|
|
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
};
|
|
|
|
return (
|
|
<div className={`${styles.fileCard} ${cardClassName}`}>
|
|
<div className={styles.cardHeader}>
|
|
<span
|
|
className={styles.typeBadge}
|
|
style={{
|
|
backgroundColor: typeColor.bg,
|
|
color: typeColor.text,
|
|
...(typeColor.border ? { border: typeColor.border } : {})
|
|
}}
|
|
>
|
|
{getTypeLabel(displayType)}
|
|
</span>
|
|
<span className={styles.fileName}>{item.name}</span>
|
|
</div>
|
|
|
|
<div className={styles.quotaSection}>
|
|
{quotaStatus === 'loading' ? (
|
|
<div className={styles.quotaMessage}>{t(`${i18nPrefix}.loading`)}</div>
|
|
) : quotaStatus === 'idle' ? (
|
|
<div className={styles.quotaMessage}>{t(`${i18nPrefix}.idle`)}</div>
|
|
) : quotaStatus === 'error' ? (
|
|
<div className={styles.quotaError}>
|
|
{t(`${i18nPrefix}.load_failed`, {
|
|
message: quotaErrorMessage
|
|
})}
|
|
</div>
|
|
) : quota ? (
|
|
renderQuotaItems(quota, t, { styles, QuotaProgressBar })
|
|
) : (
|
|
<div className={styles.quotaMessage}>{t(`${i18nPrefix}.idle`)}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const resolveQuotaErrorMessage = (
|
|
t: TFunction,
|
|
status: number | undefined,
|
|
fallback: string
|
|
): string => {
|
|
if (status === 404) return t('common.quota_update_required');
|
|
if (status === 403) return t('common.quota_check_credential');
|
|
return fallback;
|
|
};
|