feat: 实现设置页面 + 路由系统 + JWT认证体系 + 公告优化 + 错误类型系统

## 新增功能

### 设置页面(Router + Modal 叠加)
- HashRouter 路由系统,Electron file:// 协议兼容
- 设置页以居中 Modal 叠加首页,仅 X 按钮关闭
- 主题切换 Segmented 控件(与首页 Switch 共享 Context)
- 大模型产物存储仓库路径配置(原生文件夹选择对话框)
- 企业版额外:团队创作存储仓库路径
- 系统信息展示(平台/版本/版本类型/环境)
- 版本更新检查(状态机:idle→checking→no-update/available→downloading→downloaded→error)

### JWT 认证体系
- 双 Token 机制:access_token + refresh_token
- Axios 拦截器 401 自动刷新 + 并发请求去重队列
- 启动时 refresh_token 静默续期(自动登录)
- 记住密码:Base64 编码存储,表单自动回填
- setTokenRefreshHandler 解耦模式:request.ts 不知晓 auth

### 自定义错误类型系统
- RefreshError 继承 RequestError,type = AUTH_EXPIRED
- 全局拦截器捕获 RefreshError → 自动 clearToken + AUTH_REQUIRED
- 任何位置 throw new RefreshError() 即可触发登录 Modal

### 公告卡片优化
- 长文本 Tooltip 悬浮预览
- 点击卡片弹出详情 Modal(全文保留换行)

### SettingsContext(类似 Vue Pinia)
- 集中式设置状态管理
- localStorage 持久化:ele-heixiu-output-path / ele-heixiu-team-repo-path
- useSettings() 全局消费

## 架构变更
- App.tsx → HashRouter + AppRoutes + LoginPage 弹层
- Layout.tsx = NavBar + <Outlet /> 页面壳
- HomePage.tsx 从 App.tsx 提取
- NavBar 使用 useNavigate() 真实路由导航
- useUpdater 改为模块级单例 IPC 监听器(修复 MaxListenersExceededWarning)
- electron/main.ts 新增 FILE_DIALOG IPC handler

## 依赖
- 新增 react-router-dom
This commit is contained in:
2026-06-03 19:25:15 +08:00
parent 767bb8b297
commit 3becdb85d4
34 changed files with 2253 additions and 1880 deletions

View File

@@ -5,11 +5,22 @@
import { useState, useCallback, useEffect, type ReactNode } from 'react';
import type { Platform, Edition } from '@shared/types';
import { getPlatform, isDev } from '../utils/platform';
import { getPlatform, isDev } from '@/utils/platform';
import { parseEdition } from '@shared/constants/app';
import { safeIpcOn, safeIpcOff } from '../utils/ipc';
import { AppContext } from '../contexts/app-context';
import type { AppContextValue } from '../contexts/app-context';
import { safeIpcOn, safeIpcOff } from '@/utils/ipc';
import { AppContext } from '@/contexts/app-context';
import type { AppContextValue } from '@/contexts/app-context';
import type { LoginResponseBody } from '@/services/modules';
import { setToken, clearToken } from '@/services/request';
import { emit, EVENTS } from '@/utils/event-bus';
import { AuthAPI } from '@/services/modules';
import { getAutoLoginFlag, clearAutoLoginFlag } from '@/pages/login/hooks/use-auth-state';
import {
initAuthRefresh,
getStoredRefreshToken,
setStoredRefreshToken,
clearStoredRefreshToken,
} from '@/services/auth-token';
interface AppProviderProps {
children: ReactNode;
@@ -21,18 +32,79 @@ export function AppProvider({ children }: AppProviderProps) {
return parseEdition(import.meta.env.VITE_EDITION);
});
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [user, setUser] = useState<LoginResponseBody | null>(null);
const login = useCallback(() => {
// TODO: 接入真实登录流程
// ---------- 登录 / 退出 ----------
const login = useCallback((auth: LoginResponseBody) => {
setToken(auth.access_token);
setStoredRefreshToken(auth.refresh_token);
setUser(auth);
setIsLoggedIn(true);
emit(EVENTS.LOGIN_SUCCESS, auth);
}, []);
const logout = useCallback(() => {
// TODO: 清理 token、用户状态等
clearToken();
clearStoredRefreshToken();
setUser(null);
setIsLoggedIn(false);
emit(EVENTS.LOGOUT);
}, []);
// 监听主进程推送的平台/版本信息
// ---------- 初始化 token 刷新回调(给 request.ts 401 拦截器用) ----------
useEffect(() => {
initAuthRefresh();
}, []);
// ---------- 启动时自动登录refresh_token 续期) ----------
useEffect(() => {
if (!getAutoLoginFlag()) return;
const refreshToken = getStoredRefreshToken();
if (!refreshToken) return;
AuthAPI.refreshToken({ refresh_token: refreshToken })
.then((res) => {
setToken(res.access_token);
setStoredRefreshToken(res.refresh_token);
setIsLoggedIn(true);
// 补全用户信息
AuthAPI.getUserInfo().then((info) => {
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(() => {
clearStoredRefreshToken();
// 仅清除自动登录标记,保留用户名+密码供下次表单回填
clearAutoLoginFlag();
});
}, []);
// ---------- 监听主进程推送的平台/版本信息 ----------
useEffect(() => {
const handler = (_event: unknown, info: unknown) => {
const data = info as { platform?: string; edition?: string } | undefined;
@@ -52,6 +124,7 @@ export function AppProvider({ children }: AppProviderProps) {
platform,
edition,
isLoggedIn,
user,
login,
logout,
isDevMode: isDev(),