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

@@ -4,6 +4,7 @@
import { createContext, useContext } from 'react';
import type { Platform, Edition } from '@shared/types';
import type { LoginResponseBody } from '@/services/modules';
// ---------- Context 类型 ----------
@@ -14,9 +15,11 @@ export interface AppContextValue {
edition: Edition;
/** 用户是否已登录 */
isLoggedIn: boolean;
/** 登录TODO: 接入真实认证 */
login: () => void;
/** 退出登录 */
/** 当前登录用户信息(未登录时为 null结构与 LoginResponseBody 一致 */
user: LoginResponseBody | null;
/** 登录:保存 Token 并更新登录态auth 为登录接口返回值) */
login: (auth: LoginResponseBody) => void;
/** 退出登录:清除 Token 和用户状态 */
logout: () => void;
/** 是否为开发环境 */
isDevMode: boolean;

View File

@@ -0,0 +1,94 @@
// ============================================================
// SettingsContext — 设置项集中状态管理(类似 Vue Pinia store
//
// 职责:
// - 存储路径的集中管理read/write/clear
// - 持久化到 localStorage遵循 ele-heixiu-* 键名规范
// - 全局组件通过 useSettings() 消费
// ============================================================
import { createContext, useContext } 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;
}