## 新增功能 ### 设置页面(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
174 lines
5.5 KiB
TypeScript
174 lines
5.5 KiB
TypeScript
// ============================================================
|
||
// LoginPage — 登录/注册 Modal 弹层
|
||
//
|
||
// 行为:
|
||
// - 未登录时弹出,不可操作主页(Modal 遮罩)
|
||
// - 可关闭(右上 X),关闭后主页可操作,但 API 401 时会再次弹出
|
||
// - 登录成功 → 关闭 Modal,更新全局登录态
|
||
// ============================================================
|
||
|
||
import { useState, useCallback } from 'react';
|
||
import { Modal, Tabs, Typography, App } from 'antd';
|
||
|
||
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, 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;
|
||
|
||
interface LoginPageProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function LoginPage({ open, onClose }: LoginPageProps) {
|
||
const { edition, login } = useAppContext();
|
||
const { isDark } = useTheme();
|
||
const { message } = App.useApp();
|
||
|
||
const isEnterprise = edition === 'enterprise';
|
||
const [activeTab, setActiveTab] = useState<string>('login');
|
||
const [submitting, setSubmitting] = useState(false);
|
||
|
||
const authState = useAuthState();
|
||
|
||
// ========== 登录 ==========
|
||
|
||
const handleLogin = useCallback(
|
||
async (values: LoginFormValues) => {
|
||
setSubmitting(true);
|
||
try {
|
||
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 || '登录失败,请检查用户名和密码');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
},
|
||
[authState, message, login, onClose],
|
||
);
|
||
|
||
// ========== 注册 ==========
|
||
|
||
const handleRegister = useCallback(
|
||
async (values: RegisterFormValues) => {
|
||
setSubmitting(true);
|
||
try {
|
||
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 || '注册失败');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
},
|
||
[message],
|
||
);
|
||
|
||
const handleSendSms = useCallback(
|
||
async (phone: string) => {
|
||
try {
|
||
await AuthAPI.sendSms({ phone });
|
||
message.success('验证码已发送');
|
||
} catch (err) {
|
||
message.error((err as Error).message || '验证码发送失败');
|
||
}
|
||
},
|
||
[message],
|
||
);
|
||
|
||
// ========== 渲染 ==========
|
||
|
||
const loginTab = (
|
||
<LoginForm
|
||
initialUsername={authState.username}
|
||
initialPassword={getStoredPassword()}
|
||
initialRemember={authState.remember}
|
||
initialAutoLogin={authState.autoLogin}
|
||
submitting={submitting}
|
||
onSubmit={handleLogin}
|
||
onRememberChange={authState.handleRememberChange}
|
||
onAutoLoginChange={authState.handleAutoLoginChange}
|
||
/>
|
||
);
|
||
|
||
const registerTab = (
|
||
<RegisterForm submitting={submitting} onSubmit={handleRegister} onSendSms={handleSendSms} />
|
||
);
|
||
|
||
return (
|
||
<Modal
|
||
open={open}
|
||
onCancel={onClose}
|
||
footer={null}
|
||
width={440}
|
||
centered
|
||
destroyOnHidden={false}
|
||
mask={{
|
||
closable: false,
|
||
}}
|
||
styles={{
|
||
body: {
|
||
padding: '24px 32px 16px',
|
||
background: isDark ? '#1A1730' : '#FFFFFF',
|
||
},
|
||
}}
|
||
>
|
||
{/* 标题 */}
|
||
<div className="text-center mb-5">
|
||
<Text strong className="text-lg gradient-primary-text block">
|
||
船长 · HeiXiu
|
||
</Text>
|
||
<Text type="secondary" className="text-xs">
|
||
{isEnterprise ? '企业版 — 员工账号登录' : '个人版'}
|
||
</Text>
|
||
</div>
|
||
|
||
{/* 个人版 Tab | 企业版仅登录 */}
|
||
{isEnterprise ? (
|
||
loginTab
|
||
) : (
|
||
<Tabs
|
||
activeKey={activeTab}
|
||
onChange={setActiveTab}
|
||
centered
|
||
size="small"
|
||
items={[
|
||
{ key: 'login', label: '登录', children: loginTab },
|
||
{ key: 'register', label: '注册', children: registerTab },
|
||
]}
|
||
/>
|
||
)}
|
||
</Modal>
|
||
);
|
||
}
|