生产管理系统 - 激活、删除的联调

This commit is contained in:
2025-11-12 14:34:34 +08:00
parent 8fefadaf55
commit dcd7ddeb71
16 changed files with 487 additions and 458 deletions

View File

@@ -9,7 +9,6 @@
import { useReducer, useEffect, useState, useCallback, useMemo,useRef } from 'react'; import { useReducer, useEffect, useState, useCallback, useMemo,useRef } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Eye, Edit, Trash2, UserX, UserCheck } from 'lucide-react';
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -20,6 +19,7 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog';
import { Eye, Edit, Lock, UserX, UserCheck, Trash2 } from 'lucide-react';
import { UserDetailDialog } from './components/UserDetailDialog'; import { UserDetailDialog } from './components/UserDetailDialog';
import { AddUserModal } from './components/AddUserModal'; import { AddUserModal } from './components/AddUserModal';
import { EditUserModal } from './components/EditUserModal'; import { EditUserModal } from './components/EditUserModal';
@@ -45,6 +45,8 @@ interface UserManagementState {
showDetailDialog: boolean; showDetailDialog: boolean;
showAddDialog: boolean; showAddDialog: boolean;
showEditDialog: boolean; showEditDialog: boolean;
showDeactivateDialog: boolean;
showDeleteDialog: boolean;
} }
type UserManagementAction = type UserManagementAction =
@@ -58,6 +60,8 @@ type UserManagementAction =
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean } | { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
| { type: 'TOGGLE_ADD_DIALOG'; payload: boolean } | { type: 'TOGGLE_ADD_DIALOG'; payload: boolean }
| { type: 'TOGGLE_EDIT_DIALOG'; payload: boolean } | { type: 'TOGGLE_EDIT_DIALOG'; payload: boolean }
| { type: 'TOGGLE_DEACTIVATE_DIALOG'; payload: boolean }
| { type: 'TOGGLE_DELETE_DIALOG'; payload: boolean }
| { type: 'REFRESH_DATA' }; | { type: 'REFRESH_DATA' };
const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => { const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => {
@@ -88,6 +92,10 @@ const userManagementReducer = (state: UserManagementState, action: UserManagemen
return { ...state, showAddDialog: !state.showAddDialog }; return { ...state, showAddDialog: !state.showAddDialog };
case 'TOGGLE_EDIT_DIALOG': case 'TOGGLE_EDIT_DIALOG':
return { ...state, showEditDialog: !state.showEditDialog }; 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': case 'REFRESH_DATA':
return { ...state, error: null }; return { ...state, error: null };
default: default:
@@ -118,17 +126,13 @@ const initialState: UserManagementState = {
showDetailDialog: false, showDetailDialog: false,
showAddDialog: false, showAddDialog: false,
showEditDialog: false, showEditDialog: false,
showDeactivateDialog: false,
showDeleteDialog: false,
}; };
export default function TenantUserManagementPage() { export default function TenantUserManagementPage() {
const [state, dispatch] = useReducer(userManagementReducer, initialState); const [state, dispatch] = useReducer(userManagementReducer, initialState);
// 弹窗状态管理
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [actionUser, setActionUser] = useState<User | null>(null);
const [actionType, setActionType] = useState<'activate' | 'deactivate'>('activate');
// 搜索字段配置 // 搜索字段配置
const searchFields: SearchFieldConfig[] = useMemo(() => [ const searchFields: SearchFieldConfig[] = useMemo(() => [
{ {
@@ -279,7 +283,7 @@ export default function TenantUserManagementPage() {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleToggleStatus(user)} onClick={() => handleToggleStatus(user)}
title={user.isActive ? "冻结用户" : "激活用户"} title={user.isActive ? "停用用户" : "激活用户"}
> >
{user.isActive ? ( {user.isActive ? (
<UserX className="w-4 h-4 text-orange-600" /> <UserX className="w-4 h-4 text-orange-600" />
@@ -290,9 +294,9 @@ export default function TenantUserManagementPage() {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => handleDeleteUser(user)} onClick={() => handleDelete(user)}
title="删除用户" title="删除用户"
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950" className="text-red-600 hover:text-red-700 hover:bg-red-50"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
@@ -504,62 +508,60 @@ export default function TenantUserManagementPage() {
loadUsers({}); loadUsers({});
}, [loadUsers]); }, [loadUsers]);
// 切换用户状态 - 打开确认弹窗 // 切换用户状态
const handleToggleStatus = (user: User) => { const handleToggleStatus = (user: User) => {
const newStatus = !user.isActive; dispatch({ type: 'SET_SELECTED_USER', payload: user });
setActionUser(user); dispatch({ type: 'TOGGLE_DEACTIVATE_DIALOG', payload: true });
setActionType(newStatus ? 'activate' : 'deactivate');
setStatusDialogOpen(true);
}; };
// 执行状态切换 // 删除用户
const handleStatusConfirm = async () => { const handleDelete = (user: User) => {
if (!actionUser) return; dispatch({ type: 'SET_SELECTED_USER', payload: user });
dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: true });
};
// 确认切换用户状态
const confirmToggleStatus = async () => {
if (!state.selectedUser) return;
try { try {
if (actionType === 'activate') { const user = state.selectedUser;
await activateUser(actionUser.id); if (user.isActive) {
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已激活`); await deactivateUser(user.id);
toast.success(`用户 ${user.fullName || user.username} 已停用`);
} else { } else {
await deactivateUser(actionUser.id); await activateUser(user.id);
toast.success(`用户 ${actionUser.fullName || actionUser.username}停用`); toast.success(`用户 ${user.fullName || user.username}激活`);
} }
// 刷新数据 // 关闭对话框并刷新列表
dispatch({ type: 'TOGGLE_DEACTIVATE_DIALOG', payload: false });
dispatch({ type: 'SET_SELECTED_USER', payload: null });
refreshData(); refreshData();
} catch (error) { } catch (error) {
console.error(`${actionType === 'activate' ? '激活' : '停用'}用户失败:`, error); console.error('切换用户状态失败:', error);
const errorMessage = error instanceof Error ? error.message : `${actionType === 'activate' ? '激活' : '停用'}用户失败,请重试`; const errorMessage = error instanceof Error ? error.message : '操作失败,请重试';
toast.error(errorMessage); toast.error(errorMessage);
} finally {
setStatusDialogOpen(false);
setActionUser(null);
} }
}; };
// 删除用户 - 打开确认弹窗 // 确认删除用户
const handleDeleteUser = (user: User) => { const confirmDelete = async () => {
setActionUser(user); if (!state.selectedUser) return;
setDeleteDialogOpen(true);
};
// 执行删除用户
const handleDeleteConfirm = async () => {
if (!actionUser) return;
try { try {
await deleteUser(actionUser.id); const user = state.selectedUser;
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已删除`); 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(); refreshData();
} catch (error) { } catch (error) {
console.error('删除用户失败:', error); console.error('删除用户失败:', error);
const errorMessage = error instanceof Error ? error.message : '删除用户失败,请重试'; const errorMessage = error instanceof Error ? error.message : '删除失败,请重试';
toast.error(errorMessage); toast.error(errorMessage);
} finally {
setDeleteDialogOpen(false);
setActionUser(null);
} }
}; };
@@ -652,54 +654,55 @@ export default function TenantUserManagementPage() {
onSuccess={refreshData} onSuccess={refreshData}
/> />
{/* 状态切换确认对话框 */} {/* 停用/激活用户确认对话框 */}
<AlertDialog open={statusDialogOpen} onOpenChange={setStatusDialogOpen}> <AlertDialog open={state.showDeactivateDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_DEACTIVATE_DIALOG', payload: open })}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
{actionType === 'activate' ? '激活用户' : '停用用户'} {state.selectedUser?.isActive ? '停用用户' : '激活用户'}
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{actionType === 'activate' ? '激活' : '停用'} {state.selectedUser?.isActive ? '停用' : '激活'} <strong>{state.selectedUser?.fullName || state.selectedUser?.username}</strong>
<span className="font-semibold"> {state.selectedUser?.isActive && (
{actionUser?.fullName || actionUser?.username} <span className="block mt-2 text-amber-600 dark:text-amber-400">
</span> </span>
)}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel> <AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction <Button
onClick={handleStatusConfirm} onClick={confirmToggleStatus}
className={actionType === 'activate' ? 'bg-green-600 hover:bg-green-700' : 'bg-orange-600 hover:bg-orange-700'} className={state.selectedUser?.isActive ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'}
> >
{actionType === 'activate' ? '激活' : '停用'} {state.selectedUser?.isActive ? '停用' : '激活'}
</AlertDialogAction> </Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* 删除用户确认对话框 */} {/* 删除用户确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={state.showDeleteDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: open })}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle className="text-red-600"></AlertDialogTitle> <AlertDialogTitle className="text-red-600"></AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<strong>{state.selectedUser?.fullName || state.selectedUser?.username}</strong>
<span className="font-semibold text-red-600"> <span className="block mt-2 text-red-600 dark:text-red-400">
{actionUser?.fullName || actionUser?.username}
</span> </span>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel> <AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction <Button
onClick={handleDeleteConfirm} onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700" className="bg-red-600 hover:bg-red-700"
variant="destructive"
> >
</AlertDialogAction> </Button>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>

View File

@@ -46,8 +46,6 @@ export function MultiFactorEvaluation() {
const [selectedField, setSelectedField] = useState('field-1'); const [selectedField, setSelectedField] = useState('field-1');
const [showWeightConfig, setShowWeightConfig] = useState(false); const [showWeightConfig, setShowWeightConfig] = useState(false);
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false); const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
// 评价因子权重配置 // 评价因子权重配置
const [factorWeights, setFactorWeights] = useState<FactorWeight[]>([ const [factorWeights, setFactorWeights] = useState<FactorWeight[]>([
{ id: 'ph', name: 'pH值', weight: 20, unit: '' }, { id: 'ph', name: 'pH值', weight: 20, unit: '' },
@@ -67,7 +65,6 @@ export function MultiFactorEvaluation() {
// 获取当前选中的地块结果 // 获取当前选中的地块结果
const currentResult = const currentResult =
evaluationResults.find(r => r.fieldId === selectedField) || evaluationResults.find(r => r.fieldId === selectedField) ||
batchAnalysisResults.find(r => r.fieldId === selectedField) ||
evaluationResults[0]; evaluationResults[0];
// 批量分析处理函数 // 批量分析处理函数

View File

@@ -43,7 +43,7 @@ interface CropKnowledgeEntry {
riskFactors: CropRiskFactor[]; riskFactors: CropRiskFactor[];
} }
type MatchStatus = '??' | '???' | '??'; type MatchStatus = '??' | '??' | '??';
interface MatchDetail { interface MatchDetail {
factor: string; factor: string;
@@ -55,7 +55,7 @@ interface MatchDetail {
interface RecommendationResult { interface RecommendationResult {
crop: CropKnowledgeEntry; crop: CropKnowledgeEntry;
matchScore: number; matchScore: number;
suitabilityLevel: '????' | '??' | '????' | '???'; suitabilityLevel: '????' | '??' | '????' | '??';
matchDetails: MatchDetail[]; matchDetails: MatchDetail[];
applicableRisks: CropRiskFactor[]; applicableRisks: CropRiskFactor[];
expectedYield: [number, number]; expectedYield: [number, number];
@@ -201,24 +201,35 @@ interface CropRecommendationsProps {
export function CropRecommendations({ currentResult }: CropRecommendationsProps) { export function CropRecommendations({ currentResult }: CropRecommendationsProps) {
// 匹配作物推荐 // 匹配作物推荐
const matchCropsForField = (fieldFactors: any) => { const matchCropsForField = (fieldFactors: FieldFactors): RecommendationResult[] => {
return cropKnowledgeBase.map(crop => { const factorLabelMap: Record<SoilFactorKey, string> = {
ph: 'pH?',
organicMatter: '??',
soilDepth: '????',
nitrogen: '??',
phosphorus: '??',
potassium: '??',
drainage: '??'
};
return cropKnowledgeBase.map((crop) => {
let totalScore = 0; let totalScore = 0;
let factorCount = 0; let factorCount = 0;
const matchDetails: any[] = []; const matchDetails: MatchDetail[] = [];
// 评估土壤因子匹配度 (Object.entries(crop.soilRequirements) as Array<[SoilFactorKey, RangeRequirement]>).forEach(([factor, requirements]) => {
Object.entries(crop.soilRequirements).forEach(([factor, requirements]: [string, any]) => {
if (fieldFactors[factor]) {
const value = fieldFactors[factor]; const value = fieldFactors[factor];
const { optimal, acceptable } = requirements; if (typeof value !== 'number') {
return;
}
const { optimal, acceptable } = requirements;
let score = 0; let score = 0;
let status = '偏离'; let status: MatchStatus = '??';
if (value >= optimal[0] && value <= optimal[1]) { if (value >= optimal[0] && value <= optimal[1]) {
score = 100; score = 100;
status = '最佳'; status = '??';
} else if (value >= acceptable[0] && value <= acceptable[1]) { } else if (value >= acceptable[0] && value <= acceptable[1]) {
const deviation = Math.min( const deviation = Math.min(
Math.abs(value - optimal[0]), Math.abs(value - optimal[0]),
@@ -226,62 +237,64 @@ export function CropRecommendations({ currentResult }: CropRecommendationsProps)
); );
const range = optimal[1] - optimal[0]; const range = optimal[1] - optimal[0];
score = Math.max(60, 100 - (deviation / range) * 40); score = Math.max(60, 100 - (deviation / range) * 40);
status = '可接受'; status = '??';
} else { } else {
score = Math.max(0, 60 - Math.min( score = Math.max(
0,
60 -
Math.min(
Math.abs(value - acceptable[0]), Math.abs(value - acceptable[0]),
Math.abs(value - acceptable[1]) Math.abs(value - acceptable[1])
) * 2); ) *
2
);
} }
totalScore += score; totalScore += score;
factorCount++; factorCount += 1;
matchDetails.push({ matchDetails.push({
factor: factor === 'ph' ? 'pH值' : factor: factorLabelMap[factor],
factor === 'organicMatter' ? '有机质' :
factor === 'soilDepth' ? '土层厚度' :
factor === 'nitrogen' ? '全氮' :
factor === 'phosphorus' ? '全磷' :
factor === 'potassium' ? '全钾' : '排水性',
value, value,
score, score,
status status
}); });
}
}); });
// 评估气候因子(简化处理) if (typeof fieldFactors.temperature === 'number') {
if (fieldFactors.temperature) { const tempScore =
const tempScore = fieldFactors.temperature >= 18 && fieldFactors.temperature <= 25 ? 90 : 70; fieldFactors.temperature >= 18 && fieldFactors.temperature <= 25 ? 90 : 70;
totalScore += tempScore; totalScore += tempScore;
factorCount++; factorCount += 1;
} }
if (fieldFactors.rainfall) { if (typeof fieldFactors.rainfall === 'number') {
const rainScore = fieldFactors.rainfall >= 500 && fieldFactors.rainfall <= 800 ? 90 : 70; const rainScore =
fieldFactors.rainfall >= 500 && fieldFactors.rainfall <= 800 ? 90 : 70;
totalScore += rainScore; totalScore += rainScore;
factorCount++; factorCount += 1;
} }
const matchScore = Math.round(totalScore / factorCount); const matchScore = factorCount > 0 ? Math.round(totalScore / factorCount) : 0;
// 确定适宜性等级 let suitabilityLevel: RecommendationResult['suitabilityLevel'] = '??';
let suitabilityLevel = '不推荐'; if (matchScore >= 85) suitabilityLevel = '????';
if (matchScore >= 85) suitabilityLevel = '高度推荐'; else if (matchScore >= 70) suitabilityLevel = '??';
else if (matchScore >= 70) suitabilityLevel = '推荐'; else if (matchScore >= 50) suitabilityLevel = '????';
else if (matchScore >= 50) suitabilityLevel = '谨慎种植';
// 根据适宜性等级选择产量区间 let expectedYield: [number, number] = crop.expectedYield.low;
let expectedYield = crop.expectedYield.low; if (suitabilityLevel === '????') expectedYield = crop.expectedYield.high;
if (suitabilityLevel === '高度推荐') expectedYield = crop.expectedYield.high; else if (suitabilityLevel === '??') expectedYield = crop.expectedYield.medium;
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('drought') && fieldFactors.rainfall < 400) return true;
if (risk.id.includes('rust') && fieldFactors.temperature >= 15 && fieldFactors.temperature <= 22) return true; if (
return true; // 简化处理,默认显示所有风险 risk.id.includes('rust') &&
fieldFactors.temperature >= 15 &&
fieldFactors.temperature <= 22
)
return true;
return false;
}); });
return { return {
@@ -292,20 +305,25 @@ export function CropRecommendations({ currentResult }: CropRecommendationsProps)
applicableRisks, applicableRisks,
expectedYield expectedYield
}; };
}).sort((a, b) => b.matchScore - a.matchScore); });
}; };
// 获取地块因子数据 const getFactorValue = (factorId: string) =>
const fieldFactors = { currentResult.factors.find((factor) => factor.id === factorId)?.value ?? 0;
ph: currentResult.factors.find(f => f.id === 'ph')?.value || 0,
organic: currentResult.factors.find(f => f.id === 'organic')?.value || 0, const fieldFactors: FieldFactors = {
depth: currentResult.factors.find(f => f.id === 'depth')?.value || 0, ph: getFactorValue('ph'),
nitrogen: currentResult.factors.find(f => f.id === 'nitrogen')?.value || 0, organicMatter: getFactorValue('organic'),
phosphorus: currentResult.factors.find(f => f.id === 'phosphorus')?.value || 0, soilDepth: getFactorValue('depth'),
potassium: currentResult.factors.find(f => f.id === 'potassium')?.value || 0, nitrogen: getFactorValue('nitrogen'),
drainage: currentResult.factors.find(f => f.id === 'drainage')?.value || 0, phosphorus: getFactorValue('phosphorus'),
temperature: 22, // 模拟年均温度 potassium: getFactorValue('potassium'),
rainfall: 800, // 模拟年均降雨量 drainage: getFactorValue('drainage'),
temperature: 22, // ??????
rainfall: 800 // ???????
};
// // 模拟年均降雨量
}; };
// 匹配推荐作物 // 匹配推荐作物

View File

@@ -27,8 +27,8 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useAuth } from '@/components/auth/AuthContext'; import { useAuth } from '@/components/auth/AuthContext';
import { authReducer, initialAuthState, AuthState, AuthAction } from './authReducer'; import { authReducer, initialAuthState } from './authReducer';
import { getCaptchaApiV1AuthCaptchaGet, loginApiV1AuthLoginPost } from '@/lib/api/sdk.gen'; import { loginApiV1AuthLoginPost } from '@/lib/api/sdk.gen';
import type { CaptchaResponse } from '@/lib/api/types.gen'; import type { CaptchaResponse } from '@/lib/api/types.gen';
import {PERSONAL_CELTRAL_PAGE} from "@/config/constants" import {PERSONAL_CELTRAL_PAGE} from "@/config/constants"
interface LoginFormProps { interface LoginFormProps {
@@ -40,7 +40,6 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) {
const [state, dispatch] = React.useReducer(authReducer, initialAuthState); const [state, dispatch] = React.useReducer(authReducer, initialAuthState);
const [loginType, setLoginType] = useState<'password' | 'phone'>('password'); const [loginType, setLoginType] = useState<'password' | 'phone'>('password');
const [passwordCaptchaData, setPasswordCaptchaData] = useState<CaptchaResponse | null>(null); const [passwordCaptchaData, setPasswordCaptchaData] = useState<CaptchaResponse | null>(null);
const [phoneCaptchaData, setPhoneCaptchaData] = useState<CaptchaResponse | null>(null);
// 倒计时效果 // 倒计时效果
useEffect(() => { useEffect(() => {
@@ -154,9 +153,14 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) {
dispatch({ type: 'SET_ERROR', payload: '登录失败,请检查用户名和密码' }); dispatch({ type: 'SET_ERROR', payload: '登录失败,请检查用户名和密码' });
toast.error('登录失败,请检查用户名和密码'); toast.error('登录失败,请检查用户名和密码');
} }
} catch (err: any) { } catch (error: unknown) {
console.error('登录失败:', err); console.error('????:', error);
const errorMessage = err?.response?.data?.message || err?.message || '登录失败,请稍后重试'; 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 }); dispatch({ type: 'SET_ERROR', payload: errorMessage });
toast.error(errorMessage); toast.error(errorMessage);
} finally { } finally {
@@ -204,9 +208,10 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) {
dispatch({ type: 'SET_ERROR', payload: '验证码错误' }); dispatch({ type: 'SET_ERROR', payload: '验证码错误' });
toast.error('验证码错误'); toast.error('验证码错误');
} }
} catch (err) { } catch (error) {
dispatch({ type: 'SET_ERROR', payload: '登录失败,请稍后重试' }); console.error('???????:', error);
toast.error('登录失败'); dispatch({ type: 'SET_ERROR', payload: '??????????' });
toast.error('????');
} finally { } finally {
dispatch({ type: 'SET_LOADING', payload: false }); dispatch({ type: 'SET_LOADING', payload: false });
} }

View File

@@ -172,7 +172,7 @@ export default function RegisterPage() {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
toast.success('验证码发送成功测试验证码123456'); toast.success('验证码发送成功测试验证码123456');
setCountdown(60); setCountdown(60);
} catch (err) { } catch (error) {
setError('发送验证码失败,请稍后重试'); setError('发送验证码失败,请稍后重试');
toast.error('发送验证码失败'); toast.error('发送验证码失败');
} finally { } finally {
@@ -246,7 +246,7 @@ export default function RegisterPage() {
setError('短信验证码错误'); setError('短信验证码错误');
toast.error('短信验证码错误'); toast.error('短信验证码错误');
} }
} catch (err) { } catch (error) {
setError('注册失败,请稍后重试'); setError('注册失败,请稍后重试');
toast.error('注册失败'); toast.error('注册失败');
} finally { } finally {

View File

@@ -6,7 +6,7 @@ import { AuthProvider } from '@/components/auth/AuthContext';
import { ClientAuthInterceptor } from '@/components/auth/ClientAuthInterceptor'; import { ClientAuthInterceptor } from '@/components/auth/ClientAuthInterceptor';
import { ThemeProvider } from 'next-themes'; import { ThemeProvider } from 'next-themes';
import { Toaster } from '@/components/ui/sonner'; 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" import {Navbar1} from "@/components/layouts/Navbar"

View File

@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge'; 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() { export default function NotFound() {
const handleRefresh = () => { const handleRefresh = () => {

View File

@@ -2,8 +2,6 @@ import * as React from "react"
import { ChevronRight } from "lucide-react" import { ChevronRight } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { SearchForm } from "@/components/search-form"
import { VersionSwitcher } from "@/components/version-switcher"
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
@@ -15,7 +13,6 @@ import {
SidebarGroup, SidebarGroup,
SidebarGroupContent, SidebarGroupContent,
SidebarGroupLabel, SidebarGroupLabel,
SidebarHeader,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -19,7 +19,7 @@ export function CaptchaInput({ value, onChange, onCaptchaChange, className = ''
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const fetchCaptcha = async () => { const fetchCaptcha = useCallback(async () => {
setLoading(true); setLoading(true);
setError(''); setError('');
onChange(''); // 清空验证码输入 onChange(''); // 清空验证码输入
@@ -120,11 +120,11 @@ export function CaptchaInput({ value, onChange, onCaptchaChange, className = ''
captcha_id: 'fallback-' + Date.now(), captcha_id: 'fallback-' + Date.now(),
image: canvas.toDataURL() image: canvas.toDataURL()
}; };
}; }, [onCaptchaChange, onChange]);
useEffect(() => { useEffect(() => {
fetchCaptcha(); fetchCaptcha();
}, []); }, [fetchCaptcha]);
const handleRefresh = () => { const handleRefresh = () => {
fetchCaptcha(); fetchCaptcha();

View File

@@ -15,6 +15,21 @@ export function LoadingScreen({
subMessage, subMessage,
variant = 'default' variant = 'default'
}: LoadingScreenProps) { }: 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 ( return (
<div className="min-h-screen flex items-center justify-center relative overflow-hidden"> <div className="min-h-screen flex items-center justify-center relative overflow-hidden">
{/* 智慧大田动态背景 - 使用登录页面相同的背景效果 */} {/* 智慧大田动态背景 - 使用登录页面相同的背景效果 */}

View File

@@ -2,26 +2,7 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
export function SmartFieldBackground() { class SensorNode {
const canvasRef = useRef<HTMLCanvasElement>(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; x: number;
y: number; y: number;
radius: number; radius: number;
@@ -45,20 +26,17 @@ export function SmartFieldBackground() {
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
const pulse = Math.sin(this.pulsePhase) * 0.5 + 0.5; const pulse = Math.sin(this.pulsePhase) * 0.5 + 0.5;
// 节点外圈
ctx.beginPath(); ctx.beginPath();
ctx.arc(this.x, this.y, this.radius + pulse * 4, 0, Math.PI * 2); 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.strokeStyle = `rgba(34, 197, 94, ${0.3 + pulse * 0.3})`;
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
// 节点核心
ctx.beginPath(); ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(34, 197, 94, ${0.8 + pulse * 0.2})`; ctx.fillStyle = `rgba(34, 197, 94, ${0.8 + pulse * 0.2})`;
ctx.fill(); ctx.fill();
// 光晕
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius * 6); 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(0, `rgba(34, 197, 94, ${0.4 * pulse})`);
gradient.addColorStop(1, 'rgba(34, 197, 94, 0)'); gradient.addColorStop(1, 'rgba(34, 197, 94, 0)');
@@ -81,7 +59,6 @@ export function SmartFieldBackground() {
ctx.lineWidth = 1; ctx.lineWidth = 1;
ctx.stroke(); ctx.stroke();
// 数据流动效果
const dataFlowPhase = (Date.now() / 1000) % 1; const dataFlowPhase = (Date.now() / 1000) % 1;
const flowX = this.x + (node.x - this.x) * dataFlowPhase; const flowX = this.x + (node.x - this.x) * dataFlowPhase;
const flowY = this.y + (node.y - this.y) * dataFlowPhase; const flowY = this.y + (node.y - this.y) * dataFlowPhase;
@@ -93,10 +70,12 @@ export function SmartFieldBackground() {
} }
}); });
} }
} }
// 无人机 type BoundsSupplier = () => { width: number; height: number };
class Drone { type DroneTrailPoint = { x: number; y: number; alpha: number };
class Drone {
x: number; x: number;
y: number; y: number;
targetX: number; targetX: number;
@@ -104,11 +83,14 @@ export function SmartFieldBackground() {
speed: number; speed: number;
size: number; size: number;
rotorPhase: number; rotorPhase: number;
trail: { x: number; y: number; alpha: number }[]; trail: DroneTrailPoint[];
private readonly getBounds: BoundsSupplier;
constructor() { constructor(getBounds: BoundsSupplier) {
this.x = Math.random() * canvas.width; this.getBounds = getBounds;
this.y = Math.random() * canvas.height; const { width, height } = this.getBounds();
this.x = Math.random() * width;
this.y = Math.random() * height;
this.targetX = this.x; this.targetX = this.x;
this.targetY = this.y; this.targetY = this.y;
this.speed = 1.5; this.speed = 1.5;
@@ -119,8 +101,9 @@ export function SmartFieldBackground() {
} }
setNewTarget() { setNewTarget() {
this.targetX = Math.random() * canvas.width; const { width, height } = this.getBounds();
this.targetY = Math.random() * canvas.height; this.targetX = Math.random() * width;
this.targetY = Math.random() * height;
} }
update() { update() {
@@ -137,7 +120,6 @@ export function SmartFieldBackground() {
this.rotorPhase += 0.3; this.rotorPhase += 0.3;
// 更新轨迹
this.trail.push({ x: this.x, y: this.y, alpha: 1 }); this.trail.push({ x: this.x, y: this.y, alpha: 1 });
if (this.trail.length > 30) { if (this.trail.length > 30) {
this.trail.shift(); this.trail.shift();
@@ -148,7 +130,6 @@ export function SmartFieldBackground() {
} }
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D) {
// 绘制轨迹
this.trail.forEach((point, index) => { this.trail.forEach((point, index) => {
if (index > 0) { if (index > 0) {
const prev = this.trail[index - 1]; const prev = this.trail[index - 1];
@@ -161,15 +142,12 @@ export function SmartFieldBackground() {
} }
}); });
// 无人机机身
ctx.save(); ctx.save();
ctx.translate(this.x, this.y); ctx.translate(this.x, this.y);
// 机身
ctx.fillStyle = 'rgba(59, 130, 246, 0.9)'; ctx.fillStyle = 'rgba(59, 130, 246, 0.9)';
ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size); ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size);
// 四个螺旋桨
const rotorPositions = [ 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 },
@@ -196,28 +174,18 @@ export function SmartFieldBackground() {
ctx.restore(); ctx.restore();
}); });
// 扫描光束
const scanRadius = 40; const scanRadius = 40;
ctx.beginPath(); ctx.beginPath();
ctx.arc(0, 0, scanRadius, 0, Math.PI * 2); ctx.arc(0, 0, scanRadius, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(59, 130, 246, 0.3)'; ctx.strokeStyle = 'rgba(59, 130, 246, 0.2)';
ctx.lineWidth = 1; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
}
}
// 数据上传指示 class FieldWave {
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; offset: number;
speed: number; speed: number;
amplitude: number; amplitude: number;
@@ -236,11 +204,11 @@ export function SmartFieldBackground() {
this.offset += this.speed; this.offset += this.speed;
} }
draw(ctx: CanvasRenderingContext2D) { draw(ctx: CanvasRenderingContext2D, canvasWidth: number) {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, this.y); ctx.moveTo(0, this.y);
for (let x = 0; x <= canvas.width; x += 5) { for (let x = 0; x <= canvasWidth; x += 5) {
const waveY = this.y + Math.sin((x + this.offset) * this.frequency) * this.amplitude; const waveY = this.y + Math.sin((x + this.offset) * this.frequency) * this.amplitude;
ctx.lineTo(x, waveY); ctx.lineTo(x, waveY);
} }
@@ -249,10 +217,9 @@ export function SmartFieldBackground() {
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.stroke(); ctx.stroke();
} }
} }
// 数据流粒子 class DataParticle {
class DataParticle {
x: number; x: number;
y: number; y: number;
vx: number; vx: number;
@@ -286,7 +253,6 @@ export function SmartFieldBackground() {
ctx.fillStyle = `rgba(34, 197, 94, ${alpha * 0.8})`; ctx.fillStyle = `rgba(34, 197, 94, ${alpha * 0.8})`;
ctx.fill(); ctx.fill();
// 光晕
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size * 3); 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(0, `rgba(34, 197, 94, ${alpha * 0.4})`);
gradient.addColorStop(1, 'rgba(34, 197, 94, 0)'); gradient.addColorStop(1, 'rgba(34, 197, 94, 0)');
@@ -299,7 +265,33 @@ export function SmartFieldBackground() {
isDead() { isDead() {
return this.life >= this.maxLife; return this.life >= this.maxLife;
} }
} }
export function SmartFieldBackground() {
const canvasRef = useRef<HTMLCanvasElement>(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);
// 田间传感器节点
// 无人机
// 田地波浪效果
// 数据流粒子
// 初始化传感器网络 // 初始化传感器网络
const sensors: SensorNode[] = []; const sensors: SensorNode[] = [];
@@ -325,9 +317,10 @@ export function SmartFieldBackground() {
}); });
// 初始化无人机 // 初始化无人机
const canvasBounds = () => ({ width: canvas.width, height: canvas.height });
const drones: Drone[] = []; const drones: Drone[] = [];
for (let i = 0; i < 2; i++) { 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 => { waves.forEach(wave => {
wave.update(); wave.update();
wave.draw(ctx); wave.draw(ctx, canvas.width);
}); });
// 绘制传感器连接线 // 绘制传感器连接线

View File

@@ -911,7 +911,7 @@ export class SpatialIndex {
}): Field[] { }): Field[] {
const results: Field[] = []; const results: Field[] = [];
for (const [_, item] of this.rtree) { for (const [, item] of this.rtree) {
if (this._bboxesIntersect(bbox, item.bbox)) { if (this._bboxesIntersect(bbox, item.bbox)) {
results.push(item.field); results.push(item.field);
} }

View File

@@ -1,7 +1,7 @@
// 通用类型定义 // 通用类型定义
// API响应类型 // API响应类型
export interface ApiResponse<T = any> { export interface ApiResponse<T = unknown> {
code: number code: number
message: string message: string
data: T data: T
@@ -34,11 +34,11 @@ export interface SortParams {
// 搜索类型 // 搜索类型
export interface SearchParams { export interface SearchParams {
keyword?: string keyword?: string
filters?: Record<string, any> filters?: Record<string, unknown>
} }
// 表格列定义 // 表格列定义
export interface ColumnDef<T = any> { export interface ColumnDef<T = Record<string, unknown>> {
key: string key: string
title: string title: string
dataIndex: keyof T dataIndex: keyof T
@@ -46,7 +46,7 @@ export interface ColumnDef<T = any> {
fixed?: 'left' | 'right' fixed?: 'left' | 'right'
sortable?: boolean sortable?: boolean
filterable?: 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' type: 'text' | 'number' | 'select' | 'date' | 'textarea' | 'checkbox' | 'radio'
required?: boolean required?: boolean
placeholder?: string placeholder?: string
options?: Array<{ label: string; value: any }> options?: Array<{ label: string; value: string | number | boolean }>
validation?: { validation?: {
min?: number min?: number
max?: number max?: number
@@ -343,7 +343,7 @@ export interface AppConfig {
// 路由类型 // 路由类型
export interface Route { export interface Route {
path: string path: string
component: React.ComponentType<any> component: React.ComponentType<unknown>
exact?: boolean exact?: boolean
title?: string title?: string
icon?: React.ReactNode icon?: React.ReactNode
@@ -390,7 +390,7 @@ export interface DashboardStats {
export interface AppError { export interface AppError {
code: string code: string
message: string message: string
details?: any details?: unknown
timestamp: string timestamp: string
} }
@@ -402,7 +402,7 @@ export interface LogEntry {
module: string module: string
userId?: string userId?: string
timestamp: string timestamp: string
metadata?: Record<string, any> metadata?: Record<string, unknown>
} }
export enum LogLevel { export enum LogLevel {

View File

@@ -37,7 +37,7 @@ export interface MessageLog {
readTime?: string; readTime?: string;
failReason?: string; failReason?: string;
retryCount: number; retryCount: number;
variables?: Record<string, any>; variables?: Record<string, unknown>;
} }
// 消息发送记录 // 消息发送记录

View File

@@ -57,7 +57,7 @@ export interface DataDictionary {
description?: string; description?: string;
isSystem: boolean; // 是否系统内置 isSystem: boolean; // 是否系统内置
isActive: boolean; isActive: boolean;
extendData?: Record<string, any>; extendData?: Record<string, unknown>;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }

View File

@@ -15,7 +15,7 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve", "jsx": "react-jsx",
/** TODO: */ /** TODO: */
"noImplicitAny": false, "noImplicitAny": false,
"strictNullChecks": false, "strictNullChecks": false,
@@ -70,7 +70,8 @@
}, },
"include": [ "include": [
"src", "src",
".next/types/**/*.ts" ".next/types/**/*.ts",
".next/dev/types/**/*.ts"
], ],
"paths": { "paths": {
"@/*": [ "@/*": [