feat: 初始化船长·HeiXiu 桌面应用项目(错误是svg的问题)
Electron + React 19 + TypeScript + Ant Design 6 + Tailwind CSS 4 【核心架构】 - Electron 主进程(窗口管理、平台检测、图标适配) - Vite 构建链(双产物 dist/ + dist-electron/) - 共享层 shared/(类型、常量、工具函数) - IPC 通信框架 + 安全守卫 【主题系统】 - 蓝紫渐变主题(亮色/暗色双模式) - Ant Design ConfigProvider 适配(lightAlgorithm / darkAlgorithm) - Tailwind CSS 4 @theme 指令 + CSS 变量 - 设计令牌三处同步(tokens.ts / globals.css / antd-theme.ts) 【导航栏】 - 自定义 H5 导航栏替代系统菜单 - 个人版/企业版双布局 - 未登录拦截 + 事件总线驱动登录弹窗 【登录注册】 - Modal 弹层(Portal)+ Tabs 切换 - 登录/注册表单动态字段渲染 - 记住密码 / 自动登录 / 用户协议 【更新系统】 - 自定义 API 版本检查(替代 electron-updater generic provider) - 手动下载 + SHA512 校验 + spawn NSIS 安装 - 渲染进程更新状态机 Hook(useUpdater) - 检查更新按钮 + 下载进度展示 【构建工具链】 - build.mjs 构建总管(--win/--mac/--linux --personal/--enterprise --build/--clean/--sign/--upload/--publish) - scripts/ 自动化脚本(clean / generate-update-info / upload-oss / publish-release) - electron-builder NSIS 安装器(可选路径、多版本打包) - update-info.json 自动生成(fileSize/sha512/changelog/releaseDate) 【代码质量】 - ESLint + TypeScript strict + Prettier - husky + lint-staged(提交前自动检查) - commitlint(规范提交信息) - Playwright E2E + Vitest 单元测试框架 - rollup-plugin-visualizer 打包体积分析 【文档】 - README.md(快速开始 + 目录结构 + 命令速查) - UpdateA.md(发布手册:API/签名/发版流程/数据库) - CHANGELOG.md(更新日志) - .env.example(构建环境变量模板)
This commit is contained in:
216
src/pages/login/components/LoginForm.tsx
Normal file
216
src/pages/login/components/LoginForm.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
// ============================================================
|
||||
// LoginForm — 登录表单
|
||||
// ============================================================
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Input, Button, Checkbox, Typography } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
|
||||
import { loginFields } from '../config/login-fields';
|
||||
import { validateForm } from '../config/validators';
|
||||
import { LegalTextModal } from '@/components/common/LegalTextModal';
|
||||
import type { LoginFormValues } from '../types';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface LoginFormProps {
|
||||
initialUsername?: string;
|
||||
initialPassword?: string;
|
||||
initialRemember?: boolean;
|
||||
initialAutoLogin?: boolean;
|
||||
submitting?: boolean;
|
||||
onSubmit: (values: LoginFormValues) => void;
|
||||
onRememberChange: (checked: boolean) => void;
|
||||
onAutoLoginChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export function LoginForm({
|
||||
initialUsername,
|
||||
initialPassword,
|
||||
initialRemember = false,
|
||||
initialAutoLogin = false,
|
||||
submitting = false,
|
||||
onSubmit,
|
||||
onRememberChange,
|
||||
onAutoLoginChange,
|
||||
}: LoginFormProps) {
|
||||
const [username, setUsername] = useState(initialUsername || '');
|
||||
const [password, setPassword] = useState(initialPassword || '');
|
||||
const [remember, setRemember] = useState(initialRemember);
|
||||
const [autoLogin, setAutoLogin] = useState(initialAutoLogin);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
||||
|
||||
// 协议弹窗
|
||||
const [legalType, setLegalType] = useState<'service' | 'points' | null>(null);
|
||||
|
||||
const clearError = (name: string) =>
|
||||
setErrors((p) => {
|
||||
const n = { ...p };
|
||||
delete n[name];
|
||||
return n;
|
||||
});
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const values = { username, password };
|
||||
const fieldErrors = validateForm(values, loginFields);
|
||||
if (Object.keys(fieldErrors).length > 0) {
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
}
|
||||
if (!agreed) {
|
||||
setErrors({ _agreement: ['请先阅读并同意用户协议'] });
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
onSubmit({ username, password, remember, autoLogin: remember && autoLogin });
|
||||
}, [username, password, remember, autoLogin, agreed, onSubmit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-5">
|
||||
{/* 用户名 */}
|
||||
<div>
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<UserOutlined className="opacity-40" />}
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChange={(e) => {
|
||||
setUsername(e.target.value);
|
||||
clearError('username');
|
||||
}}
|
||||
status={errors.username ? 'error' : undefined}
|
||||
allowClear
|
||||
/>
|
||||
{errors.username && (
|
||||
<Text type="danger" className="text-xs ml-1">
|
||||
{errors.username[0]}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div>
|
||||
<Input.Password
|
||||
size="large"
|
||||
prefix={<LockOutlined className="opacity-40" />}
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value);
|
||||
clearError('password');
|
||||
}}
|
||||
status={errors.password ? 'error' : undefined}
|
||||
allowClear
|
||||
/>
|
||||
{errors.password && (
|
||||
<Text type="danger" className="text-xs ml-1">
|
||||
{errors.password[0]}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 记住密码 / 自动登录 / 忘记密码 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Checkbox
|
||||
checked={remember}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
setRemember(checked);
|
||||
onRememberChange(checked);
|
||||
// 取消记住密码 → 同时取消自动登录
|
||||
if (!checked) {
|
||||
setAutoLogin(false);
|
||||
onAutoLoginChange(false);
|
||||
}
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
记住密码
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={autoLogin}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
setAutoLogin(checked);
|
||||
onAutoLoginChange(checked);
|
||||
// 勾选自动登录 → 同时勾选记住密码
|
||||
if (checked && !remember) {
|
||||
setRemember(true);
|
||||
onRememberChange(true);
|
||||
}
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
自动登录
|
||||
</Checkbox>
|
||||
</div>
|
||||
<a
|
||||
className="text-xs"
|
||||
onClick={() => {
|
||||
/* TODO: 忘记密码流程 */
|
||||
}}
|
||||
>
|
||||
忘记密码?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* 用户协议 */}
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={agreed}
|
||||
onChange={(e) => {
|
||||
setAgreed(e.target.checked);
|
||||
clearError('_agreement');
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
已阅读并同意
|
||||
<a
|
||||
className="text-xs mx-0.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setLegalType('service');
|
||||
}}
|
||||
>
|
||||
《用户服务协议》
|
||||
</a>
|
||||
和
|
||||
<a
|
||||
className="text-xs mx-0.5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setLegalType('points');
|
||||
}}
|
||||
>
|
||||
《积分充值服务协议》
|
||||
</a>
|
||||
</Checkbox>
|
||||
{errors._agreement && (
|
||||
<Text type="danger" className="text-xs block mt-1">
|
||||
{errors._agreement[0]}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
icon={<UserOutlined />}
|
||||
loading={submitting}
|
||||
onClick={handleSubmit}
|
||||
className="h-11! text-base! font-medium! mt-2!"
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 协议弹窗 */}
|
||||
{legalType && <LegalTextModal open type={legalType} onClose={() => setLegalType(null)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user