## 新增功能 ### 设置页面(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
192 lines
4.9 KiB
TypeScript
192 lines
4.9 KiB
TypeScript
// ============================================================
|
||
// 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, safeIpcSend } from '@/utils/ipc';
|
||
|
||
// ---------- 类型 ----------
|
||
|
||
export type UpdaterStatus =
|
||
| 'idle'
|
||
| 'checking'
|
||
| 'no-update'
|
||
| 'available'
|
||
| 'downloading'
|
||
| 'downloaded'
|
||
| 'error';
|
||
|
||
export interface UpdateInfo {
|
||
version: string;
|
||
releaseDate?: string;
|
||
releaseNotes?: string;
|
||
forceUpdate?: boolean;
|
||
}
|
||
|
||
export interface UpdateProgress {
|
||
percent: number;
|
||
bytesPerSecond?: number;
|
||
transferred?: number;
|
||
total?: number;
|
||
}
|
||
|
||
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 = {
|
||
UPDATE_AVAILABLE: 'update-available',
|
||
UPDATE_PROGRESS: 'update-progress',
|
||
UPDATE_DOWNLOADED: 'update-downloaded',
|
||
UPDATE_ERROR: 'update-error',
|
||
UPDATE_NOT_AVAILABLE: 'update-not-available',
|
||
INSTALL_UPDATE: 'install-update',
|
||
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 [state, setState] = useState<UpdaterState>(sharedState);
|
||
const subscribed = useRef(false);
|
||
|
||
// 确保 IPC 监听器只注册一次
|
||
useEffect(() => {
|
||
ensureListeners();
|
||
}, []);
|
||
|
||
// 订阅模块级状态变更
|
||
useEffect(() => {
|
||
if (subscribed.current) return;
|
||
subscribed.current = true;
|
||
|
||
const sub: Subscriber = (newState) => setState(newState);
|
||
subscribers.add(sub);
|
||
|
||
return () => {
|
||
subscribers.delete(sub);
|
||
subscribed.current = false;
|
||
};
|
||
}, []);
|
||
|
||
// 同步初始状态(防止在订阅之前状态已变更)
|
||
useEffect(() => {
|
||
setState(sharedState);
|
||
}, []);
|
||
|
||
/** 手动检查更新 */
|
||
const checkForUpdates = useCallback(async () => {
|
||
setSharedState({ status: 'checking', error: null });
|
||
try {
|
||
await safeIpcInvoke(CH.CHECK_FOR_UPDATE);
|
||
} catch (err) {
|
||
setSharedState({
|
||
status: 'error',
|
||
error: { message: (err as Error).message || '检查更新失败' },
|
||
});
|
||
}
|
||
}, []);
|
||
|
||
/** 确认安装更新(退出 → 安装 → 重启) */
|
||
const installUpdate = useCallback(() => {
|
||
safeIpcSend(CH.INSTALL_UPDATE);
|
||
}, []);
|
||
|
||
/** 重置状态 */
|
||
const reset = useCallback(() => {
|
||
setSharedState({
|
||
status: 'idle',
|
||
info: null,
|
||
progress: { percent: 0 },
|
||
error: null,
|
||
});
|
||
}, []);
|
||
|
||
return {
|
||
status: state.status,
|
||
info: state.info,
|
||
progress: state.progress,
|
||
error: state.error,
|
||
checkForUpdates,
|
||
installUpdate,
|
||
reset,
|
||
} as const;
|
||
}
|