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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user