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:
94
src/contexts/settings-context.ts
Normal file
94
src/contexts/settings-context.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user