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

@@ -14,7 +14,9 @@ import { useAppContext } from '@/contexts/app-context.ts';
import { useTheme } from '@/hooks/use-theme.ts';
import { LoginForm } from './components/LoginForm';
import { RegisterForm } from './components/RegisterForm';
import { useAuthState } from './hooks/use-auth-state';
import { useAuthState, getStoredPassword } from './hooks/use-auth-state';
import {AuthAPI, LoginResponseBody} from '@/services/modules';
import { getDeviceId } from '@/utils/device';
import type { LoginFormValues, RegisterFormValues } from './types';
const { Text } = Typography;
@@ -41,14 +43,23 @@ export function LoginPage({ open, onClose }: LoginPageProps) {
async (values: LoginFormValues) => {
setSubmitting(true);
try {
// TODO: 接入登录 API
console.log('[Login]', values);
authState.saveAuth(values.username, values.password);
message.success('登录成功Demo');
login();
const auth:LoginResponseBody = await AuthAPI.login({
username: values.username,
password: values.password,
device_id: getDeviceId(),
user_ip: '',
});
// 记住偏好(密码 Base64 编码,自动登录靠 refresh_token
authState.saveAuth(values.username, values.password, values.remember, values.autoLogin);
// 更新全局登录态(内部完成 setToken + emit LOGIN_SUCCESS
login(auth);
message.success(`登录成功,欢迎 ${auth.display_name || auth.username}`);
onClose();
} catch (err) {
message.error((err as Error).message || '登录失败');
message.error((err as Error).message || '登录失败,请检查用户名和密码');
} finally {
setSubmitting(false);
}
@@ -62,8 +73,17 @@ export function LoginPage({ open, onClose }: LoginPageProps) {
async (values: RegisterFormValues) => {
setSubmitting(true);
try {
console.log('[Register]', values);
message.success('注册成功Demo请登录');
await AuthAPI.register({
username: values.username,
password: values.password,
phone: values.phone,
sms_code: values.sms_code,
device_id: values.device_id || getDeviceId(),
device_label: values.device_label || '',
display_name: values.display_name || '',
referral_code: values.referral_code || '',
});
message.success('注册成功,请登录');
setActiveTab('login');
} catch (err) {
message.error((err as Error).message || '注册失败');
@@ -75,9 +95,13 @@ export function LoginPage({ open, onClose }: LoginPageProps) {
);
const handleSendSms = useCallback(
(phone: string) => {
console.log('[SMS]', phone);
message.info(`验证码已发送到 ${phone}Demo`);
async (phone: string) => {
try {
await AuthAPI.sendSms({ phone });
message.success('验证码已发送');
} catch (err) {
message.error((err as Error).message || '验证码发送失败');
}
},
[message],
);
@@ -87,7 +111,7 @@ export function LoginPage({ open, onClose }: LoginPageProps) {
const loginTab = (
<LoginForm
initialUsername={authState.username}
initialPassword={authState.password}
initialPassword={getStoredPassword()}
initialRemember={authState.remember}
initialAutoLogin={authState.autoLogin}
submitting={submitting}