提交1 bmad搭建与项目启动 - ok

This commit is contained in:
2025-10-17 17:24:56 +08:00
commit ec58562661
686 changed files with 149750 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { User, AuthState } from '../../types/auth';
import {
getToken,
getUser,
saveToken,
saveUser,
clearAuth,
isTokenExpired,
refreshAuthToken,
generateToken,
} from '../../lib/authStorage';
interface AuthContextType {
authState: AuthState;
login: (user: User) => void;
logout: () => void;
updateUser: (user: User) => void;
checkAuth: () => boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [authState, setAuthState] = useState<AuthState>({
isAuthenticated: false,
user: null,
token: null,
refreshToken: null,
});
// 初始化时检查登录状态
useEffect(() => {
const initAuth = async () => {
const token = getToken();
const user = getUser();
if (token && user) {
// 检查token是否过期
if (isTokenExpired()) {
// 尝试刷新token
const refreshed = await refreshAuthToken();
if (refreshed) {
setAuthState({
isAuthenticated: true,
user,
token: refreshed.token,
refreshToken: refreshed.refreshToken,
});
} else {
// 刷新失败,自动使用默认账号登录
await autoLoginWithDefaultAccount();
}
} else {
setAuthState({
isAuthenticated: true,
user,
token,
refreshToken: getToken(),
});
}
} else {
// 没有登录信息,自动使用默认账号登录
await autoLoginWithDefaultAccount();
}
};
// 自动登录默认账号
const autoLoginWithDefaultAccount = async () => {
// 动态导入避免循环依赖
const { validatePasswordLogin } = await import('../../lib/authStorage');
// 使用默认管理员账号自动登录
const result = await validatePasswordLogin('admin', 'admin123', 'AUTO');
if (result.success && result.user) {
const newToken = generateToken();
const newRefreshToken = generateToken();
saveToken(newToken, newRefreshToken);
saveUser(result.user);
setAuthState({
isAuthenticated: true,
user: result.user,
token: newToken,
refreshToken: newRefreshToken,
});
} else {
// 自动登录失败,显示登录页面
setAuthState({
isAuthenticated: false,
user: null,
token: null,
refreshToken: null,
});
}
};
initAuth();
}, []);
// 定期检查token有效性
useEffect(() => {
if (!authState.isAuthenticated) return;
const interval = setInterval(async () => {
if (isTokenExpired()) {
const refreshed = await refreshAuthToken();
if (refreshed) {
setAuthState(prev => ({
...prev,
token: refreshed.token,
refreshToken: refreshed.refreshToken,
}));
} else {
logout();
}
}
}, 5 * 60 * 1000); // 每5分钟检查一次
return () => clearInterval(interval);
}, [authState.isAuthenticated]);
const login = (user: User) => {
const token = generateToken();
const refreshToken = generateToken();
saveToken(token, refreshToken);
saveUser(user);
setAuthState({
isAuthenticated: true,
user,
token,
refreshToken,
});
};
const logout = () => {
clearAuth();
setAuthState({
isAuthenticated: false,
user: null,
token: null,
refreshToken: null,
});
};
const updateUser = (user: User) => {
saveUser(user);
setAuthState(prev => ({
...prev,
user,
}));
};
const checkAuth = (): boolean => {
return authState.isAuthenticated && !isTokenExpired();
};
return (
<AuthContext.Provider value={{ authState, login, logout, updateUser, checkAuth }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,127 @@
import { useState, useEffect } from 'react';
import { RefreshCw } from 'lucide-react';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
interface CaptchaInputProps {
value: string;
onChange: (value: string) => void;
className?: string;
}
export function CaptchaInput({ value, onChange, className = '' }: CaptchaInputProps) {
const [captchaText, setCaptchaText] = useState('');
const [captchaImage, setCaptchaImage] = useState('');
const generateCaptcha = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let text = '';
for (let i = 0; i < 4; i++) {
text += chars.charAt(Math.floor(Math.random() * chars.length));
}
setCaptchaText(text);
generateCaptchaImage(text);
};
const generateCaptchaImage = (text: string) => {
const canvas = document.createElement('canvas');
canvas.width = 120;
canvas.height = 40;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 背景
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 干扰线
for (let i = 0; i < 3; i++) {
ctx.strokeStyle = `rgba(${Math.random() * 100}, ${Math.random() * 100}, ${Math.random() * 100}, 0.3)`;
ctx.beginPath();
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.stroke();
}
// 干扰点
for (let i = 0; i < 30; i++) {
ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.3)`;
ctx.beginPath();
ctx.arc(
Math.random() * canvas.width,
Math.random() * canvas.height,
1,
0,
2 * Math.PI
);
ctx.fill();
}
// 验证码文字
ctx.font = 'bold 24px Arial';
ctx.textBaseline = 'middle';
for (let i = 0; i < text.length; i++) {
const char = text[i];
const x = 20 + i * 25;
const y = 20 + (Math.random() - 0.5) * 6;
const angle = (Math.random() - 0.5) * 0.4;
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
// 随机颜色
const colors = ['#16a34a', '#2563eb', '#dc2626', '#ea580c', '#8b5cf6'];
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
ctx.fillText(char, 0, 0);
ctx.restore();
}
setCaptchaImage(canvas.toDataURL());
};
useEffect(() => {
generateCaptcha();
}, []);
const handleRefresh = () => {
generateCaptcha();
onChange('');
};
return (
<div className={`flex gap-2 ${className}`}>
<Input
type="text"
placeholder="请输入验证码"
value={value}
onChange={(e) => onChange(e.target.value.toUpperCase())}
maxLength={4}
className="flex-1"
/>
<div className="flex gap-2 items-center">
<div
className="h-10 w-[120px] border border-gray-300 rounded cursor-pointer hover:opacity-80 transition-opacity"
onClick={handleRefresh}
style={{
backgroundImage: `url(${captchaImage})`,
backgroundSize: 'cover',
}}
title="点击刷新验证码"
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleRefresh}
title="刷新验证码"
>
<RefreshCw className="w-4 h-4" />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,395 @@
import { useState } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { Alert, AlertDescription } from '../ui/alert';
import { CaptchaInput } from './CaptchaInput';
import { useAuth } from './AuthContext';
import { validatePasswordLogin, validatePhoneLogin, sendSmsCode } from '../../lib/authStorage';
import {
Lock,
User,
Phone,
MessageSquare,
Eye,
EyeOff,
LogIn,
Loader2,
Shield,
Smartphone,
AlertCircle,
CheckCircle,
} from 'lucide-react';
import { toast } from 'sonner@2.0.3';
interface LoginProps {
onRegisterClick: () => void;
}
export function Login({ onRegisterClick }: LoginProps) {
const { login } = useAuth();
const [loginType, setLoginType] = useState<'password' | 'phone'>('password');
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
const [countdown, setCountdown] = useState(0);
const [error, setError] = useState('');
// 密码登录表单
const [passwordForm, setPasswordForm] = useState({
username: '',
password: '',
captcha: '',
});
// 手机号登录表单
const [phoneForm, setPhoneForm] = useState({
phone: '',
code: '',
captcha: '',
});
// 发送验证码
const handleSendCode = async () => {
if (!phoneForm.phone) {
toast.error('请输入手机号');
return;
}
if (!/^1[3-9]\d{9}$/.test(phoneForm.phone)) {
toast.error('请输入正确的手机号');
return;
}
setSendingCode(true);
setError('');
try {
const result = await sendSmsCode(phoneForm.phone);
if (result.success) {
toast.success(result.message + '测试验证码123456');
// 开始倒计时
setCountdown(60);
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
} else {
setError(result.message);
}
} catch (err) {
setError('发送验证码失败,请稍后重试');
} finally {
setSendingCode(false);
}
};
// 密码登录
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!passwordForm.username || !passwordForm.password) {
setError('请输入用户名和密码');
return;
}
if (!passwordForm.captcha) {
setError('请输入图形验证码');
return;
}
setLoading(true);
try {
const result = await validatePasswordLogin(
passwordForm.username,
passwordForm.password,
passwordForm.captcha
);
if (result.success && result.user) {
login(result.user);
toast.success('登录成功!');
} else {
setError(result.message);
toast.error(result.message);
}
} catch (err) {
setError('登录失败,请稍后重试');
toast.error('登录失败');
} finally {
setLoading(false);
}
};
// 手机号登录
const handlePhoneLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!phoneForm.phone || !phoneForm.code) {
setError('请输入手机号和验证码');
return;
}
if (!phoneForm.captcha) {
setError('请输入图形验证码');
return;
}
setLoading(true);
try {
const result = await validatePhoneLogin(
phoneForm.phone,
phoneForm.code,
phoneForm.captcha
);
if (result.success && result.user) {
login(result.user);
toast.success('登录成功!');
} else {
setError(result.message);
toast.error(result.message);
}
} catch (err) {
setError('登录失败,请稍后重试');
toast.error('登录失败');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-blue-50 to-cyan-50 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Logo和标题 */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-600 rounded-2xl mb-4">
<Shield className="w-8 h-8 text-white" />
</div>
<h1 className="text-green-900 mb-2"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
<Card className="p-8 shadow-xl">
<Tabs value={loginType} onValueChange={(v) => setLoginType(v as 'password' | 'phone')}>
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="password" className="flex items-center gap-2">
<Lock className="w-4 h-4" />
</TabsTrigger>
<TabsTrigger value="phone" className="flex items-center gap-2">
<Smartphone className="w-4 h-4" />
</TabsTrigger>
</TabsList>
{error && (
<Alert className="mb-4 bg-red-50 border-red-200">
<AlertCircle className="w-4 h-4 text-red-600" />
<AlertDescription className="text-red-700">{error}</AlertDescription>
</Alert>
)}
{/* 密码登录 */}
<TabsContent value="password">
<form onSubmit={handlePasswordLogin} className="space-y-4">
<div>
<Label></Label>
<div className="relative mt-2">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="请输入用户名"
value={passwordForm.username}
onChange={(e) => setPasswordForm({ ...passwordForm, username: e.target.value })}
className="pl-10"
disabled={loading}
/>
</div>
</div>
<div>
<Label></Label>
<div className="relative mt-2">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type={showPassword ? 'text' : 'password'}
placeholder="请输入密码"
value={passwordForm.password}
onChange={(e) => setPasswordForm({ ...passwordForm, password: e.target.value })}
className="pl-10 pr-10"
disabled={loading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div>
<Label></Label>
<CaptchaInput
value={passwordForm.captcha}
onChange={(value) => setPasswordForm({ ...passwordForm, captcha: value })}
className="mt-2"
/>
</div>
<Button type="submit" className="w-full bg-green-600 hover:bg-green-700" disabled={loading}>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
</form>
<div className="mt-4 text-center">
<p className="text-sm text-muted-foreground">
admin / admin123 zhangsan / zhang123
</p>
</div>
</TabsContent>
{/* 手机号登录 */}
<TabsContent value="phone">
<form onSubmit={handlePhoneLogin} className="space-y-4">
<div>
<Label></Label>
<div className="relative mt-2">
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="tel"
placeholder="请输入手机号"
value={phoneForm.phone}
onChange={(e) => setPhoneForm({ ...phoneForm, phone: e.target.value })}
className="pl-10"
maxLength={11}
disabled={loading}
/>
</div>
</div>
<div>
<Label></Label>
<div className="flex gap-2 mt-2">
<div className="relative flex-1">
<MessageSquare className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="请输入验证码"
value={phoneForm.code}
onChange={(e) => setPhoneForm({ ...phoneForm, code: e.target.value })}
className="pl-10"
maxLength={6}
disabled={loading}
/>
</div>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={sendingCode || countdown > 0 || loading}
className="min-w-[120px]"
>
{sendingCode ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : countdown > 0 ? (
`${countdown}秒后重试`
) : (
'发送验证码'
)}
</Button>
</div>
</div>
<div>
<Label></Label>
<CaptchaInput
value={phoneForm.captcha}
onChange={(value) => setPhoneForm({ ...phoneForm, captcha: value })}
className="mt-2"
/>
</div>
<Button type="submit" className="w-full bg-green-600 hover:bg-green-700" disabled={loading}>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
</form>
<div className="mt-4 text-center">
<p className="text-sm text-muted-foreground">
13800138000 13800138001123456
</p>
</div>
</TabsContent>
</Tabs>
{/* 注册入口 */}
<div className="mt-6 pt-6 border-t text-center">
<p className="text-sm text-muted-foreground">
{' '}
<button
onClick={onRegisterClick}
className="text-green-600 hover:text-green-700 hover:underline"
>
</button>
</p>
</div>
{/* 安全提示 */}
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<div className="flex items-start gap-2">
<Shield className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-xs text-blue-900">
<p className="mb-1"></p>
<ul className="space-y-0.5">
<li> </li>
<li> 24</li>
<li> IP</li>
</ul>
</div>
</div>
</div>
</Card>
{/* 页脚 */}
<div className="mt-6 text-center text-xs text-muted-foreground">
<p>© 2024 . All rights reserved.</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,519 @@
import { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Alert, AlertDescription } from '../ui/alert';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { CaptchaInput } from './CaptchaInput';
import { useAuth } from './AuthContext';
import { registerUser, sendSmsCode, getAllEnterprises } from '../../lib/authStorage';
import { Enterprise } from '../../types/auth';
import {
User,
Lock,
Phone,
MessageSquare,
Mail,
UserPlus,
Eye,
EyeOff,
Loader2,
Shield,
AlertCircle,
CheckCircle,
ArrowLeft,
Building2,
} from 'lucide-react';
import { toast } from 'sonner@2.0.3';
interface RegisterProps {
onBackToLogin: () => void;
}
export function Register({ onBackToLogin }: RegisterProps) {
const { login } = useAuth();
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [sendingCode, setSendingCode] = useState(false);
const [countdown, setCountdown] = useState(0);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
const [form, setForm] = useState({
username: '',
password: '',
confirmPassword: '',
phone: '',
code: '',
realName: '',
email: '',
enterpriseId: '',
captcha: '',
});
// 加载企业列表
useEffect(() => {
const enterpriseList = getAllEnterprises();
setEnterprises(enterpriseList);
}, []);
const [validation, setValidation] = useState({
username: '',
password: '',
confirmPassword: '',
phone: '',
realName: '',
});
// 验证用户名
const validateUsername = (username: string) => {
if (!username) return '请输入用户名';
if (username.length < 3) return '用户名至少3个字符';
if (username.length > 20) return '用户名最多20个字符';
if (!/^[a-zA-Z0-9_]+$/.test(username)) return '用户名只能包含字母、数字和下划线';
return '';
};
// 验证密码
const validatePassword = (password: string) => {
if (!password) return '请输入密码';
if (password.length < 6) return '密码至少6个字符';
if (password.length > 20) return '密码最多20个字符';
return '';
};
// 验证确认密码
const validateConfirmPassword = (confirmPassword: string, password: string) => {
if (!confirmPassword) return '请确认密码';
if (confirmPassword !== password) return '两次密码输入不一致';
return '';
};
// 验证手机号
const validatePhone = (phone: string) => {
if (!phone) return '请输入手机号';
if (!/^1[3-9]\d{9}$/.test(phone)) return '请输入正确的手机号';
return '';
};
// 验证真实姓名
const validateRealName = (realName: string) => {
if (!realName) return '请输入真实姓名';
if (realName.length < 2) return '姓名至少2个字符';
if (realName.length > 20) return '姓名最多20个字符';
return '';
};
// 发送验证码
const handleSendCode = async () => {
const phoneError = validatePhone(form.phone);
if (phoneError) {
setValidation({ ...validation, phone: phoneError });
toast.error(phoneError);
return;
}
setSendingCode(true);
setError('');
try {
const result = await sendSmsCode(form.phone);
if (result.success) {
toast.success(result.message + '测试验证码123456');
setCountdown(60);
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
} else {
setError(result.message);
toast.error(result.message);
}
} catch (err) {
setError('发送验证码失败,请稍后重试');
toast.error('发送验证码失败');
} finally {
setSendingCode(false);
}
};
// 处理注册
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess('');
// 验证所有字段
const usernameError = validateUsername(form.username);
const passwordError = validatePassword(form.password);
const confirmPasswordError = validateConfirmPassword(form.confirmPassword, form.password);
const phoneError = validatePhone(form.phone);
const realNameError = validateRealName(form.realName);
setValidation({
username: usernameError,
password: passwordError,
confirmPassword: confirmPasswordError,
phone: phoneError,
realName: realNameError,
});
if (usernameError || passwordError || confirmPasswordError || phoneError || realNameError) {
setError('请检查表单填写是否正确');
return;
}
if (!form.code) {
setError('请输入短信验证码');
return;
}
if (!form.captcha) {
setError('请输入图形验证码');
return;
}
setLoading(true);
try {
const result = await registerUser({
username: form.username,
password: form.password,
phone: form.phone,
code: form.code,
realName: form.realName,
email: form.email,
enterpriseId: form.enterpriseId,
captcha: form.captcha,
});
if (result.success && result.user) {
setSuccess('注册成功!正在为您自动登录...');
toast.success('注册成功!');
// 自动登录
setTimeout(() => {
login(result.user!);
}, 1500);
} else {
setError(result.message);
toast.error(result.message);
}
} catch (err) {
setError('注册失败,请稍后重试');
toast.error('注册失败');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-blue-50 to-cyan-50 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Logo和标题 */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-600 rounded-2xl mb-4">
<Shield className="w-8 h-8 text-white" />
</div>
<h1 className="text-green-900 mb-2"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
<Card className="p-8 shadow-xl">
{error && (
<Alert className="mb-4 bg-red-50 border-red-200">
<AlertCircle className="w-4 h-4 text-red-600" />
<AlertDescription className="text-red-700">{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="mb-4 bg-green-50 border-green-200">
<CheckCircle className="w-4 h-4 text-green-600" />
<AlertDescription className="text-green-700">{success}</AlertDescription>
</Alert>
)}
<form onSubmit={handleRegister} className="space-y-4">
{/* 用户名 */}
<div>
<Label></Label>
<div className="relative mt-2">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="请输入用户名(字母、数字、下划线)"
value={form.username}
onChange={(e) => {
setForm({ ...form, username: e.target.value });
setValidation({ ...validation, username: '' });
}}
onBlur={(e) => setValidation({ ...validation, username: validateUsername(e.target.value) })}
className="pl-10"
disabled={loading}
/>
</div>
{validation.username && (
<p className="text-xs text-red-600 mt-1">{validation.username}</p>
)}
</div>
{/* 真实姓名 */}
<div>
<Label></Label>
<div className="relative mt-2">
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="请输入真实姓名"
value={form.realName}
onChange={(e) => {
setForm({ ...form, realName: e.target.value });
setValidation({ ...validation, realName: '' });
}}
onBlur={(e) => setValidation({ ...validation, realName: validateRealName(e.target.value) })}
className="pl-10"
disabled={loading}
/>
</div>
{validation.realName && (
<p className="text-xs text-red-600 mt-1">{validation.realName}</p>
)}
</div>
{/* 手机号 */}
<div>
<Label></Label>
<div className="relative mt-2">
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="tel"
placeholder="请输入手机号"
value={form.phone}
onChange={(e) => {
setForm({ ...form, phone: e.target.value });
setValidation({ ...validation, phone: '' });
}}
onBlur={(e) => setValidation({ ...validation, phone: validatePhone(e.target.value) })}
className="pl-10"
maxLength={11}
disabled={loading}
/>
</div>
{validation.phone && (
<p className="text-xs text-red-600 mt-1">{validation.phone}</p>
)}
</div>
{/* 短信验证码 */}
<div>
<Label></Label>
<div className="flex gap-2 mt-2">
<div className="relative flex-1">
<MessageSquare className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="text"
placeholder="请输入验证码"
value={form.code}
onChange={(e) => setForm({ ...form, code: e.target.value })}
className="pl-10"
maxLength={6}
disabled={loading}
/>
</div>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={sendingCode || countdown > 0 || loading}
className="min-w-[120px]"
>
{sendingCode ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : countdown > 0 ? (
`${countdown}秒后重试`
) : (
'发送验证码'
)}
</Button>
</div>
</div>
{/* 所属企业 */}
<div>
<Label></Label>
<Select
value={form.enterpriseId}
onValueChange={(value) => setForm({ ...form, enterpriseId: value })}
disabled={loading}
>
<SelectTrigger className="mt-2">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-muted-foreground" />
<SelectValue placeholder="请选择所属企业" />
</div>
</SelectTrigger>
<SelectContent>
{enterprises.map((enterprise) => (
<SelectItem key={enterprise.id} value={enterprise.id}>
<div className="flex flex-col">
<span>{enterprise.name}</span>
<span className="text-xs text-muted-foreground">
{enterprise.code} · {enterprise.type}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
{!form.enterpriseId && (
<p className="text-xs text-muted-foreground mt-1">
</p>
)}
</div>
{/* 邮箱(可选) */}
<div>
<Label></Label>
<div className="relative mt-2">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type="email"
placeholder="请输入邮箱"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
className="pl-10"
disabled={loading}
/>
</div>
</div>
{/* 密码 */}
<div>
<Label></Label>
<div className="relative mt-2">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type={showPassword ? 'text' : 'password'}
placeholder="请输入密码至少6位"
value={form.password}
onChange={(e) => {
setForm({ ...form, password: e.target.value });
setValidation({ ...validation, password: '' });
}}
onBlur={(e) => setValidation({ ...validation, password: validatePassword(e.target.value) })}
className="pl-10 pr-10"
disabled={loading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
{validation.password && (
<p className="text-xs text-red-600 mt-1">{validation.password}</p>
)}
</div>
{/* 确认密码 */}
<div>
<Label></Label>
<div className="relative mt-2">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
type={showConfirmPassword ? 'text' : 'password'}
placeholder="请再次输入密码"
value={form.confirmPassword}
onChange={(e) => {
setForm({ ...form, confirmPassword: e.target.value });
setValidation({ ...validation, confirmPassword: '' });
}}
onBlur={(e) => setValidation({ ...validation, confirmPassword: validateConfirmPassword(e.target.value, form.password) })}
className="pl-10 pr-10"
disabled={loading}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
{validation.confirmPassword && (
<p className="text-xs text-red-600 mt-1">{validation.confirmPassword}</p>
)}
</div>
{/* 图形验证码 */}
<div>
<Label></Label>
<CaptchaInput
value={form.captcha}
onChange={(value) => setForm({ ...form, captcha: value })}
className="mt-2"
/>
</div>
{/* 注册按钮 */}
<Button type="submit" className="w-full bg-green-600 hover:bg-green-700" disabled={loading}>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<UserPlus className="w-4 h-4 mr-2" />
</>
)}
</Button>
</form>
{/* 返回登录 */}
<div className="mt-6 pt-6 border-t text-center">
<Button variant="ghost" onClick={onBackToLogin} className="text-green-600 hover:text-green-700">
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 提示信息 */}
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<div className="flex items-start gap-2">
<Shield className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-xs text-blue-900">
<p className="mb-1"></p>
<ul className="space-y-0.5">
<li> </li>
<li> </li>
<li> 6</li>
<li> 123456</li>
<li> </li>
</ul>
</div>
</div>
</div>
</Card>
{/* 页脚 */}
<div className="mt-6 text-center text-xs text-muted-foreground">
<p>© 2024 . All rights reserved.</p>
</div>
</div>
</div>
);
}