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

@@ -1,26 +1,31 @@
// ============================================================
// useUpdater — 渲染进程中的更新状态管理
// useUpdater — 渲染进程中的更新状态管理(单例 IPC 监听)
//
// 用法:
// const { status, progress, checkForUpdates, installUpdate } = useUpdater();
//
// 状态机idle → checking → (no-update | available → downloading → downloaded → idle)
//
// 关键设计:
// - IPC 监听器在模块级别只注册一次(单例),避免多个组件同时
// 调用 useUpdater() 导致 MaxListenersExceededWarning
// - 所有 useUpdater() 实例通过订阅机制共享同一份状态
// ============================================================
import { useState, useEffect, useCallback, useRef } from 'react';
import { safeIpcInvoke, safeIpcOn, safeIpcOff, safeIpcSend } from '@/utils/ipc';
import { safeIpcInvoke, safeIpcOn, safeIpcSend } from '@/utils/ipc';
// ---------- 类型 ----------
export type UpdaterStatus =
| 'idle' // 空闲
| 'checking' // 正在检查
| 'no-update' // 已是最新
| 'available' // 发现新版本,准备下载
| 'downloading' // 下载中
| 'downloaded' // 下载完成,待安装
| 'error'; // 出错
| 'idle'
| 'checking'
| 'no-update'
| 'available'
| 'downloading'
| 'downloaded'
| 'error';
export interface UpdateInfo {
version: string;
@@ -40,6 +45,43 @@ export interface UpdateError {
message: string;
}
// ---------- 模块级共享状态 ----------
interface UpdaterState {
status: UpdaterStatus;
info: UpdateInfo | null;
progress: UpdateProgress;
error: UpdateError | null;
}
type Subscriber = (state: UpdaterState) => void;
let sharedState: UpdaterState = {
status: 'idle',
info: null,
progress: { percent: 0 },
error: null,
};
const subscribers = new Set<Subscriber>();
let listenersRegistered = false;
function notifyAll(): void {
const snapshot = { ...sharedState };
subscribers.forEach((fn) => {
try {
fn(snapshot);
} catch {
/* 单个订阅者异常不影响其他 */
}
});
}
function setSharedState(patch: Partial<UpdaterState>): void {
sharedState = { ...sharedState, ...patch };
notifyAll();
}
// ---------- IPC 通道名(与主进程 updater.ts 保持一致)----------
const CH = {
@@ -52,62 +94,73 @@ const CH = {
CHECK_FOR_UPDATE: 'check-for-update',
} as const;
// ---------- 注册 IPC 监听器(模块级,仅一次)----------
function ensureListeners(): void {
if (listenersRegistered) return;
listenersRegistered = true;
safeIpcOn(CH.UPDATE_AVAILABLE, (_event: unknown, ...args: unknown[]) => {
setSharedState({ info: args[0] as UpdateInfo, status: 'available' });
});
safeIpcOn(CH.UPDATE_PROGRESS, (_event: unknown, ...args: unknown[]) => {
setSharedState({ status: 'downloading', progress: args[0] as UpdateProgress });
});
safeIpcOn(CH.UPDATE_DOWNLOADED, () => {
setSharedState({ status: 'downloaded' });
});
safeIpcOn(CH.UPDATE_ERROR, (_event: unknown, ...args: unknown[]) => {
setSharedState({ error: args[0] as UpdateError, status: 'error' });
});
safeIpcOn(CH.UPDATE_NOT_AVAILABLE, () => {
setSharedState({ status: 'no-update' });
});
}
// ---------- Hook ----------
export function useUpdater() {
const [status, setStatus] = useState<UpdaterStatus>('idle');
const [info, setInfo] = useState<UpdateInfo | null>(null);
const [progress, setProgress] = useState<UpdateProgress>({ percent: 0 });
const [error, setError] = useState<UpdateError | null>(null);
// 防止 StrictMode 双重监听
const registered = useRef(false);
const [state, setState] = useState<UpdaterState>(sharedState);
const subscribed = useRef(false);
// 确保 IPC 监听器只注册一次
useEffect(() => {
if (registered.current) return;
registered.current = true;
ensureListeners();
}, []);
safeIpcOn(CH.UPDATE_AVAILABLE, (_event: unknown, ...args: unknown[]) => {
setInfo(args[0] as UpdateInfo);
setStatus('available');
});
// 订阅模块级状态变更
useEffect(() => {
if (subscribed.current) return;
subscribed.current = true;
safeIpcOn(CH.UPDATE_PROGRESS, (_event: unknown, ...args: unknown[]) => {
setStatus('downloading');
setProgress(args[0] as UpdateProgress);
});
safeIpcOn(CH.UPDATE_DOWNLOADED, () => {
setStatus('downloaded');
});
safeIpcOn(CH.UPDATE_ERROR, (_event: unknown, ...args: unknown[]) => {
setError(args[0] as UpdateError);
setStatus('error');
});
safeIpcOn(CH.UPDATE_NOT_AVAILABLE, () => {
setStatus('no-update');
});
const sub: Subscriber = (newState) => setState(newState);
subscribers.add(sub);
return () => {
safeIpcOff(CH.UPDATE_AVAILABLE, () => {});
safeIpcOff(CH.UPDATE_PROGRESS, () => {});
safeIpcOff(CH.UPDATE_DOWNLOADED, () => {});
safeIpcOff(CH.UPDATE_ERROR, () => {});
safeIpcOff(CH.UPDATE_NOT_AVAILABLE, () => {});
subscribers.delete(sub);
subscribed.current = false;
};
}, []);
// 同步初始状态(防止在订阅之前状态已变更)
useEffect(() => {
setState(sharedState);
}, []);
/** 手动检查更新 */
const checkForUpdates = useCallback(async () => {
setStatus('checking');
setError(null);
setSharedState({ status: 'checking', error: null });
try {
await safeIpcInvoke(CH.CHECK_FOR_UPDATE);
} catch (err) {
setStatus('error');
setError({ message: (err as Error).message || '检查更新失败' });
setSharedState({
status: 'error',
error: { message: (err as Error).message || '检查更新失败' },
});
}
}, []);
@@ -118,17 +171,19 @@ export function useUpdater() {
/** 重置状态 */
const reset = useCallback(() => {
setStatus('idle');
setInfo(null);
setProgress({ percent: 0 });
setError(null);
setSharedState({
status: 'idle',
info: null,
progress: { percent: 0 },
error: null,
});
}, []);
return {
status,
info,
progress,
error,
status: state.status,
info: state.info,
progress: state.progress,
error: state.error,
checkForUpdates,
installUpdate,
reset,