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:
Supra4E8C
2025-12-07 11:32:31 +08:00
parent 8e4132200d
commit 450964fb1a
144 changed files with 14223 additions and 21647 deletions

9
src/stores/index.ts Normal file
View 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
View 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
);
}

View 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;
}
}));

View 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
}
)
);

View 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: [] });
}
}));

View 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
}
)
);