Files
ele-HeiXiu/src/pages/login/hooks/use-auth-state.ts
YoungestSongMo 3becdb85d4 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
2026-06-03 19:25:15 +08:00

142 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// useAuthState — 记住账号 / 记住密码 / 自动登录 状态管理
//
// "记住密码":下次打开页面自动回填用户名 + 密码Base64 编码)
// "自动登录"登录成功时标记AppProvider 启动时用 refresh_token 静默续期
//
// 注意:密码 Base64 仅作基本混淆,非安全加密。
// 安全性由 refresh_token + access_token 的 JWT 双重机制保障。
// ============================================================
import { useState, useCallback } from 'react';
const STORAGE_KEY = 'ele-heixiu-auth';
interface StoredAuth {
/** 上次登录的用户名(表单回填用) */
username: string;
/** 密码的 Base64 编码(仅"记住密码"勾选时存储) */
passwordBase64?: string;
/** 是否记住密码(勾选后表单自动回填用户名+密码) */
remember: boolean;
/** 是否勾选了自动登录AppProvider 据此决定启动时是否刷新 token */
autoLogin: boolean;
}
function readAuth(): StoredAuth | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw) as StoredAuth;
} catch {
return null;
}
}
function writeAuth(auth: StoredAuth): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(auth));
} catch {
/* 静默 */
}
}
// ---------- 工具函数(不依赖 ReactAppProvider 可直接调用)----------
/** 读取自动登录标记 */
export function getAutoLoginFlag(): boolean {
return readAuth()?.autoLogin || false;
}
/** 读取存储的密码Base64 解码后返回明文,表单回填用) */
export function getStoredPassword(): string {
const auth = readAuth();
if (!auth?.passwordBase64) return '';
try {
return atob(auth.passwordBase64);
} catch {
return '';
}
}
/** 仅清除自动登录标记(保留用户名和密码,下次还能回填表单) */
export function clearAutoLoginFlag(): void {
const auth = readAuth();
if (!auth) return;
writeAuth({ ...auth, autoLogin: false });
}
/** 清除所有保存的认证信息 */
export function clearSavedAuth(): void {
try {
localStorage.removeItem(STORAGE_KEY);
} catch {
/* 静默 */
}
}
// ---------- Hook ----------
export function useAuthState() {
const saved = readAuth();
const [username, setUsername] = useState(saved?.username || '');
// 密码不存 state直接从 localStorage 读取,避免 stale closure 问题
const [remember, setRemember] = useState(saved?.remember || false);
const [autoLogin, setAutoLogin] = useState(saved?.autoLogin || false);
const handleRememberChange = useCallback((checked: boolean) => {
setRemember(checked);
if (!checked) {
setAutoLogin(false);
clearSavedAuth();
}
}, []);
const handleAutoLoginChange = useCallback((checked: boolean) => {
setAutoLogin(checked);
}, []);
/**
* 登录成功后调用。
* 参数由调用方LoginPage直接从表单值传入避免 useCallback 的 stale closure。
*
* @param user - 用户名
* @param password - 明文密码(仅在"记住密码"勾选时存储Base64 编码)
* @param doRemember - 是否勾选"记住密码"
* @param doAutoLogin - 是否勾选"自动登录"
*/
const saveAuth = useCallback(
(user: string, password: string, doRemember: boolean, doAutoLogin: boolean) => {
setUsername(user);
if (doRemember || doAutoLogin) {
const payload: StoredAuth = {
username: user,
remember: doRemember,
autoLogin: doAutoLogin,
};
// 仅在勾选"记住密码"时存储密码
if (doRemember && password) {
try {
payload.passwordBase64 = btoa(password);
} catch {
/* Base64 编码失败则放弃存储密码 */
}
}
writeAuth(payload);
}
},
[],
);
return {
username,
remember,
autoLogin,
setUsername,
handleRememberChange,
handleAutoLoginChange,
saveAuth,
};
}