diff --git a/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx b/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx index 2f50462..6511622 100644 --- a/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx +++ b/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx @@ -9,7 +9,6 @@ import { useReducer, useEffect, useState, useCallback, useMemo,useRef } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; -import { Eye, Edit, Trash2, UserX, UserCheck } from 'lucide-react'; import { AlertDialog, AlertDialogAction, @@ -20,6 +19,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; +import { Eye, Edit, Lock, UserX, UserCheck, Trash2 } from 'lucide-react'; import { UserDetailDialog } from './components/UserDetailDialog'; import { AddUserModal } from './components/AddUserModal'; import { EditUserModal } from './components/EditUserModal'; @@ -45,6 +45,8 @@ interface UserManagementState { showDetailDialog: boolean; showAddDialog: boolean; showEditDialog: boolean; + showDeactivateDialog: boolean; + showDeleteDialog: boolean; } type UserManagementAction = @@ -58,6 +60,8 @@ type UserManagementAction = | { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean } | { type: 'TOGGLE_ADD_DIALOG'; payload: boolean } | { type: 'TOGGLE_EDIT_DIALOG'; payload: boolean } + | { type: 'TOGGLE_DEACTIVATE_DIALOG'; payload: boolean } + | { type: 'TOGGLE_DELETE_DIALOG'; payload: boolean } | { type: 'REFRESH_DATA' }; const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => { @@ -88,6 +92,10 @@ const userManagementReducer = (state: UserManagementState, action: UserManagemen return { ...state, showAddDialog: !state.showAddDialog }; case 'TOGGLE_EDIT_DIALOG': return { ...state, showEditDialog: !state.showEditDialog }; + case 'TOGGLE_DEACTIVATE_DIALOG': + return { ...state, showDeactivateDialog: !state.showDeactivateDialog }; + case 'TOGGLE_DELETE_DIALOG': + return { ...state, showDeleteDialog: !state.showDeleteDialog }; case 'REFRESH_DATA': return { ...state, error: null }; default: @@ -118,17 +126,13 @@ const initialState: UserManagementState = { showDetailDialog: false, showAddDialog: false, showEditDialog: false, + showDeactivateDialog: false, + showDeleteDialog: false, }; export default function TenantUserManagementPage() { const [state, dispatch] = useReducer(userManagementReducer, initialState); - // 弹窗状态管理 - const [statusDialogOpen, setStatusDialogOpen] = useState(false); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [actionUser, setActionUser] = useState(null); - const [actionType, setActionType] = useState<'activate' | 'deactivate'>('activate'); - // 搜索字段配置 const searchFields: SearchFieldConfig[] = useMemo(() => [ { @@ -279,7 +283,7 @@ export default function TenantUserManagementPage() { variant="ghost" size="sm" onClick={() => handleToggleStatus(user)} - title={user.isActive ? "冻结用户" : "激活用户"} + title={user.isActive ? "停用用户" : "激活用户"} > {user.isActive ? ( @@ -290,9 +294,9 @@ export default function TenantUserManagementPage() { @@ -504,62 +508,60 @@ export default function TenantUserManagementPage() { loadUsers({}); }, [loadUsers]); - // 切换用户状态 - 打开确认弹窗 + // 切换用户状态 const handleToggleStatus = (user: User) => { - const newStatus = !user.isActive; - setActionUser(user); - setActionType(newStatus ? 'activate' : 'deactivate'); - setStatusDialogOpen(true); + dispatch({ type: 'SET_SELECTED_USER', payload: user }); + dispatch({ type: 'TOGGLE_DEACTIVATE_DIALOG', payload: true }); }; - // 执行状态切换 - const handleStatusConfirm = async () => { - if (!actionUser) return; + // 删除用户 + const handleDelete = (user: User) => { + dispatch({ type: 'SET_SELECTED_USER', payload: user }); + dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: true }); + }; + + // 确认切换用户状态 + const confirmToggleStatus = async () => { + if (!state.selectedUser) return; try { - if (actionType === 'activate') { - await activateUser(actionUser.id); - toast.success(`用户 ${actionUser.fullName || actionUser.username} 已激活`); + const user = state.selectedUser; + if (user.isActive) { + await deactivateUser(user.id); + toast.success(`用户 ${user.fullName || user.username} 已停用`); } else { - await deactivateUser(actionUser.id); - toast.success(`用户 ${actionUser.fullName || actionUser.username} 已停用`); + await activateUser(user.id); + toast.success(`用户 ${user.fullName || user.username} 已激活`); } - // 刷新数据 + // 关闭对话框并刷新列表 + dispatch({ type: 'TOGGLE_DEACTIVATE_DIALOG', payload: false }); + dispatch({ type: 'SET_SELECTED_USER', payload: null }); refreshData(); } catch (error) { - console.error(`${actionType === 'activate' ? '激活' : '停用'}用户失败:`, error); - const errorMessage = error instanceof Error ? error.message : `${actionType === 'activate' ? '激活' : '停用'}用户失败,请重试`; + console.error('切换用户状态失败:', error); + const errorMessage = error instanceof Error ? error.message : '操作失败,请重试'; toast.error(errorMessage); - } finally { - setStatusDialogOpen(false); - setActionUser(null); } }; - // 删除用户 - 打开确认弹窗 - const handleDeleteUser = (user: User) => { - setActionUser(user); - setDeleteDialogOpen(true); - }; - - // 执行删除用户 - const handleDeleteConfirm = async () => { - if (!actionUser) return; + // 确认删除用户 + const confirmDelete = async () => { + if (!state.selectedUser) return; try { - await deleteUser(actionUser.id); - toast.success(`用户 ${actionUser.fullName || actionUser.username} 已删除`); + const user = state.selectedUser; + await deleteUser(user.id); + toast.success(`用户 ${user.fullName || user.username} 已删除`); - // 刷新数据 + // 关闭对话框并刷新列表 + dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: false }); + dispatch({ type: 'SET_SELECTED_USER', payload: null }); refreshData(); } catch (error) { console.error('删除用户失败:', error); - const errorMessage = error instanceof Error ? error.message : '删除用户失败,请重试'; + const errorMessage = error instanceof Error ? error.message : '删除失败,请重试'; toast.error(errorMessage); - } finally { - setDeleteDialogOpen(false); - setActionUser(null); } }; @@ -652,54 +654,55 @@ export default function TenantUserManagementPage() { onSuccess={refreshData} /> - {/* 状态切换确认对话框 */} - + {/* 停用/激活用户确认对话框 */} + dispatch({ type: 'TOGGLE_DEACTIVATE_DIALOG', payload: open })}> - {actionType === 'activate' ? '激活用户' : '停用用户'} + {state.selectedUser?.isActive ? '停用用户' : '激活用户'} - 确定要{actionType === 'activate' ? '激活' : '停用'}用户 - - {actionUser?.fullName || actionUser?.username} - - 吗?此操作将影响该用户的登录权限。 + 确定要{state.selectedUser?.isActive ? '停用' : '激活'}用户 {state.selectedUser?.fullName || state.selectedUser?.username} 吗? + {state.selectedUser?.isActive && ( + + 停用后,该用户将无法登录系统。 + + )} 取消 - - 确认{actionType === 'activate' ? '激活' : '停用'} - + {state.selectedUser?.isActive ? '停用' : '激活'} + {/* 删除用户确认对话框 */} - + dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: open })}> 删除用户 - 确定要删除用户 - - {actionUser?.fullName || actionUser?.username} + 确定要删除用户 {state.selectedUser?.fullName || state.selectedUser?.username} 吗? + + ⚠️ 此操作不可恢复,用户的所有数据将被永久删除! - 吗?此操作不可恢复,该用户的所有数据将被永久删除。 取消 - 确认删除 - + diff --git a/crop-x-new/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx b/crop-x-new/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx index de20805..31602f1 100644 --- a/crop-x-new/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx +++ b/crop-x-new/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx @@ -46,8 +46,6 @@ export function MultiFactorEvaluation() { const [selectedField, setSelectedField] = useState('field-1'); const [showWeightConfig, setShowWeightConfig] = useState(false); const [showKnowledgeBase, setShowKnowledgeBase] = useState(false); - const [batchAnalysisResults, setBatchAnalysisResults] = useState([]); - // 评价因子权重配置 const [factorWeights, setFactorWeights] = useState([ { id: 'ph', name: 'pH值', weight: 20, unit: '' }, @@ -67,7 +65,6 @@ export function MultiFactorEvaluation() { // 获取当前选中的地块结果 const currentResult = evaluationResults.find(r => r.fieldId === selectedField) || - batchAnalysisResults.find(r => r.fieldId === selectedField) || evaluationResults[0]; // 批量分析处理函数 diff --git a/crop-x-new/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx b/crop-x-new/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx index 9488ffb..475cc1e 100644 --- a/crop-x-new/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx +++ b/crop-x-new/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx @@ -43,7 +43,7 @@ interface CropKnowledgeEntry { riskFactors: CropRiskFactor[]; } -type MatchStatus = '??' | '???' | '??'; +type MatchStatus = '??' | '??' | '??'; interface MatchDetail { factor: string; @@ -55,7 +55,7 @@ interface MatchDetail { interface RecommendationResult { crop: CropKnowledgeEntry; matchScore: number; - suitabilityLevel: '????' | '??' | '????' | '???'; + suitabilityLevel: '????' | '??' | '????' | '??'; matchDetails: MatchDetail[]; applicableRisks: CropRiskFactor[]; expectedYield: [number, number]; @@ -201,87 +201,100 @@ interface CropRecommendationsProps { export function CropRecommendations({ currentResult }: CropRecommendationsProps) { // 匹配作物推荐 - const matchCropsForField = (fieldFactors: any) => { - return cropKnowledgeBase.map(crop => { + const matchCropsForField = (fieldFactors: FieldFactors): RecommendationResult[] => { + const factorLabelMap: Record = { + ph: 'pH?', + organicMatter: '??', + soilDepth: '????', + nitrogen: '??', + phosphorus: '??', + potassium: '??', + drainage: '??' + }; + + return cropKnowledgeBase.map((crop) => { let totalScore = 0; let factorCount = 0; - const matchDetails: any[] = []; + const matchDetails: MatchDetail[] = []; - // 评估土壤因子匹配度 - Object.entries(crop.soilRequirements).forEach(([factor, requirements]: [string, any]) => { - if (fieldFactors[factor]) { - const value = fieldFactors[factor]; - const { optimal, acceptable } = requirements; - - let score = 0; - let status = '偏离'; - - if (value >= optimal[0] && value <= optimal[1]) { - score = 100; - status = '最佳'; - } else if (value >= acceptable[0] && value <= acceptable[1]) { - const deviation = Math.min( - Math.abs(value - optimal[0]), - Math.abs(value - optimal[1]) - ); - const range = optimal[1] - optimal[0]; - score = Math.max(60, 100 - (deviation / range) * 40); - status = '可接受'; - } else { - score = Math.max(0, 60 - Math.min( - Math.abs(value - acceptable[0]), - Math.abs(value - acceptable[1]) - ) * 2); - } - - totalScore += score; - factorCount++; - - matchDetails.push({ - factor: factor === 'ph' ? 'pH值' : - factor === 'organicMatter' ? '有机质' : - factor === 'soilDepth' ? '土层厚度' : - factor === 'nitrogen' ? '全氮' : - factor === 'phosphorus' ? '全磷' : - factor === 'potassium' ? '全钾' : '排水性', - value, - score, - status - }); + (Object.entries(crop.soilRequirements) as Array<[SoilFactorKey, RangeRequirement]>).forEach(([factor, requirements]) => { + const value = fieldFactors[factor]; + if (typeof value !== 'number') { + return; } + + const { optimal, acceptable } = requirements; + let score = 0; + let status: MatchStatus = '??'; + + if (value >= optimal[0] && value <= optimal[1]) { + score = 100; + status = '??'; + } else if (value >= acceptable[0] && value <= acceptable[1]) { + const deviation = Math.min( + Math.abs(value - optimal[0]), + Math.abs(value - optimal[1]) + ); + const range = optimal[1] - optimal[0]; + score = Math.max(60, 100 - (deviation / range) * 40); + status = '??'; + } else { + score = Math.max( + 0, + 60 - + Math.min( + Math.abs(value - acceptable[0]), + Math.abs(value - acceptable[1]) + ) * + 2 + ); + } + + totalScore += score; + factorCount += 1; + + matchDetails.push({ + factor: factorLabelMap[factor], + value, + score, + status + }); }); - // 评估气候因子(简化处理) - if (fieldFactors.temperature) { - const tempScore = fieldFactors.temperature >= 18 && fieldFactors.temperature <= 25 ? 90 : 70; + if (typeof fieldFactors.temperature === 'number') { + const tempScore = + fieldFactors.temperature >= 18 && fieldFactors.temperature <= 25 ? 90 : 70; totalScore += tempScore; - factorCount++; + factorCount += 1; } - if (fieldFactors.rainfall) { - const rainScore = fieldFactors.rainfall >= 500 && fieldFactors.rainfall <= 800 ? 90 : 70; + if (typeof fieldFactors.rainfall === 'number') { + const rainScore = + fieldFactors.rainfall >= 500 && fieldFactors.rainfall <= 800 ? 90 : 70; totalScore += rainScore; - factorCount++; + factorCount += 1; } - const matchScore = Math.round(totalScore / factorCount); + const matchScore = factorCount > 0 ? Math.round(totalScore / factorCount) : 0; - // 确定适宜性等级 - let suitabilityLevel = '不推荐'; - if (matchScore >= 85) suitabilityLevel = '高度推荐'; - else if (matchScore >= 70) suitabilityLevel = '推荐'; - else if (matchScore >= 50) suitabilityLevel = '谨慎种植'; + let suitabilityLevel: RecommendationResult['suitabilityLevel'] = '??'; + if (matchScore >= 85) suitabilityLevel = '????'; + else if (matchScore >= 70) suitabilityLevel = '??'; + else if (matchScore >= 50) suitabilityLevel = '????'; - // 根据适宜性等级选择产量区间 - let expectedYield = crop.expectedYield.low; - if (suitabilityLevel === '高度推荐') expectedYield = crop.expectedYield.high; - else if (suitabilityLevel === '推荐') expectedYield = crop.expectedYield.medium; + let expectedYield: [number, number] = crop.expectedYield.low; + if (suitabilityLevel === '????') expectedYield = crop.expectedYield.high; + else if (suitabilityLevel === '??') expectedYield = crop.expectedYield.medium; - // 识别适用风险 - const applicableRisks = crop.riskFactors.filter(risk => { + const applicableRisks = crop.riskFactors.filter((risk) => { if (risk.id.includes('drought') && fieldFactors.rainfall < 400) return true; - if (risk.id.includes('rust') && fieldFactors.temperature >= 15 && fieldFactors.temperature <= 22) return true; - return true; // 简化处理,默认显示所有风险 + if ( + risk.id.includes('rust') && + fieldFactors.temperature >= 15 && + fieldFactors.temperature <= 22 + ) + return true; + return false; }); return { @@ -292,20 +305,25 @@ export function CropRecommendations({ currentResult }: CropRecommendationsProps) applicableRisks, expectedYield }; - }).sort((a, b) => b.matchScore - a.matchScore); + }); }; - // 获取地块因子数据 - const fieldFactors = { - ph: currentResult.factors.find(f => f.id === 'ph')?.value || 0, - organic: currentResult.factors.find(f => f.id === 'organic')?.value || 0, - depth: currentResult.factors.find(f => f.id === 'depth')?.value || 0, - nitrogen: currentResult.factors.find(f => f.id === 'nitrogen')?.value || 0, - phosphorus: currentResult.factors.find(f => f.id === 'phosphorus')?.value || 0, - potassium: currentResult.factors.find(f => f.id === 'potassium')?.value || 0, - drainage: currentResult.factors.find(f => f.id === 'drainage')?.value || 0, - temperature: 22, // 模拟年均温度 - rainfall: 800, // 模拟年均降雨量 + const getFactorValue = (factorId: string) => + currentResult.factors.find((factor) => factor.id === factorId)?.value ?? 0; + + const fieldFactors: FieldFactors = { + ph: getFactorValue('ph'), + organicMatter: getFactorValue('organic'), + soilDepth: getFactorValue('depth'), + nitrogen: getFactorValue('nitrogen'), + phosphorus: getFactorValue('phosphorus'), + potassium: getFactorValue('potassium'), + drainage: getFactorValue('drainage'), + temperature: 22, // ?????? + rainfall: 800 // ??????? + }; + + // // 模拟年均降雨量 }; // 匹配推荐作物 diff --git a/crop-x-new/src/app/(auth)/login/components/LoginForm.tsx b/crop-x-new/src/app/(auth)/login/components/LoginForm.tsx index 4ab1aef..d68c52a 100644 --- a/crop-x-new/src/app/(auth)/login/components/LoginForm.tsx +++ b/crop-x-new/src/app/(auth)/login/components/LoginForm.tsx @@ -27,8 +27,8 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import { useAuth } from '@/components/auth/AuthContext'; -import { authReducer, initialAuthState, AuthState, AuthAction } from './authReducer'; -import { getCaptchaApiV1AuthCaptchaGet, loginApiV1AuthLoginPost } from '@/lib/api/sdk.gen'; +import { authReducer, initialAuthState } from './authReducer'; +import { loginApiV1AuthLoginPost } from '@/lib/api/sdk.gen'; import type { CaptchaResponse } from '@/lib/api/types.gen'; import {PERSONAL_CELTRAL_PAGE} from "@/config/constants" interface LoginFormProps { @@ -40,8 +40,7 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) { 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) { @@ -154,9 +153,14 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) { dispatch({ type: 'SET_ERROR', payload: '登录失败,请检查用户名和密码' }); toast.error('登录失败,请检查用户名和密码'); } - } catch (err: any) { - console.error('登录失败:', err); - const errorMessage = err?.response?.data?.message || err?.message || '登录失败,请稍后重试'; + } catch (error: unknown) { + console.error('????:', error); + const apiMessage = + typeof error === 'object' && error !== null && 'response' in error + ? (error as { response?: { data?: { message?: string } } }).response?.data?.message + : undefined; + const fallbackMessage = error instanceof Error ? error.message : undefined; + const errorMessage = apiMessage || fallbackMessage || '??????????'; dispatch({ type: 'SET_ERROR', payload: errorMessage }); toast.error(errorMessage); } finally { @@ -204,9 +208,10 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) { dispatch({ type: 'SET_ERROR', payload: '验证码错误' }); toast.error('验证码错误'); } - } catch (err) { - dispatch({ type: 'SET_ERROR', payload: '登录失败,请稍后重试' }); - toast.error('登录失败'); + } catch (error) { + console.error('???????:', error); + dispatch({ type: 'SET_ERROR', payload: '??????????' }); + toast.error('????'); } finally { dispatch({ type: 'SET_LOADING', payload: false }); } diff --git a/crop-x-new/src/app/(auth)/register/page.tsx b/crop-x-new/src/app/(auth)/register/page.tsx index 0194f3f..d50459d 100644 --- a/crop-x-new/src/app/(auth)/register/page.tsx +++ b/crop-x-new/src/app/(auth)/register/page.tsx @@ -172,7 +172,7 @@ export default function RegisterPage() { await new Promise(resolve => setTimeout(resolve, 1000)); toast.success('验证码发送成功!(测试验证码:123456)'); setCountdown(60); - } catch (err) { + } catch (error) { setError('发送验证码失败,请稍后重试'); toast.error('发送验证码失败'); } finally { @@ -246,7 +246,7 @@ export default function RegisterPage() { setError('短信验证码错误'); toast.error('短信验证码错误'); } - } catch (err) { + } catch (error) { setError('注册失败,请稍后重试'); toast.error('注册失败'); } finally { diff --git a/crop-x-new/src/app/layout.tsx b/crop-x-new/src/app/layout.tsx index 9144bc3..65699fc 100644 --- a/crop-x-new/src/app/layout.tsx +++ b/crop-x-new/src/app/layout.tsx @@ -6,7 +6,7 @@ import { AuthProvider } from '@/components/auth/AuthContext'; import { ClientAuthInterceptor } from '@/components/auth/ClientAuthInterceptor'; import { ThemeProvider } from 'next-themes'; import { Toaster } from '@/components/ui/sonner'; -import { Building2, Users, Cog, Activity, Mail, UserCircle, Database, Map, BarChart3, Cloud, TrendingUp, GitCompare, AlertTriangle, FileText, MapPin, Settings, User, Package, Navigation, Zap, Target, PieChart, Calendar, Shield, Tractor, Clipboard, ClipboardCheck, Brain, Droplets, Book, ShoppingCart } from 'lucide-react'; +import { Building2, Users, Cog, Activity, Mail, UserCircle, Database, Map, BarChart3, Cloud, TrendingUp, GitCompare, AlertTriangle, FileText, Settings, User, Package, Navigation, Zap, Target, PieChart, Calendar, Shield, Tractor, Clipboard, ClipboardCheck, Brain, Droplets, Book, ShoppingCart } from 'lucide-react'; // 导入导航和侧边栏组件 import {Navbar1} from "@/components/layouts/Navbar" diff --git a/crop-x-new/src/app/not-found.tsx b/crop-x-new/src/app/not-found.tsx index 23ca3bb..ba75613 100644 --- a/crop-x-new/src/app/not-found.tsx +++ b/crop-x-new/src/app/not-found.tsx @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; -import { Home, ArrowLeft, RefreshCw, Search, AlertTriangle, ExternalLink } from 'lucide-react'; +import { Home, ArrowLeft, RefreshCw, Search, AlertTriangle } from 'lucide-react'; export default function NotFound() { const handleRefresh = () => { diff --git a/crop-x-new/src/components/app-sidebar.tsx b/crop-x-new/src/components/app-sidebar.tsx index 9add399..b5d8aea 100644 --- a/crop-x-new/src/components/app-sidebar.tsx +++ b/crop-x-new/src/components/app-sidebar.tsx @@ -2,8 +2,6 @@ import * as React from "react" import { ChevronRight } from "lucide-react" import Link from "next/link" -import { SearchForm } from "@/components/search-form" -import { VersionSwitcher } from "@/components/version-switcher" import { Collapsible, CollapsibleContent, @@ -15,7 +13,6 @@ import { SidebarGroup, SidebarGroupContent, SidebarGroupLabel, - SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, diff --git a/crop-x-new/src/components/auth/CaptchaInput.tsx b/crop-x-new/src/components/auth/CaptchaInput.tsx index a64224d..c0b81d4 100644 --- a/crop-x-new/src/components/auth/CaptchaInput.tsx +++ b/crop-x-new/src/components/auth/CaptchaInput.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -19,7 +19,7 @@ export function CaptchaInput({ value, onChange, onCaptchaChange, className = '' const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - const fetchCaptcha = async () => { + const fetchCaptcha = useCallback(async () => { setLoading(true); setError(''); onChange(''); // 清空验证码输入 @@ -120,11 +120,11 @@ export function CaptchaInput({ value, onChange, onCaptchaChange, className = '' captcha_id: 'fallback-' + Date.now(), image: canvas.toDataURL() }; - }; + }, [onCaptchaChange, onChange]); useEffect(() => { fetchCaptcha(); - }, []); + }, [fetchCaptcha]); const handleRefresh = () => { fetchCaptcha(); diff --git a/crop-x-new/src/components/auth/LoadingScreen.tsx b/crop-x-new/src/components/auth/LoadingScreen.tsx index ccf8787..633cc36 100644 --- a/crop-x-new/src/components/auth/LoadingScreen.tsx +++ b/crop-x-new/src/components/auth/LoadingScreen.tsx @@ -15,6 +15,21 @@ export function LoadingScreen({ subMessage, variant = 'default' }: LoadingScreenProps) { + const variantTitles = { + default: '??????????????', + auth: '??????????', + redirect: '???????????' + } as const; + + const variantSubtitles = { + default: '??????????', + auth: '????????????', + redirect: '????????' + } as const; + + const primaryMessage = message || variantTitles[variant]; + const secondaryMessage = subMessage || variantSubtitles[variant]; + return (
{/* 智慧大田动态背景 - 使用登录页面相同的背景效果 */} diff --git a/crop-x-new/src/components/auth/SmartFieldBackground.tsx b/crop-x-new/src/components/auth/SmartFieldBackground.tsx index 74221aa..5d1ef63 100644 --- a/crop-x-new/src/components/auth/SmartFieldBackground.tsx +++ b/crop-x-new/src/components/auth/SmartFieldBackground.tsx @@ -2,6 +2,271 @@ import { useEffect, useRef } from 'react'; +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(); + } + }); + } +} + +type BoundsSupplier = () => { width: number; height: number }; +type DroneTrailPoint = { x: number; y: number; alpha: number }; + +class Drone { + x: number; + y: number; + targetX: number; + targetY: number; + speed: number; + size: number; + rotorPhase: number; + trail: DroneTrailPoint[]; + private readonly getBounds: BoundsSupplier; + + constructor(getBounds: BoundsSupplier) { + this.getBounds = getBounds; + const { width, height } = this.getBounds(); + this.x = Math.random() * width; + this.y = Math.random() * height; + this.targetX = this.x; + this.targetY = this.y; + this.speed = 1.5; + this.size = 12; + this.rotorPhase = 0; + this.trail = []; + this.setNewTarget(); + } + + setNewTarget() { + const { width, height } = this.getBounds(); + this.targetX = Math.random() * width; + this.targetY = Math.random() * 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.2)'; + ctx.lineWidth = 2; + ctx.stroke(); + + ctx.restore(); + } +} + +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, canvasWidth: number) { + ctx.beginPath(); + ctx.moveTo(0, this.y); + + for (let x = 0; x <= canvasWidth; 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; + } +} + export function SmartFieldBackground() { const canvasRef = useRef(null); @@ -21,285 +286,12 @@ export function SmartFieldBackground() { 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[] = []; @@ -325,9 +317,10 @@ export function SmartFieldBackground() { }); // 初始化无人机 + const canvasBounds = () => ({ width: canvas.width, height: canvas.height }); const drones: Drone[] = []; for (let i = 0; i < 2; i++) { - drones.push(new Drone()); + drones.push(new Drone(canvasBounds)); } // 初始化田地波浪 @@ -349,7 +342,7 @@ export function SmartFieldBackground() { // 绘制田地波浪 waves.forEach(wave => { wave.update(); - wave.draw(ctx); + wave.draw(ctx, canvas.width); }); // 绘制传感器连接线 diff --git a/crop-x-new/src/lib/spatialDataService.ts b/crop-x-new/src/lib/spatialDataService.ts index c7e06dc..227fb64 100644 --- a/crop-x-new/src/lib/spatialDataService.ts +++ b/crop-x-new/src/lib/spatialDataService.ts @@ -911,7 +911,7 @@ export class SpatialIndex { }): Field[] { const results: Field[] = []; - for (const [_, item] of this.rtree) { + for (const [, item] of this.rtree) { if (this._bboxesIntersect(bbox, item.bbox)) { results.push(item.field); } diff --git a/crop-x-new/src/types/index.ts b/crop-x-new/src/types/index.ts index fcbfb34..1181d7c 100644 --- a/crop-x-new/src/types/index.ts +++ b/crop-x-new/src/types/index.ts @@ -1,7 +1,7 @@ // 通用类型定义 // API响应类型 -export interface ApiResponse { +export interface ApiResponse { code: number message: string data: T @@ -34,11 +34,11 @@ export interface SortParams { // 搜索类型 export interface SearchParams { keyword?: string - filters?: Record + filters?: Record } // 表格列定义 -export interface ColumnDef { +export interface ColumnDef> { key: string title: string dataIndex: keyof T @@ -46,7 +46,7 @@ export interface ColumnDef { fixed?: 'left' | 'right' sortable?: boolean filterable?: boolean - render?: (value: any, record: T, index: number) => React.ReactNode + render?: (value: T[keyof T], record: T, index: number) => React.ReactNode } // 表单字段类型 @@ -56,7 +56,7 @@ export interface FormField { type: 'text' | 'number' | 'select' | 'date' | 'textarea' | 'checkbox' | 'radio' required?: boolean placeholder?: string - options?: Array<{ label: string; value: any }> + options?: Array<{ label: string; value: string | number | boolean }> validation?: { min?: number max?: number @@ -343,7 +343,7 @@ export interface AppConfig { // 路由类型 export interface Route { path: string - component: React.ComponentType + component: React.ComponentType exact?: boolean title?: string icon?: React.ReactNode @@ -390,7 +390,7 @@ export interface DashboardStats { export interface AppError { code: string message: string - details?: any + details?: unknown timestamp: string } @@ -402,7 +402,7 @@ export interface LogEntry { module: string userId?: string timestamp: string - metadata?: Record + metadata?: Record } export enum LogLevel { diff --git a/crop-x-new/src/types/message.ts b/crop-x-new/src/types/message.ts index 98fe27f..7fcd089 100644 --- a/crop-x-new/src/types/message.ts +++ b/crop-x-new/src/types/message.ts @@ -37,7 +37,7 @@ export interface MessageLog { readTime?: string; failReason?: string; retryCount: number; - variables?: Record; + variables?: Record; } // 消息发送记录 diff --git a/crop-x-new/src/types/system-params.ts b/crop-x-new/src/types/system-params.ts index 279eca1..db466da 100644 --- a/crop-x-new/src/types/system-params.ts +++ b/crop-x-new/src/types/system-params.ts @@ -57,7 +57,7 @@ export interface DataDictionary { description?: string; isSystem: boolean; // 是否系统内置 isActive: boolean; - extendData?: Record; + extendData?: Record; createdAt: string; updatedAt: string; } diff --git a/crop-x-new/tsconfig.json b/crop-x-new/tsconfig.json index a2540c6..12eb419 100644 --- a/crop-x-new/tsconfig.json +++ b/crop-x-new/tsconfig.json @@ -15,7 +15,7 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "preserve", + "jsx": "react-jsx", /** TODO: 暂时禁用校验 */ "noImplicitAny": false, "strictNullChecks": false, @@ -70,9 +70,10 @@ }, "include": [ "src", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" ], - "paths": { + "paths": { "@/*": [ "./src/*" ]