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:
@@ -5,11 +5,22 @@
|
||||
import { useState, useCallback, useEffect, type ReactNode } from 'react';
|
||||
import type { Platform, Edition } from '@shared/types';
|
||||
|
||||
import { getPlatform, isDev } from '../utils/platform';
|
||||
import { getPlatform, isDev } from '@/utils/platform';
|
||||
import { parseEdition } from '@shared/constants/app';
|
||||
import { safeIpcOn, safeIpcOff } from '../utils/ipc';
|
||||
import { AppContext } from '../contexts/app-context';
|
||||
import type { AppContextValue } from '../contexts/app-context';
|
||||
import { safeIpcOn, safeIpcOff } from '@/utils/ipc';
|
||||
import { AppContext } from '@/contexts/app-context';
|
||||
import type { AppContextValue } from '@/contexts/app-context';
|
||||
import type { LoginResponseBody } from '@/services/modules';
|
||||
import { setToken, clearToken } from '@/services/request';
|
||||
import { emit, EVENTS } from '@/utils/event-bus';
|
||||
import { AuthAPI } from '@/services/modules';
|
||||
import { getAutoLoginFlag, clearAutoLoginFlag } from '@/pages/login/hooks/use-auth-state';
|
||||
import {
|
||||
initAuthRefresh,
|
||||
getStoredRefreshToken,
|
||||
setStoredRefreshToken,
|
||||
clearStoredRefreshToken,
|
||||
} from '@/services/auth-token';
|
||||
|
||||
interface AppProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -21,18 +32,79 @@ export function AppProvider({ children }: AppProviderProps) {
|
||||
return parseEdition(import.meta.env.VITE_EDITION);
|
||||
});
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [user, setUser] = useState<LoginResponseBody | null>(null);
|
||||
|
||||
const login = useCallback(() => {
|
||||
// TODO: 接入真实登录流程
|
||||
// ---------- 登录 / 退出 ----------
|
||||
|
||||
const login = useCallback((auth: LoginResponseBody) => {
|
||||
setToken(auth.access_token);
|
||||
setStoredRefreshToken(auth.refresh_token);
|
||||
setUser(auth);
|
||||
setIsLoggedIn(true);
|
||||
emit(EVENTS.LOGIN_SUCCESS, auth);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
// TODO: 清理 token、用户状态等
|
||||
clearToken();
|
||||
clearStoredRefreshToken();
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
emit(EVENTS.LOGOUT);
|
||||
}, []);
|
||||
|
||||
// 监听主进程推送的平台/版本信息
|
||||
// ---------- 初始化 token 刷新回调(给 request.ts 401 拦截器用) ----------
|
||||
|
||||
useEffect(() => {
|
||||
initAuthRefresh();
|
||||
}, []);
|
||||
|
||||
// ---------- 启动时自动登录(refresh_token 续期) ----------
|
||||
|
||||
useEffect(() => {
|
||||
if (!getAutoLoginFlag()) return;
|
||||
|
||||
const refreshToken = getStoredRefreshToken();
|
||||
if (!refreshToken) return;
|
||||
|
||||
AuthAPI.refreshToken({ refresh_token: refreshToken })
|
||||
.then((res) => {
|
||||
setToken(res.access_token);
|
||||
setStoredRefreshToken(res.refresh_token);
|
||||
setIsLoggedIn(true);
|
||||
// 补全用户信息
|
||||
AuthAPI.getUserInfo().then((info) => {
|
||||
const u: LoginResponseBody = {
|
||||
access_token: res.access_token,
|
||||
refresh_token: res.refresh_token,
|
||||
token_type: res.token_type,
|
||||
expires_in: res.expires_in,
|
||||
refresh_expires_in: res.refresh_expires_in,
|
||||
user_id: info.id,
|
||||
username: info.username,
|
||||
role: info.role,
|
||||
email: info.email,
|
||||
balance_cent: info.balance_cent,
|
||||
account_type: info.account_type,
|
||||
display_name: info.display_name,
|
||||
company_id: info.company_id,
|
||||
real_name: info.real_name,
|
||||
phone: info.phone,
|
||||
license_code: '',
|
||||
admin_permissions: info.admin_permissions,
|
||||
};
|
||||
setUser(u);
|
||||
emit(EVENTS.LOGIN_SUCCESS, u);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
clearStoredRefreshToken();
|
||||
// 仅清除自动登录标记,保留用户名+密码供下次表单回填
|
||||
clearAutoLoginFlag();
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ---------- 监听主进程推送的平台/版本信息 ----------
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (_event: unknown, info: unknown) => {
|
||||
const data = info as { platform?: string; edition?: string } | undefined;
|
||||
@@ -52,6 +124,7 @@ export function AppProvider({ children }: AppProviderProps) {
|
||||
platform,
|
||||
edition,
|
||||
isLoggedIn,
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
isDevMode: isDev(),
|
||||
|
||||
Reference in New Issue
Block a user