生产管理系统 - 激活、删除的联调
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
// 批量分析处理函数
|
// 批量分析处理函数
|
||||||
|
|||||||
@@ -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,87 +201,100 @@ 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]) => {
|
const value = fieldFactors[factor];
|
||||||
if (fieldFactors[factor]) {
|
if (typeof value !== 'number') {
|
||||||
const value = fieldFactors[factor];
|
return;
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (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 // ???????
|
||||||
|
};
|
||||||
|
|
||||||
|
// // 模拟年均降雨量
|
||||||
};
|
};
|
||||||
|
|
||||||
// 匹配推荐作物
|
// 匹配推荐作物
|
||||||
|
|||||||
@@ -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,8 +40,7 @@ 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(() => {
|
||||||
if (state.countdown > 0) {
|
if (state.countdown > 0) {
|
||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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">
|
||||||
{/* 智慧大田动态背景 - 使用登录页面相同的背景效果 */}
|
{/* 智慧大田动态背景 - 使用登录页面相同的背景效果 */}
|
||||||
|
|||||||
@@ -2,6 +2,271 @@
|
|||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
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() {
|
export function SmartFieldBackground() {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
@@ -21,285 +286,12 @@ export function SmartFieldBackground() {
|
|||||||
window.addEventListener('resize', resizeCanvas);
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
|
||||||
// 田间传感器节点
|
// 田间传感器节点
|
||||||
class SensorNode {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
radius: number;
|
|
||||||
pulsePhase: number;
|
|
||||||
pulseSpeed: number;
|
|
||||||
connections: SensorNode[];
|
|
||||||
|
|
||||||
constructor(x: number, y: number) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.radius = 4;
|
|
||||||
this.pulsePhase = Math.random() * Math.PI * 2;
|
|
||||||
this.pulseSpeed = 0.03 + Math.random() * 0.02;
|
|
||||||
this.connections = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.pulsePhase += this.pulseSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(ctx: CanvasRenderingContext2D) {
|
|
||||||
const pulse = Math.sin(this.pulsePhase) * 0.5 + 0.5;
|
|
||||||
|
|
||||||
// 节点外圈
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, this.radius + pulse * 4, 0, Math.PI * 2);
|
|
||||||
ctx.strokeStyle = `rgba(34, 197, 94, ${0.3 + pulse * 0.3})`;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 节点核心
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = `rgba(34, 197, 94, ${0.8 + pulse * 0.2})`;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 光晕
|
|
||||||
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius * 6);
|
|
||||||
gradient.addColorStop(0, `rgba(34, 197, 94, ${0.4 * pulse})`);
|
|
||||||
gradient.addColorStop(1, 'rgba(34, 197, 94, 0)');
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, this.radius * 6, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
drawConnections(ctx: CanvasRenderingContext2D) {
|
|
||||||
this.connections.forEach(node => {
|
|
||||||
const distance = Math.hypot(node.x - this.x, node.y - this.y);
|
|
||||||
const opacity = Math.max(0, 1 - distance / 250);
|
|
||||||
|
|
||||||
if (opacity > 0) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(this.x, this.y);
|
|
||||||
ctx.lineTo(node.x, node.y);
|
|
||||||
ctx.strokeStyle = `rgba(34, 197, 94, ${opacity * 0.2})`;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// 数据流动效果
|
|
||||||
const dataFlowPhase = (Date.now() / 1000) % 1;
|
|
||||||
const flowX = this.x + (node.x - this.x) * dataFlowPhase;
|
|
||||||
const flowY = this.y + (node.y - this.y) * dataFlowPhase;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(flowX, flowY, 2, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = `rgba(34, 197, 94, ${opacity * 0.6})`;
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无人机
|
// 无人机
|
||||||
class Drone {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
targetX: number;
|
|
||||||
targetY: number;
|
|
||||||
speed: number;
|
|
||||||
size: number;
|
|
||||||
rotorPhase: number;
|
|
||||||
trail: { x: number; y: number; alpha: number }[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.x = Math.random() * canvas.width;
|
|
||||||
this.y = Math.random() * canvas.height;
|
|
||||||
this.targetX = this.x;
|
|
||||||
this.targetY = this.y;
|
|
||||||
this.speed = 1.5;
|
|
||||||
this.size = 12;
|
|
||||||
this.rotorPhase = 0;
|
|
||||||
this.trail = [];
|
|
||||||
this.setNewTarget();
|
|
||||||
}
|
|
||||||
|
|
||||||
setNewTarget() {
|
|
||||||
this.targetX = Math.random() * canvas.width;
|
|
||||||
this.targetY = Math.random() * canvas.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
const dx = this.targetX - this.x;
|
|
||||||
const dy = this.targetY - this.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (distance < 10) {
|
|
||||||
this.setNewTarget();
|
|
||||||
} else {
|
|
||||||
this.x += (dx / distance) * this.speed;
|
|
||||||
this.y += (dy / distance) * this.speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rotorPhase += 0.3;
|
|
||||||
|
|
||||||
// 更新轨迹
|
|
||||||
this.trail.push({ x: this.x, y: this.y, alpha: 1 });
|
|
||||||
if (this.trail.length > 30) {
|
|
||||||
this.trail.shift();
|
|
||||||
}
|
|
||||||
this.trail.forEach((point, index) => {
|
|
||||||
point.alpha = index / this.trail.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(ctx: CanvasRenderingContext2D) {
|
|
||||||
// 绘制轨迹
|
|
||||||
this.trail.forEach((point, index) => {
|
|
||||||
if (index > 0) {
|
|
||||||
const prev = this.trail[index - 1];
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(prev.x, prev.y);
|
|
||||||
ctx.lineTo(point.x, point.y);
|
|
||||||
ctx.strokeStyle = `rgba(59, 130, 246, ${point.alpha * 0.3})`;
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 无人机机身
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(this.x, this.y);
|
|
||||||
|
|
||||||
// 机身
|
|
||||||
ctx.fillStyle = 'rgba(59, 130, 246, 0.9)';
|
|
||||||
ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size);
|
|
||||||
|
|
||||||
// 四个螺旋桨
|
|
||||||
const rotorPositions = [
|
|
||||||
{ x: -this.size * 0.7, y: -this.size * 0.7 },
|
|
||||||
{ x: this.size * 0.7, y: -this.size * 0.7 },
|
|
||||||
{ x: -this.size * 0.7, y: this.size * 0.7 },
|
|
||||||
{ x: this.size * 0.7, y: this.size * 0.7 },
|
|
||||||
];
|
|
||||||
|
|
||||||
rotorPositions.forEach(pos => {
|
|
||||||
ctx.save();
|
|
||||||
ctx.translate(pos.x, pos.y);
|
|
||||||
ctx.rotate(this.rotorPhase);
|
|
||||||
|
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(-6, 0);
|
|
||||||
ctx.lineTo(6, 0);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, -6);
|
|
||||||
ctx.lineTo(0, 6);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 扫描光束
|
|
||||||
const scanRadius = 40;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(0, 0, scanRadius, 0, Math.PI * 2);
|
|
||||||
ctx.strokeStyle = 'rgba(59, 130, 246, 0.3)';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
// 数据上传指示
|
|
||||||
if (Math.random() < 0.1) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = 'rgba(34, 197, 94, 0.8)';
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 田地波浪效果
|
// 田地波浪效果
|
||||||
class FieldWave {
|
|
||||||
offset: number;
|
|
||||||
speed: number;
|
|
||||||
amplitude: number;
|
|
||||||
frequency: number;
|
|
||||||
y: number;
|
|
||||||
|
|
||||||
constructor(y: number) {
|
|
||||||
this.offset = Math.random() * 1000;
|
|
||||||
this.speed = 0.5 + Math.random() * 0.5;
|
|
||||||
this.amplitude = 15 + Math.random() * 10;
|
|
||||||
this.frequency = 0.01 + Math.random() * 0.01;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.offset += this.speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(ctx: CanvasRenderingContext2D) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, this.y);
|
|
||||||
|
|
||||||
for (let x = 0; x <= canvas.width; x += 5) {
|
|
||||||
const waveY = this.y + Math.sin((x + this.offset) * this.frequency) * this.amplitude;
|
|
||||||
ctx.lineTo(x, waveY);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.strokeStyle = 'rgba(34, 197, 94, 0.15)';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据流粒子
|
// 数据流粒子
|
||||||
class DataParticle {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
vx: number;
|
|
||||||
vy: number;
|
|
||||||
life: number;
|
|
||||||
maxLife: number;
|
|
||||||
size: number;
|
|
||||||
|
|
||||||
constructor(x: number, y: number) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
const angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.5;
|
|
||||||
const speed = 2 + Math.random() * 2;
|
|
||||||
this.vx = Math.cos(angle) * speed;
|
|
||||||
this.vy = Math.sin(angle) * speed;
|
|
||||||
this.life = 0;
|
|
||||||
this.maxLife = 60 + Math.random() * 40;
|
|
||||||
this.size = 2 + Math.random() * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
update() {
|
|
||||||
this.x += this.vx;
|
|
||||||
this.y += this.vy;
|
|
||||||
this.life++;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(ctx: CanvasRenderingContext2D) {
|
|
||||||
const alpha = 1 - this.life / this.maxLife;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = `rgba(34, 197, 94, ${alpha * 0.8})`;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// 光晕
|
|
||||||
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size * 3);
|
|
||||||
gradient.addColorStop(0, `rgba(34, 197, 94, ${alpha * 0.4})`);
|
|
||||||
gradient.addColorStop(1, 'rgba(34, 197, 94, 0)');
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(this.x, this.y, this.size * 3, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
|
|
||||||
isDead() {
|
|
||||||
return this.life >= this.maxLife;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化传感器网络
|
// 初始化传感器网络
|
||||||
const sensors: SensorNode[] = [];
|
const 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绘制传感器连接线
|
// 绘制传感器连接线
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息发送记录
|
// 消息发送记录
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +70,10 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user