Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db376c7504 | ||
|
|
8232812ac2 |
@@ -36,6 +36,7 @@ import {
|
|||||||
useThemeStore,
|
useThemeStore,
|
||||||
} from '@/stores';
|
} from '@/stores';
|
||||||
import { configApi, versionApi } from '@/services/api';
|
import { configApi, versionApi } from '@/services/api';
|
||||||
|
import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||||
|
|
||||||
const sidebarIcons: Record<string, ReactNode> = {
|
const sidebarIcons: Record<string, ReactNode> = {
|
||||||
dashboard: <IconLayoutDashboard size={18} />,
|
dashboard: <IconLayoutDashboard size={18} />,
|
||||||
@@ -384,12 +385,22 @@ export function MainLayout() {
|
|||||||
|
|
||||||
const handleRefreshAll = async () => {
|
const handleRefreshAll = async () => {
|
||||||
clearCache();
|
clearCache();
|
||||||
try {
|
const results = await Promise.allSettled([
|
||||||
await fetchConfig(undefined, true);
|
fetchConfig(undefined, true),
|
||||||
showNotification(t('notification.data_refreshed'), 'success');
|
triggerHeaderRefresh()
|
||||||
} catch (error: any) {
|
]);
|
||||||
showNotification(`${t('notification.refresh_failed')}: ${error?.message || ''}`, 'error');
|
const rejected = results.find((result) => result.status === 'rejected');
|
||||||
|
if (rejected && rejected.status === 'rejected') {
|
||||||
|
const reason = rejected.reason;
|
||||||
|
const message =
|
||||||
|
typeof reason === 'string' ? reason : reason instanceof Error ? reason.message : '';
|
||||||
|
showNotification(
|
||||||
|
`${t('notification.refresh_failed')}${message ? `: ${message}` : ''}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
showNotification(t('notification.data_refreshed'), 'success');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVersionCheck = async () => {
|
const handleVersionCheck = async () => {
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export { useLocalStorage } from './useLocalStorage';
|
|||||||
export { useInterval } from './useInterval';
|
export { useInterval } from './useInterval';
|
||||||
export { useMediaQuery } from './useMediaQuery';
|
export { useMediaQuery } from './useMediaQuery';
|
||||||
export { usePagination } from './usePagination';
|
export { usePagination } from './usePagination';
|
||||||
|
export { useHeaderRefresh } from './useHeaderRefresh';
|
||||||
|
|||||||
24
src/hooks/useHeaderRefresh.ts
Normal file
24
src/hooks/useHeaderRefresh.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export type HeaderRefreshHandler = () => void | Promise<void>;
|
||||||
|
|
||||||
|
let activeHeaderRefreshHandler: HeaderRefreshHandler | null = null;
|
||||||
|
|
||||||
|
export const triggerHeaderRefresh = async () => {
|
||||||
|
if (!activeHeaderRefreshHandler) return;
|
||||||
|
await activeHeaderRefreshHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHeaderRefresh = (handler?: HeaderRefreshHandler | null) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!handler) return;
|
||||||
|
|
||||||
|
activeHeaderRefreshHandler = handler;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (activeHeaderRefreshHandler === handler) {
|
||||||
|
activeHeaderRefreshHandler = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [handler]);
|
||||||
|
};
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useInterval } from '@/hooks/useInterval';
|
import { useInterval } from '@/hooks/useInterval';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
@@ -270,6 +271,12 @@ export function AuthFilesPage() {
|
|||||||
}
|
}
|
||||||
}, [showNotification, t]);
|
}, [showNotification, t]);
|
||||||
|
|
||||||
|
const handleHeaderRefresh = useCallback(async () => {
|
||||||
|
await Promise.all([loadFiles(), loadKeyStats(), loadExcluded()]);
|
||||||
|
}, [loadFiles, loadKeyStats, loadExcluded]);
|
||||||
|
|
||||||
|
useHeaderRefresh(handleHeaderRefresh);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFiles();
|
loadFiles();
|
||||||
loadKeyStats();
|
loadKeyStats();
|
||||||
@@ -719,9 +726,11 @@ export function AuthFilesPage() {
|
|||||||
const renderFileCard = (item: AuthFileItem) => {
|
const renderFileCard = (item: AuthFileItem) => {
|
||||||
const fileStats = resolveAuthFileStats(item, keyStats);
|
const fileStats = resolveAuthFileStats(item, keyStats);
|
||||||
const isRuntimeOnly = isRuntimeOnlyAuthFile(item);
|
const isRuntimeOnly = isRuntimeOnlyAuthFile(item);
|
||||||
|
const isAistudio = (item.type || '').toLowerCase() === 'aistudio';
|
||||||
|
const showModelsButton = !isRuntimeOnly || isAistudio;
|
||||||
const typeColor = getTypeColor(item.type || 'unknown');
|
const typeColor = getTypeColor(item.type || 'unknown');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.name} className={styles.fileCard}>
|
<div key={item.name} className={styles.fileCard}>
|
||||||
<div className={styles.cardHeader}>
|
<div className={styles.cardHeader}>
|
||||||
<span
|
<span
|
||||||
@@ -753,29 +762,29 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
{/* 状态监测栏 */}
|
{/* 状态监测栏 */}
|
||||||
{renderStatusBar(item)}
|
{renderStatusBar(item)}
|
||||||
|
|
||||||
<div className={styles.cardActions}>
|
<div className={styles.cardActions}>
|
||||||
{isRuntimeOnly ? (
|
{showModelsButton && (
|
||||||
<div className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</div>
|
<Button
|
||||||
) : (
|
variant="secondary"
|
||||||
<>
|
size="sm"
|
||||||
<Button
|
onClick={() => showModels(item)}
|
||||||
variant="secondary"
|
className={styles.iconButton}
|
||||||
size="sm"
|
title={t('auth_files.models_button', { defaultValue: '模型' })}
|
||||||
onClick={() => showModels(item)}
|
disabled={disableControls}
|
||||||
className={styles.iconButton}
|
>
|
||||||
title={t('auth_files.models_button', { defaultValue: '模型' })}
|
<IconBot className={styles.actionIcon} size={16} />
|
||||||
disabled={disableControls}
|
</Button>
|
||||||
>
|
)}
|
||||||
<IconBot className={styles.actionIcon} size={16} />
|
{!isRuntimeOnly && (
|
||||||
</Button>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => showDetails(item)}
|
onClick={() => showDetails(item)}
|
||||||
className={styles.iconButton}
|
className={styles.iconButton}
|
||||||
title={t('common.info', { defaultValue: '关于' })}
|
title={t('common.info', { defaultValue: '关于' })}
|
||||||
disabled={disableControls}
|
disabled={disableControls}
|
||||||
>
|
>
|
||||||
<IconInfo className={styles.actionIcon} size={16} />
|
<IconInfo className={styles.actionIcon} size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -799,13 +808,16 @@ export function AuthFilesPage() {
|
|||||||
>
|
>
|
||||||
{deleting === item.name ? (
|
{deleting === item.name ? (
|
||||||
<LoadingSpinner size={14} />
|
<LoadingSpinner size={14} />
|
||||||
) : (
|
) : (
|
||||||
<IconTrash2 className={styles.actionIcon} size={16} />
|
<IconTrash2 className={styles.actionIcon} size={16} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
{isRuntimeOnly && (
|
||||||
|
<div className={styles.virtualBadge}>{t('auth_files.type_virtual') || '虚拟认证文件'}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -819,11 +831,16 @@ export function AuthFilesPage() {
|
|||||||
|
|
||||||
<Card
|
<Card
|
||||||
title={t('auth_files.title_section')}
|
title={t('auth_files.title_section')}
|
||||||
extra={
|
extra={
|
||||||
<div className={styles.headerActions}>
|
<div className={styles.headerActions}>
|
||||||
<Button variant="secondary" size="sm" onClick={() => { loadFiles(); loadKeyStats(); }} disabled={loading}>
|
<Button
|
||||||
{t('common.refresh')}
|
variant="secondary"
|
||||||
</Button>
|
size="sm"
|
||||||
|
onClick={handleHeaderRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t('common.refresh')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
IconTrash2,
|
IconTrash2,
|
||||||
IconX,
|
IconX,
|
||||||
} from '@/components/ui/icons';
|
} from '@/components/ui/icons';
|
||||||
|
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||||
import { logsApi } from '@/services/api/logs';
|
import { logsApi } from '@/services/api/logs';
|
||||||
import { MANAGEMENT_API_PREFIX } from '@/utils/constants';
|
import { MANAGEMENT_API_PREFIX } from '@/utils/constants';
|
||||||
@@ -474,6 +475,8 @@ export function LogsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useHeaderRefresh(() => loadLogs(false));
|
||||||
|
|
||||||
const clearLogs = async () => {
|
const clearLogs = async () => {
|
||||||
if (!window.confirm(t('logs.clear_confirm'))) return;
|
if (!window.confirm(t('logs.clear_confirm'))) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -115,6 +115,13 @@
|
|||||||
margin-top: $spacing-sm;
|
margin-top: $spacing-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.geminiProjectField {
|
||||||
|
:global(.form-group) {
|
||||||
|
margin-top: $spacing-sm;
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filePicker {
|
.filePicker {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -327,19 +327,21 @@ export function OAuthPage() {
|
|||||||
>
|
>
|
||||||
<div className="hint">{t(provider.hintKey)}</div>
|
<div className="hint">{t(provider.hintKey)}</div>
|
||||||
{provider.id === 'gemini-cli' && (
|
{provider.id === 'gemini-cli' && (
|
||||||
<Input
|
<div className={styles.geminiProjectField}>
|
||||||
label={t('auth_login.gemini_cli_project_id_label')}
|
<Input
|
||||||
hint={t('auth_login.gemini_cli_project_id_hint')}
|
label={t('auth_login.gemini_cli_project_id_label')}
|
||||||
value={state.projectId || ''}
|
hint={t('auth_login.gemini_cli_project_id_hint')}
|
||||||
error={state.projectIdError}
|
value={state.projectId || ''}
|
||||||
onChange={(e) =>
|
error={state.projectIdError}
|
||||||
updateProviderState(provider.id, {
|
onChange={(e) =>
|
||||||
projectId: e.target.value,
|
updateProviderState(provider.id, {
|
||||||
projectIdError: undefined
|
projectId: e.target.value,
|
||||||
})
|
projectIdError: undefined
|
||||||
}
|
})
|
||||||
placeholder={t('auth_login.gemini_cli_project_id_placeholder')}
|
}
|
||||||
/>
|
placeholder={t('auth_login.gemini_cli_project_id_placeholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{state.url && (
|
{state.url && (
|
||||||
<div className={`connection-box ${styles.authUrlBox}`}>
|
<div className={`connection-box ${styles.authUrlBox}`}>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||||
import { useAuthStore } from '@/stores';
|
import { useAuthStore } from '@/stores';
|
||||||
import { authFilesApi } from '@/services/api';
|
import { authFilesApi, configFileApi } from '@/services/api';
|
||||||
import {
|
import {
|
||||||
QuotaSection,
|
QuotaSection,
|
||||||
ANTIGRAVITY_CONFIG,
|
ANTIGRAVITY_CONFIG,
|
||||||
@@ -26,6 +26,15 @@ export function QuotaPage() {
|
|||||||
|
|
||||||
const disableControls = connectionStatus !== 'connected';
|
const disableControls = connectionStatus !== 'connected';
|
||||||
|
|
||||||
|
const loadConfig = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await configFileApi.fetchConfigYaml();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : t('notification.refresh_failed');
|
||||||
|
setError((prev) => prev || errorMessage);
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
const loadFiles = useCallback(async () => {
|
const loadFiles = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -40,20 +49,22 @@ export function QuotaPage() {
|
|||||||
}
|
}
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
const handleHeaderRefresh = useCallback(async () => {
|
||||||
|
await Promise.all([loadConfig(), loadFiles()]);
|
||||||
|
}, [loadConfig, loadFiles]);
|
||||||
|
|
||||||
|
useHeaderRefresh(handleHeaderRefresh);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFiles();
|
loadFiles();
|
||||||
}, [loadFiles]);
|
loadConfig();
|
||||||
|
}, [loadFiles, loadConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.pageHeader}>
|
<div className={styles.pageHeader}>
|
||||||
<h1 className={styles.pageTitle}>{t('quota_management.title')}</h1>
|
<h1 className={styles.pageTitle}>{t('quota_management.title')}</h1>
|
||||||
<p className={styles.description}>{t('quota_management.description')}</p>
|
<p className={styles.description}>{t('quota_management.description')}</p>
|
||||||
<div className={styles.headerActions}>
|
|
||||||
<Button variant="secondary" size="sm" onClick={loadFiles} disabled={loading}>
|
|
||||||
{t('quota_management.refresh_files')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <div className={styles.errorBox}>{error}</div>}
|
{error && <div className={styles.errorBox}>{error}</div>}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||||
|
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||||
import { useThemeStore } from '@/stores';
|
import { useThemeStore } from '@/stores';
|
||||||
import {
|
import {
|
||||||
StatCards,
|
StatCards,
|
||||||
@@ -63,6 +64,8 @@ export function UsagePage() {
|
|||||||
importing
|
importing
|
||||||
} = useUsageData();
|
} = useUsageData();
|
||||||
|
|
||||||
|
useHeaderRefresh(loadUsage);
|
||||||
|
|
||||||
// Chart lines state
|
// Chart lines state
|
||||||
const [chartLines, setChartLines] = useState<string[]>(['all']);
|
const [chartLines, setChartLines] = useState<string[]>(['all']);
|
||||||
const MAX_CHART_LINES = 9;
|
const MAX_CHART_LINES = 9;
|
||||||
|
|||||||
Reference in New Issue
Block a user