diff --git a/crop-x/next.config.js b/crop-x/next.config.js index ca84e3a..ee9ebcc 100644 --- a/crop-x/next.config.js +++ b/crop-x/next.config.js @@ -23,6 +23,15 @@ const nextConfig = { }, // 解决工作区根目录问题 outputFileTracingRoot: process.cwd(), + // 添加代理配置解决CORS问题 + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com/api/:path*', + }, + ]; + }, }; export default nextConfig; \ No newline at end of file diff --git a/crop-x/src/app/(auth)/layout.tsx b/crop-x/src/app/(auth)/layout.tsx index 39fe322..1e6e87a 100644 --- a/crop-x/src/app/(auth)/layout.tsx +++ b/crop-x/src/app/(auth)/layout.tsx @@ -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 ( -
-
+ +
{children}
-
+ ) } \ No newline at end of file diff --git a/crop-x/src/app/(auth)/login/components/LoginForm.tsx b/crop-x/src/app/(auth)/login/components/LoginForm.tsx new file mode 100644 index 0000000..f8cbcc0 --- /dev/null +++ b/crop-x/src/app/(auth)/login/components/LoginForm.tsx @@ -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(null); + const [phoneCaptchaData, setPhoneCaptchaData] = useState(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 ( + + setLoginType(v as 'password' | 'phone')}> + + + + 密码登录 + + + + 手机登录 + + + + {state.error && ( +
+

{state.error}

+
+ )} + + {/* 密码登录 */} + +
+
+ +
+ + dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { username: e.target.value } })} + className="pl-10" + disabled={state.loading} + /> +
+
+ +
+ +
+ + dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { password: e.target.value } })} + className="pl-10 pr-10" + disabled={state.loading} + /> + +
+
+ +
+ + dispatch({ type: 'UPDATE_PASSWORD_FORM', payload: { captcha: value } })} + onCaptchaChange={(captchaData) => setPasswordCaptchaData(captchaData)} + className="mt-2" + /> +
+ + +
+ +
+ + {/* 手机号登录 */} + +
+
+ +
+ + dispatch({ type: 'UPDATE_PHONE_FORM', payload: { phone: e.target.value } })} + className="pl-10" + maxLength={11} + disabled={state.loading} + /> +
+
+ +
+ +
+
+ + dispatch({ type: 'UPDATE_PHONE_FORM', payload: { code: e.target.value } })} + className="pl-10" + maxLength={6} + disabled={state.loading} + /> +
+ +
+
+ +
+ + dispatch({ type: 'UPDATE_PHONE_FORM', payload: { captcha: value } })} + className="mt-2" + /> +
+ + +
+ +
+
+ + {/* 注册跳转 */} +
+

+ 还没有账号? + +

+
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/app/(auth)/login/components/authReducer.tsx b/crop-x/src/app/(auth)/login/components/authReducer.tsx new file mode 100644 index 0000000..804f922 --- /dev/null +++ b/crop-x/src/app/(auth)/login/components/authReducer.tsx @@ -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 } + | { type: 'UPDATE_PHONE_FORM'; payload: Partial } + | { type: 'UPDATE_REGISTER_FORM'; payload: Partial } + | { type: 'UPDATE_VALIDATION'; payload: Partial } + | { 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: '', + }, +}; \ No newline at end of file diff --git a/crop-x/src/app/(auth)/login/page.tsx b/crop-x/src/app/(auth)/login/page.tsx index f45d758..b7fdc16 100644 --- a/crop-x/src/app/(auth)/login/page.tsx +++ b/crop-x/src/app/(auth)/login/page.tsx @@ -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 ( -
-
-
🌱
-

- Crop-X 智慧农业 -

-

- 登录您的账户 -

-
+
+ {/* 智慧大田动态背景 */} + -
-
- - -
- -
- - -
- -
-
- - + {/* 内容区域 */} +
+ {/* Logo和标题 */} +
+
+
- - 忘记密码? - +

智慧农业生产管理系统

+

安全、智能、高效的农业管理平台

- - + {/* 登录表单 */} + -
-

- 还没有账户?{' '} - - 立即注册 - -

-
+ {/* 安全提示 */} +
+
+ +
+

安全保障:

+
    +
  • • 登录信息加密传输
  • +
  • • 会话自动管理,24小时有效期
  • +
  • • 记录登录IP、设备信息用于安全审计
  • +
+
+
+
-
-

- © 2024 Crop-X. All rights reserved. -

+ {/* 页脚 */} +
+

© 2024 智慧农业生产管理系统. All rights reserved.

+
- ) + ); } \ No newline at end of file diff --git a/crop-x/src/app/(auth)/register/layout.tsx b/crop-x/src/app/(auth)/register/layout.tsx new file mode 100644 index 0000000..87d67ec --- /dev/null +++ b/crop-x/src/app/(auth)/register/layout.tsx @@ -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 ( + +
+ {children} +
+
+ ) +} \ No newline at end of file diff --git a/crop-x/src/app/(auth)/register/page.tsx b/crop-x/src/app/(auth)/register/page.tsx index 2746a71..16e09f7 100644 --- a/crop-x/src/app/(auth)/register/page.tsx +++ b/crop-x/src/app/(auth)/register/page.tsx @@ -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([]); + + 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 ( -
-
-
🌱
-

- Crop-X 智慧农业 -

-

- 创建您的账户 -

-
+
+ {/* 智慧大田动态背景 */} + -
-
-
- - + {/* 内容区域 */} +
+ {/* Logo和标题 */} +
+
+ +
+

智慧农业生产管理系统

+

安全、智能、高效的农业管理平台

+
+ + + {/* 返回登录 */} +
+
-
- - + {error && ( + + + {error} + + )} + + {success && ( + + + {success} + + )} + + + {/* 用户名 */} +
+ +
+ + { + setForm({ ...form, username: e.target.value }); + setValidation({ ...validation, username: '' }); + }} + onBlur={(e) => setValidation({ ...validation, username: validateUsername(e.target.value) })} + className="pl-10" + disabled={loading} + /> +
+ {validation.username && ( +

{validation.username}

+ )} +
+ + {/* 真实姓名 */} +
+ +
+ + { + setForm({ ...form, realName: e.target.value }); + setValidation({ ...validation, realName: '' }); + }} + onBlur={(e) => setValidation({ ...validation, realName: validateRealName(e.target.value) })} + className="pl-10" + disabled={loading} + /> +
+ {validation.realName && ( +

{validation.realName}

+ )} +
+ + {/* 手机号 */} +
+ +
+ + { + 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} + /> +
+ {validation.phone && ( +

{validation.phone}

+ )} +
+ + {/* 短信验证码 */} +
+ +
+
+ + setForm({ ...form, code: e.target.value })} + className="pl-10" + maxLength={6} + disabled={loading} + /> +
+ +
+
+ + {/* 所属企业 */} +
+ + +
+ + {/* 邮箱(可选) */} +
+ +
+ + setForm({ ...form, email: e.target.value })} + className="pl-10" + disabled={loading} + /> +
+
+ + {/* 密码 */} +
+ +
+ + { + 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} + /> + +
+ {validation.password && ( +

{validation.password}

+ )} +
+ + {/* 确认密码 */} +
+ +
+ + { + 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} + /> + +
+ {validation.confirmPassword && ( +

{validation.confirmPassword}

+ )} +
+ + {/* 图形验证码 */} +
+ + setForm({ ...form, captcha: value })} + className="mt-2" + /> +
+ + {/* 注册按钮 */} + + + + {/* 提示信息 */} +
+
+ +
+

注册说明:

+
    +
  • • 请先选择所属企业或农场
  • +
  • • 用户名和手机号必须唯一
  • +
  • • 密码至少6位,建议包含字母和数字
  • +
  • • 测试验证码:123456
  • +
  • • 注册成功后自动登录系统
  • +
+
+
+ + + {/* 页脚 */} +
+

© 2024 智慧农业生产管理系统. All rights reserved.

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - -
-

- 已有账户?{' '} - - 立即登录 - -

-
- -
-

- © 2024 Crop-X. All rights reserved. -

- ) + ); } \ No newline at end of file diff --git a/crop-x/src/components/auth/AuthContext.tsx b/crop-x/src/components/auth/AuthContext.tsx new file mode 100644 index 0000000..280973e --- /dev/null +++ b/crop-x/src/components/auth/AuthContext.tsx @@ -0,0 +1,70 @@ +'use client'; + +import React, { createContext, useContext, useState, ReactNode } from 'react'; + +interface User { + id: string; + username: string; + realName: string; + email?: string; + phone?: string; + enterpriseId?: string; +} + +interface AuthContextType { + user: User | null; + login: (user: User) => void; + logout: () => void; + isAuthenticated: boolean; +} + +const AuthContext = createContext(undefined); + +interface AuthProviderProps { + children: ReactNode; +} + +export function AuthProvider({ children }: AuthProviderProps) { + const [user, setUser] = useState(null); + + const login = (userData: User) => { + setUser(userData); + // 存储到 localStorage + localStorage.setItem('user', JSON.stringify(userData)); + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('user'); + }; + + // 初始化时检查 localStorage + React.useEffect(() => { + const storedUser = localStorage.getItem('user'); + if (storedUser) { + try { + setUser(JSON.parse(storedUser)); + } catch (error) { + console.error('Failed to parse stored user data:', error); + localStorage.removeItem('user'); + } + } + }, []); + + const value: AuthContextType = { + user, + login, + logout, + isAuthenticated: !!user, + }; + + return {children}; +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} \ No newline at end of file diff --git a/crop-x/src/components/auth/CaptchaInput.tsx b/crop-x/src/components/auth/CaptchaInput.tsx new file mode 100644 index 0000000..a64224d --- /dev/null +++ b/crop-x/src/components/auth/CaptchaInput.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { getCaptchaApiV1AuthCaptchaGet } from '@/lib/api/sdk.gen'; +import type { CaptchaResponse } from '@/lib/api/types.gen'; + +interface CaptchaInputProps { + value: string; + onChange: (value: string) => void; + onCaptchaChange?: (captchaData: CaptchaResponse | null) => void; + className?: string; +} + +export function CaptchaInput({ value, onChange, onCaptchaChange, className = '' }: CaptchaInputProps) { + const [captchaData, setCaptchaData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const fetchCaptcha = async () => { + setLoading(true); + setError(''); + onChange(''); // 清空验证码输入 + + try { + const response = await getCaptchaApiV1AuthCaptchaGet(); + console.log('API验证码获取成功:', response); + setCaptchaData(response.data); + if (onCaptchaChange) { + onCaptchaChange(response.data); + } + } catch (err) { + console.error('验证码获取失败:', err); + + // 如果API失败,使用备用验证码 + const fallbackCaptcha = generateFallbackCaptcha(); + console.log('生成备用验证码:', fallbackCaptcha); + setCaptchaData(fallbackCaptcha); + if (onCaptchaChange) { + onCaptchaChange(fallbackCaptcha); + } + setError(''); // 清除错误状态,因为备用验证码已生成 + } finally { + setLoading(false); + } + }; + + const generateFallbackCaptcha = (): CaptchaResponse => { + // 备用验证码生成(使用Canvas) + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + let text = ''; + for (let i = 0; i < 4; i++) { + text += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + const canvas = document.createElement('canvas'); + canvas.width = 120; + canvas.height = 40; + const ctx = canvas.getContext('2d'); + + if (!ctx) { + return { + captcha_id: 'fallback-' + Date.now(), + image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' + }; + } + + // 背景 + 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(); + } + + return { + captcha_id: 'fallback-' + Date.now(), + image: canvas.toDataURL() + }; + }; + + useEffect(() => { + fetchCaptcha(); + }, []); + + const handleRefresh = () => { + fetchCaptcha(); + }; + + const handleCaptchaChange = (inputValue: string) => { + onChange(inputValue); + }; + + return ( +
+ handleCaptchaChange(e.target.value.toUpperCase())} + maxLength={4} + className="flex-1" + disabled={loading} + /> +
+ {loading ? ( +
+
+
+ ) : error ? ( +
+ 获取失败 +
+ ) : ( +
+ {captchaData && ( + 验证码 { + // 如果图片加载失败,隐藏错误图片 + e.currentTarget.style.display = 'none'; + setError('图片加载失败'); + }} + /> + )} +
+ )} + +
+
+ ); +} \ No newline at end of file diff --git a/crop-x/src/components/auth/SmartFieldBackground.tsx b/crop-x/src/components/auth/SmartFieldBackground.tsx new file mode 100644 index 0000000..74221aa --- /dev/null +++ b/crop-x/src/components/auth/SmartFieldBackground.tsx @@ -0,0 +1,441 @@ +'use client'; + +import { useEffect, useRef } from 'react'; + +export function SmartFieldBackground() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // 设置canvas尺寸 + const resizeCanvas = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); + + // 田间传感器节点 + class SensorNode { + x: number; + y: number; + radius: number; + pulsePhase: number; + pulseSpeed: number; + connections: SensorNode[]; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + this.radius = 4; + this.pulsePhase = Math.random() * Math.PI * 2; + this.pulseSpeed = 0.03 + Math.random() * 0.02; + this.connections = []; + } + + update() { + this.pulsePhase += this.pulseSpeed; + } + + draw(ctx: CanvasRenderingContext2D) { + const pulse = Math.sin(this.pulsePhase) * 0.5 + 0.5; + + // 节点外圈 + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius + pulse * 4, 0, Math.PI * 2); + ctx.strokeStyle = `rgba(34, 197, 94, ${0.3 + pulse * 0.3})`; + ctx.lineWidth = 2; + ctx.stroke(); + + // 节点核心 + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + ctx.fillStyle = `rgba(34, 197, 94, ${0.8 + pulse * 0.2})`; + ctx.fill(); + + // 光晕 + const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius * 6); + gradient.addColorStop(0, `rgba(34, 197, 94, ${0.4 * pulse})`); + gradient.addColorStop(1, 'rgba(34, 197, 94, 0)'); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.radius * 6, 0, Math.PI * 2); + ctx.fill(); + } + + drawConnections(ctx: CanvasRenderingContext2D) { + this.connections.forEach(node => { + const distance = Math.hypot(node.x - this.x, node.y - this.y); + const opacity = Math.max(0, 1 - distance / 250); + + if (opacity > 0) { + ctx.beginPath(); + ctx.moveTo(this.x, this.y); + ctx.lineTo(node.x, node.y); + ctx.strokeStyle = `rgba(34, 197, 94, ${opacity * 0.2})`; + ctx.lineWidth = 1; + ctx.stroke(); + + // 数据流动效果 + const dataFlowPhase = (Date.now() / 1000) % 1; + const flowX = this.x + (node.x - this.x) * dataFlowPhase; + const flowY = this.y + (node.y - this.y) * dataFlowPhase; + + ctx.beginPath(); + ctx.arc(flowX, flowY, 2, 0, Math.PI * 2); + ctx.fillStyle = `rgba(34, 197, 94, ${opacity * 0.6})`; + ctx.fill(); + } + }); + } + } + + // 无人机 + class Drone { + x: number; + y: number; + targetX: number; + targetY: number; + speed: number; + size: number; + rotorPhase: number; + trail: { x: number; y: number; alpha: number }[]; + + constructor() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.targetX = this.x; + this.targetY = this.y; + this.speed = 1.5; + this.size = 12; + this.rotorPhase = 0; + this.trail = []; + this.setNewTarget(); + } + + setNewTarget() { + this.targetX = Math.random() * canvas.width; + this.targetY = Math.random() * canvas.height; + } + + update() { + const dx = this.targetX - this.x; + const dy = this.targetY - this.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 10) { + this.setNewTarget(); + } else { + this.x += (dx / distance) * this.speed; + this.y += (dy / distance) * this.speed; + } + + this.rotorPhase += 0.3; + + // 更新轨迹 + this.trail.push({ x: this.x, y: this.y, alpha: 1 }); + if (this.trail.length > 30) { + this.trail.shift(); + } + this.trail.forEach((point, index) => { + point.alpha = index / this.trail.length; + }); + } + + draw(ctx: CanvasRenderingContext2D) { + // 绘制轨迹 + this.trail.forEach((point, index) => { + if (index > 0) { + const prev = this.trail[index - 1]; + ctx.beginPath(); + ctx.moveTo(prev.x, prev.y); + ctx.lineTo(point.x, point.y); + ctx.strokeStyle = `rgba(59, 130, 246, ${point.alpha * 0.3})`; + ctx.lineWidth = 2; + ctx.stroke(); + } + }); + + // 无人机机身 + ctx.save(); + ctx.translate(this.x, this.y); + + // 机身 + ctx.fillStyle = 'rgba(59, 130, 246, 0.9)'; + ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size); + + // 四个螺旋桨 + const rotorPositions = [ + { x: -this.size * 0.7, y: -this.size * 0.7 }, + { x: this.size * 0.7, y: -this.size * 0.7 }, + { x: -this.size * 0.7, y: this.size * 0.7 }, + { x: this.size * 0.7, y: this.size * 0.7 }, + ]; + + rotorPositions.forEach(pos => { + ctx.save(); + ctx.translate(pos.x, pos.y); + ctx.rotate(this.rotorPhase); + + ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(-6, 0); + ctx.lineTo(6, 0); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, -6); + ctx.lineTo(0, 6); + ctx.stroke(); + + ctx.restore(); + }); + + // 扫描光束 + const scanRadius = 40; + ctx.beginPath(); + ctx.arc(0, 0, scanRadius, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(59, 130, 246, 0.3)'; + ctx.lineWidth = 1; + ctx.stroke(); + + ctx.restore(); + + // 数据上传指示 + if (Math.random() < 0.1) { + ctx.beginPath(); + ctx.arc(this.x, this.y, 3, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(34, 197, 94, 0.8)'; + ctx.fill(); + } + } + } + + // 田地波浪效果 + class FieldWave { + offset: number; + speed: number; + amplitude: number; + frequency: number; + y: number; + + constructor(y: number) { + this.offset = Math.random() * 1000; + this.speed = 0.5 + Math.random() * 0.5; + this.amplitude = 15 + Math.random() * 10; + this.frequency = 0.01 + Math.random() * 0.01; + this.y = y; + } + + update() { + this.offset += this.speed; + } + + draw(ctx: CanvasRenderingContext2D) { + ctx.beginPath(); + ctx.moveTo(0, this.y); + + for (let x = 0; x <= canvas.width; x += 5) { + const waveY = this.y + Math.sin((x + this.offset) * this.frequency) * this.amplitude; + ctx.lineTo(x, waveY); + } + + ctx.strokeStyle = 'rgba(34, 197, 94, 0.15)'; + ctx.lineWidth = 2; + ctx.stroke(); + } + } + + // 数据流粒子 + class DataParticle { + x: number; + y: number; + vx: number; + vy: number; + life: number; + maxLife: number; + size: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + const angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.5; + const speed = 2 + Math.random() * 2; + this.vx = Math.cos(angle) * speed; + this.vy = Math.sin(angle) * speed; + this.life = 0; + this.maxLife = 60 + Math.random() * 40; + this.size = 2 + Math.random() * 2; + } + + update() { + this.x += this.vx; + this.y += this.vy; + this.life++; + } + + draw(ctx: CanvasRenderingContext2D) { + const alpha = 1 - this.life / this.maxLife; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = `rgba(34, 197, 94, ${alpha * 0.8})`; + ctx.fill(); + + // 光晕 + const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size * 3); + gradient.addColorStop(0, `rgba(34, 197, 94, ${alpha * 0.4})`); + gradient.addColorStop(1, 'rgba(34, 197, 94, 0)'); + ctx.fillStyle = gradient; + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size * 3, 0, Math.PI * 2); + ctx.fill(); + } + + isDead() { + return this.life >= this.maxLife; + } + } + + // 初始化传感器网络 + const sensors: SensorNode[] = []; + const gridSize = 150; + for (let x = gridSize; x < canvas.width; x += gridSize) { + for (let y = gridSize; y < canvas.height; y += gridSize) { + const offsetX = (Math.random() - 0.5) * 40; + const offsetY = (Math.random() - 0.5) * 40; + sensors.push(new SensorNode(x + offsetX, y + offsetY)); + } + } + + // 建立传感器连接 + sensors.forEach(sensor => { + sensors.forEach(other => { + if (sensor !== other) { + const distance = Math.hypot(other.x - sensor.x, other.y - sensor.y); + if (distance < 250 && sensor.connections.length < 3) { + sensor.connections.push(other); + } + } + }); + }); + + // 初始化无人机 + const drones: Drone[] = []; + for (let i = 0; i < 2; i++) { + drones.push(new Drone()); + } + + // 初始化田地波浪 + const waves: FieldWave[] = []; + for (let i = 0; i < 8; i++) { + waves.push(new FieldWave((canvas.height / 8) * i)); + } + + // 数据粒子 + const dataParticles: DataParticle[] = []; + + // 动画循环 + let animationId: number; + const animate = () => { + // 渐变背景(不完全清除,产生拖尾效果) + ctx.fillStyle = 'rgba(15, 118, 110, 0.08)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // 绘制田地波浪 + waves.forEach(wave => { + wave.update(); + wave.draw(ctx); + }); + + // 绘制传感器连接线 + sensors.forEach(sensor => { + sensor.drawConnections(ctx); + }); + + // 绘制传感器节点 + sensors.forEach(sensor => { + sensor.update(); + sensor.draw(ctx); + }); + + // 随机生成数据粒子(从传感器节点) + if (Math.random() < 0.3 && sensors.length > 0) { + const randomSensor = sensors[Math.floor(Math.random() * sensors.length)]; + dataParticles.push(new DataParticle(randomSensor.x, randomSensor.y)); + } + + // 更新和绘制数据粒子 + for (let i = dataParticles.length - 1; i >= 0; i--) { + const particle = dataParticles[i]; + particle.update(); + particle.draw(ctx); + + if (particle.isDead()) { + dataParticles.splice(i, 1); + } + } + + // 绘制无人机 + drones.forEach(drone => { + drone.update(); + drone.draw(ctx); + }); + + // 绘制网格线(农田分界) + ctx.strokeStyle = 'rgba(34, 197, 94, 0.08)'; + ctx.lineWidth = 1; + const gridSpacing = 100; + for (let x = 0; x < canvas.width; x += gridSpacing) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, canvas.height); + ctx.stroke(); + } + for (let y = 0; y < canvas.height; y += gridSpacing) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(canvas.width, y); + ctx.stroke(); + } + + animationId = requestAnimationFrame(animate); + }; + + animate(); + + return () => { + window.removeEventListener('resize', resizeCanvas); + cancelAnimationFrame(animationId); + }; + }, []); + + return ( + <> + {/* 静态渐变背景 - 固定定位覆盖整个视口 */} +
+ + {/* Canvas动态背景 - 固定定位覆盖整个视口 */} + + + {/* 顶部光效 - 固定定位 */} +
+ + {/* 底部装饰光 - 固定定位 */} +
+ + ); +} \ No newline at end of file diff --git a/crop-x/src/lib/api/client.gen.ts b/crop-x/src/lib/api/client.gen.ts index d00cfd9..9a6e01e 100644 --- a/crop-x/src/lib/api/client.gen.ts +++ b/crop-x/src/lib/api/client.gen.ts @@ -14,5 +14,5 @@ import type { ClientOptions as ClientOptions2 } from './types.gen'; export type CreateClientConfig = (override?: Config) => Config & T>; export const client = createClient(createConfig({ - baseUrl: 'https://gitea-admin-hm-smart-agri-app.dev.maimaiag.com' + baseUrl: '' // 使用相对路径,通过 Next.js 代理到真实 API }));