生产管理系统 - 激活、删除的联调
This commit is contained in:
@@ -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<User | null>(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 ? (
|
||||
<UserX className="w-4 h-4 text-orange-600" />
|
||||
@@ -290,9 +294,9 @@ export default function TenantUserManagementPage() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUser(user)}
|
||||
onClick={() => handleDelete(user)}
|
||||
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" />
|
||||
</Button>
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
{/* 状态切换确认对话框 */}
|
||||
<AlertDialog open={statusDialogOpen} onOpenChange={setStatusDialogOpen}>
|
||||
{/* 停用/激活用户确认对话框 */}
|
||||
<AlertDialog open={state.showDeactivateDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_DEACTIVATE_DIALOG', payload: open })}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{actionType === 'activate' ? '激活用户' : '停用用户'}
|
||||
{state.selectedUser?.isActive ? '停用用户' : '激活用户'}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要{actionType === 'activate' ? '激活' : '停用'}用户
|
||||
<span className="font-semibold">
|
||||
{actionUser?.fullName || actionUser?.username}
|
||||
确定要{state.selectedUser?.isActive ? '停用' : '激活'}用户 <strong>{state.selectedUser?.fullName || state.selectedUser?.username}</strong> 吗?
|
||||
{state.selectedUser?.isActive && (
|
||||
<span className="block mt-2 text-amber-600 dark:text-amber-400">
|
||||
停用后,该用户将无法登录系统。
|
||||
</span>
|
||||
吗?此操作将影响该用户的登录权限。
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleStatusConfirm}
|
||||
className={actionType === 'activate' ? 'bg-green-600 hover:bg-green-700' : 'bg-orange-600 hover:bg-orange-700'}
|
||||
<Button
|
||||
onClick={confirmToggleStatus}
|
||||
className={state.selectedUser?.isActive ? 'bg-orange-600 hover:bg-orange-700' : 'bg-green-600 hover:bg-green-700'}
|
||||
>
|
||||
确认{actionType === 'activate' ? '激活' : '停用'}
|
||||
</AlertDialogAction>
|
||||
{state.selectedUser?.isActive ? '停用' : '激活'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 删除用户确认对话框 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialog open={state.showDeleteDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_DELETE_DIALOG', payload: open })}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-red-600">删除用户</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除用户
|
||||
<span className="font-semibold text-red-600">
|
||||
{actionUser?.fullName || actionUser?.username}
|
||||
确定要删除用户 <strong>{state.selectedUser?.fullName || state.selectedUser?.username}</strong> 吗?
|
||||
<span className="block mt-2 text-red-600 dark:text-red-400">
|
||||
⚠️ 此操作不可恢复,用户的所有数据将被永久删除!
|
||||
</span>
|
||||
吗?此操作不可恢复,该用户的所有数据将被永久删除。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteConfirm}
|
||||
<Button
|
||||
onClick={confirmDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
variant="destructive"
|
||||
>
|
||||
确认删除
|
||||
</AlertDialogAction>
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
@@ -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<SuitabilityResult[]>([]);
|
||||
|
||||
// 评价因子权重配置
|
||||
const [factorWeights, setFactorWeights] = useState<FactorWeight[]>([
|
||||
{ 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];
|
||||
|
||||
// 批量分析处理函数
|
||||
|
||||
@@ -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,24 +201,35 @@ interface CropRecommendationsProps {
|
||||
|
||||
export function CropRecommendations({ currentResult }: CropRecommendationsProps) {
|
||||
// 匹配作物推荐
|
||||
const matchCropsForField = (fieldFactors: any) => {
|
||||
return cropKnowledgeBase.map(crop => {
|
||||
const matchCropsForField = (fieldFactors: FieldFactors): RecommendationResult[] => {
|
||||
const factorLabelMap: Record<SoilFactorKey, string> = {
|
||||
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]) {
|
||||
(Object.entries(crop.soilRequirements) as Array<[SoilFactorKey, RangeRequirement]>).forEach(([factor, requirements]) => {
|
||||
const value = fieldFactors[factor];
|
||||
const { optimal, acceptable } = requirements;
|
||||
if (typeof value !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { optimal, acceptable } = requirements;
|
||||
let score = 0;
|
||||
let status = '偏离';
|
||||
let status: MatchStatus = '??';
|
||||
|
||||
if (value >= optimal[0] && value <= optimal[1]) {
|
||||
score = 100;
|
||||
status = '最佳';
|
||||
status = '??';
|
||||
} else if (value >= acceptable[0] && value <= acceptable[1]) {
|
||||
const deviation = Math.min(
|
||||
Math.abs(value - optimal[0]),
|
||||
@@ -226,62 +237,64 @@ export function CropRecommendations({ currentResult }: CropRecommendationsProps)
|
||||
);
|
||||
const range = optimal[1] - optimal[0];
|
||||
score = Math.max(60, 100 - (deviation / range) * 40);
|
||||
status = '可接受';
|
||||
status = '??';
|
||||
} else {
|
||||
score = Math.max(0, 60 - Math.min(
|
||||
score = Math.max(
|
||||
0,
|
||||
60 -
|
||||
Math.min(
|
||||
Math.abs(value - acceptable[0]),
|
||||
Math.abs(value - acceptable[1])
|
||||
) * 2);
|
||||
) *
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
totalScore += score;
|
||||
factorCount++;
|
||||
factorCount += 1;
|
||||
|
||||
matchDetails.push({
|
||||
factor: factor === 'ph' ? 'pH值' :
|
||||
factor === 'organicMatter' ? '有机质' :
|
||||
factor === 'soilDepth' ? '土层厚度' :
|
||||
factor === 'nitrogen' ? '全氮' :
|
||||
factor === 'phosphorus' ? '全磷' :
|
||||
factor === 'potassium' ? '全钾' : '排水性',
|
||||
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 // ???????
|
||||
};
|
||||
|
||||
// // 模拟年均降雨量
|
||||
};
|
||||
|
||||
// 匹配推荐作物
|
||||
|
||||
@@ -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,7 +40,6 @@ export function LoginForm({ onRegisterClick }: LoginFormProps) {
|
||||
const [state, dispatch] = React.useReducer(authReducer, initialAuthState);
|
||||
const [loginType, setLoginType] = useState<'password' | 'phone'>('password');
|
||||
const [passwordCaptchaData, setPasswordCaptchaData] = useState<CaptchaResponse | null>(null);
|
||||
const [phoneCaptchaData, setPhoneCaptchaData] = useState<CaptchaResponse | null>(null);
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center relative overflow-hidden">
|
||||
{/* 智慧大田动态背景 - 使用登录页面相同的背景效果 */}
|
||||
|
||||
@@ -2,25 +2,6 @@
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
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);
|
||||
|
||||
// 田间传感器节点
|
||||
class SensorNode {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -45,20 +26,17 @@ export function SmartFieldBackground() {
|
||||
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)');
|
||||
@@ -81,7 +59,6 @@ export function SmartFieldBackground() {
|
||||
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;
|
||||
@@ -95,7 +72,9 @@ export function SmartFieldBackground() {
|
||||
}
|
||||
}
|
||||
|
||||
// 无人机
|
||||
type BoundsSupplier = () => { width: number; height: number };
|
||||
type DroneTrailPoint = { x: number; y: number; alpha: number };
|
||||
|
||||
class Drone {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -104,11 +83,14 @@ export function SmartFieldBackground() {
|
||||
speed: number;
|
||||
size: number;
|
||||
rotorPhase: number;
|
||||
trail: { x: number; y: number; alpha: number }[];
|
||||
trail: DroneTrailPoint[];
|
||||
private readonly getBounds: BoundsSupplier;
|
||||
|
||||
constructor() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
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;
|
||||
@@ -119,8 +101,9 @@ export function SmartFieldBackground() {
|
||||
}
|
||||
|
||||
setNewTarget() {
|
||||
this.targetX = Math.random() * canvas.width;
|
||||
this.targetY = Math.random() * canvas.height;
|
||||
const { width, height } = this.getBounds();
|
||||
this.targetX = Math.random() * width;
|
||||
this.targetY = Math.random() * height;
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -137,7 +120,6 @@ export function SmartFieldBackground() {
|
||||
|
||||
this.rotorPhase += 0.3;
|
||||
|
||||
// 更新轨迹
|
||||
this.trail.push({ x: this.x, y: this.y, alpha: 1 });
|
||||
if (this.trail.length > 30) {
|
||||
this.trail.shift();
|
||||
@@ -148,7 +130,6 @@ export function SmartFieldBackground() {
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
// 绘制轨迹
|
||||
this.trail.forEach((point, index) => {
|
||||
if (index > 0) {
|
||||
const prev = this.trail[index - 1];
|
||||
@@ -161,15 +142,12 @@ export function SmartFieldBackground() {
|
||||
}
|
||||
});
|
||||
|
||||
// 无人机机身
|
||||
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 },
|
||||
@@ -196,27 +174,17 @@ export function SmartFieldBackground() {
|
||||
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.strokeStyle = 'rgba(59, 130, 246, 0.2)';
|
||||
ctx.lineWidth = 2;
|
||||
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;
|
||||
@@ -236,11 +204,11 @@ export function SmartFieldBackground() {
|
||||
this.offset += this.speed;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
draw(ctx: CanvasRenderingContext2D, canvasWidth: number) {
|
||||
ctx.beginPath();
|
||||
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;
|
||||
ctx.lineTo(x, waveY);
|
||||
}
|
||||
@@ -251,7 +219,6 @@ export function SmartFieldBackground() {
|
||||
}
|
||||
}
|
||||
|
||||
// 数据流粒子
|
||||
class DataParticle {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -286,7 +253,6 @@ export function SmartFieldBackground() {
|
||||
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)');
|
||||
@@ -301,6 +267,32 @@ export function SmartFieldBackground() {
|
||||
}
|
||||
}
|
||||
|
||||
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 gridSize = 150;
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
// 绘制传感器连接线
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 通用类型定义
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
@@ -34,11 +34,11 @@ export interface SortParams {
|
||||
// 搜索类型
|
||||
export interface SearchParams {
|
||||
keyword?: string
|
||||
filters?: Record<string, any>
|
||||
filters?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
export interface ColumnDef<T = any> {
|
||||
export interface ColumnDef<T = Record<string, unknown>> {
|
||||
key: string
|
||||
title: string
|
||||
dataIndex: keyof T
|
||||
@@ -46,7 +46,7 @@ export interface ColumnDef<T = any> {
|
||||
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<any>
|
||||
component: React.ComponentType<unknown>
|
||||
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<string, any>
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface MessageLog {
|
||||
readTime?: string;
|
||||
failReason?: string;
|
||||
retryCount: number;
|
||||
variables?: Record<string, any>;
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 消息发送记录
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface DataDictionary {
|
||||
description?: string;
|
||||
isSystem: boolean; // 是否系统内置
|
||||
isActive: boolean;
|
||||
extendData?: Record<string, any>;
|
||||
extendData?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"jsx": "react-jsx",
|
||||
/** TODO: 暂时禁用校验 */
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false,
|
||||
@@ -70,7 +70,8 @@
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
".next/types/**/*.ts"
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
|
||||
Reference in New Issue
Block a user