mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
feat: add models cache store and fix search placeholder truncation
- Add useModelsStore with 30s cache for model list to reduce API calls - Refactor SystemPage to use cached models from store - Shorten ConfigPage search placeholder to prevent text truncation
This commit is contained in:
@@ -551,7 +551,7 @@
|
|||||||
"save_success": "Configuration saved successfully",
|
"save_success": "Configuration saved successfully",
|
||||||
"error_yaml_not_supported": "Server did not return YAML. Verify the /config.yaml endpoint is available.",
|
"error_yaml_not_supported": "Server did not return YAML. Verify the /config.yaml endpoint is available.",
|
||||||
"editor_placeholder": "key: value",
|
"editor_placeholder": "key: value",
|
||||||
"search_placeholder": "Type then click the search button (or press Enter) to search",
|
"search_placeholder": "Search config...",
|
||||||
"search_button": "Search",
|
"search_button": "Search",
|
||||||
"search_no_results": "No results",
|
"search_no_results": "No results",
|
||||||
"search_prev": "Previous",
|
"search_prev": "Previous",
|
||||||
|
|||||||
@@ -551,7 +551,7 @@
|
|||||||
"save_success": "配置已保存",
|
"save_success": "配置已保存",
|
||||||
"error_yaml_not_supported": "服务器未返回 YAML 格式,请确认 /config.yaml 接口可用",
|
"error_yaml_not_supported": "服务器未返回 YAML 格式,请确认 /config.yaml 接口可用",
|
||||||
"editor_placeholder": "key: value",
|
"editor_placeholder": "key: value",
|
||||||
"search_placeholder": "输入关键字后点击右侧搜索按钮(或 Enter)进行搜索",
|
"search_placeholder": "搜索配置内容...",
|
||||||
"search_button": "搜索",
|
"search_button": "搜索",
|
||||||
"search_no_results": "无结果",
|
"search_no_results": "无结果",
|
||||||
"search_prev": "上一个",
|
"search_prev": "上一个",
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export function ConfigPage() {
|
|||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
onKeyDown={handleSearchKeyDown}
|
onKeyDown={handleSearchKeyDown}
|
||||||
placeholder={t('config_management.search_placeholder', {
|
placeholder={t('config_management.search_placeholder', {
|
||||||
defaultValue: '输入关键字后点击右侧搜索按钮(或 Enter)进行搜索'
|
defaultValue: '搜索配置内容...'
|
||||||
})}
|
})}
|
||||||
disabled={disableControls || loading}
|
disabled={disableControls || loading}
|
||||||
className={styles.searchInput}
|
className={styles.searchInput}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore, useModelsStore } from '@/stores';
|
||||||
import { modelsApi } from '@/services/api/models';
|
|
||||||
import { apiKeysApi } from '@/services/api/apiKeys';
|
import { apiKeysApi } from '@/services/api/apiKeys';
|
||||||
import { classifyModels, type ModelInfo } from '@/utils/models';
|
import { classifyModels } from '@/utils/models';
|
||||||
import styles from './SystemPage.module.scss';
|
import styles from './SystemPage.module.scss';
|
||||||
|
|
||||||
export function SystemPage() {
|
export function SystemPage() {
|
||||||
@@ -15,10 +14,12 @@ export function SystemPage() {
|
|||||||
const config = useConfigStore((state) => state.config);
|
const config = useConfigStore((state) => state.config);
|
||||||
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
||||||
|
|
||||||
const [models, setModels] = useState<ModelInfo[]>([]);
|
const models = useModelsStore((state) => state.models);
|
||||||
const [loadingModels, setLoadingModels] = useState(false);
|
const modelsLoading = useModelsStore((state) => state.loading);
|
||||||
|
const modelsError = useModelsStore((state) => state.error);
|
||||||
|
const fetchModelsFromStore = useModelsStore((state) => state.fetchModels);
|
||||||
|
|
||||||
const [modelStatus, setModelStatus] = useState<{ type: 'success' | 'warning' | 'error' | 'muted'; message: string }>();
|
const [modelStatus, setModelStatus] = useState<{ type: 'success' | 'warning' | 'error' | 'muted'; message: string }>();
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const apiKeysCache = useRef<string[]>([]);
|
const apiKeysCache = useRef<string[]>([]);
|
||||||
|
|
||||||
@@ -68,13 +69,12 @@ export function SystemPage() {
|
|||||||
}
|
}
|
||||||
}, [config?.apiKeys]);
|
}, [config?.apiKeys]);
|
||||||
|
|
||||||
const fetchModels = async ({ forceRefreshKeys = false }: { forceRefreshKeys?: boolean } = {}) => {
|
const fetchModels = async ({ forceRefresh = false }: { forceRefresh?: boolean } = {}) => {
|
||||||
if (auth.connectionStatus !== 'connected') {
|
if (auth.connectionStatus !== 'connected') {
|
||||||
setModelStatus({
|
setModelStatus({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
message: t('notification.connection_required')
|
message: t('notification.connection_required')
|
||||||
});
|
});
|
||||||
setModels([]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,18 +83,15 @@ export function SystemPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceRefreshKeys) {
|
if (forceRefresh) {
|
||||||
apiKeysCache.current = [];
|
apiKeysCache.current = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoadingModels(true);
|
|
||||||
setError('');
|
|
||||||
setModelStatus({ type: 'muted', message: t('system_info.models_loading') });
|
setModelStatus({ type: 'muted', message: t('system_info.models_loading') });
|
||||||
try {
|
try {
|
||||||
const apiKeys = await resolveApiKeysForModels();
|
const apiKeys = await resolveApiKeysForModels();
|
||||||
const primaryKey = apiKeys[0];
|
const primaryKey = apiKeys[0];
|
||||||
const list = await modelsApi.fetchModels(auth.apiBase, primaryKey);
|
const list = await fetchModelsFromStore(auth.apiBase, primaryKey, forceRefresh);
|
||||||
setModels(list);
|
|
||||||
const hasModels = list.length > 0;
|
const hasModels = list.length > 0;
|
||||||
setModelStatus({
|
setModelStatus({
|
||||||
type: hasModels ? 'success' : 'warning',
|
type: hasModels ? 'success' : 'warning',
|
||||||
@@ -102,11 +99,7 @@ export function SystemPage() {
|
|||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = `${t('system_info.models_error')}: ${err?.message || ''}`;
|
const message = `${t('system_info.models_error')}: ${err?.message || ''}`;
|
||||||
setError(message);
|
|
||||||
setModels([]);
|
|
||||||
setModelStatus({ type: 'error', message });
|
setModelStatus({ type: 'error', message });
|
||||||
} finally {
|
|
||||||
setLoadingModels(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,15 +149,15 @@ export function SystemPage() {
|
|||||||
<Card
|
<Card
|
||||||
title={t('system_info.models_title')}
|
title={t('system_info.models_title')}
|
||||||
extra={
|
extra={
|
||||||
<Button variant="secondary" size="sm" onClick={() => fetchModels({ forceRefreshKeys: true })} loading={loadingModels}>
|
<Button variant="secondary" size="sm" onClick={() => fetchModels({ forceRefresh: true })} loading={modelsLoading}>
|
||||||
{t('common.refresh')}
|
{t('common.refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p className={styles.sectionDescription}>{t('system_info.models_desc')}</p>
|
<p className={styles.sectionDescription}>{t('system_info.models_desc')}</p>
|
||||||
{modelStatus && <div className={`status-badge ${modelStatus.type}`}>{modelStatus.message}</div>}
|
{modelStatus && <div className={`status-badge ${modelStatus.type}`}>{modelStatus.message}</div>}
|
||||||
{error && <div className="error-box">{error}</div>}
|
{modelsError && <div className="error-box">{modelsError}</div>}
|
||||||
{loadingModels ? (
|
{modelsLoading ? (
|
||||||
<div className="hint">{t('common.loading')}</div>
|
<div className="hint">{t('common.loading')}</div>
|
||||||
) : models.length === 0 ? (
|
) : models.length === 0 ? (
|
||||||
<div className="hint">{t('system_info.models_empty')}</div>
|
<div className="hint">{t('system_info.models_empty')}</div>
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export { useThemeStore } from './useThemeStore';
|
|||||||
export { useLanguageStore } from './useLanguageStore';
|
export { useLanguageStore } from './useLanguageStore';
|
||||||
export { useAuthStore } from './useAuthStore';
|
export { useAuthStore } from './useAuthStore';
|
||||||
export { useConfigStore } from './useConfigStore';
|
export { useConfigStore } from './useConfigStore';
|
||||||
|
export { useModelsStore } from './useModelsStore';
|
||||||
|
|||||||
76
src/stores/useModelsStore.ts
Normal file
76
src/stores/useModelsStore.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 模型列表状态管理(带缓存)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { modelsApi } from '@/services/api/models';
|
||||||
|
import { CACHE_EXPIRY_MS } from '@/utils/constants';
|
||||||
|
import type { ModelInfo } from '@/utils/models';
|
||||||
|
|
||||||
|
interface ModelsCache {
|
||||||
|
data: ModelInfo[];
|
||||||
|
timestamp: number;
|
||||||
|
apiBase: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelsState {
|
||||||
|
models: ModelInfo[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
cache: ModelsCache | null;
|
||||||
|
|
||||||
|
fetchModels: (apiBase: string, apiKey?: string, forceRefresh?: boolean) => Promise<ModelInfo[]>;
|
||||||
|
clearCache: () => void;
|
||||||
|
isCacheValid: (apiBase: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModelsStore = create<ModelsState>((set, get) => ({
|
||||||
|
models: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
cache: null,
|
||||||
|
|
||||||
|
fetchModels: async (apiBase, apiKey, forceRefresh = false) => {
|
||||||
|
const { cache, isCacheValid } = get();
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (!forceRefresh && isCacheValid(apiBase) && cache) {
|
||||||
|
set({ models: cache.data, error: null });
|
||||||
|
return cache.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ loading: true, error: null });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const list = await modelsApi.fetchModels(apiBase, apiKey);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
set({
|
||||||
|
models: list,
|
||||||
|
loading: false,
|
||||||
|
cache: { data: list, timestamp: now, apiBase }
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
} catch (error: any) {
|
||||||
|
const message = error?.message || 'Failed to fetch models';
|
||||||
|
set({
|
||||||
|
error: message,
|
||||||
|
loading: false,
|
||||||
|
models: []
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCache: () => {
|
||||||
|
set({ cache: null, models: [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
isCacheValid: (apiBase) => {
|
||||||
|
const { cache } = get();
|
||||||
|
if (!cache) return false;
|
||||||
|
if (cache.apiBase !== apiBase) return false;
|
||||||
|
return Date.now() - cache.timestamp < CACHE_EXPIRY_MS;
|
||||||
|
}
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user