refactor: 重组 components/ 和 pages/ 文件结构,按职责分层

components/ 拆为三层:
  - providers/   — 状态管理(App/Theme/SettingsProvider)
  - shell/       — 应用外壳(Layout/NavBar/PageDispatcher)
  - ui/          — 通用 UI 原子(FloatingPanel/LegalTextModal)

pages/ 重命名 + 重组:
  - headers/ → panels/        NavBar 触发的面板页
  - login/   → auth/          认证模块(含注册/忘记密码)
  - settings/blocks/ → sections/
  - home/components/ → panels/ features/ controls/ hooks/
    按职责分:panels(布局壳) + features(功能面板) + controls(表单控件) + hooks

其他清理:
  - 删除空占位 Test3.tsx
  - HomeContext 从 HomeContent 拆出独立文件
  - uploadValidation 内联到 ImageInput
  - 修复过时注释(useUI → controls)
This commit is contained in:
2026-06-15 11:19:06 +08:00
parent bc33b81121
commit 0e6d996718
63 changed files with 123 additions and 123 deletions

View File

@@ -0,0 +1,208 @@
// ============================================================
// AppProvider — 全局应用状态 Context + Provider + Hook
//
// 导出:
// AppContext, useAppContext() — Context + Consumer Hook
// AppContextValue — 类型
// AppProvider — Provider 组件
// ============================================================
import { createContext, useContext, useState, useMemo, useCallback, useEffect, type ReactNode } from 'react';
import type { Platform, Edition } from '@shared/types';
import { getPlatform, isDev } from '@/utils/platform';
import { safeIpcOn, safeIpcOff } from '@/utils/ipc';
import { AuthAPI, type LoginResponseBody } from '@/services/modules/auth';
import { setToken, clearToken, initTokenStore } from '@/services/request';
import { emit, EVENTS } from '@/utils/event-bus';
import { logger } from '@/utils/logger';
import { getAutoLoginFlag, clearAutoLoginFlag } from '@/services/auth-persistence';
import {
initAuthRefresh,
getStoredRefreshToken,
setStoredRefreshToken,
clearStoredRefreshToken,
initRefreshTokenStore,
scheduleProactiveRefresh,
clearProactiveRefresh,
} from '@/services/auth-token';
import {initModelCache} from '@/services/cache/model-cache';
// ---------- Context 类型 ----------
export interface AppContextValue {
/** 当前操作系统平台 */
platform: Platform;
/** 产品版本(个人版 / 企业版) */
edition: Edition;
/** 用户是否已登录 */
isLoggedIn: boolean;
/** 当前登录用户信息(未登录时为 null结构与 LoginResponseBody 一致) */
user: LoginResponseBody | null;
/** 登录:保存 Token 并更新登录态auth 为登录接口返回值,异步加密存储) */
login: (auth: LoginResponseBody) => Promise<void>;
/** 退出登录:清除 Token 和用户状态 */
logout: () => void;
/** 是否为开发环境 */
isDevMode: boolean;
}
export const AppContext = createContext<AppContextValue | null>(null);
// ---------- Consumer Hook ----------
export function useAppContext(): AppContextValue {
const ctx = useContext(AppContext);
if (!ctx) {
throw new Error('useAppContext() 必须在 <AppProvider> 内部调用');
}
return ctx;
}
// ---------- Provider 组件 ----------
interface AppProviderProps {
children: ReactNode;
}
export function AppProvider({ children }: AppProviderProps) {
const [platform] = useState<Platform>(getPlatform);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [user, setUser] = useState<LoginResponseBody | null>(null);
/**
* edition 从登录用户的 account_type 派生(运行时判断,无需构建变量)
*
* 映射关系:
* "individual" → personal ← 个人用户
* "company" | "employee" → enterprise ← 企业用户
* null未登录 → personal ← 默认最小功能集
*
* 安全性edition 是 useMemo 派生值,不可独立赋值;
* 不写入 localStorage重启/登出后回退默认值,必须重新登录才能确权。
*/
const edition: Edition = useMemo(() => {
if (!user) return 'personal';
return user.account_type === 'individual' ? 'personal' : 'enterprise';
}, [user]);
// ---------- 登录 / 退出 ----------
const login = useCallback(async (auth: LoginResponseBody) => {
await setToken(auth.access_token);
await setStoredRefreshToken(auth.refresh_token);
// 启动主动刷新定时器(在 access_token 过期前自动续期)
scheduleProactiveRefresh(auth.access_token);
setUser(auth);
setIsLoggedIn(true);
logger.info('auth', 'User logged in', {
userId: auth.user_id,
username: auth.username,
});
emit(EVENTS.LOGIN_SUCCESS, auth);
}, []);
const logout = useCallback(() => {
logger.info('auth', 'User logged out');
clearToken();
clearStoredRefreshToken();
clearProactiveRefresh();
setUser(null);
setIsLoggedIn(false);
emit(EVENTS.LOGOUT);
}, []);
// ---------- 启动时初始化 token 存储 + 自动登录 ----------
//
// 注意React StrictMode开发模式会挂载→卸载→重新挂载以暴露副作用问题。
// aborted 标志确保卸载后不再执行异步回调,避免 refreshToken / getUserInfo 重复请求。
useEffect(() => {
let aborted = false;
// 从加密存储恢复 token + 模型缓存 到内存缓存
Promise.all([initTokenStore(), initRefreshTokenStore(), initModelCache()]).then(() => {
if (aborted) return;
// 初始化 401 刷新回调token 缓存就绪后才注册)
initAuthRefresh();
// 自动登录refresh_token 续期)
if (!getAutoLoginFlag()) return;
const refreshToken = getStoredRefreshToken();
if (!refreshToken) return;
AuthAPI.refreshToken({ refresh_token: refreshToken })
.then(async (res) => {
if (aborted) return;
await setToken(res.access_token);
await setStoredRefreshToken(res.refresh_token);
scheduleProactiveRefresh(res.access_token);
setIsLoggedIn(true);
logger.info('auth', 'Auto-login succeeded');
// 补全用户信息
AuthAPI.getUserInfo().then((info) => {
if (aborted) return;
const u: LoginResponseBody = {
access_token: res.access_token,
refresh_token: res.refresh_token,
token_type: res.token_type,
expires_in: res.expires_in,
refresh_expires_in: res.refresh_expires_in,
user_id: info.id,
username: info.username,
role: info.role,
email: info.email ?? '',
balance_cent: info.balance_cent,
account_type: info.account_type,
display_name: info.display_name ?? '',
company_id: info.company_id ?? '',
real_name: info.real_name ?? '',
phone: info.phone,
license_code: '',
admin_permissions: info.admin_permissions ?? [],
};
setUser(u);
emit(EVENTS.LOGIN_SUCCESS, u);
});
})
.catch(() => {
if (aborted) return;
logger.warn('auth', 'Auto-login failed, refresh_token expired');
clearStoredRefreshToken();
// 仅清除自动登录标记,保留用户名供下次表单回填
clearAutoLoginFlag();
});
});
return () => { aborted = true; };
}, []);
// ---------- 监听主进程推送的平台/版本信息 ----------
useEffect(() => {
const handler = (_event: unknown, info: unknown) => {
const data = info as { platform?: string; edition?: string } | undefined;
if (data) {
logger.debug('app', 'Received platform info from main process', data as Record<string, unknown>);
}
};
safeIpcOn('platform-info', handler);
return () => {
safeIpcOff('platform-info', handler);
};
}, []);
const value: AppContextValue = {
platform,
edition,
isLoggedIn,
user,
login,
logout,
isDevMode: isDev(),
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

View File

@@ -0,0 +1,140 @@
// ============================================================
// SettingsProvider — 设置项集中状态管理 Context + Provider + Hook
//
// 职责:
// - 存储路径的集中管理read/write/clear
// - 持久化到 localStorage遵循 ele-heixiu-* 键名规范
// - 全局组件通过 useSettings() 消费
// ============================================================
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
// ---------- localStorage 键 ----------
const OUTPUT_PATH_KEY = 'ele-heixiu-output-path';
const TEAM_REPO_PATH_KEY = 'ele-heixiu-team-repo-path';
// ---------- Context 类型 ----------
export interface SettingsContextValue {
/** 大模型产物存储仓库路径 */
outputPath: string;
/** 团队创作存储仓库路径(企业版) */
teamRepoPath: string;
/** 设置产物输出路径 */
setOutputPath: (path: string) => void;
/** 设置团队创作仓库路径 */
setTeamRepoPath: (path: string) => void;
/** 清除产物输出路径 */
clearOutputPath: () => void;
/** 清除团队创作仓库路径 */
clearTeamRepoPath: () => void;
}
export const SettingsContext = createContext<SettingsContextValue | null>(null);
// ---------- 工具函数localStorage 读写)----------
export function readStoredOutputPath(): string {
try {
return localStorage.getItem(OUTPUT_PATH_KEY) || '';
} catch {
return '';
}
}
export function writeStoredOutputPath(path: string): void {
try {
localStorage.setItem(OUTPUT_PATH_KEY, path);
} catch {
/* 静默忽略 */
}
}
export function clearStoredOutputPath(): void {
try {
localStorage.removeItem(OUTPUT_PATH_KEY);
} catch {
/* 静默忽略 */
}
}
export function readStoredTeamRepoPath(): string {
try {
return localStorage.getItem(TEAM_REPO_PATH_KEY) || '';
} catch {
return '';
}
}
export function writeStoredTeamRepoPath(path: string): void {
try {
localStorage.setItem(TEAM_REPO_PATH_KEY, path);
} catch {
/* 静默忽略 */
}
}
export function clearStoredTeamRepoPath(): void {
try {
localStorage.removeItem(TEAM_REPO_PATH_KEY);
} catch {
/* 静默忽略 */
}
}
// ---------- Consumer Hook ----------
export function useSettings(): SettingsContextValue {
const ctx = useContext(SettingsContext);
if (!ctx) {
throw new Error('useSettings() 必须在 <SettingsProvider> 内部调用');
}
return ctx;
}
// ---------- Provider 组件 ----------
interface SettingsProviderProps {
children: ReactNode;
}
export function SettingsProvider({ children }: SettingsProviderProps) {
const [outputPath, setOutputPathState] = useState<string>(readStoredOutputPath);
const [teamRepoPath, setTeamRepoPathState] = useState<string>(readStoredTeamRepoPath);
// ---------- outputPath ----------
const setOutputPath = useCallback((path: string) => {
setOutputPathState(path);
writeStoredOutputPath(path);
}, []);
const clearOutputPath = useCallback(() => {
setOutputPathState('');
clearStoredOutputPath();
}, []);
// ---------- teamRepoPath ----------
const setTeamRepoPath = useCallback((path: string) => {
setTeamRepoPathState(path);
writeStoredTeamRepoPath(path);
}, []);
const clearTeamRepoPath = useCallback(() => {
setTeamRepoPathState('');
clearStoredTeamRepoPath();
}, []);
const value: SettingsContextValue = {
outputPath,
teamRepoPath,
setOutputPath,
setTeamRepoPath,
clearOutputPath,
clearTeamRepoPath,
};
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
}

View File

@@ -0,0 +1,161 @@
// ============================================================
// ThemeProvider — 主题 Context + Provider + Hook + 工具函数
//
// 桌面端Electron使用 View Transitions API 驱动主题切换:
// GPU 合成器截取旧/新两帧做一次 cross-fade替代 80+ 个
// 独立 CSS 过渡,消除 Electron Chromium 的合成器卡顿。
// 浏览器不支持时回退到 CSS transition 方案。
// ============================================================
import { createContext, useContext, useState, useEffect, useLayoutEffect, useCallback, type ReactNode } from 'react';
import { flushSync } from 'react-dom';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { getThemeConfig } from '../../theme/antd-theme';
// ---------- Context 类型 ----------
export interface ThemeContextValue {
isDark: boolean;
toggleTheme: () => void;
setLight: () => void;
setDark: () => void;
}
export const ThemeContext = createContext<ThemeContextValue | null>(null);
// ---------- localStorage 工具函数 ----------
export const STORAGE_KEY = 'ele-heixiu-theme';
export function readStoredTheme(): boolean {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'dark') return true;
if (stored === 'light') return false;
} catch {
/* 静默忽略 */
}
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return false;
}
export function writeStoredTheme(isDark: boolean): void {
try {
localStorage.setItem(STORAGE_KEY, isDark ? 'dark' : 'light');
} catch {
/* 静默忽略 */
}
}
export function applyHtmlTheme(isDark: boolean): void {
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
// ---------- Consumer Hook ----------
export function useTheme(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) {
throw new Error('useTheme() 必须在 <ThemeProvider> 内部调用');
}
return ctx;
}
// ---------- Provider 组件 ----------
interface ThemeProviderProps {
children: ReactNode;
}
/** 检测 View Transitions API 是否可用Chromium 111+Electron 28+ */
function supportsViewTransition(): boolean {
return typeof document !== 'undefined' && 'startViewTransition' in document;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const [isDark, setIsDark] = useState<boolean>(readStoredTheme);
useLayoutEffect(() => {
applyHtmlTheme(isDark);
}, [isDark]);
const toggleTheme = useCallback(() => {
if (supportsViewTransition()) {
// View Transitions APIGPU 合成器 cross-fade1 个动画替代 N 个 CSS 过渡
document.startViewTransition(() => {
flushSync(() => {
setIsDark((prev) => {
const next = !prev;
writeStoredTheme(next);
return next;
});
});
});
} else {
// 回退:普通 React 状态更新 + CSS transition
setIsDark((prev) => {
const next = !prev;
writeStoredTheme(next);
return next;
});
}
}, []);
const setLight = useCallback(() => {
if (supportsViewTransition()) {
document.startViewTransition(() => {
flushSync(() => {
setIsDark(false);
writeStoredTheme(false);
});
});
} else {
setIsDark(false);
writeStoredTheme(false);
}
}, []);
const setDark = useCallback(() => {
if (supportsViewTransition()) {
document.startViewTransition(() => {
flushSync(() => {
setIsDark(true);
writeStoredTheme(true);
});
});
} else {
setIsDark(true);
writeStoredTheme(true);
}
}, []);
// 监听系统主题变化(仅用户从未手动切换时跟随)
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === null) {
setIsDark(e.matches);
}
};
mq.addEventListener('change', handleChange);
return () => mq.removeEventListener('change', handleChange);
}, []);
const ctx: ThemeContextValue = { isDark, toggleTheme, setLight, setDark };
return (
<ThemeContext.Provider value={ctx}>
<ConfigProvider theme={getThemeConfig(isDark)} locale={zhCN}>{children}</ConfigProvider>
</ThemeContext.Provider>
);
}