## 新增功能 ### 设置页面(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
142 lines
4.2 KiB
TypeScript
142 lines
4.2 KiB
TypeScript
// ============================================================
|
||
// 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 {
|
||
/* 静默 */
|
||
}
|
||
}
|
||
|
||
// ---------- 工具函数(不依赖 React,AppProvider 可直接调用)----------
|
||
|
||
/** 读取自动登录标记 */
|
||
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,
|
||
};
|
||
}
|