mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 18:50:49 +08:00
feat: initialize new React application structure with TypeScript, ESLint, and Prettier configurations, while removing legacy files and adding new components and pages for enhanced functionality
This commit is contained in:
9
src/stores/index.ts
Normal file
9
src/stores/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Zustand Stores 统一导出
|
||||
*/
|
||||
|
||||
export { useNotificationStore } from './useNotificationStore';
|
||||
export { useThemeStore } from './useThemeStore';
|
||||
export { useLanguageStore } from './useLanguageStore';
|
||||
export { useAuthStore } from './useAuthStore';
|
||||
export { useConfigStore } from './useConfigStore';
|
||||
201
src/stores/useAuthStore.ts
Normal file
201
src/stores/useAuthStore.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 认证状态管理
|
||||
* 从原项目 src/modules/login.js 和 src/core/connection.js 迁移
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import type { AuthState, LoginCredentials, ConnectionStatus } from '@/types';
|
||||
import { STORAGE_KEY_AUTH } from '@/utils/constants';
|
||||
import { secureStorage } from '@/services/storage/secureStorage';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
import { configApi } from '@/services/api/config';
|
||||
import { useConfigStore } from './useConfigStore';
|
||||
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
|
||||
|
||||
interface AuthStoreState extends AuthState {
|
||||
connectionStatus: ConnectionStatus;
|
||||
connectionError: string | null;
|
||||
|
||||
// 操作
|
||||
login: (credentials: LoginCredentials) => Promise<void>;
|
||||
logout: () => void;
|
||||
checkAuth: () => Promise<boolean>;
|
||||
restoreSession: () => Promise<boolean>;
|
||||
updateServerVersion: (version: string | null, buildDate?: string | null) => void;
|
||||
updateConnectionStatus: (status: ConnectionStatus, error?: string | null) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthStoreState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// 初始状态
|
||||
isAuthenticated: false,
|
||||
apiBase: '',
|
||||
managementKey: '',
|
||||
serverVersion: null,
|
||||
serverBuildDate: null,
|
||||
connectionStatus: 'disconnected',
|
||||
connectionError: null,
|
||||
|
||||
// 恢复会话并自动登录
|
||||
restoreSession: async () => {
|
||||
secureStorage.migratePlaintextKeys(['apiBase', 'apiUrl', 'managementKey']);
|
||||
|
||||
const wasLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
||||
const legacyBase =
|
||||
secureStorage.getItem<string>('apiBase') ||
|
||||
secureStorage.getItem<string>('apiUrl', { encrypt: true });
|
||||
const legacyKey = secureStorage.getItem<string>('managementKey');
|
||||
|
||||
const { apiBase, managementKey } = get();
|
||||
const resolvedBase = normalizeApiBase(apiBase || legacyBase || detectApiBaseFromLocation());
|
||||
const resolvedKey = managementKey || legacyKey || '';
|
||||
|
||||
set({ apiBase: resolvedBase, managementKey: resolvedKey });
|
||||
apiClient.setConfig({ apiBase: resolvedBase, managementKey: resolvedKey });
|
||||
|
||||
if (wasLoggedIn && resolvedBase && resolvedKey) {
|
||||
try {
|
||||
await get().login({ apiBase: resolvedBase, managementKey: resolvedKey });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Auto login failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
// 登录
|
||||
login: async (credentials) => {
|
||||
const apiBase = normalizeApiBase(credentials.apiBase);
|
||||
const managementKey = credentials.managementKey.trim();
|
||||
|
||||
try {
|
||||
set({ connectionStatus: 'connecting' });
|
||||
|
||||
// 配置 API 客户端
|
||||
apiClient.setConfig({
|
||||
apiBase,
|
||||
managementKey
|
||||
});
|
||||
|
||||
// 测试连接 - 获取配置
|
||||
await configApi.getConfig();
|
||||
|
||||
// 登录成功
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
apiBase,
|
||||
managementKey,
|
||||
connectionStatus: 'connected',
|
||||
connectionError: null
|
||||
});
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
} catch (error: any) {
|
||||
set({
|
||||
connectionStatus: 'error',
|
||||
connectionError: error.message || 'Connection failed'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout: () => {
|
||||
useConfigStore.getState().clearCache();
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
apiBase: '',
|
||||
managementKey: '',
|
||||
serverVersion: null,
|
||||
serverBuildDate: null,
|
||||
connectionStatus: 'disconnected',
|
||||
connectionError: null
|
||||
});
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
},
|
||||
|
||||
// 检查认证状态
|
||||
checkAuth: async () => {
|
||||
const { managementKey, apiBase } = get();
|
||||
|
||||
if (!managementKey || !apiBase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 重新配置客户端
|
||||
apiClient.setConfig({ apiBase, managementKey });
|
||||
|
||||
// 验证连接
|
||||
await configApi.getConfig();
|
||||
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
connectionStatus: 'connected'
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
set({
|
||||
isAuthenticated: false,
|
||||
connectionStatus: 'error'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 更新服务器版本
|
||||
updateServerVersion: (version, buildDate) => {
|
||||
set({ serverVersion: version || null, serverBuildDate: buildDate || null });
|
||||
},
|
||||
|
||||
// 更新连接状态
|
||||
updateConnectionStatus: (status, error = null) => {
|
||||
set({
|
||||
connectionStatus: status,
|
||||
connectionError: error
|
||||
});
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY_AUTH,
|
||||
storage: createJSONStorage(() => ({
|
||||
getItem: (name) => {
|
||||
const data = secureStorage.getItem<AuthStoreState>(name);
|
||||
return data ? JSON.stringify(data) : null;
|
||||
},
|
||||
setItem: (name, value) => {
|
||||
secureStorage.setItem(name, JSON.parse(value));
|
||||
},
|
||||
removeItem: (name) => {
|
||||
secureStorage.removeItem(name);
|
||||
}
|
||||
})),
|
||||
partialize: (state) => ({
|
||||
apiBase: state.apiBase,
|
||||
managementKey: state.managementKey,
|
||||
serverVersion: state.serverVersion,
|
||||
serverBuildDate: state.serverBuildDate
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 监听全局未授权事件
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('unauthorized', () => {
|
||||
useAuthStore.getState().logout();
|
||||
});
|
||||
|
||||
window.addEventListener(
|
||||
'server-version-update',
|
||||
((e: CustomEvent) => {
|
||||
const detail = e.detail || {};
|
||||
useAuthStore.getState().updateServerVersion(detail.version || null, detail.buildDate || null);
|
||||
}) as EventListener
|
||||
);
|
||||
}
|
||||
219
src/stores/useConfigStore.ts
Normal file
219
src/stores/useConfigStore.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 配置状态管理
|
||||
* 从原项目 src/core/config-service.js 迁移
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { Config } from '@/types';
|
||||
import type { RawConfigSection } from '@/types/config';
|
||||
import { configApi } from '@/services/api/config';
|
||||
import { CACHE_EXPIRY_MS } from '@/utils/constants';
|
||||
|
||||
interface ConfigCache {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ConfigState {
|
||||
config: Config | null;
|
||||
cache: Map<string, ConfigCache>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// 操作
|
||||
fetchConfig: (section?: RawConfigSection, forceRefresh?: boolean) => Promise<Config | any>;
|
||||
updateConfigValue: (section: RawConfigSection, value: any) => void;
|
||||
clearCache: (section?: RawConfigSection) => void;
|
||||
isCacheValid: (section?: RawConfigSection) => boolean;
|
||||
}
|
||||
|
||||
const SECTION_KEYS: RawConfigSection[] = [
|
||||
'debug',
|
||||
'proxy-url',
|
||||
'request-retry',
|
||||
'quota-exceeded',
|
||||
'usage-statistics-enabled',
|
||||
'request-log',
|
||||
'logging-to-file',
|
||||
'ws-auth',
|
||||
'api-keys',
|
||||
'gemini-api-key',
|
||||
'codex-api-key',
|
||||
'claude-api-key',
|
||||
'openai-compatibility',
|
||||
'oauth-excluded-models'
|
||||
];
|
||||
|
||||
const extractSectionValue = (config: Config | null, section?: RawConfigSection) => {
|
||||
if (!config) return undefined;
|
||||
switch (section) {
|
||||
case 'debug':
|
||||
return config.debug;
|
||||
case 'proxy-url':
|
||||
return config.proxyUrl;
|
||||
case 'request-retry':
|
||||
return config.requestRetry;
|
||||
case 'quota-exceeded':
|
||||
return config.quotaExceeded;
|
||||
case 'usage-statistics-enabled':
|
||||
return config.usageStatisticsEnabled;
|
||||
case 'request-log':
|
||||
return config.requestLog;
|
||||
case 'logging-to-file':
|
||||
return config.loggingToFile;
|
||||
case 'ws-auth':
|
||||
return config.wsAuth;
|
||||
case 'api-keys':
|
||||
return config.apiKeys;
|
||||
case 'gemini-api-key':
|
||||
return config.geminiApiKeys;
|
||||
case 'codex-api-key':
|
||||
return config.codexApiKeys;
|
||||
case 'claude-api-key':
|
||||
return config.claudeApiKeys;
|
||||
case 'openai-compatibility':
|
||||
return config.openaiCompatibility;
|
||||
case 'oauth-excluded-models':
|
||||
return config.oauthExcludedModels;
|
||||
default:
|
||||
if (!section) return undefined;
|
||||
return config.raw?.[section];
|
||||
}
|
||||
};
|
||||
|
||||
export const useConfigStore = create<ConfigState>((set, get) => ({
|
||||
config: null,
|
||||
cache: new Map(),
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
fetchConfig: async (section, forceRefresh = false) => {
|
||||
const { cache, isCacheValid } = get();
|
||||
|
||||
// 检查缓存
|
||||
const cacheKey = section || '__full__';
|
||||
if (!forceRefresh && isCacheValid(section)) {
|
||||
const cached = cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached.data;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取新数据
|
||||
set({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const data = await configApi.getConfig();
|
||||
const now = Date.now();
|
||||
|
||||
// 更新缓存
|
||||
const newCache = new Map(cache);
|
||||
newCache.set('__full__', { data, timestamp: now });
|
||||
SECTION_KEYS.forEach((key) => {
|
||||
const value = extractSectionValue(data, key);
|
||||
if (value !== undefined) {
|
||||
newCache.set(key, { data: value, timestamp: now });
|
||||
}
|
||||
});
|
||||
|
||||
set({
|
||||
config: data,
|
||||
cache: newCache,
|
||||
loading: false
|
||||
});
|
||||
|
||||
return section ? extractSectionValue(data, section) : data;
|
||||
} catch (error: any) {
|
||||
set({
|
||||
error: error.message || 'Failed to fetch config',
|
||||
loading: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateConfigValue: (section, value) => {
|
||||
set((state) => {
|
||||
const raw = { ...(state.config?.raw || {}) };
|
||||
raw[section] = value;
|
||||
const nextConfig: Config = { ...(state.config || {}), raw };
|
||||
|
||||
switch (section) {
|
||||
case 'debug':
|
||||
nextConfig.debug = value;
|
||||
break;
|
||||
case 'proxy-url':
|
||||
nextConfig.proxyUrl = value;
|
||||
break;
|
||||
case 'request-retry':
|
||||
nextConfig.requestRetry = value;
|
||||
break;
|
||||
case 'quota-exceeded':
|
||||
nextConfig.quotaExceeded = value;
|
||||
break;
|
||||
case 'usage-statistics-enabled':
|
||||
nextConfig.usageStatisticsEnabled = value;
|
||||
break;
|
||||
case 'request-log':
|
||||
nextConfig.requestLog = value;
|
||||
break;
|
||||
case 'logging-to-file':
|
||||
nextConfig.loggingToFile = value;
|
||||
break;
|
||||
case 'ws-auth':
|
||||
nextConfig.wsAuth = value;
|
||||
break;
|
||||
case 'api-keys':
|
||||
nextConfig.apiKeys = value;
|
||||
break;
|
||||
case 'gemini-api-key':
|
||||
nextConfig.geminiApiKeys = value;
|
||||
break;
|
||||
case 'codex-api-key':
|
||||
nextConfig.codexApiKeys = value;
|
||||
break;
|
||||
case 'claude-api-key':
|
||||
nextConfig.claudeApiKeys = value;
|
||||
break;
|
||||
case 'openai-compatibility':
|
||||
nextConfig.openaiCompatibility = value;
|
||||
break;
|
||||
case 'oauth-excluded-models':
|
||||
nextConfig.oauthExcludedModels = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return { config: nextConfig };
|
||||
});
|
||||
|
||||
// 清除该 section 的缓存
|
||||
get().clearCache(section);
|
||||
},
|
||||
|
||||
clearCache: (section) => {
|
||||
const { cache } = get();
|
||||
const newCache = new Map(cache);
|
||||
|
||||
if (section) {
|
||||
newCache.delete(section);
|
||||
// 同时清除完整配置缓存
|
||||
newCache.delete('__full__');
|
||||
} else {
|
||||
newCache.clear();
|
||||
}
|
||||
|
||||
set({ cache: newCache });
|
||||
},
|
||||
|
||||
isCacheValid: (section) => {
|
||||
const { cache } = get();
|
||||
const cacheKey = section || '__full__';
|
||||
const cached = cache.get(cacheKey);
|
||||
|
||||
if (!cached) return false;
|
||||
|
||||
return Date.now() - cached.timestamp < CACHE_EXPIRY_MS;
|
||||
}
|
||||
}));
|
||||
39
src/stores/useLanguageStore.ts
Normal file
39
src/stores/useLanguageStore.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 语言状态管理
|
||||
* 从原项目 src/modules/language.js 迁移
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { Language } from '@/types';
|
||||
import { STORAGE_KEY_LANGUAGE } from '@/utils/constants';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
interface LanguageState {
|
||||
language: Language;
|
||||
setLanguage: (language: Language) => void;
|
||||
toggleLanguage: () => void;
|
||||
}
|
||||
|
||||
export const useLanguageStore = create<LanguageState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
language: 'zh-CN',
|
||||
|
||||
setLanguage: (language) => {
|
||||
// 切换 i18next 语言
|
||||
i18n.changeLanguage(language);
|
||||
set({ language });
|
||||
},
|
||||
|
||||
toggleLanguage: () => {
|
||||
const { language, setLanguage } = get();
|
||||
const newLanguage: Language = language === 'zh-CN' ? 'en' : 'zh-CN';
|
||||
setLanguage(newLanguage);
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY_LANGUAGE
|
||||
}
|
||||
)
|
||||
);
|
||||
53
src/stores/useNotificationStore.ts
Normal file
53
src/stores/useNotificationStore.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 通知状态管理
|
||||
* 替代原项目中的 showNotification 方法
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { Notification, NotificationType } from '@/types';
|
||||
import { generateId } from '@/utils/helpers';
|
||||
import { NOTIFICATION_DURATION_MS } from '@/utils/constants';
|
||||
|
||||
interface NotificationState {
|
||||
notifications: Notification[];
|
||||
showNotification: (message: string, type?: NotificationType, duration?: number) => void;
|
||||
removeNotification: (id: string) => void;
|
||||
clearAll: () => void;
|
||||
}
|
||||
|
||||
export const useNotificationStore = create<NotificationState>((set) => ({
|
||||
notifications: [],
|
||||
|
||||
showNotification: (message, type = 'info', duration = NOTIFICATION_DURATION_MS) => {
|
||||
const id = generateId();
|
||||
const notification: Notification = {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
duration
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
notifications: [...state.notifications, notification]
|
||||
}));
|
||||
|
||||
// 自动移除通知
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
set((state) => ({
|
||||
notifications: state.notifications.filter((n) => n.id !== id)
|
||||
}));
|
||||
}, duration);
|
||||
}
|
||||
},
|
||||
|
||||
removeNotification: (id) => {
|
||||
set((state) => ({
|
||||
notifications: state.notifications.filter((n) => n.id !== id)
|
||||
}));
|
||||
},
|
||||
|
||||
clearAll: () => {
|
||||
set({ notifications: [] });
|
||||
}
|
||||
}));
|
||||
70
src/stores/useThemeStore.ts
Normal file
70
src/stores/useThemeStore.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 主题状态管理
|
||||
* 从原项目 src/modules/theme.js 迁移
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import type { Theme } from '@/types';
|
||||
import { STORAGE_KEY_THEME } from '@/utils/constants';
|
||||
|
||||
interface ThemeState {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
toggleTheme: () => void;
|
||||
initializeTheme: () => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
theme: 'light',
|
||||
|
||||
setTheme: (theme) => {
|
||||
// 应用主题到 DOM
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
|
||||
set({ theme });
|
||||
},
|
||||
|
||||
toggleTheme: () => {
|
||||
const { theme, setTheme } = get();
|
||||
const newTheme: Theme = theme === 'light' ? 'dark' : 'light';
|
||||
setTheme(newTheme);
|
||||
},
|
||||
|
||||
initializeTheme: () => {
|
||||
const { theme, setTheme } = get();
|
||||
|
||||
// 检查系统偏好
|
||||
if (
|
||||
!localStorage.getItem(STORAGE_KEY_THEME) &&
|
||||
window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
) {
|
||||
setTheme('dark');
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用已保存的主题
|
||||
setTheme(theme);
|
||||
|
||||
// 监听系统主题变化(仅在用户未手动设置时)
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (!localStorage.getItem(STORAGE_KEY_THEME)) {
|
||||
setTheme(e.matches ? 'dark' : 'light');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: STORAGE_KEY_THEME
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user