## 构建、打包与分发系统重构 (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>
221 lines
7.6 KiB
TypeScript
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)} />}
|
|
</>
|
|
);
|
|
}
|