生产管理系统 - 登录,二维码功能集成

This commit is contained in:
2025-10-31 11:49:11 +08:00
parent 2fa64e66c9
commit 46ff61eaed
11 changed files with 1867 additions and 185 deletions

View File

@@ -1,15 +1,16 @@
import { ReactNode } from 'react'
import { AuthProvider } from '@/components/auth/AuthContext'
import '@/styles/globals.css'
export default function AuthLayout({
children,
}: {
children: ReactNode
}) {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-blue-50 to-purple-50 flex items-center justify-center">
<div className="w-full max-w-md">
<AuthProvider>
<div className="min-h-screen relative">
{children}
</div>
</div>
</AuthProvider>
)
}

View File

@@ -0,0 +1,387 @@
/**
* filekorolheader: 登录表单组件 - 用户密码登录和手机号登录功能
* 功能:密码登录、手机号登录、表单验证、验证码处理
* 路径:/login/components/LoginForm.tsx
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui语义化样式支持暗色主题
*/
'use client';
import React, { useEffect, useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CaptchaInput } from '@/components/auth/CaptchaInput';
import {
Lock,
User,
Phone,
MessageSquare,
Eye,
EyeOff,
LogIn,
Loader2,
Smartphone,
} from 'lucide-react';
import { toast } from 'sonner';
import { useAuth } from '@/components/auth/AuthContext';
import { authReducer, initialAuthState, AuthState, AuthAction } from './authReducer';
import { loginApiV1AuthLoginPost } from '@/lib/api/sdk.gen';
import type { CaptchaResponse } from '@/lib/api/types.gen';
interface LoginFormProps {
onRegisterClick: () => void;
}
export function LoginForm({ onRegisterClick }: LoginFormProps) {
const { login } = useAuth();
const [state, dispatch] = React.useReducer(authReducer, initialAuthState);
const [loginType, setLoginType] = useState<'password' | 'phone'>('password');
const [passwordCaptchaData, setPasswordCaptchaData] = useState<CaptchaResponse | null>(null);
const [phoneCaptchaData, setPhoneCaptchaData] = useState<CaptchaResponse | null>(null);
// 倒计时效果
useEffect(() => {
if (state.countdown > 0) {
const timer = setTimeout(() => {
dispatch({ type: 'SET_COUNTDOWN', payload: state.countdown - 1 });
}, 1000);
return () => clearTimeout(timer);
}
}, [state.countdown]);
// 发送验证码
const handleSendCode = async () => {
if (!state.phoneForm.phone) {
toast.error('请输入手机号');
return;
}
if (!/^1[3-9]\d{9}$/.test(state.phoneForm.phone)) {
toast.error('请输入正确的手机号');
return;
}
dispatch({ type: 'SET_SENDING_CODE', payload: true });
dispatch({ type: 'SET_ERROR', payload: '' });
try {
// 模拟发送验证码
await new Promise(resolve => setTimeout(resolve, 1000));
toast.success('验证码发送成功测试验证码123456');
dispatch({ type: 'SET_COUNTDOWN', payload: 60 });
} catch (err) {
dispatch({ type: 'SET_ERROR', payload: '发送验证码失败,请稍后重试' });
toast.error('发送验证码失败');
} finally {
dispatch({ type: 'SET_SENDING_CODE', payload: false });
}
};
// 密码登录
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: 'SET_ERROR', payload: '' });
if (!state.passwordForm.username || !state.passwordForm.password) {
dispatch({ type: 'SET_ERROR', payload: '请输入用户名和密码' });
toast.error('请输入用户名和密码');
return;
}
if (!state.passwordForm.captcha) {
dispatch({ type: 'SET_ERROR', payload: '请输入图形验证码' });
toast.error('请输入图形验证码');
return;
}
if (!passwordCaptchaData?.captcha_id) {
dispatch({ type: 'SET_ERROR', payload: '验证码数据未获取,请刷新验证码' });
toast.error('验证码数据未获取,请刷新验证码');
return;
}
dispatch({ type: 'SET_LOADING', payload: true });
try {
const response = await loginApiV1AuthLoginPost({
body: {
identifier: state.passwordForm.username,
password: state.passwordForm.password,
captcha_id: passwordCaptchaData.captcha_id,
captcha_text: state.passwordForm.captcha,
},
});
if (response.data) {
// 登录成功,提取用户信息
const userData = {
id: response.data.user_id || '1',
username: state.passwordForm.username,
realName: response.data.real_name || state.passwordForm.username,
phone: response.data.phone || '',
email: response.data.email || '',
role: response.data.role || 'user',
permissions: response.data.permissions || [],
enterpriseId: response.data.enterprise_id || '',
enterpriseName: response.data.enterprise_name || '',
createdAt: response.data.created_at || new Date().toISOString(),
};
// 打印登录成功日志
console.log('🎉 登录成功!', {
user: userData,
apiResponse: response.data,
timestamp: new Date().toISOString()
});
login(userData);
toast.success('登录成功!');
// 暂时不实现页面跳转
console.log('✅ 登录流程完成,等待后续页面跳转实现');
} else {
dispatch({ type: 'SET_ERROR', payload: '登录失败,请检查用户名和密码' });
toast.error('登录失败,请检查用户名和密码');
}
} catch (err: any) {
console.error('登录失败:', err);
const errorMessage = err?.response?.data?.message || err?.message || '登录失败,请稍后重试';
dispatch({ type: 'SET_ERROR', payload: errorMessage });
toast.error(errorMessage);
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
// 手机号登录
const handlePhoneLogin = async (e: React.FormEvent) => {
e.preventDefault();
dispatch({ type: 'SET_ERROR', payload: '' });
if (!state.phoneForm.phone || !state.phoneForm.code) {
dispatch({ type: 'SET_ERROR', payload: '请输入手机号和验证码' });
return;
}
if (!state.phoneForm.captcha) {
dispatch({ type: 'SET_ERROR', payload: '请输入图形验证码' });
return;
}
dispatch({ type: 'SET_LOADING', payload: true });
try {
// 模拟手机号登录
await new Promise(resolve => setTimeout(resolve, 1000));
if (state.phoneForm.code === '123456') {
login({
id: '2',
username: 'user_' + state.phoneForm.phone.slice(-4),
realName: '用户',
phone: state.phoneForm.phone,
email: '',
role: 'user',
permissions: [],
enterpriseId: '',
enterpriseName: '',
createdAt: new Date().toISOString(),
});
toast.success('登录成功!');
window.location.href = '/';
} else {
dispatch({ type: 'SET_ERROR', payload: '验证码错误' });
toast.error('验证码错误');
}
} catch (err) {
dispatch({ type: 'SET_ERROR', payload: '登录失败,请稍后重试' });
toast.error('登录失败');
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
return (
<Card className="p-8 shadow-2xl bg-white/95 backdrop-blur-md border-white/20">
<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>
{state.error && (
<div className="mb-4 p-3 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-700 dark:text-red-300">{state.error}</p>
</div>
)}
{/* 密码登录 */}
<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={state.passwordForm.username}
onChange={(e) => dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { username: e.target.value } })}
className="pl-10"
disabled={state.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={state.showPassword ? 'text' : 'password'}
placeholder="请输入密码"
value={state.passwordForm.password}
onChange={(e) => dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { password: e.target.value } })}
className="pl-10 pr-10"
disabled={state.loading}
/>
<button
type="button"
onClick={() => dispatch({ type: 'TOGGLE_PASSWORD_VISIBILITY' })}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{state.showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div>
<Label></Label>
<CaptchaInput
value={state.passwordForm.captcha}
onChange={(value) => dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { captcha: value } })}
onCaptchaChange={(captchaData) => setPasswordCaptchaData(captchaData)}
className="mt-2"
/>
</div>
<Button type="submit" className="w-full bg-green-600 hover:bg-green-700" disabled={state.loading}>
{state.loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
</form>
</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={state.phoneForm.phone}
onChange={(e) => dispatch({ type: 'UPDATE_PHONE_FORM', payload: { phone: e.target.value } })}
className="pl-10"
maxLength={11}
disabled={state.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={state.phoneForm.code}
onChange={(e) => dispatch({ type: 'UPDATE_PHONE_FORM', payload: { code: e.target.value } })}
className="pl-10"
maxLength={6}
disabled={state.loading}
/>
</div>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={state.sendingCode || state.countdown > 0 || state.loading}
className="min-w-[120px]"
>
{state.sendingCode ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : state.countdown > 0 ? (
`${state.countdown}秒后重试`
) : (
'发送验证码'
)}
</Button>
</div>
</div>
<div>
<Label></Label>
<CaptchaInput
value={state.phoneForm.captcha}
onChange={(value) => dispatch({ type: 'UPDATE_PHONE_FORM', payload: { captcha: value } })}
className="mt-2"
/>
</div>
<Button type="submit" className="w-full bg-green-600 hover:bg-green-700" disabled={state.loading}>
{state.loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
...
</>
) : (
<>
<LogIn className="w-4 h-4 mr-2" />
</>
)}
</Button>
</form>
</TabsContent>
</Tabs>
{/* 注册跳转 */}
<div className="mt-6 text-center">
<p className="text-sm text-muted-foreground">
<button
type="button"
onClick={onRegisterClick}
className="ml-1 text-green-600 hover:text-green-700 font-medium"
>
</button>
</p>
</div>
</Card>
);
}

View File

@@ -0,0 +1,161 @@
/**
* filekorolheader: 认证状态管理 - 登录注册页面状态管理核心
* 功能:状态管理、表单验证、倒计时管理、错误处理
* 路径:/login/components/authReducer.tsx
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理shadcn语义化样式
*/
'use client';
import { Enterprise } from '@/types/auth';
// 状态接口定义
export interface AuthState {
loading: boolean;
showPassword: boolean;
showConfirmPassword: boolean;
sendingCode: boolean;
countdown: number;
error: string;
success: string;
enterprises: Enterprise[];
passwordForm: {
username: string;
password: string;
captcha: string;
};
phoneForm: {
phone: string;
code: string;
captcha: string;
};
registerForm: {
username: string;
password: string;
confirmPassword: string;
phone: string;
code: string;
realName: string;
email: string;
enterpriseId: string;
captcha: string;
};
validation: {
username: string;
password: string;
confirmPassword: string;
phone: string;
realName: string;
};
}
// Action类型定义
export type AuthAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'TOGGLE_PASSWORD_VISIBILITY' }
| { type: 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY' }
| { type: 'SET_SENDING_CODE'; payload: boolean }
| { type: 'SET_COUNTDOWN'; payload: number }
| { type: 'SET_ERROR'; payload: string }
| { type: 'SET_SUCCESS'; payload: string }
| { type: 'SET_ENTERPRISES'; payload: Enterprise[] }
| { type: 'UPDATE_PASSWORD_FORM'; payload: Partial<AuthState['passwordForm']> }
| { type: 'UPDATE_PHONE_FORM'; payload: Partial<AuthState['phoneForm']> }
| { type: 'UPDATE_REGISTER_FORM'; payload: Partial<AuthState['registerForm']> }
| { type: 'UPDATE_VALIDATION'; payload: Partial<AuthState['validation']> }
| { type: 'CLEAR_VALIDATION' }
| { type: 'RESET_FORMS' };
// Reducer函数
export function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'TOGGLE_PASSWORD_VISIBILITY':
return { ...state, showPassword: !state.showPassword };
case 'TOGGLE_CONFIRM_PASSWORD_VISIBILITY':
return { ...state, showConfirmPassword: !state.showConfirmPassword };
case 'SET_SENDING_CODE':
return { ...state, sendingCode: action.payload };
case 'SET_COUNTDOWN':
return { ...state, countdown: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, success: '' };
case 'SET_SUCCESS':
return { ...state, success: action.payload, error: '' };
case 'SET_ENTERPRISES':
return { ...state, enterprises: action.payload };
case 'UPDATE_PASSWORD_FORM':
return { ...state, passwordForm: { ...state.passwordForm, ...action.payload } };
case 'UPDATE_PHONE_FORM':
return { ...state, phoneForm: { ...state.phoneForm, ...action.payload } };
case 'UPDATE_REGISTER_FORM':
return { ...state, registerForm: { ...state.registerForm, ...action.payload } };
case 'UPDATE_VALIDATION':
return { ...state, validation: { ...state.validation, ...action.payload } };
case 'CLEAR_VALIDATION':
return { ...state, validation: { username: '', password: '', confirmPassword: '', phone: '', realName: '' } };
case 'RESET_FORMS':
return {
...state,
passwordForm: { username: '', password: '', captcha: '' },
phoneForm: { phone: '', code: '', captcha: '' },
registerForm: {
username: '',
password: '',
confirmPassword: '',
phone: '',
code: '',
realName: '',
email: '',
enterpriseId: '',
captcha: '',
},
validation: { username: '', password: '', confirmPassword: '', phone: '', realName: '' },
error: '',
success: '',
};
default:
return state;
}
}
// 初始状态
export const initialAuthState: AuthState = {
loading: false,
showPassword: false,
showConfirmPassword: false,
sendingCode: false,
countdown: 0,
error: '',
success: '',
enterprises: [],
passwordForm: {
username: '',
password: '',
captcha: '',
},
phoneForm: {
phone: '',
code: '',
captcha: '',
},
registerForm: {
username: '',
password: '',
confirmPassword: '',
phone: '',
code: '',
realName: '',
email: '',
enterpriseId: '',
captcha: '',
},
validation: {
username: '',
password: '',
confirmPassword: '',
phone: '',
realName: '',
},
};

View File

@@ -1,84 +1,61 @@
import { Metadata } from 'next'
import Link from 'next/link'
/**
* filekorolheader: 用户登录页面 - 统一身份认证入口
* 功能:用户密码登录、手机号登录、表单验证、图形验证码
* 路径:/login
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui语义化样式支持暗色主题
*/
export const metadata: Metadata = {
title: '登录 - Crop-X 智慧农业管理系统',
description: '用户登录页面',
}
'use client';
import React from 'react';
import { SmartFieldBackground } from '@/components/auth/SmartFieldBackground';
import { Shield } from 'lucide-react';
import { LoginForm } from './components/LoginForm';
export default function LoginPage() {
const handleRegisterClick = () => {
window.location.href = '/register';
};
return (
<div className="bg-white rounded-lg shadow-xl p-8">
<div className="text-center mb-8">
<div className="text-4xl mb-4">🌱</div>
<h1 className="text-2xl font-bold text-gray-800 mb-2">
Crop-X
</h1>
<p className="text-gray-600">
</p>
</div>
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
{/* 智慧大田动态背景 */}
<SmartFieldBackground />
<form className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="请输入用户名"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="请输入密码"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-700">
</label>
{/* 内容区域 */}
<div className="w-full max-w-md relative z-10">
{/* Logo和标题 */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-white/90 backdrop-blur-sm rounded-2xl mb-4 shadow-lg">
<Shield className="w-8 h-8 text-green-600" />
</div>
<a href="#" className="text-sm text-green-600 hover:text-green-500">
</a>
<h1 className="text-white mb-2 drop-shadow-lg"></h1>
<p className="text-sm text-white/90 drop-shadow-md"></p>
</div>
<button
type="submit"
className="w-full py-2 px-4 bg-green-600 hover:bg-green-700 rounded-md text-white font-medium transition-colors"
>
</button>
</form>
{/* 登录表单 */}
<LoginForm onRegisterClick={handleRegisterClick} />
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
{' '}
<Link href="/register" className="text-green-600 hover:text-green-500 font-medium">
</Link>
</p>
</div>
{/* 安全提示 */}
<div className="mt-6 p-3 bg-green-50 dark:bg-green-950 rounded-lg">
<div className="flex items-start gap-2">
<Shield className="w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5" />
<div className="text-xs text-green-900 dark:text-green-100">
<p className="mb-1"></p>
<ul className="space-y-0.5">
<li> </li>
<li> 24</li>
<li> IP</li>
</ul>
</div>
</div>
</div>
<div className="mt-8 pt-6 border-t border-gray-200">
<p className="text-xs text-gray-500 text-center">
© 2024 Crop-X. All rights reserved.
</p>
{/* 页脚 */}
<div className="mt-6 text-center text-xs text-white/80 drop-shadow-md">
<p>© 2024 . All rights reserved.</p>
</div>
</div>
</div>
)
);
}

View File

@@ -0,0 +1,17 @@
import { ReactNode } from 'react'
import { AuthProvider } from '@/components/auth/AuthContext'
import '@/styles/globals.css'
export default function RegisterLayout({
children,
}: {
children: ReactNode
}) {
return (
<AuthProvider>
<div className="min-h-screen relative">
{children}
</div>
</AuthProvider>
)
}

View File

@@ -1,121 +1,550 @@
import { Metadata } from 'next'
import Link from 'next/link'
/**
* filekorolheader: 用户注册页面 - 新用户注册功能入口
* 功能:用户注册、表单验证、企业选择、短信验证
* 路径:/register
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui语义化样式支持暗色主题
*/
export const metadata: Metadata = {
title: '注册 - Crop-X 智慧农业管理系统',
description: '用户注册页面',
}
'use client';
import React, { useEffect, useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { CaptchaInput } from '@/components/auth/CaptchaInput';
import { useAuth } from '@/components/auth/AuthContext';
import { SmartFieldBackground } from '@/components/auth/SmartFieldBackground';
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';
export default function RegisterPage() {
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: '',
});
const [validation, setValidation] = useState({
username: '',
password: '',
confirmPassword: '',
phone: '',
realName: '',
});
// 加载企业列表
useEffect(() => {
const mockEnterprises: Enterprise[] = [
{
id: '1',
name: '智慧农业示范农场',
code: 'DEMO001',
type: '示范农场',
status: 'active',
createdAt: new Date().toISOString(),
},
{
id: '2',
name: '绿色生态农业基地',
code: 'ECO002',
type: '生态农场',
status: 'active',
createdAt: new Date().toISOString(),
},
{
id: '3',
name: '科技农业产业园',
code: 'TECH003',
type: '产业园区',
status: 'active',
createdAt: new Date().toISOString(),
},
];
setEnterprises(mockEnterprises);
}, []);
// 倒计时效果
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => {
setCountdown(prev => prev - 1);
}, 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
// 验证函数
const validateUsername = (username: string): 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): string => {
if (!password) return '请输入密码';
if (password.length < 6) return '密码至少6个字符';
if (password.length > 20) return '密码最多20个字符';
return '';
};
const validateConfirmPassword = (confirmPassword: string, password: string): string => {
if (!confirmPassword) return '请确认密码';
if (confirmPassword !== password) return '两次密码输入不一致';
return '';
};
const validatePhone = (phone: string): string => {
if (!phone) return '请输入手机号';
if (!/^1[3-9]\d{9}$/.test(phone)) return '请输入正确的手机号';
return '';
};
const validateRealName = (realName: string): 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(prev => ({ ...prev, phone: phoneError }));
toast.error(phoneError);
return;
}
setSendingCode(true);
setError('');
try {
// 模拟发送验证码
await new Promise(resolve => setTimeout(resolve, 1000));
toast.success('验证码发送成功测试验证码123456');
setCountdown(60);
} 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 {
// 模拟注册
await new Promise(resolve => setTimeout(resolve, 1500));
if (form.code === '123456') {
setSuccess('注册成功!正在为您自动登录...');
toast.success('注册成功!');
// 自动登录
setTimeout(() => {
login({
id: '3',
username: form.username,
realName: form.realName,
email: form.email,
phone: form.phone,
role: 'user',
permissions: [],
enterpriseId: form.enterpriseId,
enterpriseName: enterprises.find(e => e.id === form.enterpriseId)?.name || '',
createdAt: new Date().toISOString(),
});
window.location.href = '/';
}, 1500);
} else {
setError('短信验证码错误');
toast.error('短信验证码错误');
}
} catch (err) {
setError('注册失败,请稍后重试');
toast.error('注册失败');
} finally {
setLoading(false);
}
};
return (
<div className="bg-white rounded-lg shadow-xl p-8">
<div className="text-center mb-8">
<div className="text-4xl mb-4">🌱</div>
<h1 className="text-2xl font-bold text-gray-800 mb-2">
Crop-X
</h1>
<p className="text-gray-600">
</p>
</div>
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
{/* 智慧大田动态背景 */}
<SmartFieldBackground />
<form className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="请输入姓名"
/>
{/* 内容区域 */}
<div className="w-full max-w-md relative z-10">
{/* Logo和标题 */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-white/90 backdrop-blur-sm rounded-2xl mb-4 shadow-lg">
<Shield className="w-8 h-8 text-green-600" />
</div>
<h1 className="text-white mb-2 drop-shadow-lg"></h1>
<p className="text-sm text-white/90 drop-shadow-md"></p>
</div>
<Card className="p-8 shadow-2xl bg-white/95 backdrop-blur-md border-white/20">
{/* 返回登录 */}
<div className="mb-6">
<button
type="button"
onClick={() => window.location.href = '/login'}
className="flex items-center text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="w-4 h-4 mr-2" />
</button>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="请输入邮箱"
/>
{error && (
<Alert className="mb-4 bg-red-50 dark:bg-red-950 border-red-200 dark:border-red-800">
<AlertCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
<AlertDescription className="text-red-700 dark:text-red-300">{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="mb-4 bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-green-700 dark:text-green-300">{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>
</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-4 p-3 bg-blue-50 dark:bg-blue-950 rounded-lg">
<div className="flex items-start gap-2">
<Shield className="w-4 h-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-xs text-blue-900 dark:text-blue-100">
<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-white/80 drop-shadow-md">
<p>© 2024 . All rights reserved.</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="请输入用户名"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="请输入密码"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="password"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
placeholder="请再次输入密码"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-700">
{' '}
<a href="#" className="text-green-600 hover:text-green-500">
</a>{' '}
{' '}
<a href="#" className="text-green-600 hover:text-green-500">
</a>
</label>
</div>
<button
type="submit"
className="w-full py-2 px-4 bg-green-600 hover:bg-green-700 rounded-md text-white font-medium transition-colors"
>
</button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
{' '}
<Link href="/login" className="text-green-600 hover:text-green-500 font-medium">
</Link>
</p>
</div>
<div className="mt-8 pt-6 border-t border-gray-200">
<p className="text-xs text-gray-500 text-center">
© 2024 Crop-X. All rights reserved.
</p>
</div>
</div>
)
);
}