Files
ele-HeiXiu/src/modules/auth/views/components/LoginForm.tsx
YoungestSongMo 337d6c1205 feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一
## 构建、打包与分发系统重构 (build/)

- 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本

- electron-builder 配置从 package.json 提取为 build/electron-builder.js

- 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数

- OSS 路径按渠道隔离

- 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验

- 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成

- updater.ts 发送 channel/edition/deviceFingerprint

- 新增 src/shared/utils/rollout.ts 灰度测试工具

- 新增 10 条 NPM 脚本

## VCHSM 五层架构迁移

- 7 个业务模块 + ~500 处导入路径同步

## 修复

- MediaCardContextMenu 改用共享 ContextMenu 组件

- ComboBox 尺寸参数显示统一 (size-format.ts)

## 文档

- UpdateA.md / build/README.md / README.md / .env.example 更新

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 18:41:07 +08:00

221 lines
7.6 KiB
TypeScript

// ============================================================
// LoginForm — 登录表单
// ============================================================
import { useState, useCallback } from 'react';
import { Input, Button, Checkbox, Typography } from 'antd';
import { UserOutlined, LockOutlined, EyeTwoTone, EyeInvisibleOutlined } from '@ant-design/icons';
import { loginFields } from '../../settings/login-fields';
import { validateForm } from '../../settings/validators';
import { LegalTextModal } from '@/shared/components/LegalTextModal';
import type { LoginFormValues } from '../../model/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;
onForgotPassword?: () => void;
}
export function LoginForm({
initialUsername,
initialPassword,
initialRemember = false,
initialAutoLogin = false,
submitting = false,
onSubmit,
onRememberChange,
onAutoLoginChange,
onForgotPassword,
}: 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(true);
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');
}}
iconRender={(visible) => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
onPressEnter={handleSubmit}
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={() => onForgotPassword?.()}>
?
</a>
</div>
{/* 用户协议 */}
<div>
<Checkbox
checked={agreed}
onChange={(e) => {
setAgreed(e.target.checked);
clearError('_agreement');
}}
onKeyDown={(event)=>{
if (event.key === 'Enter' || event.keyCode === 13) {
setAgreed(!agreed);
}
}}
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)} />}
</>
);
}