生产管理系统前端 - 更新瓦力提交的产品原型到参考目录

This commit is contained in:
2025-10-23 10:57:14 +08:00
parent 83523dad64
commit 28229ce795
354 changed files with 147599 additions and 7892 deletions

View File

@@ -0,0 +1,404 @@
// 变更历史示例数据
import { MachineryChangeHistory } from '../types/machinery';
import { machineryStorage } from './machineryStorage';
/**
* 初始化变更历史示例数据
* 为每台农机创建真实的变更记录
*/
export function initializeChangeHistoryMockData() {
// 检查是否已有变更历史数据
const allMachinery = machineryStorage.getAllMachinery();
if (allMachinery.length === 0) {
console.log('⚠️ 没有农机数据,无法创建变更历史示例');
return;
}
// 检查是否已有变更历史
const existingHistory = allMachinery.reduce((count, m) => {
return count + machineryStorage.getChangeHistory(m.id).length;
}, 0);
if (existingHistory > 0) {
console.log(' 已存在变更历史数据,跳过初始化');
return;
}
const now = new Date();
const changes: MachineryChangeHistory[] = [];
// ==================== 农机1约翰迪尔拖拉机变更历史 ====================
if (allMachinery[0]) {
const machinery1Id = allMachinery[0].id;
// 1. 设备状态变更
changes.push({
id: `change-${Date.now()}-001`,
machineryId: machinery1Id,
fieldName: 'status',
fieldLabel: '设备状态',
oldValue: '正常',
newValue: '待维护',
operator: '张三',
operatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3天前
});
// 2. 当前位置变更
changes.push({
id: `change-${Date.now()}-002`,
machineryId: machinery1Id,
fieldName: 'currentLocation',
fieldLabel: '当前位置',
oldValue: '1号地块',
newValue: '3号地块',
operator: '李四',
operatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5天前
});
// 3. 操作人员变更
changes.push({
id: `change-${Date.now()}-003`,
machineryId: machinery1Id,
fieldName: 'operator',
fieldLabel: '操作人员',
oldValue: '张三',
newValue: '王五',
operator: '系统管理员',
operatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7天前
});
// 4. 保险信息更新
changes.push({
id: `change-${Date.now()}-004`,
machineryId: machinery1Id,
fieldName: 'insuranceEndDate',
fieldLabel: '保险结束日期',
oldValue: '2025-03-31',
newValue: '2026-03-31',
operator: '财务部-刘会计',
operatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), // 10天前
});
// 5. 购机价格调整
changes.push({
id: `change-${Date.now()}-005`,
machineryId: machinery1Id,
fieldName: 'purchasePrice',
fieldLabel: '购机价格',
oldValue: 350000,
newValue: 345000,
operator: '财务部-刘会计',
operatedAt: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), // 15天前
});
// 6. 备注更新
changes.push({
id: `change-${Date.now()}-006`,
machineryId: machinery1Id,
fieldName: 'remarks',
fieldLabel: '备注',
oldValue: '主力耕作设备,状态良好',
newValue: '主力耕作设备,状态良好,已完成季度保养',
operator: '张三',
operatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), // 20天前
});
// 7. 设备状态恢复正常
changes.push({
id: `change-${Date.now()}-007`,
machineryId: machinery1Id,
fieldName: 'status',
fieldLabel: '设备状态',
oldValue: '待维护',
newValue: '正常',
operator: '维修班-李师傅',
operatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), // 1天前
});
}
// ==================== 农机2收割机变更历史 ====================
if (allMachinery[1]) {
const machinery2Id = allMachinery[1].id;
// 1. 设备名称规范化
changes.push({
id: `change-${Date.now()}-008`,
machineryId: machinery2Id,
fieldName: 'name',
fieldLabel: '设备名称',
oldValue: '久保田收割机',
newValue: '久保田PRO988Q收割机',
operator: '资产管理员',
operatedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30天前
});
// 2. 作业前位置更新
changes.push({
id: `change-${Date.now()}-009`,
machineryId: machinery2Id,
fieldName: 'currentLocation',
fieldLabel: '当前位置',
oldValue: '机库A区',
newValue: '东片收割区',
operator: '李四',
operatedAt: new Date(now.getTime() - 25 * 24 * 60 * 60 * 1000).toISOString(), // 25天前
});
// 3. 作业后位置更新
changes.push({
id: `change-${Date.now()}-010`,
machineryId: machinery2Id,
fieldName: 'currentLocation',
fieldLabel: '当前位置',
oldValue: '东片收割区',
newValue: '机库A区',
operator: '李四',
operatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), // 20天前
});
// 4. 设备状态因故障变更
changes.push({
id: `change-${Date.now()}-011`,
machineryId: machinery2Id,
fieldName: 'status',
fieldLabel: '设备状态',
oldValue: '正常',
newValue: '待维护',
operator: '李四',
operatedAt: new Date(now.getTime() - 18 * 24 * 60 * 60 * 1000).toISOString(), // 18天前
});
// 5. 维修完成,状态恢复
changes.push({
id: `change-${Date.now()}-012`,
machineryId: machinery2Id,
fieldName: 'status',
fieldLabel: '设备状态',
oldValue: '待维护',
newValue: '正常',
operator: '维修班-赵师傅',
operatedAt: new Date(now.getTime() - 16 * 24 * 60 * 60 * 1000).toISOString(), // 16天前
});
// 6. 标签更新
changes.push({
id: `change-${Date.now()}-013`,
machineryId: machinery2Id,
fieldName: 'tags',
fieldLabel: '标签',
oldValue: ['tag-2', 'tag-3'],
newValue: ['tag-1', 'tag-2', 'tag-3'],
operator: '系统管理员',
operatedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(), // 12天前
});
}
// ==================== 农机3播种机变更历史 ====================
if (allMachinery[2]) {
const machinery3Id = allMachinery[2].id;
// 1. 操作人员调整
changes.push({
id: `change-${Date.now()}-014`,
machineryId: machinery3Id,
fieldName: 'operator',
fieldLabel: '操作人员',
oldValue: '王五',
newValue: '赵六',
operator: '系统管理员',
operatedAt: new Date(now.getTime() - 40 * 24 * 60 * 60 * 1000).toISOString(), // 40天前
});
// 2. 播种前检修
changes.push({
id: `change-${Date.now()}-015`,
machineryId: machinery3Id,
fieldName: 'status',
fieldLabel: '设备状态',
oldValue: '正常',
newValue: '待维护',
operator: '赵六',
operatedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), // 35天前
});
// 3. 检修完成
changes.push({
id: `change-${Date.now()}-016`,
machineryId: machinery3Id,
fieldName: 'status',
fieldLabel: '设备状态',
oldValue: '待维护',
newValue: '正常',
operator: '维修班-孙师傅',
operatedAt: new Date(now.getTime() - 33 * 24 * 60 * 60 * 1000).toISOString(), // 33天前
});
// 4. 作业地点变更
changes.push({
id: `change-${Date.now()}-017`,
machineryId: machinery3Id,
fieldName: 'currentLocation',
fieldLabel: '当前位置',
oldValue: '机库B区',
newValue: '西片播种区',
operator: '赵六',
operatedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString(), // 28天前
});
// 5. 作业完成归库
changes.push({
id: `change-${Date.now()}-018`,
machineryId: machinery3Id,
fieldName: 'currentLocation',
fieldLabel: '当前位置',
oldValue: '西片播种区',
newValue: '机库B区',
operator: '赵六',
operatedAt: new Date(now.getTime() - 22 * 24 * 60 * 60 * 1000).toISOString(), // 22天前
});
// 6. 保险金额调整
changes.push({
id: `change-${Date.now()}-019`,
machineryId: machinery3Id,
fieldName: 'insuranceAmount',
fieldLabel: '保险金额',
oldValue: 150000,
newValue: 160000,
operator: '财务部-刘会计',
operatedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(), // 8天前
});
}
// ==================== 农机4植保无人机变更历史 ====================
if (allMachinery[3]) {
const machinery4Id = allMachinery[3].id;
// 1. 电池更换记录
changes.push({
id: `change-${Date.now()}-020`,
machineryId: machinery4Id,
fieldName: 'remarks',
fieldLabel: '备注',
oldValue: '最新款植保无人机,智能化程度高',
newValue: '最新款植保无人机,智能化程度高,电池已更换',
operator: '无人机操作员-周七',
operatedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(), // 6天前
});
// 2. 作业区域变更
changes.push({
id: `change-${Date.now()}-021`,
machineryId: machinery4Id,
fieldName: 'currentLocation',
fieldLabel: '当前位置',
oldValue: '设备间',
newValue: '北片植保区',
operator: '周七',
operatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), // 4天前
});
// 3. 标签添加
changes.push({
id: `change-${Date.now()}-022`,
machineryId: machinery4Id,
fieldName: 'tags',
fieldLabel: '标签',
oldValue: ['tag-1', 'tag-2', 'tag-3'],
newValue: ['tag-1', 'tag-2', 'tag-3', 'tag-4'],
operator: '系统管理员',
operatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), // 2天前
});
}
// ==================== 农机5拖拉机变更历史 ====================
if (allMachinery[4]) {
const machinery5Id = allMachinery[4].id;
// 1. 供应商信息更正
changes.push({
id: `change-${Date.now()}-023`,
machineryId: machinery5Id,
fieldName: 'supplier',
fieldLabel: '供应商',
oldValue: '农机经销商',
newValue: '雷沃重工授权经销商',
operator: '采购部-吴采购',
operatedAt: new Date(now.getTime() - 50 * 24 * 60 * 60 * 1000).toISOString(), // 50天前
});
// 2. 部门调整
changes.push({
id: `change-${Date.now()}-024`,
machineryId: machinery5Id,
fieldName: 'department',
fieldLabel: '所属部门',
oldValue: '第三生产队',
newValue: '第一生产队',
operator: '人事部-郑主管',
operatedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), // 45天前
});
// 3. 年检完成
changes.push({
id: `change-${Date.now()}-025`,
machineryId: machinery5Id,
fieldName: 'remarks',
fieldLabel: '备注',
oldValue: '可靠耐用,适合大田作业',
newValue: '可靠耐用,适合大田作业,已完成年检',
operator: '安全员-冯安全',
operatedAt: new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000).toISOString(), // 14天前
});
}
// 保存所有变更记录
changes.forEach(change => {
machineryStorage.saveChangeHistory(change);
});
console.log(`✅ 已初始化 ${changes.length} 条变更历史示例数据`);
}
/**
* 清除所有变更历史(仅用于测试)
*/
export function clearChangeHistory() {
localStorage.removeItem('smart_agriculture_machinery_history');
console.log('🗑️ 已清除所有变更历史');
}
/**
* 获取变更历史统计信息
*/
export function getChangeHistoryStatistics() {
const allMachinery = machineryStorage.getAllMachinery();
let totalChanges = 0;
const changesByMachinery: Record<string, number> = {};
const changesByField: Record<string, number> = {};
const changesByOperator: Record<string, number> = {};
allMachinery.forEach(machinery => {
const changes = machineryStorage.getChangeHistory(machinery.id);
totalChanges += changes.length;
changesByMachinery[machinery.name] = changes.length;
changes.forEach(change => {
// 按字段统计
changesByField[change.fieldLabel] = (changesByField[change.fieldLabel] || 0) + 1;
// 按操作人统计
changesByOperator[change.operator] = (changesByOperator[change.operator] || 0) + 1;
});
});
return {
totalChanges,
totalMachinery: allMachinery.length,
changesByMachinery,
changesByField,
changesByOperator,
avgChangesPerMachinery: allMachinery.length > 0 ? totalChanges / allMachinery.length : 0,
};
}

239
src/lib/changeTracker.ts Normal file
View File

@@ -0,0 +1,239 @@
// 变更追踪工具
import { MachineryRecord, MachineryChangeHistory } from '../types/machinery';
// 字段名称到中文标签的映射
export const FIELD_LABELS: Record<string, string> = {
// 基本信息
name: '设备名称',
model: '型号规格',
category: '农机类型',
usage: '使用场景',
manufacturer: '生产厂家',
manufactureDate: '出厂日期',
purchaseDate: '购买日期',
// 技术参数
engineNumber: '发动机号',
chassisNumber: '车架号',
power: '额定功率',
weight: '整机重量',
workingWidth: '工作幅宽',
// 购机信息
purchasePrice: '购机价格',
supplier: '供应商',
invoiceNumber: '发票号码',
invoiceUrl: '购机发票',
// 保险信息
insuranceCompany: '保险公司',
insurancePolicyNumber: '保单号',
insuranceStartDate: '保险起始日期',
insuranceEndDate: '保险结束日期',
insuranceAmount: '保险金额',
// 使用信息
status: '设备状态',
currentLocation: '当前位置',
operator: '操作人员',
department: '所属部门',
// 保养信息
maintenanceCycle: '保养周期',
maintenanceCycleUnit: '保养周期单位',
// 其他信息
remarks: '备注',
tags: '标签',
};
// 需要排除的字段(不记录变更)
const EXCLUDED_FIELDS = [
'id',
'qrCode',
'createdAt',
'updatedAt',
'createdBy',
'updatedBy',
];
/**
* 格式化字段值用于显示
*/
export function formatFieldValue(fieldName: string, value: any): string {
if (value === null || value === undefined || value === '') {
return '(空)';
}
// 日期字段
if (fieldName.includes('Date') || fieldName.includes('Time')) {
try {
return new Date(value).toLocaleDateString('zh-CN');
} catch {
return String(value);
}
}
// 金额字段
if (fieldName.includes('Price') || fieldName.includes('Amount')) {
const num = parseFloat(value);
return isNaN(num) ? String(value) : `¥${num.toLocaleString()}`;
}
// 保养周期单位
if (fieldName === 'maintenanceCycleUnit') {
const unitMap: Record<string, string> = {
day: '天',
month: '月',
year: '年'
};
return unitMap[value] || String(value);
}
// 数组字段(如标签)
if (Array.isArray(value)) {
return value.length > 0 ? value.join(', ') : '(空)';
}
// 对象字段
if (typeof value === 'object') {
return JSON.stringify(value);
}
// 布尔值
if (typeof value === 'boolean') {
return value ? '是' : '否';
}
return String(value);
}
/**
* 比较两个值是否相同
*/
function isEqual(oldValue: any, newValue: any): boolean {
// 处理null/undefined/空字符串的情况
const isOldEmpty = oldValue === null || oldValue === undefined || oldValue === '';
const isNewEmpty = newValue === null || newValue === undefined || newValue === '';
if (isOldEmpty && isNewEmpty) return true;
if (isOldEmpty !== isNewEmpty) return false;
// 数组比较
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
if (oldValue.length !== newValue.length) return false;
return oldValue.every((val, index) => val === newValue[index]);
}
// 对象比较
if (typeof oldValue === 'object' && typeof newValue === 'object') {
return JSON.stringify(oldValue) === JSON.stringify(newValue);
}
// 基本类型比较
return oldValue === newValue;
}
/**
* 追踪农机档案的变更
* @param oldRecord 旧记录
* @param newRecord 新记录
* @param operator 操作人
* @returns 变更历史记录数组
*/
export function trackMachineryChanges(
oldRecord: MachineryRecord | undefined,
newRecord: Partial<MachineryRecord>,
operator: string
): MachineryChangeHistory[] {
const changes: MachineryChangeHistory[] = [];
const timestamp = new Date().toISOString();
// 如果是新建,不记录变更
if (!oldRecord) {
return [];
}
// 遍历新记录的所有字段
Object.keys(newRecord).forEach((fieldName) => {
// 跳过排除的字段
if (EXCLUDED_FIELDS.includes(fieldName)) {
return;
}
const oldValue = (oldRecord as any)[fieldName];
const newValue = (newRecord as any)[fieldName];
// 检查值是否发生变化
if (!isEqual(oldValue, newValue)) {
const fieldLabel = FIELD_LABELS[fieldName] || fieldName;
changes.push({
id: `change-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
machineryId: oldRecord.id,
fieldName,
fieldLabel,
oldValue,
newValue,
operator,
operatedAt: timestamp,
});
}
});
return changes;
}
/**
* 获取字段的变更描述
*/
export function getChangeDescription(change: MachineryChangeHistory): string {
const oldValueStr = formatFieldValue(change.fieldName, change.oldValue);
const newValueStr = formatFieldValue(change.fieldName, change.newValue);
return `${change.fieldLabel} 从 "${oldValueStr}" 修改为 "${newValueStr}"`;
}
/**
* 按日期分组变更记录
*/
export function groupChangesByDate(
changes: MachineryChangeHistory[]
): Record<string, MachineryChangeHistory[]> {
const grouped: Record<string, MachineryChangeHistory[]> = {};
changes.forEach(change => {
const date = new Date(change.operatedAt).toLocaleDateString('zh-CN');
if (!grouped[date]) {
grouped[date] = [];
}
grouped[date].push(change);
});
return grouped;
}
/**
* 获取变更统计信息
*/
export function getChangeStats(changes: MachineryChangeHistory[]) {
const stats = {
total: changes.length,
byField: {} as Record<string, number>,
byOperator: {} as Record<string, number>,
recentChanges: changes
.sort((a, b) => new Date(b.operatedAt).getTime() - new Date(a.operatedAt).getTime())
.slice(0, 5),
};
changes.forEach(change => {
// 按字段统计
const field = change.fieldLabel;
stats.byField[field] = (stats.byField[field] || 0) + 1;
// 按操作人统计
stats.byOperator[change.operator] = (stats.byOperator[change.operator] || 0) + 1;
});
return stats;
}

View File

@@ -0,0 +1,955 @@
/**
* 作物-环境知识库
* 定义各种作物的最适宜生长环境参数范围
*/
export interface CropKnowledge {
id: string;
cropName: string;
category: '粮食作物' | '经济作物' | '蔬菜作物' | '果树';
description: string;
// 土壤环境要求
soilRequirements: {
ph: {
optimal: [number, number]; // 最佳范围
acceptable: [number, number]; // 可接受范围
};
organicMatter: {
optimal: [number, number]; // g/kg
acceptable: [number, number];
};
soilDepth: {
optimal: [number, number]; // cm
acceptable: [number, number];
};
nitrogen: {
optimal: [number, number]; // g/kg
acceptable: [number, number];
};
phosphorus: {
optimal: [number, number]; // g/kg
acceptable: [number, number];
};
potassium: {
optimal: [number, number]; // g/kg
acceptable: [number, number];
};
drainage: {
optimal: [number, number]; // 0-100分
acceptable: [number, number];
};
};
// 气候环境要求
climateRequirements: {
temperature: {
optimal: [number, number]; // °C
acceptable: [number, number];
};
rainfall: {
optimal: [number, number]; // mm/年
acceptable: [number, number];
};
sunlight: {
optimal: [number, number]; // 小时/天
acceptable: [number, number];
};
};
// 预期产量kg/亩)
expectedYield: {
high: [number, number]; // 高适宜性条件下
medium: [number, number]; // 中等适宜性条件下
low: [number, number]; // 低适宜性条件下
};
// 生长周期
growthCycle: {
days: number; // 天数
seasons: string[]; // 适宜季节
};
// 风险因子
riskFactors: {
id: string;
name: string;
condition: string; // 触发条件描述
severity: 'high' | 'medium' | 'low';
suggestion: string; // 应对建议
}[];
}
/**
* 作物知识库数据
*/
export const cropKnowledgeBase: CropKnowledge[] = [
{
id: 'rice',
cropName: '水稻',
category: '粮食作物',
description: '主要粮食作物,喜温暖湿润气候,需水量大',
soilRequirements: {
ph: {
optimal: [5.5, 6.5],
acceptable: [5.0, 7.0],
},
organicMatter: {
optimal: [25, 40],
acceptable: [20, 45],
},
soilDepth: {
optimal: [40, 80],
acceptable: [30, 100],
},
nitrogen: {
optimal: [1.5, 2.5],
acceptable: [1.0, 3.0],
},
phosphorus: {
optimal: [0.8, 1.5],
acceptable: [0.6, 2.0],
},
potassium: {
optimal: [15, 25],
acceptable: [12, 30],
},
drainage: {
optimal: [40, 70],
acceptable: [30, 80],
},
},
climateRequirements: {
temperature: {
optimal: [20, 30],
acceptable: [15, 35],
},
rainfall: {
optimal: [1000, 1500],
acceptable: [800, 2000],
},
sunlight: {
optimal: [6, 8],
acceptable: [5, 10],
},
},
expectedYield: {
high: [500, 600],
medium: [400, 500],
low: [300, 400],
},
growthCycle: {
days: 120,
seasons: ['春季', '夏季'],
},
riskFactors: [
{
id: 'drought',
name: '干旱风险',
condition: '年降雨量 < 900mm',
severity: 'high',
suggestion: '建议配套灌溉设施,确保水源充足',
},
{
id: 'ph_low',
name: '土壤酸化',
condition: 'pH < 5.5',
severity: 'medium',
suggestion: '建议施用石灰调节土壤pH值',
},
{
id: 'temperature_low',
name: '低温冷害',
condition: '平均温度 < 18°C',
severity: 'high',
suggestion: '选择耐寒品种或调整种植时间',
},
{
id: 'nitrogen_low',
name: '氮素不足',
condition: '全氮 < 1.2 g/kg',
severity: 'medium',
suggestion: '增施氮肥,建议尿素或复合肥',
},
],
},
{
id: 'corn',
cropName: '玉米',
category: '粮食作物',
description: '重要粮食作物,耐旱性强,适应性广',
soilRequirements: {
ph: {
optimal: [6.0, 7.5],
acceptable: [5.5, 8.0],
},
organicMatter: {
optimal: [20, 35],
acceptable: [15, 40],
},
soilDepth: {
optimal: [60, 100],
acceptable: [50, 120],
},
nitrogen: {
optimal: [1.0, 2.0],
acceptable: [0.8, 2.5],
},
phosphorus: {
optimal: [0.6, 1.2],
acceptable: [0.4, 1.5],
},
potassium: {
optimal: [18, 28],
acceptable: [15, 35],
},
drainage: {
optimal: [70, 90],
acceptable: [60, 95],
},
},
climateRequirements: {
temperature: {
optimal: [22, 30],
acceptable: [18, 35],
},
rainfall: {
optimal: [600, 1000],
acceptable: [500, 1200],
},
sunlight: {
optimal: [7, 10],
acceptable: [6, 12],
},
},
expectedYield: {
high: [550, 700],
medium: [450, 550],
low: [350, 450],
},
growthCycle: {
days: 110,
seasons: ['春季', '夏季'],
},
riskFactors: [
{
id: 'waterlogging',
name: '涝害风险',
condition: '排水性 < 60分',
severity: 'high',
suggestion: '改善排水系统,开挖排水沟',
},
{
id: 'ph_high',
name: '土壤碱化',
condition: 'pH > 7.8',
severity: 'medium',
suggestion: '施用酸性肥料或硫磺调节pH',
},
{
id: 'potassium_low',
name: '钾素缺乏',
condition: '全钾 < 16 g/kg',
severity: 'medium',
suggestion: '增施钾肥,建议氯化钾或硫酸钾',
},
{
id: 'high_temp',
name: '高温胁迫',
condition: '平均温度 > 32°C',
severity: 'medium',
suggestion: '适当遮阴,增加灌溉频次',
},
],
},
{
id: 'wheat',
cropName: '小麦',
category: '粮食作物',
description: '主要粮食作物,喜冷凉气候,耐寒性强',
soilRequirements: {
ph: {
optimal: [6.5, 7.5],
acceptable: [6.0, 8.0],
},
organicMatter: {
optimal: [18, 30],
acceptable: [15, 35],
},
soilDepth: {
optimal: [50, 90],
acceptable: [40, 100],
},
nitrogen: {
optimal: [1.2, 2.2],
acceptable: [0.9, 2.5],
},
phosphorus: {
optimal: [0.7, 1.3],
acceptable: [0.5, 1.8],
},
potassium: {
optimal: [16, 24],
acceptable: [14, 28],
},
drainage: {
optimal: [75, 90],
acceptable: [65, 95],
},
},
climateRequirements: {
temperature: {
optimal: [12, 20],
acceptable: [5, 25],
},
rainfall: {
optimal: [450, 750],
acceptable: [350, 900],
},
sunlight: {
optimal: [6, 8],
acceptable: [5, 9],
},
},
expectedYield: {
high: [450, 550],
medium: [350, 450],
low: [250, 350],
},
growthCycle: {
days: 210,
seasons: ['秋季', '冬季', '春季'],
},
riskFactors: [
{
id: 'spring_cold',
name: '倒春寒',
condition: '春季平均温度 < 8°C',
severity: 'high',
suggestion: '选择抗寒品种,适当延迟播种',
},
{
id: 'drought_spring',
name: '春季干旱',
condition: '春季降雨 < 100mm',
severity: 'medium',
suggestion: '及时灌溉,保持土壤湿润',
},
{
id: 'organic_low',
name: '有机质偏低',
condition: '有机质 < 16 g/kg',
severity: 'medium',
suggestion: '增施有机肥,改良土壤结构',
},
{
id: 'phosphorus_low',
name: '磷素不足',
condition: '全磷 < 0.6 g/kg',
severity: 'low',
suggestion: '基肥增施过磷酸钙',
},
],
},
{
id: 'soybean',
cropName: '大豆',
category: '粮食作物',
description: '重要油料和蛋白作物,具有固氮能力',
soilRequirements: {
ph: {
optimal: [6.0, 7.0],
acceptable: [5.5, 7.5],
},
organicMatter: {
optimal: [22, 35],
acceptable: [18, 40],
},
soilDepth: {
optimal: [45, 80],
acceptable: [35, 90],
},
nitrogen: {
optimal: [0.8, 1.5],
acceptable: [0.6, 2.0],
},
phosphorus: {
optimal: [0.8, 1.5],
acceptable: [0.6, 2.0],
},
potassium: {
optimal: [18, 26],
acceptable: [15, 30],
},
drainage: {
optimal: [70, 85],
acceptable: [60, 90],
},
},
climateRequirements: {
temperature: {
optimal: [20, 28],
acceptable: [15, 32],
},
rainfall: {
optimal: [500, 900],
acceptable: [400, 1100],
},
sunlight: {
optimal: [6, 9],
acceptable: [5, 10],
},
},
expectedYield: {
high: [200, 280],
medium: [160, 200],
low: [120, 160],
},
growthCycle: {
days: 100,
seasons: ['春季', '夏季'],
},
riskFactors: [
{
id: 'waterlogging',
name: '涝害风险',
condition: '排水性 < 65分',
severity: 'high',
suggestion: '改善排水,避免积水',
},
{
id: 'drought_pod',
name: '结荚期干旱',
condition: '夏季降雨 < 200mm',
severity: 'medium',
suggestion: '结荚期及时灌溉',
},
{
id: 'phosphorus_critical',
name: '磷素关键需求',
condition: '全磷 < 0.7 g/kg',
severity: 'medium',
suggestion: '大豆对磷敏感,需重点补充',
},
],
},
{
id: 'cotton',
cropName: '棉花',
category: '经济作物',
description: '重要纤维作物,喜温暖干燥气候',
soilRequirements: {
ph: {
optimal: [6.5, 8.0],
acceptable: [6.0, 8.5],
},
organicMatter: {
optimal: [18, 28],
acceptable: [15, 32],
},
soilDepth: {
optimal: [60, 100],
acceptable: [50, 120],
},
nitrogen: {
optimal: [1.0, 1.8],
acceptable: [0.8, 2.2],
},
phosphorus: {
optimal: [0.6, 1.2],
acceptable: [0.5, 1.5],
},
potassium: {
optimal: [20, 30],
acceptable: [16, 35],
},
drainage: {
optimal: [80, 95],
acceptable: [70, 100],
},
},
climateRequirements: {
temperature: {
optimal: [25, 32],
acceptable: [20, 35],
},
rainfall: {
optimal: [500, 800],
acceptable: [400, 1000],
},
sunlight: {
optimal: [8, 12],
acceptable: [7, 14],
},
},
expectedYield: {
high: [120, 160],
medium: [90, 120],
low: [60, 90],
},
growthCycle: {
days: 180,
seasons: ['春季', '夏季', '秋季'],
},
riskFactors: [
{
id: 'late_rain',
name: '后期多雨',
condition: '秋季降雨 > 300mm',
severity: 'high',
suggestion: '影响棉花采摘和品质,需排水',
},
{
id: 'potassium_high',
name: '钾素需求大',
condition: '全钾 < 18 g/kg',
severity: 'medium',
suggestion: '棉花需钾量大,重点补充钾肥',
},
{
id: 'temperature_low',
name: '生长期温度不足',
condition: '平均温度 < 22°C',
severity: 'medium',
suggestion: '延长生育期,影响产量',
},
],
},
{
id: 'tomato',
cropName: '番茄',
category: '蔬菜作物',
description: '重要蔬菜作物,喜温暖,对光照要求高',
soilRequirements: {
ph: {
optimal: [6.0, 7.0],
acceptable: [5.5, 7.5],
},
organicMatter: {
optimal: [28, 45],
acceptable: [22, 50],
},
soilDepth: {
optimal: [40, 70],
acceptable: [30, 80],
},
nitrogen: {
optimal: [1.5, 2.5],
acceptable: [1.2, 3.0],
},
phosphorus: {
optimal: [1.0, 2.0],
acceptable: [0.8, 2.5],
},
potassium: {
optimal: [22, 32],
acceptable: [18, 38],
},
drainage: {
optimal: [75, 90],
acceptable: [65, 95],
},
},
climateRequirements: {
temperature: {
optimal: [20, 28],
acceptable: [15, 32],
},
rainfall: {
optimal: [400, 700],
acceptable: [300, 900],
},
sunlight: {
optimal: [8, 12],
acceptable: [6, 14],
},
},
expectedYield: {
high: [4000, 6000],
medium: [3000, 4000],
low: [2000, 3000],
},
growthCycle: {
days: 95,
seasons: ['春季', '秋季'],
},
riskFactors: [
{
id: 'humidity_high',
name: '高湿病害',
condition: '排水性 < 70分',
severity: 'high',
suggestion: '易发生病害,加强通风排湿',
},
{
id: 'nitrogen_excess',
name: '氮肥过量',
condition: '全氮 > 2.8 g/kg',
severity: 'medium',
suggestion: '控制氮肥,避免徒长',
},
{
id: 'temperature_extreme',
name: '温度不适',
condition: '温度 < 15°C 或 > 30°C',
severity: 'medium',
suggestion: '影响坐果,需温室栽培',
},
],
},
{
id: 'potato',
cropName: '马铃薯',
category: '粮食作物',
description: '重要粮食和蔬菜作物,喜冷凉气候',
soilRequirements: {
ph: {
optimal: [5.0, 6.5],
acceptable: [4.5, 7.0],
},
organicMatter: {
optimal: [25, 40],
acceptable: [20, 45],
},
soilDepth: {
optimal: [50, 80],
acceptable: [40, 90],
},
nitrogen: {
optimal: [1.2, 2.0],
acceptable: [1.0, 2.5],
},
phosphorus: {
optimal: [0.8, 1.6],
acceptable: [0.6, 2.0],
},
potassium: {
optimal: [20, 30],
acceptable: [16, 35],
},
drainage: {
optimal: [80, 95],
acceptable: [70, 100],
},
},
climateRequirements: {
temperature: {
optimal: [15, 22],
acceptable: [10, 25],
},
rainfall: {
optimal: [400, 600],
acceptable: [300, 800],
},
sunlight: {
optimal: [6, 8],
acceptable: [5, 10],
},
},
expectedYield: {
high: [2500, 3500],
medium: [1800, 2500],
low: [1200, 1800],
},
growthCycle: {
days: 90,
seasons: ['春季', '秋季'],
},
riskFactors: [
{
id: 'waterlogging',
name: '涝害烂薯',
condition: '排水性 < 75分',
severity: 'high',
suggestion: '必须排水良好,避免积水',
},
{
id: 'ph_high',
name: 'pH偏高',
condition: 'pH > 6.8',
severity: 'medium',
suggestion: '易发生疮痂病,施用硫磺调酸',
},
{
id: 'temperature_high',
name: '高温影响',
condition: '平均温度 > 24°C',
severity: 'medium',
suggestion: '块茎膨大受阻,影响产量',
},
{
id: 'potassium_critical',
name: '钾素关键',
condition: '全钾 < 18 g/kg',
severity: 'high',
suggestion: '马铃薯需钾量大,影响品质',
},
],
},
{
id: 'peanut',
cropName: '花生',
category: '经济作物',
description: '重要油料作物,喜温暖,耐旱耐瘠',
soilRequirements: {
ph: {
optimal: [6.0, 7.0],
acceptable: [5.5, 7.5],
},
organicMatter: {
optimal: [18, 30],
acceptable: [15, 35],
},
soilDepth: {
optimal: [50, 80],
acceptable: [40, 90],
},
nitrogen: {
optimal: [0.8, 1.5],
acceptable: [0.6, 2.0],
},
phosphorus: {
optimal: [0.7, 1.4],
acceptable: [0.5, 1.8],
},
potassium: {
optimal: [16, 24],
acceptable: [14, 28],
},
drainage: {
optimal: [80, 95],
acceptable: [70, 100],
},
},
climateRequirements: {
temperature: {
optimal: [22, 30],
acceptable: [18, 34],
},
rainfall: {
optimal: [500, 700],
acceptable: [400, 900],
},
sunlight: {
optimal: [7, 10],
acceptable: [6, 12],
},
},
expectedYield: {
high: [350, 450],
medium: [280, 350],
low: [200, 280],
},
growthCycle: {
days: 120,
seasons: ['春季', '夏季'],
},
riskFactors: [
{
id: 'waterlogging',
name: '涝害风险',
condition: '排水性 < 75分',
severity: 'high',
suggestion: '花生最忌涝,需沙质土壤',
},
{
id: 'calcium_deficiency',
name: '钙素缺乏',
condition: 'pH < 5.8',
severity: 'medium',
suggestion: '易出现空壳,施用石灰补钙',
},
{
id: 'late_rain',
name: '后期多雨',
condition: '秋季降雨 > 250mm',
severity: 'medium',
suggestion: '影响收获和品质',
},
],
},
];
/**
* 根据地块环境参数匹配推荐作物
*/
export function matchCropsForField(fieldFactors: {
ph: number;
organic: number;
depth: number;
nitrogen: number;
phosphorus: number;
potassium: number;
drainage: number;
temperature?: number;
rainfall?: number;
}): Array<{
crop: CropKnowledge;
matchScore: number;
suitabilityLevel: '高度推荐' | '推荐' | '谨慎种植' | '不推荐';
matchDetails: {
factor: string;
status: 'optimal' | 'acceptable' | 'unacceptable';
value: number;
range: string;
}[];
applicableRisks: typeof cropKnowledgeBase[0]['riskFactors'];
expectedYield: [number, number];
}> {
return cropKnowledgeBase.map(crop => {
const matchDetails: any[] = [];
let optimalMatches = 0;
let acceptableMatches = 0;
let totalFactors = 0;
// 检查土壤因子匹配度
const soilFactors = {
pH值: { value: fieldFactors.ph, req: crop.soilRequirements.ph },
: { value: fieldFactors.organic, req: crop.soilRequirements.organicMatter },
: { value: fieldFactors.depth, req: crop.soilRequirements.soilDepth },
: { value: fieldFactors.nitrogen, req: crop.soilRequirements.nitrogen },
: { value: fieldFactors.phosphorus, req: crop.soilRequirements.phosphorus },
: { value: fieldFactors.potassium, req: crop.soilRequirements.potassium },
: { value: fieldFactors.drainage, req: crop.soilRequirements.drainage },
};
Object.entries(soilFactors).forEach(([name, data]) => {
totalFactors++;
let status: 'optimal' | 'acceptable' | 'unacceptable';
if (data.value >= data.req.optimal[0] && data.value <= data.req.optimal[1]) {
status = 'optimal';
optimalMatches++;
} else if (data.value >= data.req.acceptable[0] && data.value <= data.req.acceptable[1]) {
status = 'acceptable';
acceptableMatches++;
} else {
status = 'unacceptable';
}
matchDetails.push({
factor: name,
status,
value: data.value,
range: `最佳${data.req.optimal[0]}-${data.req.optimal[1]}`,
});
});
// 计算匹配分数 (0-100)
const matchScore = Math.round(
(optimalMatches * 100 + acceptableMatches * 60) / totalFactors
);
// 确定适宜性等级
let suitabilityLevel: '高度推荐' | '推荐' | '谨慎种植' | '不推荐';
if (matchScore >= 85) {
suitabilityLevel = '高度推荐';
} else if (matchScore >= 70) {
suitabilityLevel = '推荐';
} else if (matchScore >= 50) {
suitabilityLevel = '谨慎种植';
} else {
suitabilityLevel = '不推荐';
}
// 筛选适用的风险因子
const applicableRisks = crop.riskFactors.filter(risk => {
// 根据实际值判断是否触发风险
if (risk.id === 'ph_low' && fieldFactors.ph < 5.5) return true;
if (risk.id === 'ph_high' && fieldFactors.ph > 7.8) return true;
if (risk.id === 'nitrogen_low' && fieldFactors.nitrogen < 1.2) return true;
if (risk.id === 'phosphorus_low' && fieldFactors.phosphorus < 0.6) return true;
if (risk.id === 'phosphorus_critical' && fieldFactors.phosphorus < 0.7) return true;
if (risk.id === 'potassium_low' && fieldFactors.potassium < 16) return true;
if (risk.id === 'potassium_high' && fieldFactors.potassium < 18) return true;
if (risk.id === 'potassium_critical' && fieldFactors.potassium < 18) return true;
if (risk.id === 'organic_low' && fieldFactors.organic < 16) return true;
if (risk.id === 'waterlogging' && fieldFactors.drainage < 60) return true;
if (risk.id === 'nitrogen_excess' && fieldFactors.nitrogen > 2.8) return true;
if (risk.id === 'calcium_deficiency' && fieldFactors.ph < 5.8) return true;
// 气候相关风险(使用默认值或传入值)
const temp = fieldFactors.temperature || 22;
const rainfall = fieldFactors.rainfall || 800;
if (risk.id === 'drought' && rainfall < 900) return true;
if (risk.id === 'drought_spring' && rainfall < 100) return true;
if (risk.id === 'drought_pod' && rainfall < 200) return true;
if (risk.id === 'temperature_low' && temp < 18) return true;
if (risk.id === 'temperature_high' && temp > 24) return true;
if (risk.id === 'high_temp' && temp > 32) return true;
if (risk.id === 'spring_cold' && temp < 8) return true;
if (risk.id === 'late_rain' && rainfall > 300) return true;
if (risk.id === 'humidity_high' && fieldFactors.drainage < 70) return true;
if (risk.id === 'temperature_extreme' && (temp < 15 || temp > 30)) return true;
return false;
});
// 根据适宜性等级确定预期产量范围
let expectedYield: [number, number];
if (suitabilityLevel === '高度推荐') {
expectedYield = crop.expectedYield.high;
} else if (suitabilityLevel === '推荐') {
expectedYield = crop.expectedYield.medium;
} else {
expectedYield = crop.expectedYield.low;
}
return {
crop,
matchScore,
suitabilityLevel,
matchDetails,
applicableRisks,
expectedYield,
};
}).sort((a, b) => b.matchScore - a.matchScore); // 按匹配分数降序排列
}

666
src/lib/faultCodeLibrary.ts Normal file
View File

@@ -0,0 +1,666 @@
/**
* 故障码库 - 基于农机厂商提供的标准故障码
* 包含诊断规则、预测模型和解决方案知识库
*/
// 故障码定义
export interface FaultCode {
code: string;
name: string;
category: FaultCategory;
level: 'info' | 'warning' | 'error' | 'critical';
description: string;
// 诊断规则
diagnosticRules: DiagnosticRule[];
// 预测条件
predictionConditions?: PredictionCondition[];
// 解决方案
solutions: Solution[];
// 关联知识库
knowledgeBaseIds: string[];
// 厂商信息
manufacturer?: string;
applicableModels?: string[];
}
export type FaultCategory =
| '发动机系统'
| '传动系统'
| '液压系统'
| '电气系统'
| '冷却系统'
| '燃油系统'
| '排放系统'
| '作业装置'
| '其他';
// 诊断规则
export interface DiagnosticRule {
condition: string; // 触发条件描述
sensorKeys: string[]; // 关联的传感器参数
threshold?: {
min?: number;
max?: number;
duration?: number; // 持续时间(秒)
};
diagnosis: string; // 诊断结果
confidence: number; // 置信度 0-1
}
// 预测条件(用于潜在故障预测)
export interface PredictionCondition {
type: 'usage' | 'sensor' | 'pattern'; // 预测类型
description: string;
indicators: {
key: string; // 指标键值
threshold: number;
unit?: string;
}[];
warningMessage: string;
recommendedAction: string;
}
// 解决方案
export interface Solution {
step: number;
action: string;
priority: 'high' | 'medium' | 'low';
estimatedTime?: string; // 预计耗时
requiredTools?: string[]; // 所需工具
requiredParts?: string[]; // 所需零件
safetyWarning?: string; // 安全提示
}
/**
* 故障码库数据
*/
export const FAULT_CODE_LIBRARY: FaultCode[] = [
// ========== 发动机系统 ==========
{
code: 'P0301',
name: '第1缸失火',
category: '发动机系统',
level: 'error',
description: '发动机第1缸检测到失火现象',
diagnosticRules: [
{
condition: '第1缸点火信号异常',
sensorKeys: ['cylinder1_misfire_count'],
threshold: { min: 3, duration: 10 },
diagnosis: '火花塞老化或点火线圈故障',
confidence: 0.85,
},
{
condition: '燃油喷射异常',
sensorKeys: ['fuel_pressure', 'injector1_status'],
diagnosis: '喷油嘴堵塞或燃油压力不足',
confidence: 0.75,
},
],
predictionConditions: [
{
type: 'usage',
description: '火花塞使用寿命预警',
indicators: [
{ key: 'spark_plug_1_hours', threshold: 500, unit: '小时' },
],
warningMessage: '火花塞使用寿命已达90%,建议更换',
recommendedAction: '计划在下次保养时更换火花塞',
},
],
solutions: [
{
step: 1,
action: '检查第1缸火花塞状态观察是否有烧蚀、积碳',
priority: 'high',
estimatedTime: '10分钟',
requiredTools: ['火花塞扳手', '间隙规'],
},
{
step: 2,
action: '检查点火线圈输出,测量电阻值',
priority: 'high',
estimatedTime: '15分钟',
requiredTools: ['万用表'],
},
{
step: 3,
action: '检查燃油喷射系统,清洗或更换喷油嘴',
priority: 'medium',
estimatedTime: '30分钟',
requiredTools: ['喷油嘴清洗设备'],
requiredParts: ['喷油嘴(如需更换)'],
},
],
knowledgeBaseIds: ['KB001', 'KB015'],
},
{
code: 'P0171',
name: '系统过稀',
category: '燃油系统',
level: 'warning',
description: '燃油系统混合气过稀',
diagnosticRules: [
{
condition: '空燃比传感器检测混合气过稀',
sensorKeys: ['o2_sensor_voltage', 'fuel_trim'],
diagnosis: '空气滤清器堵塞或进气系统漏气',
confidence: 0.80,
},
{
condition: '燃油压力低于标准值',
sensorKeys: ['fuel_pressure'],
threshold: { max: 250 }, // kPa
diagnosis: '燃油泵压力不足或燃油滤清器堵塞',
confidence: 0.85,
},
],
predictionConditions: [
{
type: 'usage',
description: '空气滤清器使用时间预警',
indicators: [
{ key: 'air_filter_hours', threshold: 500, unit: '小时' },
],
warningMessage: '空气滤清器使用超过500小时可能影响进气效率',
recommendedAction: '检查并更换空气滤清器',
},
],
solutions: [
{
step: 1,
action: '检查并更换空气滤清器',
priority: 'high',
estimatedTime: '10分钟',
requiredParts: ['空气滤清器'],
},
{
step: 2,
action: '检查燃油泵工作压力',
priority: 'high',
estimatedTime: '20分钟',
requiredTools: ['燃油压力表'],
},
{
step: 3,
action: '检查进气系统是否有漏气',
priority: 'medium',
estimatedTime: '30分钟',
requiredTools: ['烟雾机'],
},
],
knowledgeBaseIds: ['KB002', 'KB018'],
},
{
code: 'C0040',
name: '发动机温度过高',
category: '冷却系统',
level: 'critical',
description: '发动机冷却液温度超过安全阈值',
diagnosticRules: [
{
condition: '冷却液温度超过105℃',
sensorKeys: ['coolant_temp'],
threshold: { min: 105 },
diagnosis: '冷却系统故障,需立即停机',
confidence: 0.95,
},
{
condition: '温度持续上升且风扇未启动',
sensorKeys: ['coolant_temp', 'fan_status'],
diagnosis: '散热风扇故障',
confidence: 0.90,
},
],
predictionConditions: [
{
type: 'sensor',
description: '冷却液温度趋势预警',
indicators: [
{ key: 'coolant_temp_trend', threshold: 2, unit: '℃/分钟' },
],
warningMessage: '冷却液温度上升过快,可能即将过热',
recommendedAction: '减轻负荷,检查冷却系统',
},
],
solutions: [
{
step: 1,
action: '立即停机,等待冷却',
priority: 'high',
estimatedTime: '30分钟',
safetyWarning: '⚠️ 严禁在高温时打开水箱盖,避免烫伤!',
},
{
step: 2,
action: '检查冷却液液位,必要时补充',
priority: 'high',
estimatedTime: '10分钟',
requiredParts: ['冷却液'],
safetyWarning: '确保发动机已完全冷却',
},
{
step: 3,
action: '检查水泵工作状态',
priority: 'high',
estimatedTime: '20分钟',
},
{
step: 4,
action: '检查散热器是否堵塞',
priority: 'medium',
estimatedTime: '15分钟',
},
],
knowledgeBaseIds: ['KB003', 'KB025'],
},
// ========== 预测性故障 ==========
{
code: 'W1001',
name: '发动机积碳预警',
category: '发动机系统',
level: 'warning',
description: '根据运行模式和燃油消耗分析,预测发动机可能存在积碳',
diagnosticRules: [
{
condition: '长时间低转速运行',
sensorKeys: ['engine_rpm', 'low_rpm_duration'],
threshold: { max: 1200, duration: 7200 }, // 低于1200转持续2小时
diagnosis: '长期低转速运行容易产生积碳',
confidence: 0.70,
},
{
condition: '燃油消耗率异常升高',
sensorKeys: ['fuel_consumption_rate'],
diagnosis: '燃烧效率下降,可能积碳严重',
confidence: 0.75,
},
],
predictionConditions: [
{
type: 'pattern',
description: '发动机积碳风险评估',
indicators: [
{ key: 'low_rpm_hours_ratio', threshold: 0.6, unit: '占比' },
{ key: 'fuel_efficiency_drop', threshold: 15, unit: '%' },
],
warningMessage: '发动机存在积碳风险,建议进行清洗保养',
recommendedAction: '安排发动机清洗维护,使用积碳清洗剂',
},
],
solutions: [
{
step: 1,
action: '使用发动机积碳清洗剂',
priority: 'medium',
estimatedTime: '1小时',
requiredParts: ['积碳清洗剂'],
},
{
step: 2,
action: '适当提高发动机转速运行30分钟',
priority: 'medium',
estimatedTime: '30分钟',
},
{
step: 3,
action: '检查火花塞积碳情况,必要时更换',
priority: 'low',
estimatedTime: '20分钟',
},
],
knowledgeBaseIds: ['KB030', 'KB031'],
},
{
code: 'W1002',
name: '皮带磨损预警',
category: '传动系统',
level: 'warning',
description: '根据使用时间和振动数据,预测传动皮带可能需要更换',
diagnosticRules: [
{
condition: '皮带使用时间接近更换周期',
sensorKeys: ['belt_usage_hours'],
threshold: { min: 1800 }, // 1800小时
diagnosis: '皮带接近使用寿命',
confidence: 0.80,
},
{
condition: '振动传感器检测异常',
sensorKeys: ['vibration_level'],
threshold: { min: 0.5 },
diagnosis: '皮带松弛或磨损导致振动增大',
confidence: 0.75,
},
],
predictionConditions: [
{
type: 'usage',
description: '皮带寿命预测',
indicators: [
{ key: 'belt_usage_hours', threshold: 1800, unit: '小时' },
{ key: 'belt_tension_drop', threshold: 20, unit: '%' },
],
warningMessage: '皮带磨损严重,建议及时更换避免断裂',
recommendedAction: '准备备用皮带,计划更换时间',
},
],
solutions: [
{
step: 1,
action: '检查皮带张紧度',
priority: 'high',
estimatedTime: '10分钟',
requiredTools: ['张紧度测量仪'],
},
{
step: 2,
action: '检查皮带表面磨损、裂纹',
priority: 'high',
estimatedTime: '10分钟',
},
{
step: 3,
action: '更换传动皮带',
priority: 'high',
estimatedTime: '45分钟',
requiredParts: ['传动皮带'],
requiredTools: ['扳手组', '张紧工具'],
},
],
knowledgeBaseIds: ['KB040', 'KB041'],
},
{
code: 'W1003',
name: '液压油变质预警',
category: '液压系统',
level: 'warning',
description: '根据液压油使用时间和温度数据,预测液压油可能需要更换',
diagnosticRules: [
{
condition: '液压油使用时间超过标准',
sensorKeys: ['hydraulic_oil_hours'],
threshold: { min: 500 },
diagnosis: '液压油接近更换周期',
confidence: 0.85,
},
{
condition: '液压油温度频繁过高',
sensorKeys: ['hydraulic_oil_temp', 'over_temp_count'],
threshold: { min: 80, duration: 3600 },
diagnosis: '高温加速液压油老化',
confidence: 0.80,
},
],
predictionConditions: [
{
type: 'sensor',
description: '液压油质量评估',
indicators: [
{ key: 'hydraulic_oil_hours', threshold: 500, unit: '小时' },
{ key: 'over_temp_hours', threshold: 50, unit: '小时' },
],
warningMessage: '液压油性能下降,建议更换以保护液压系统',
recommendedAction: '安排液压油更换和系统清洗',
},
],
solutions: [
{
step: 1,
action: '抽取液压油样本,检查颜色和杂质',
priority: 'medium',
estimatedTime: '10分钟',
},
{
step: 2,
action: '更换液压油和滤芯',
priority: 'high',
estimatedTime: '2小时',
requiredParts: ['液压油', '液压滤芯'],
requiredTools: ['油桶', '扳手'],
},
{
step: 3,
action: '清洗液压油箱',
priority: 'medium',
estimatedTime: '1小时',
},
],
knowledgeBaseIds: ['KB050', 'KB051'],
},
// ========== 电气系统 ==========
{
code: 'U0100',
name: 'CAN通信故障',
category: '电气系统',
level: 'error',
description: 'CAN总线通信中断或数据异常',
diagnosticRules: [
{
condition: 'CAN总线无数据',
sensorKeys: ['can_bus_status'],
diagnosis: '总线连接故障或控制器故障',
confidence: 0.90,
},
],
solutions: [
{
step: 1,
action: '检查CAN总线插头连接',
priority: 'high',
estimatedTime: '15分钟',
},
{
step: 2,
action: '使用诊断仪检测总线电压',
priority: 'high',
estimatedTime: '20分钟',
requiredTools: ['CAN总线诊断仪'],
},
],
knowledgeBaseIds: ['KB060'],
},
];
/**
* 解决方案知识库
*/
export interface KnowledgeBaseArticle {
id: string;
title: string;
category: string;
tags: string[];
content: string;
diagrams?: string[]; // 图片URL
videos?: string[]; // 视频URL
relatedFaultCodes: string[];
difficulty: 'easy' | 'medium' | 'hard';
estimatedTime: string;
author: string;
createdAt: string;
updatedAt: string;
viewCount: number;
helpfulCount: number;
}
export const KNOWLEDGE_BASE: KnowledgeBaseArticle[] = [
{
id: 'KB001',
title: '火花塞检查与更换指南',
category: '发动机维修',
tags: ['火花塞', '发动机', '失火', '点火系统'],
content: `
# 火花塞检查与更换指南
## 检查步骤
1. 拆下火花塞
2. 观察电极磨损情况
3. 检查间隙标准0.8-0.9mm
4. 查看颜色(正常为灰白色)
## 更换标准
- 电极磨损严重
- 绝缘体开裂
- 积碳严重无法清理
- 使用超过500小时
## 注意事项
⚠️ 更换时注意扭矩规格
⚠️ 使用正确型号的火花塞
`,
relatedFaultCodes: ['P0301', 'P0302', 'P0303', 'P0304'],
difficulty: 'easy',
estimatedTime: '30分钟',
author: '技术团队',
createdAt: '2024-01-01',
updatedAt: '2024-10-01',
viewCount: 1250,
helpfulCount: 980,
},
{
id: 'KB003',
title: '发动机过热故障排查流程',
category: '冷却系统',
tags: ['过热', '冷却系统', '水温', '散热器'],
content: `
# 发动机过热故障排查流程
## 紧急处理
1. 立即停机,避免拉缸
2. 等待完全冷却至少30分钟
3. 检查冷却液液位
## 常见原因
- 冷却液不足
- 水泵故障
- 节温器卡死
- 散热器堵塞
- 风扇不转
## 排查顺序
1. 检查液位(最简单)
2. 检查风扇(观察即可)
3. 检查水泵皮带
4. 检查散热器
5. 检查节温器
## 预防措施
✅ 定期检查冷却液
✅ 清洗散热器
✅ 检查风扇工作
`,
relatedFaultCodes: ['C0040'],
difficulty: 'medium',
estimatedTime: '1-2小时',
author: '技术团队',
createdAt: '2024-01-15',
updatedAt: '2024-09-20',
viewCount: 2100,
helpfulCount: 1850,
},
{
id: 'KB030',
title: '发动机积碳清理与预防',
category: '发动机维护',
tags: ['积碳', '清洗', '保养', '燃烧室'],
content: `
# 发动机积碳清理与预<E4B88E><E9A284><EFBFBD>
## 积碳产生原因
- 长期低速运行
- 燃油质量差
- 进气系统污染
- 火花塞老化
## 清理方法
### 1. 化学清洗
使用专用积碳清洗剂,从进气道注入
### 2. 拆解清洗
拆卸火花塞,刮除积碳
### 3. 核桃砂清洗
专业设备清洗进气道
## 预防措施
✅ 定期高转速运行
✅ 使用优质燃油
✅ 定期更换空滤
✅ 添加燃油添加剂
`,
relatedFaultCodes: ['W1001'],
difficulty: 'medium',
estimatedTime: '2-3小时',
author: '资深技师',
createdAt: '2024-03-10',
updatedAt: '2024-10-05',
viewCount: 1580,
helpfulCount: 1320,
},
];
/**
* 根据故障码查找
*/
export function getFaultCodeInfo(code: string): FaultCode | undefined {
return FAULT_CODE_LIBRARY.find(f => f.code === code);
}
/**
* 根据分类查找故障码
*/
export function getFaultCodesByCategory(category: FaultCategory): FaultCode[] {
return FAULT_CODE_LIBRARY.filter(f => f.category === category);
}
/**
* 根据故障码查找知识库文章
*/
export function getKnowledgeBaseArticles(faultCode: string): KnowledgeBaseArticle[] {
return KNOWLEDGE_BASE.filter(kb => kb.relatedFaultCodes.includes(faultCode));
}
/**
* 分析传感器数据,预测潜在故障
*/
export function predictPotentialFaults(sensorData: Record<string, any>): {
faultCode: string;
confidence: number;
message: string;
action: string;
}[] {
const predictions: any[] = [];
FAULT_CODE_LIBRARY.forEach(fault => {
if (!fault.predictionConditions) return;
fault.predictionConditions.forEach(condition => {
const allIndicatorsMet = condition.indicators.every(indicator => {
const value = sensorData[indicator.key];
return value !== undefined && value >= indicator.threshold;
});
if (allIndicatorsMet) {
predictions.push({
faultCode: fault.code,
confidence: 0.75,
message: condition.warningMessage,
action: condition.recommendedAction,
});
}
});
});
return predictions;
}

View File

@@ -0,0 +1,232 @@
import { Field, FieldVersion, FieldVersionChange } from '../types/field';
/**
* 地块版本管理工具
*/
// 生成唯一ID
const generateId = () => {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
// 比较两个值是否不同
const isDifferent = (oldValue: any, newValue: any): boolean => {
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
return JSON.stringify(oldValue) !== JSON.stringify(newValue);
}
return oldValue !== newValue;
};
// 检测字段变更
const detectChanges = (oldField: Partial<Field>, newField: Partial<Field>): FieldVersionChange[] => {
const changes: FieldVersionChange[] = [];
// 需要监控的字段
const monitoredFields = [
'name',
'code',
'area',
'perimeter',
'location',
'soilType',
'landUseType',
'plantingMode',
'owner',
'ownerPhone',
'contractPeriod',
'certificateNumber',
'tags',
'status',
'elevation',
'slope',
'aspect',
'waterSource',
'remarks',
];
for (const field of monitoredFields) {
const oldValue = (oldField as any)[field];
const newValue = (newField as any)[field];
if (isDifferent(oldValue, newValue)) {
changes.push({
field,
oldValue,
newValue,
});
}
}
return changes;
};
// 检测边界变更
const isBoundaryChanged = (oldCoordinates: any[], newCoordinates: any[]): boolean => {
if (!oldCoordinates || !newCoordinates) return false;
if (oldCoordinates.length !== newCoordinates.length) return true;
return JSON.stringify(oldCoordinates) !== JSON.stringify(newCoordinates);
};
/**
* 创建版本记录
*/
export const createFieldVersion = (
oldField: Field | null,
newField: Field,
changeType: FieldVersion['changeType'],
changedBy: string,
remarks?: string
): FieldVersion | null => {
// 如果是新建,创建初始版本
if (changeType === 'create') {
return {
id: generateId(),
fieldId: newField.id,
version: 1,
changeType: 'create',
changes: [],
coordinates: newField.coordinates,
attributes: { ...newField },
changedBy,
changedAt: new Date().toISOString(),
remarks: remarks || '创建地块',
};
}
// 如果没有旧数据,无法比较
if (!oldField) return null;
// 检测变更
const attributeChanges = detectChanges(oldField, newField);
const boundaryChanged = isBoundaryChanged(oldField.coordinates, newField.coordinates);
// 如果有边界变更,添加到变更列表
if (boundaryChanged) {
attributeChanges.push({
field: 'coordinates',
oldValue: oldField.coordinates.length,
newValue: newField.coordinates.length,
});
}
// 如果没有任何变更,不创建版本
if (attributeChanges.length === 0 && !boundaryChanged) {
return null;
}
// 确定变更类型
let finalChangeType = changeType;
if (changeType === 'update-boundary' || changeType === 'update-attributes') {
// 自动检测
if (boundaryChanged && attributeChanges.length > 1) {
finalChangeType = 'update-boundary'; // 边界变更优先
} else if (boundaryChanged) {
finalChangeType = 'update-boundary';
} else {
finalChangeType = 'update-attributes';
}
}
return {
id: generateId(),
fieldId: newField.id,
version: (oldField.currentVersion || 0) + 1,
changeType: finalChangeType,
changes: attributeChanges,
coordinates: boundaryChanged ? newField.coordinates : undefined,
attributes: { ...newField },
changedBy,
changedAt: new Date().toISOString(),
remarks,
};
};
/**
* 保存版本到localStorage
*/
export const saveFieldVersion = (version: FieldVersion): void => {
try {
const key = `field_versions_${version.fieldId}`;
const data = localStorage.getItem(key);
let versions: FieldVersion[] = data ? JSON.parse(data) : [];
// 添加新版本
versions.push(version);
// 保存
localStorage.setItem(key, JSON.stringify(versions));
} catch (error) {
console.error('保存版本失败:', error);
throw error;
}
};
/**
* 加载地块的所有版本
*/
export const loadFieldVersions = (fieldId: string): FieldVersion[] => {
try {
const key = `field_versions_${fieldId}`;
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : [];
} catch (error) {
console.error('加载版本失败:', error);
return [];
}
};
/**
* 获取特定版本
*/
export const getFieldVersion = (fieldId: string, version: number): FieldVersion | null => {
const versions = loadFieldVersions(fieldId);
return versions.find(v => v.version === version) || null;
};
/**
* 从版本恢复地块数据
*/
export const restoreFromVersion = (version: FieldVersion): Partial<Field> => {
if (!version.attributes) {
throw new Error('版本数据不完整');
}
const restored: Partial<Field> = { ...version.attributes };
// 如果有边界数据,使用版本的边界
if (version.coordinates) {
restored.coordinates = version.coordinates;
}
return restored;
};
/**
* 删除地块的所有版本记录
*/
export const deleteFieldVersions = (fieldId: string): void => {
try {
const key = `field_versions_${fieldId}`;
localStorage.removeItem(key);
} catch (error) {
console.error('删除版本失败:', error);
}
};
/**
* 获取版本统计信息
*/
export const getVersionStatistics = (fieldId: string) => {
const versions = loadFieldVersions(fieldId);
return {
total: versions.length,
byType: versions.reduce((acc, v) => {
acc[v.changeType] = (acc[v.changeType] || 0) + 1;
return acc;
}, {} as Record<FieldVersion['changeType'], number>),
latestVersion: versions.length > 0
? Math.max(...versions.map(v => v.version))
: 0,
};
};

View File

@@ -0,0 +1,307 @@
import { FieldVersion } from '../types/field';
/**
* 地块版本历史示例数据
* 用于演示版本管理功能
*/
/**
* 为指定地块生成示例版本历史数据
*/
export const generateSampleVersions = (fieldId: string, fieldCode: string): FieldVersion[] => {
const baseDate = new Date('2024-01-15');
return [
// 版本1创建地块
{
id: `${fieldId}-v1`,
fieldId,
version: 1,
changeType: 'create',
changes: [],
coordinates: [
{ lat: 30.5728, lng: 114.2943 },
{ lat: 30.5730, lng: 114.2943 },
{ lat: 30.5730, lng: 114.2948 },
{ lat: 30.5728, lng: 114.2948 },
],
attributes: {
id: fieldId,
code: fieldCode,
name: '东区1号地块',
area: 15.5,
perimeter: 520,
location: '湖北省武汉市江夏区',
soilType: 'loamy',
landUseType: 'farmland',
plantingMode: 'open-field',
owner: '张三',
ownerPhone: '13800138001',
contractPeriod: 30,
tags: ['优质地块'],
status: 'normal',
},
changedBy: '系统管理员',
changedAt: baseDate.toISOString(),
remarks: '创建地块',
},
// 版本2更新基本信息
{
id: `${fieldId}-v2`,
fieldId,
version: 2,
changeType: 'update-attributes',
changes: [
{
field: 'area',
oldValue: 15.5,
newValue: 16.2,
},
{
field: 'owner',
oldValue: '张三',
newValue: '张三(已确权)',
},
{
field: 'certificateNumber',
oldValue: undefined,
newValue: 'JX202401001',
},
],
changedBy: '王芳',
changedAt: new Date(baseDate.getTime() + 15 * 24 * 60 * 60 * 1000).toISOString(),
remarks: '完善权属信息,更新实测面积',
},
// 版本3添加标签
{
id: `${fieldId}-v3`,
fieldId,
version: 3,
changeType: 'update-attributes',
changes: [
{
field: 'tags',
oldValue: ['优质地块'],
newValue: ['优质地块', '高产田', '水源充足'],
},
],
changedBy: '李明',
changedAt: new Date(baseDate.getTime() + 45 * 24 * 60 * 60 * 1000).toISOString(),
remarks: '根据实地考察添加标签',
},
// 版本4调整边界
{
id: `${fieldId}-v4`,
fieldId,
version: 4,
changeType: 'update-boundary',
changes: [
{
field: 'coordinates',
oldValue: 4,
newValue: 5,
},
{
field: 'area',
oldValue: 16.2,
newValue: 16.8,
},
{
field: 'perimeter',
oldValue: 520,
newValue: 545,
},
],
coordinates: [
{ lat: 30.5728, lng: 114.2943 },
{ lat: 30.5730, lng: 114.2943 },
{ lat: 30.5731, lng: 114.2946 },
{ lat: 30.5730, lng: 114.2948 },
{ lat: 30.5728, lng: 114.2948 },
],
changedBy: '测绘组-赵强',
changedAt: new Date(baseDate.getTime() + 90 * 24 * 60 * 60 * 1000).toISOString(),
remarks: '使用RTK重新测绘边界更精确',
},
// 版本5更新土壤信息
{
id: `${fieldId}-v5`,
fieldId,
version: 5,
changeType: 'update-attributes',
changes: [
{
field: 'soilType',
oldValue: 'loamy',
newValue: 'clay',
},
{
field: 'elevation',
oldValue: undefined,
newValue: 45,
},
{
field: 'slope',
oldValue: undefined,
newValue: 3.5,
},
],
changedBy: '农技员-孙莉',
changedAt: new Date(baseDate.getTime() + 120 * 24 * 60 * 60 * 1000).toISOString(),
remarks: '土壤检测后更新土壤类型,补充地形数据',
},
// 版本6更改种植模式
{
id: `${fieldId}-v6`,
fieldId,
version: 6,
changeType: 'update-attributes',
changes: [
{
field: 'plantingMode',
oldValue: 'open-field',
newValue: 'greenhouse',
},
{
field: 'waterSource',
oldValue: undefined,
newValue: '水渠灌溉',
},
],
changedBy: '张三',
changedAt: new Date(baseDate.getTime() + 180 * 24 * 60 * 60 * 1000).toISOString(),
remarks: '建设温室大棚,改变种植模式',
},
// 版本7补充坡向信息
{
id: `${fieldId}-v7`,
fieldId,
version: 7,
changeType: 'update-attributes',
changes: [
{
field: 'aspect',
oldValue: undefined,
newValue: '东南',
},
{
field: 'remarks',
oldValue: undefined,
newValue: '光照条件良好,适合蔬菜种植',
},
],
changedBy: '农技员-孙莉',
changedAt: new Date(baseDate.getTime() + 210 * 24 * 60 * 60 * 1000).toISOString(),
remarks: '完善地块基础信息',
},
// 版本8更新联系方式
{
id: `${fieldId}-v8`,
fieldId,
version: 8,
changeType: 'update-attributes',
changes: [
{
field: 'ownerPhone',
oldValue: '13800138001',
newValue: '13900139001',
},
],
changedBy: '系统管理员',
changedAt: new Date(baseDate.getTime() + 250 * 24 * 60 * 60 * 1000).toISOString(),
remarks: '应权属人要求更新联系电话',
},
];
};
/**
* 初始化示例版本数据到localStorage
*/
export const initializeSampleVersions = (fieldId: string, fieldCode: string): void => {
try {
const key = `field_versions_${fieldId}`;
// 检查是否已有数据
const existing = localStorage.getItem(key);
if (existing) {
console.log(`地块 ${fieldCode} 的版本数据已存在,跳过初始化`);
return;
}
// 生成示例数据
const versions = generateSampleVersions(fieldId, fieldCode);
// 保存到localStorage
localStorage.setItem(key, JSON.stringify(versions));
console.log(`已为地块 ${fieldCode} (ID: ${fieldId}) 生成 ${versions.length} 条版本历史`);
} catch (error) {
console.error('初始化示例版本数据失败:', error);
}
};
/**
* 批量初始化多个地块的示例版本
*/
export const initializeMultipleFieldVersions = (): void => {
try {
// 获取所有地块
const fieldsData = localStorage.getItem('smart_agriculture_fields');
if (!fieldsData) {
console.log('未找到地块数据,无法初始化版本历史');
return;
}
const fields = JSON.parse(fieldsData);
// 只为前3个地块生成示例数据
const fieldsToInit = fields.slice(0, 3);
fieldsToInit.forEach((field: any) => {
initializeSampleVersions(field.id, field.code);
});
console.log(`已为 ${fieldsToInit.length} 个地块初始化版本历史`);
} catch (error) {
console.error('批量初始化失败:', error);
}
};
/**
* 清除指定地块的示例版本数据
*/
export const clearSampleVersions = (fieldId: string): void => {
try {
const key = `field_versions_${fieldId}`;
localStorage.removeItem(key);
console.log(`已清除地块 ${fieldId} 的版本数据`);
} catch (error) {
console.error('清除版本数据失败:', error);
}
};
/**
* 清除所有地块的版本数据
*/
export const clearAllVersions = (): void => {
try {
const fieldsData = localStorage.getItem('smart_agriculture_fields');
if (!fieldsData) return;
const fields = JSON.parse(fieldsData);
fields.forEach((field: any) => {
clearSampleVersions(field.id);
});
console.log('已清除所有地块的版本数据');
} catch (error) {
console.error('清除所有版本数据失败:', error);
}
};

526
src/lib/gisMapEngine.ts Normal file
View File

@@ -0,0 +1,526 @@
/**
* GIS地图引擎 - 统一的地图渲染和管理引擎
* 支持多种地图服务商和占位模式
*/
export type MapProvider = 'amap' | 'leaflet' | 'placeholder';
export type MapLayer = 'satellite' | 'street' | 'terrain' | 'hybrid';
export interface MapConfig {
provider: MapProvider;
container: string | HTMLElement;
center?: [number, number]; // [lng, lat]
zoom?: number;
layer?: MapLayer;
features?: MapFeature[];
}
export interface MapFeature {
controls?: {
zoom?: boolean;
scale?: boolean;
layers?: boolean;
fullscreen?: boolean;
measure?: boolean;
};
interactions?: {
drag?: boolean;
zoom?: boolean;
rotate?: boolean;
};
}
export interface MapPosition {
lng: number;
lat: number;
}
export interface MapBounds {
northeast: MapPosition;
southwest: MapPosition;
}
export interface Marker {
id: string;
position: MapPosition;
title?: string;
content?: string;
icon?: string;
color?: string;
onClick?: () => void;
}
export interface Polygon {
id: string;
path: MapPosition[];
fillColor?: string;
strokeColor?: string;
fillOpacity?: number;
strokeWeight?: number;
onClick?: () => void;
}
/**
* GIS地图引擎类
*/
export class GISMapEngine {
private provider: MapProvider;
private map: any = null;
private markers: Map<string, any> = new Map();
private polygons: Map<string, any> = new Map();
private currentLayer: MapLayer = 'satellite';
private container: HTMLElement | null = null;
constructor(config: MapConfig) {
this.provider = config.provider;
this.initialize(config);
}
/**
* 初始化地图
*/
private async initialize(config: MapConfig) {
const container = typeof config.container === 'string'
? document.getElementById(config.container)
: config.container;
if (!container) {
console.error('地图容器不存在');
return;
}
this.container = container;
switch (this.provider) {
case 'amap':
await this.initAMap(config);
break;
case 'leaflet':
await this.initLeaflet(config);
break;
case 'placeholder':
this.initPlaceholder(config);
break;
}
}
/**
* 初始化高德地图
*/
private async initAMap(config: MapConfig) {
try {
// 检查是否已加载高德地图
if (!window.AMap) {
console.warn('高德地图SDK未加载切换到占位模式');
this.provider = 'placeholder';
this.initPlaceholder(config);
return;
}
const center = config.center || [116.4074, 39.9042]; // 默认北京
const zoom = config.zoom || 13;
this.map = new window.AMap.Map(this.container, {
center: center,
zoom: zoom,
viewMode: '2D',
});
// 设置图层
this.setLayer(config.layer || 'satellite');
console.log('✅ 高德地图初始化成功');
} catch (error) {
console.error('高德地图初始化失败:', error);
this.provider = 'placeholder';
this.initPlaceholder(config);
}
}
/**
* 初始化Leaflet地图使用OpenStreetMap
*/
private async initLeaflet(config: MapConfig) {
try {
// 动态加载Leaflet
if (!window.L) {
await this.loadLeaflet();
}
const center = config.center || [39.9042, 116.4074]; // Leaflet用 [lat, lng]
const zoom = config.zoom || 13;
this.map = window.L.map(this.container).setView([center[1], center[0]], zoom);
// 设置图层
this.setLayer(config.layer || 'street');
console.log('✅ Leaflet地图初始化成功');
} catch (error) {
console.error('Leaflet地图初始化失败:', error);
this.provider = 'placeholder';
this.initPlaceholder(config);
}
}
/**
* 加载Leaflet库
*/
private async loadLeaflet(): Promise<void> {
return new Promise((resolve, reject) => {
// 加载CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
document.head.appendChild(link);
// 加载JS
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Leaflet加载失败'));
document.head.appendChild(script);
});
}
/**
* 初始化占位地图
*/
private initPlaceholder(config: MapConfig) {
if (!this.container) return;
this.container.innerHTML = `
<div class="gis-placeholder-map" style="
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f0fdf4 0%, #dbeafe 100%);
position: relative;
overflow: hidden;
">
<div style="
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px);
background-size: 50px 50px;
"></div>
</div>
`;
console.log('✅ 占位地图初始化成功(功能完整)');
}
/**
* 设置地图图层
*/
setLayer(layer: MapLayer) {
this.currentLayer = layer;
if (this.provider === 'amap' && this.map) {
// 高德地图图层
this.map.setLayers([this.getAMapLayer(layer)]);
} else if (this.provider === 'leaflet' && this.map) {
// Leaflet图层
this.getLeafletLayer(layer).addTo(this.map);
}
}
/**
* 获取高德地图图层
*/
private getAMapLayer(layer: MapLayer) {
switch (layer) {
case 'satellite':
return new window.AMap.TileLayer.Satellite();
case 'street':
return new window.AMap.TileLayer();
case 'terrain':
return new window.AMap.TileLayer();
case 'hybrid':
return [
new window.AMap.TileLayer.Satellite(),
new window.AMap.TileLayer.RoadNet()
];
default:
return new window.AMap.TileLayer();
}
}
/**
* 获取Leaflet图层
*/
private getLeafletLayer(layer: MapLayer) {
const baseLayers: Record<MapLayer, string> = {
satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
street: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
terrain: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
hybrid: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
};
return window.L.tileLayer(baseLayers[layer], {
attribution: '© OpenStreetMap contributors'
});
}
/**
* 添加标记点
*/
addMarker(marker: Marker) {
if (this.provider === 'amap' && this.map) {
const amapMarker = new window.AMap.Marker({
position: [marker.position.lng, marker.position.lat],
title: marker.title,
});
if (marker.onClick) {
amapMarker.on('click', marker.onClick);
}
this.map.add(amapMarker);
this.markers.set(marker.id, amapMarker);
} else if (this.provider === 'leaflet' && this.map) {
const leafletMarker = window.L.marker([marker.position.lat, marker.position.lng])
.addTo(this.map);
if (marker.title) {
leafletMarker.bindPopup(marker.title);
}
if (marker.onClick) {
leafletMarker.on('click', marker.onClick);
}
this.markers.set(marker.id, leafletMarker);
} else if (this.provider === 'placeholder') {
// 占位模式:在容器中添加标记点
this.addPlaceholderMarker(marker);
}
}
/**
* 占位模式添加标记
*/
private addPlaceholderMarker(marker: Marker) {
if (!this.container) return;
const markerEl = document.createElement('div');
markerEl.id = `marker-${marker.id}`;
markerEl.style.cssText = `
position: absolute;
left: ${Math.random() * 80 + 10}%;
top: ${Math.random() * 80 + 10}%;
transform: translate(-50%, -50%);
width: 24px;
height: 24px;
background: ${marker.color || '#22c55e'};
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
cursor: pointer;
z-index: 10;
`;
if (marker.onClick) {
markerEl.addEventListener('click', marker.onClick);
}
this.container.querySelector('.gis-placeholder-map')?.appendChild(markerEl);
this.markers.set(marker.id, markerEl);
}
/**
* 添加多边形
*/
addPolygon(polygon: Polygon) {
if (this.provider === 'amap' && this.map) {
const amapPolygon = new window.AMap.Polygon({
path: polygon.path.map(p => [p.lng, p.lat]),
fillColor: polygon.fillColor || '#22c55e',
strokeColor: polygon.strokeColor || '#166534',
fillOpacity: polygon.fillOpacity || 0.3,
strokeWeight: polygon.strokeWeight || 2,
});
if (polygon.onClick) {
amapPolygon.on('click', polygon.onClick);
}
this.map.add(amapPolygon);
this.polygons.set(polygon.id, amapPolygon);
} else if (this.provider === 'leaflet' && this.map) {
const leafletPolygon = window.L.polygon(
polygon.path.map(p => [p.lat, p.lng]),
{
color: polygon.strokeColor || '#166534',
fillColor: polygon.fillColor || '#22c55e',
fillOpacity: polygon.fillOpacity || 0.3,
weight: polygon.strokeWeight || 2,
}
).addTo(this.map);
if (polygon.onClick) {
leafletPolygon.on('click', polygon.onClick);
}
this.polygons.set(polygon.id, leafletPolygon);
}
}
/**
* 移除标记
*/
removeMarker(id: string) {
const marker = this.markers.get(id);
if (!marker) return;
if (this.provider === 'amap' && this.map) {
this.map.remove(marker);
} else if (this.provider === 'leaflet') {
marker.remove();
} else if (this.provider === 'placeholder') {
marker.remove();
}
this.markers.delete(id);
}
/**
* 移除多边形
*/
removePolygon(id: string) {
const polygon = this.polygons.get(id);
if (!polygon) return;
if (this.provider === 'amap' && this.map) {
this.map.remove(polygon);
} else if (this.provider === 'leaflet') {
polygon.remove();
}
this.polygons.delete(id);
}
/**
* 设置中心点
*/
setCenter(position: MapPosition, zoom?: number) {
if (this.provider === 'amap' && this.map) {
this.map.setCenter([position.lng, position.lat]);
if (zoom) this.map.setZoom(zoom);
} else if (this.provider === 'leaflet' && this.map) {
this.map.setView([position.lat, position.lng], zoom || this.map.getZoom());
}
}
/**
* 适应边界
*/
fitBounds(bounds: MapBounds) {
if (this.provider === 'amap' && this.map) {
this.map.setBounds(
new window.AMap.Bounds(
[bounds.southwest.lng, bounds.southwest.lat],
[bounds.northeast.lng, bounds.northeast.lat]
)
);
} else if (this.provider === 'leaflet' && this.map) {
this.map.fitBounds([
[bounds.southwest.lat, bounds.southwest.lng],
[bounds.northeast.lat, bounds.northeast.lng]
]);
}
}
/**
* 缩放
*/
setZoom(zoom: number) {
if (this.provider === 'amap' && this.map) {
this.map.setZoom(zoom);
} else if (this.provider === 'leaflet' && this.map) {
this.map.setZoom(zoom);
}
}
/**
* 获取当前缩放级别
*/
getZoom(): number {
if (this.provider === 'amap' && this.map) {
return this.map.getZoom();
} else if (this.provider === 'leaflet' && this.map) {
return this.map.getZoom();
}
return 13; // 默认缩放
}
/**
* 清除所有标记
*/
clearMarkers() {
this.markers.forEach((marker, id) => {
this.removeMarker(id);
});
}
/**
* 清除所有多边形
*/
clearPolygons() {
this.polygons.forEach((polygon, id) => {
this.removePolygon(id);
});
}
/**
* 清除所有覆盖物
*/
clearAll() {
this.clearMarkers();
this.clearPolygons();
}
/**
* 销毁地图
*/
destroy() {
this.clearAll();
if (this.map) {
if (this.provider === 'amap') {
this.map.destroy();
} else if (this.provider === 'leaflet') {
this.map.remove();
}
this.map = null;
}
if (this.container) {
this.container.innerHTML = '';
}
}
/**
* 获取地图实例
*/
getMapInstance() {
return this.map;
}
/**
* 获取当前提供商
*/
getProvider(): MapProvider {
return this.provider;
}
}
// 全局类型声明
declare global {
interface Window {
AMap: any;
L: any;
_AMapSecurityConfig: any;
}
}

View File

@@ -1,15 +1,41 @@
// 农机档案数据存储管理
import { MachineryRecord, MachineryChangeHistory, MachineryTag } from '../types/machinery';
import { MachineryRecord, MachineryChangeHistory, MachineryTag, MaintenanceRecord } from '../types/machinery';
const MACHINERY_KEY = 'smart_agriculture_machinery';
const HISTORY_KEY = 'smart_agriculture_machinery_history';
const TAGS_KEY = 'smart_agriculture_machinery_tags';
const MAINTENANCE_KEY = 'smart_agriculture_machinery_maintenance';
// 数据迁移:将旧状态值转换为新状态值
function migrateStatusValue(status: string): string {
const statusMap: Record<string, string> = {
'运行中': '正常',
'空闲中': '正常',
'维修中': '待维护',
'报废': '已报废',
};
return statusMap[status] || status;
}
export const machineryStorage = {
// 获取所有农机档案
getAllMachinery(): MachineryRecord[] {
const data = localStorage.getItem(MACHINERY_KEY);
return data ? JSON.parse(data) : [];
if (!data) return [];
const machinery: MachineryRecord[] = JSON.parse(data);
// 自动迁移旧状态值
const migrated = machinery.map(m => ({
...m,
status: migrateStatusValue(m.status)
}));
// 如果有数据被迁移保存回localStorage
if (JSON.stringify(machinery) !== JSON.stringify(migrated)) {
localStorage.setItem(MACHINERY_KEY, JSON.stringify(migrated));
}
return migrated;
},
// 获取单个农机档案
@@ -81,5 +107,88 @@ export const machineryStorage = {
getMachineryByQRCode(qrCode: string): MachineryRecord | undefined {
const all = this.getAllMachinery();
return all.find(m => m.qrCode === qrCode);
},
// 获取某个农机的所有维护记录
getMaintenanceRecords(machineryId: string): MaintenanceRecord[] {
const data = localStorage.getItem(MAINTENANCE_KEY);
const all: MaintenanceRecord[] = data ? JSON.parse(data) : [];
// 数据迁移将旧格式的partsAndMaterials(string[])转换为新格式(MaterialUsage[])
const migrated = all.map(record => {
if (record.partsAndMaterials && record.partsAndMaterials.length > 0) {
// 检查是否是旧格式string数组
const firstItem = record.partsAndMaterials[0];
if (typeof firstItem === 'string') {
// 旧格式,转换为新格式
return {
...record,
partsAndMaterials: (record.partsAndMaterials as any[]).map((id: string) => ({
materialId: id,
quantity: 1
}))
};
}
}
return record;
});
// 如果有数据被迁移保存回localStorage
if (JSON.stringify(all) !== JSON.stringify(migrated)) {
localStorage.setItem(MAINTENANCE_KEY, JSON.stringify(migrated));
}
return migrated.filter(m => m.machineryId === machineryId).sort((a, b) =>
new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
);
},
// 获取所有维护记录
getAllMaintenanceRecords(): MaintenanceRecord[] {
const data = localStorage.getItem(MAINTENANCE_KEY);
const all: MaintenanceRecord[] = data ? JSON.parse(data) : [];
// 数据迁移将旧格式的partsAndMaterials(string[])转换为新格式(MaterialUsage[])
const migrated = all.map(record => {
if (record.partsAndMaterials && record.partsAndMaterials.length > 0) {
const firstItem = record.partsAndMaterials[0];
if (typeof firstItem === 'string') {
return {
...record,
partsAndMaterials: (record.partsAndMaterials as any[]).map((id: string) => ({
materialId: id,
quantity: 1
}))
};
}
}
return record;
});
// 如果有数据被迁移保存回localStorage
if (JSON.stringify(all) !== JSON.stringify(migrated)) {
localStorage.setItem(MAINTENANCE_KEY, JSON.stringify(migrated));
}
return migrated;
},
// 保存维护记录
saveMaintenanceRecord(record: MaintenanceRecord): void {
const all = this.getAllMaintenanceRecords();
const index = all.findIndex(m => m.id === record.id);
if (index >= 0) {
all[index] = record;
} else {
all.push(record);
}
localStorage.setItem(MAINTENANCE_KEY, JSON.stringify(all));
},
// 删除维护记录
deleteMaintenanceRecord(id: string): void {
const all = this.getAllMaintenanceRecords();
const filtered = all.filter(m => m.id !== id);
localStorage.setItem(MAINTENANCE_KEY, JSON.stringify(filtered));
}
};

View File

@@ -0,0 +1,320 @@
// 维护记录示例数据
import { MaintenanceRecord, MaterialUsage } from '../types/machinery';
import { machineryStorage } from './machineryStorage';
/**
* 初始化维护记录示例数据
* 包含多种维护类型、配件使用、维护场景等
*/
export function initializeMaintenanceMockData() {
// 检查是否已有数据(避免重复初始化)
const allRecords = machineryStorage.getAllMaintenanceRecords();
if (allRecords.length > 0) {
return; // 已有数据
}
// 获取所有农机为前3台农机创建维护记录
const allMachinery = machineryStorage.getAllMachinery();
if (allMachinery.length === 0) {
return; // 没有农机,无法创建维护记录
}
const mockRecords: MaintenanceRecord[] = [];
const now = new Date();
// ==================== 农机1约翰迪尔拖拉机维护记录 ====================
if (allMachinery[0]) {
const machinery1Id = allMachinery[0].id;
// 1. 日常保养记录
mockRecords.push({
id: `maintenance-${Date.now()}-001`,
machineryId: machinery1Id,
type: '日常保养',
startTime: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7天前
endTime: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000).toISOString(), // 持续2小时
workHours: 2,
nextMaintenanceTime: new Date(now.getTime() + 23 * 24 * 60 * 60 * 1000).toISOString(), // 23天后
maintenanceItems: '更换机油、机油滤芯,检查空气滤芯,润滑各部位,检查轮胎气压,清洁设备外观',
partsAndMaterials: [
{ materialId: 'material-oil-001', quantity: 15 }, // 液压油 15升
{ materialId: 'material-filter-001', quantity: 2 }, // 机油滤芯 2个
],
cost: 850,
technician: '李师傅',
remarks: '定期保养,设备运行正常,无异常发现',
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
// 2. 定期维护记录
mockRecords.push({
id: `maintenance-${Date.now()}-002`,
machineryId: machinery1Id,
type: '定期维护',
startTime: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), // 45天前
endTime: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000).toISOString(), // 持续4小时
workHours: 4,
nextMaintenanceTime: new Date(now.getTime() + 135 * 24 * 60 * 60 * 1000).toISOString(), // 135天后半年维护
maintenanceItems: '更换空气滤芯、燃油滤芯,检查刹车系统,调整传动皮带,检查液压系统,校准作业深度传感器,全面检查电气系统',
partsAndMaterials: [
{ materialId: 'material-filter-002', quantity: 1 }, // 空气滤芯 1个
{ materialId: 'material-filter-003', quantity: 1 }, // 燃油滤芯 1个
{ materialId: 'material-oil-001', quantity: 20 }, // 液压油 20升
{ materialId: 'material-parts-001', quantity: 2 }, // 传动皮带 2条
],
cost: 2380,
technician: '王工',
remarks: '半年度定期维护,更换了老化的传动皮带,刹车系统调整正常,液压系统压力正常',
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
// 3. 故障维修记录
mockRecords.push({
id: `maintenance-${Date.now()}-003`,
machineryId: machinery1Id,
type: '故障维修',
startTime: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), // 20天前
endTime: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000).toISOString(), // 持续6小时
workHours: 6,
maintenanceItems: '修复液压系统漏油问题更换损坏的油封和O型圈检查并紧固各连接部位清洗液压油箱补充液压油',
partsAndMaterials: [
{ materialId: 'material-parts-002', quantity: 3 }, // 油封 3个
{ materialId: 'material-parts-003', quantity: 5 }, // O型圈 5个
{ materialId: 'material-oil-001', quantity: 8 }, // 液压油 8升
],
cost: 1560,
technician: '李师傅、张技师',
remarks: '发现液压油管接头处油封老化导致漏油,已更换新油封,测试正常。建议加强日常检查。',
createdAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
// 4. 年检记录
mockRecords.push({
id: `maintenance-${Date.now()}-004`,
machineryId: machinery1Id,
type: '年检',
startTime: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(), // 90天前
endTime: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000).toISOString(), // 持续3小时
workHours: 3,
nextMaintenanceTime: new Date(now.getTime() + 275 * 24 * 60 * 60 * 1000).toISOString(), // 275天后下次年检
maintenanceItems: '年度安全检查,排放检测,噪音测试,制动性能测试,灯光系统检查,安全装置检查,更换易损件',
partsAndMaterials: [
{ materialId: 'material-parts-004', quantity: 4 }, // 刹车片 4片
{ materialId: 'material-parts-005', quantity: 2 }, // 灯泡 2个
],
cost: 1200,
technician: '农机监理站-刘检验员',
remarks: '年检合格所有安全项目符合标准。建议更换刹车片已现场更换。年检证书编号AJ-2024-001',
createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
}
// ==================== 农机2收割机维护记录 ====================
if (allMachinery[1]) {
const machinery2Id = allMachinery[1].id;
// 1. 作业季前检修
mockRecords.push({
id: `maintenance-${Date.now()}-005`,
machineryId: machinery2Id,
type: '定期维护',
startTime: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(), // 60天前
endTime: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000 + 8 * 60 * 60 * 1000).toISOString(), // 持续8小时
workHours: 8,
nextMaintenanceTime: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(),
maintenanceItems: '收获季节前全面检修:更换刀片组件,调整脱粒滚筒间隙,清理筛网,检查传动链条张紧度,润滑各轴承点,检查液压升降系统,更换磨损严重的传动带',
partsAndMaterials: [
{ materialId: 'material-parts-006', quantity: 8 }, // 收割刀片 8片
{ materialId: 'material-parts-001', quantity: 3 }, // 传动皮带 3条
{ materialId: 'material-oil-002', quantity: 5 }, // 齿轮油 5升
{ materialId: 'material-parts-007', quantity: 1 }, // 传动链条 1条
],
cost: 4200,
technician: '赵师傅、孙师傅',
remarks: '收获季节前全面检修,刀片已全部更换为新件,脱粒系统调整到最佳状态,试运行正常。',
createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
// 2. 作业中故障抢修
mockRecords.push({
id: `maintenance-${Date.now()}-006`,
machineryId: machinery2Id,
type: '故障维修',
startTime: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30天前
endTime: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000 + 5 * 60 * 60 * 1000).toISOString(), // 持续5小时
workHours: 5,
maintenanceItems: '紧急抢修:更换损坏的传动轴承,修复电气线路短路问题,调整清粮风机,紧固松动的螺栓',
partsAndMaterials: [
{ materialId: 'material-parts-008', quantity: 2 }, // 轴承 2个
{ materialId: 'material-parts-009', quantity: 10 }, // 电线 10米
{ materialId: 'material-parts-010', quantity: 20 }, // 螺栓螺母 20套
],
cost: 1850,
technician: '李师傅、王工(紧急出车)',
remarks: '收割作业中突发故障,传动轴承损坏导致异响。现场紧急更换轴承,检查发现部分电气线路老化短路,一并处理。抢修后恢复作业。',
createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
// 3. 作业季后保养
mockRecords.push({
id: `maintenance-${Date.now()}-007`,
machineryId: machinery2Id,
type: '日常保养',
startTime: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), // 10天前
endTime: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000).toISOString(), // 持续4小时
workHours: 4,
nextMaintenanceTime: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
maintenanceItems: '收获季节后保养:全面清理机身杂物和积尘,更换机油和滤芯,检查所有润滑点,防锈处理,存放准备',
partsAndMaterials: [
{ materialId: 'material-oil-003', quantity: 18 }, // 机油 18升
{ materialId: 'material-filter-001', quantity: 1 }, // 机油滤芯 1个
{ materialId: 'material-filter-002', quantity: 1 }, // 空气滤芯 1个
{ materialId: 'material-parts-011', quantity: 2 }, // 防锈剂 2瓶
],
cost: 950,
technician: '张师傅',
remarks: '收获季节结束后的保养,设备已清洗干净,关键部位已涂抹防锈剂,准备入库存放。',
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
}
// ==================== 农机3播种机维护记录 ====================
if (allMachinery[2]) {
const machinery3Id = allMachinery[2].id;
// 1. 播种前调试
mockRecords.push({
id: `maintenance-${Date.now()}-008`,
machineryId: machinery3Id,
type: '定期维护',
startTime: new Date(now.getTime() - 50 * 24 * 60 * 60 * 1000).toISOString(), // 50天前
endTime: new Date(now.getTime() - 50 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000).toISOString(), // 持续3小时
workHours: 3,
nextMaintenanceTime: new Date(now.getTime() + 40 * 24 * 60 * 60 * 1000).toISOString(),
maintenanceItems: '播种前调试:检查并调整排种器间距,校准播种深度,检查施肥装置,清洗种子箱,润滑传动部件,调整镇压轮',
partsAndMaterials: [
{ materialId: 'material-parts-012', quantity: 6 }, // 排种轮 6个
{ materialId: 'material-oil-004', quantity: 3 }, // 润滑脂 3kg
],
cost: 680,
technician: '赵师傅',
remarks: '播种前例行调试,排种器调整到最佳状态,播种深度校准完成,试播效果良好。',
createdAt: new Date(now.getTime() - 50 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
// 2. 日常保养
mockRecords.push({
id: `maintenance-${Date.now()}-009`,
machineryId: machinery3Id,
type: '日常保养',
startTime: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), // 15天前
endTime: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000).toISOString(), // 持续2小时
workHours: 2,
nextMaintenanceTime: new Date(now.getTime() + 15 * 24 * 60 * 60 * 1000).toISOString(),
maintenanceItems: '作业中保养:清理种子箱残留,检查排种器磨损情况,润滑各活动部件,紧固松动螺栓',
partsAndMaterials: [
{ materialId: 'material-oil-004', quantity: 1 }, // 润滑脂 1kg
],
cost: 150,
technician: '李师傅',
remarks: '播种作业中的日常保养,设备运行正常,无异常磨损。',
createdAt: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
// 3. 小修记录
mockRecords.push({
id: `maintenance-${Date.now()}-010`,
machineryId: machinery3Id,
type: '故障维修',
startTime: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), // 5天前
endTime: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000 + 1.5 * 60 * 60 * 1000).toISOString(), // 持续1.5小时
workHours: 1.5,
maintenanceItems: '更换磨损的开沟器刀片,调整施肥管道,修复堵塞的排种孔',
partsAndMaterials: [
{ materialId: 'material-parts-013', quantity: 4 }, // 开沟器刀片 4个
],
cost: 420,
technician: '张师傅',
remarks: '开沟器刀片磨损严重影响作业质量,现场更换新刀片,调整后播种深度一致性良好。',
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
}
// ==================== 通用维护案例 ====================
// 如果有更多农机,添加更多示例
if (allMachinery[3]) {
const machinery4Id = allMachinery[3].id;
// 预防性维护示例
mockRecords.push({
id: `maintenance-${Date.now()}-011`,
machineryId: machinery4Id,
type: '定期维护',
startTime: new Date(now.getTime() - 25 * 24 * 60 * 60 * 1000).toISOString(),
endTime: new Date(now.getTime() - 25 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000).toISOString(),
workHours: 3,
nextMaintenanceTime: new Date(now.getTime() + 65 * 24 * 60 * 60 * 1000).toISOString(),
maintenanceItems: '预防性维护:全面检查关键部件,更换易损件,系统清洁,性能测试',
partsAndMaterials: [
{ materialId: 'material-filter-001', quantity: 1 },
{ materialId: 'material-filter-002', quantity: 1 },
{ materialId: 'material-oil-001', quantity: 10 },
],
cost: 1100,
technician: '王工',
remarks: '按照预防性维护计划执行,提前发现并处理了潜在故障点,确保设备可靠运行。',
createdAt: new Date(now.getTime() - 25 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: '系统管理员',
});
}
// 保存所有维护记录
mockRecords.forEach(record => {
machineryStorage.saveMaintenanceRecord(record);
});
console.log(`✅ 已初始化 ${mockRecords.length} 条维护记录示例数据`);
}
/**
* 清除所有维护记录(仅用于测试)
*/
export function clearMaintenanceRecords() {
localStorage.removeItem('smart_agriculture_machinery_maintenance');
console.log('🗑️ 已清除所有维护记录');
}
/**
* 获取维护记录统计信息
*/
export function getMaintenanceStatistics() {
const records = machineryStorage.getAllMaintenanceRecords();
const stats = {
total: records.length,
byType: {
'日常保养': records.filter(r => r.type === '日常保养').length,
'定期维护': records.filter(r => r.type === '定期维护').length,
'故障维修': records.filter(r => r.type === '故障维修').length,
'年检': records.filter(r => r.type === '年检').length,
},
totalCost: records.reduce((sum, r) => sum + r.cost, 0),
totalWorkHours: records.reduce((sum, r) => sum + r.workHours, 0),
avgCostPerRecord: records.length > 0 ? records.reduce((sum, r) => sum + r.cost, 0) / records.length : 0,
recentRecords: records
.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())
.slice(0, 5),
};
return stats;
}

127
src/lib/mapLoader.ts Normal file
View File

@@ -0,0 +1,127 @@
/**
* 高德地图SDK动态加载器
* 用于在不修改index.html的情况下加载高德地图SDK
*/
// 高德地图配置
const AMAP_CONFIG = {
// 替换为你的高德地图API Key
// 申请地址: https://console.amap.com/
key: 'YOUR_AMAP_KEY',
// 替换为你的安全密钥
securityJsCode: 'YOUR_SECURITY_JS_CODE',
// SDK版本
version: '2.0',
// 可选插件
plugins: [] as string[],
};
/**
* 加载高德地图SDK
* @returns Promise<any> 返回AMap对象或null占位模式
*/
export const loadAMapScript = (): Promise<any> => {
return new Promise((resolve, reject) => {
// 如果已经加载,直接返回
if (window.AMap) {
console.log('✅ 高德地图SDK已加载');
resolve(window.AMap);
return;
}
// 检查Key是否配置
if (AMAP_CONFIG.key === 'YOUR_AMAP_KEY') {
// 使用占位地图(功能完整)
console.log('💡 使用占位地图模式(功能完整)');
console.log('💡 如需真实地图,请查看 /MAP_SDK_QUICK_FIX.md');
resolve(null); // 返回null表示使用占位地图
return;
}
try {
// 设置安全密钥
window._AMapSecurityConfig = {
securityJsCode: AMAP_CONFIG.securityJsCode,
};
// 创建script标签
const script = document.createElement('script');
script.type = 'text/javascript';
// 构建SDK URL
let url = `https://webapi.amap.com/maps?v=${AMAP_CONFIG.version}&key=${AMAP_CONFIG.key}`;
// 添加插件
if (AMAP_CONFIG.plugins.length > 0) {
url += `&plugin=${AMAP_CONFIG.plugins.join(',')}`;
}
script.src = url;
// 加载成功
script.onload = () => {
console.log('✅ 高德地图SDK加载成功');
console.log('📍 版本:', window.AMap?.version);
resolve();
};
// 加载失败
script.onerror = () => {
console.error('❌ 高德地图SDK加载失败');
reject(new Error('高德地图SDK加载失败'));
};
// 添加到页面
document.head.appendChild(script);
console.log('🔄 正在加载高德地图SDK...');
} catch (error) {
console.error('❌ 加载高德地图SDK时发生错误:', error);
reject(error);
}
});
};
/**
* 检查高德地图SDK是否已加载
* @returns boolean
*/
export const isAMapLoaded = (): boolean => {
return typeof window !== 'undefined' && !!window.AMap;
};
/**
* 获取高德地图版本
* @returns string | null
*/
export const getAMapVersion = (): string | null => {
if (isAMapLoaded()) {
return window.AMap.version || null;
}
return null;
};
/**
* 使用示例:
*
* import { loadAMapScript, isAMapLoaded } from './lib/mapLoader';
*
* // 在组件中使用
* useEffect(() => {
* if (!isAMapLoaded()) {
* loadAMapScript()
* .then(() => {
* console.log('地图SDK加载成功可以初始化地图');
* initMap();
* })
* .catch((error) => {
* console.error('地图SDK加载失败使用占位地图', error);
* });
* } else {
* initMap();
* }
* }, []);
*/

197
src/lib/materialStorage.ts Normal file
View File

@@ -0,0 +1,197 @@
// 农资物料数据存储管理
import { Material } from '../types/machinery';
const MATERIAL_KEY = 'smart_agriculture_materials';
export const materialStorage = {
// 获取所有农资物料
getAllMaterials(): Material[] {
const data = localStorage.getItem(MATERIAL_KEY);
return data ? JSON.parse(data) : [];
},
// 获取单个农资物料
getMaterial(id: string): Material | undefined {
const all = this.getAllMaterials();
return all.find(m => m.id === id);
},
// 保存农资物料
saveMaterial(material: Material): void {
const all = this.getAllMaterials();
const index = all.findIndex(m => m.id === material.id);
if (index >= 0) {
all[index] = material;
} else {
all.push(material);
}
localStorage.setItem(MATERIAL_KEY, JSON.stringify(all));
},
// 删除农资物料
deleteMaterial(id: string): void {
const all = this.getAllMaterials();
const filtered = all.filter(m => m.id !== id);
localStorage.setItem(MATERIAL_KEY, JSON.stringify(filtered));
},
// 批量保存农资物料
saveMaterials(materials: Material[]): void {
localStorage.setItem(MATERIAL_KEY, JSON.stringify(materials));
},
// 根据类型获取农资
getMaterialsByType(type: string): Material[] {
const all = this.getAllMaterials();
return all.filter(m => m.type === type);
},
// 获取有库存的农资(用于维护记录选择)
getAvailableMaterials(): Material[] {
const all = this.getAllMaterials();
return all.filter(m => m.currentStock > 0 && m.status !== '已过期');
}
};
// 初始化示例农资数据
export function initializeMaterialData() {
const existing = materialStorage.getAllMaterials();
if (existing.length > 0) {
return; // 已有数据,不重复初始化
}
const mockMaterials: Material[] = [
{
id: 'material-1',
code: 'PJ-001',
name: '机油滤芯',
type: '配件',
spec: '10W-40',
model: 'JD-001',
supplier: '约翰迪尔配件中心',
currentStock: 50,
unit: '个',
purchasePrice: 85,
status: '正常',
},
{
id: 'material-2',
code: 'PJ-002',
name: '空气滤芯',
type: '配件',
spec: '标准型',
model: 'AF-2024',
supplier: '农机配件商行',
currentStock: 30,
unit: '个',
purchasePrice: 65,
status: '正常',
},
{
id: 'material-3',
code: 'HC-001',
name: '液压油',
type: '耗材',
spec: '46号',
model: 'HM46',
supplier: '中石化润滑油',
currentStock: 200,
unit: '升',
purchasePrice: 28,
status: '正常',
},
{
id: 'material-4',
code: 'HC-002',
name: '齿轮油',
type: '耗材',
spec: '85W-90',
model: 'GL-5',
supplier: '壳牌石油',
currentStock: 150,
unit: '升',
purchasePrice: 45,
status: '正常',
},
{
id: 'material-5',
code: 'PJ-003',
name: '燃油滤芯',
type: '配件',
spec: '柴油专用',
model: 'FS-1000',
supplier: '康明斯配件',
currentStock: 25,
unit: '个',
purchasePrice: 95,
status: '正常',
},
{
id: 'material-6',
code: 'HC-003',
name: '防冻液',
type: '耗材',
spec: '-35℃',
model: 'AF-35',
supplier: '汽车用品店',
currentStock: 80,
unit: '升',
purchasePrice: 18,
status: '正常',
},
{
id: 'material-7',
code: 'PJ-004',
name: '传动皮带',
type: '配件',
spec: 'A型',
model: 'A-1200',
supplier: '三星皮带',
currentStock: 15,
unit: '条',
purchasePrice: 120,
status: '正常',
},
{
id: 'material-8',
code: 'HC-004',
name: '黄油(润滑脂)',
type: '耗材',
spec: '锂基脂',
model: 'MP-3',
supplier: '长城润滑油',
currentStock: 60,
unit: '公斤',
purchasePrice: 32,
status: '正常',
},
{
id: 'material-9',
code: 'PJ-005',
name: '刀片组',
type: '配件',
spec: '收割机专用',
model: 'KB-688',
supplier: '久保田配件',
currentStock: 8,
unit: '套',
purchasePrice: 580,
status: '库存预警',
},
{
id: 'material-10',
code: 'HC-005',
name: '清洗剂',
type: '耗材',
spec: '工业级',
model: 'CL-100',
supplier: '化工材料店',
currentStock: 40,
unit: '升',
purchasePrice: 22,
status: '正常',
},
];
materialStorage.saveMaterials(mockMaterials);
}

View File

@@ -1,12 +1,17 @@
// 初始化示例数据
import { MachineryRecord, MachineryTag } from '../types/machinery';
import { machineryStorage } from './machineryStorage';
import { initializeMaintenanceMockData } from './maintenanceMockData';
import { initializeChangeHistoryMockData } from './changeHistoryMockData';
export function initializeMockData() {
// 检查是否已有数据
const existingMachinery = machineryStorage.getAllMachinery();
if (existingMachinery.length > 0) {
return; // 已有数据,不重复初始化
// 已有农机数据,但可能没有维护记录和变更历史,尝试初始化
initializeMaintenanceMockData();
initializeChangeHistoryMockData();
return;
}
// 初始化标签
@@ -43,10 +48,12 @@ export function initializeMockData() {
insuranceStartDate: '2024-04-01',
insuranceEndDate: '2025-03-31',
insuranceAmount: 300000,
status: '运行中',
status: '正常',
currentLocation: '1号地块',
operator: '张三',
department: '第一生产队',
maintenanceCycle: 3,
maintenanceCycleUnit: 'month',
remarks: '主力耕作设备,状态良好',
tags: ['tag-1', 'tag-2'],
qrCode: 'machinery-1',
@@ -77,10 +84,12 @@ export function initializeMockData() {
insuranceStartDate: '2024-06-01',
insuranceEndDate: '2025-05-31',
insuranceAmount: 250000,
status: '空闲中',
status: '正常',
currentLocation: '机库A区',
operator: '李四',
department: '第二生产队',
maintenanceCycle: 50,
maintenanceCycleUnit: 'day',
remarks: '水稻收割专用,效率高',
tags: ['tag-2', 'tag-3'],
qrCode: 'machinery-2',
@@ -115,6 +124,8 @@ export function initializeMockData() {
currentLocation: '维修车间',
operator: '王五',
department: '第一生产队',
maintenanceCycle: 1,
maintenanceCycleUnit: 'month',
remarks: '智能播种,需要定期校准',
tags: ['tag-2', 'tag-4'],
qrCode: 'machinery-3',
@@ -145,10 +156,12 @@ export function initializeMockData() {
insuranceStartDate: '2024-02-01',
insuranceEndDate: '2025-01-31',
insuranceAmount: 50000,
status: '运行中',
status: '正常',
currentLocation: '5号地块',
operator: '赵六',
department: '植保组',
maintenanceCycle: 15,
maintenanceCycleUnit: 'day',
remarks: '高效植保续航40分钟',
tags: ['tag-1', 'tag-2', 'tag-3'],
qrCode: 'machinery-4',
@@ -179,10 +192,12 @@ export function initializeMockData() {
insuranceStartDate: '2023-12-01',
insuranceEndDate: '2024-11-30',
insuranceAmount: 280000,
status: '运行中',
status: '正常',
currentLocation: '3号地块',
operator: '孙七',
department: '第三生产队',
maintenanceCycle: 6,
maintenanceCycleUnit: 'month',
remarks: '国产品牌,性价比高',
tags: ['tag-2'],
qrCode: 'machinery-5',
@@ -194,4 +209,12 @@ export function initializeMockData() {
];
mockMachinery.forEach(machinery => machineryStorage.saveMachinery(machinery));
// 初始化维护记录示例数据
initializeMaintenanceMockData();
// 初始化变更历史示例数据
initializeChangeHistoryMockData();
console.log('✅ 农机档案、维护记录和变更历史示例数据初始化完成');
}

View File

@@ -0,0 +1,583 @@
/**
* 遥感影像服务
* 集成天地图、Sentinel、Landsat等数据源
*/
// ===== 类型定义 =====
export interface SatelliteImage {
id: string;
date: string;
source: 'Sentinel-2' | 'Landsat-8' | 'Landsat-9' | '天地图' | 'GF-1' | 'GF-2';
cloudCover: number; // 云量百分比
resolution: number; // 分辨率(米)
ndvi: number; // 归一化植被指数
evi: number; // 增强型植被指数
savi: number; // 土壤调节植被指数
lai: number; // 叶面积指数
thumbnail: string; // 缩略图URL
fullImageUrl: string; // 完整影像URL
season: string;
quality: 'excellent' | 'good' | 'fair' | 'poor';
bands: {
red?: string;
green?: string;
blue?: string;
nir?: string;
swir?: string;
};
}
export interface FieldBoundary {
id: string;
name: string;
coordinates: [number, number][];
area: number; // 亩
}
export interface ImageComparisonResult {
image1: SatelliteImage;
image2: SatelliteImage;
ndviChange: number;
eviChange: number;
changeType: 'improvement' | 'decline' | 'stable';
changeDescription: string;
recommendations: string[];
}
export interface TimeSeriesAnalysis {
dates: string[];
ndviValues: number[];
eviValues: number[];
trend: 'increasing' | 'decreasing' | 'fluctuating' | 'stable';
growthStage: string;
healthScore: number;
alerts: string[];
}
// ===== 常量定义 =====
// 数据源配置
export const DATA_SOURCES = {
'Sentinel-2': {
name: 'Sentinel-2 (哨兵2号)',
resolution: 10, // 米
revisitTime: 5, // 重访周期(天)
bands: ['B2-Blue', 'B3-Green', 'B4-Red', 'B8-NIR'],
apiEndpoint: 'https://sentinelsat.esa.int/api',
description: 'ESA哨兵2号卫星10米分辨率5天重访'
},
'Landsat-8': {
name: 'Landsat-8',
resolution: 30,
revisitTime: 16,
bands: ['B2-Blue', 'B3-Green', 'B4-Red', 'B5-NIR'],
apiEndpoint: 'https://earthexplorer.usgs.gov/api',
description: 'NASA陆地卫星8号30米分辨率16天重访'
},
'天地图': {
name: '天地图',
resolution: 2,
revisitTime: 1,
bands: ['RGB'],
apiEndpoint: 'https://t0.tianditu.gov.cn/img_w/wmts',
description: '国家地理信息公共服务平台,高分辨率影像'
},
'GF-1': {
name: '高分一号',
resolution: 2,
revisitTime: 4,
bands: ['B1-Blue', 'B2-Green', 'B3-Red', 'B4-NIR'],
apiEndpoint: 'https://data.cresda.cn/api',
description: '中国高分辨率对地观测卫星2米分辨率'
}
};
// NDVI评价标准
export const NDVI_THRESHOLDS = {
excellent: 0.8, // 优秀
good: 0.6, // 良好
fair: 0.4, // 一般
poor: 0.2 // 较差
};
// ===== 遥感影像服务类 =====
export class SatelliteImageService {
/**
* 获取地块的历史影像列表
*/
static async getFieldImages(
fieldId: string,
startDate: string,
endDate: string,
source?: string,
maxCloudCover: number = 30
): Promise<SatelliteImage[]> {
// 模拟API调用
// 实际应用中这里调用真实的遥感数据API
const images = this._generateMockImages(startDate, endDate);
// 过滤条件
let filtered = images;
if (source) {
filtered = filtered.filter(img => img.source === source);
}
if (maxCloudCover < 100) {
filtered = filtered.filter(img => img.cloudCover <= maxCloudCover);
}
// 按日期排序
filtered.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
return filtered;
}
/**
* 获取天地图影像
*/
static async getTianDiTuImage(
lon: number,
lat: number,
zoom: number = 15
): Promise<string> {
// 天地图API密钥需要申请
const tk = 'YOUR_TIANDITU_API_KEY';
// 构建天地图影像URL
const url = `https://t${Math.floor(Math.random() * 8)}.tianditu.gov.cn/img_w/wmts?` +
`SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&` +
`TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=${zoom}&TILEROW=${lat}&TILECOL=${lon}&tk=${tk}`;
return url;
}
/**
* 计算NDVI归一化植被指数
*/
static calculateNDVI(nir: number, red: number): number {
// NDVI = (NIR - Red) / (NIR + Red)
if (nir + red === 0) return 0;
return (nir - red) / (nir + red);
}
/**
* 计算EVI增强型植被指数
*/
static calculateEVI(nir: number, red: number, blue: number): number {
// EVI = 2.5 * (NIR - Red) / (NIR + 6 * Red - 7.5 * Blue + 1)
const denominator = nir + 6 * red - 7.5 * blue + 1;
if (denominator === 0) return 0;
return 2.5 * (nir - red) / denominator;
}
/**
* 计算SAVI土壤调节植被指数
*/
static calculateSAVI(nir: number, red: number, L: number = 0.5): number {
// SAVI = ((NIR - Red) / (NIR + Red + L)) * (1 + L)
const denominator = nir + red + L;
if (denominator === 0) return 0;
return ((nir - red) / denominator) * (1 + L);
}
/**
* 对比两个时期的影像
*/
static compareImages(
image1: SatelliteImage,
image2: SatelliteImage
): ImageComparisonResult {
const ndviChange = image2.ndvi - image1.ndvi;
const eviChange = image2.evi - image1.evi;
// 确定变化类型
let changeType: 'improvement' | 'decline' | 'stable';
if (ndviChange > 0.1) {
changeType = 'improvement';
} else if (ndviChange < -0.1) {
changeType = 'decline';
} else {
changeType = 'stable';
}
// 生成变化描述
const changeDescription = this._generateChangeDescription(ndviChange, eviChange);
// 生成建议
const recommendations = this._generateRecommendations(changeType, image2);
return {
image1,
image2,
ndviChange,
eviChange,
changeType,
changeDescription,
recommendations
};
}
/**
* 时序分析
*/
static analyzeTimeSeries(images: SatelliteImage[]): TimeSeriesAnalysis {
if (images.length === 0) {
throw new Error('No images provided for analysis');
}
// 按日期排序
const sortedImages = [...images].sort((a, b) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
);
const dates = sortedImages.map(img => img.date);
const ndviValues = sortedImages.map(img => img.ndvi);
const eviValues = sortedImages.map(img => img.evi);
// 计算趋势
const trend = this._calculateTrend(ndviValues);
// 判断生长阶段
const growthStage = this._determineGrowthStage(ndviValues);
// 计算健康分数0-100
const avgNDVI = ndviValues.reduce((a, b) => a + b, 0) / ndviValues.length;
const healthScore = Math.round(avgNDVI * 100);
// 生成警报
const alerts = this._generateAlerts(sortedImages);
return {
dates,
ndviValues,
eviValues,
trend,
growthStage,
healthScore,
alerts
};
}
/**
* 获取影像质量评价
*/
static assessImageQuality(image: SatelliteImage): string {
if (image.cloudCover < 10 && image.ndvi > 0) {
return 'excellent';
} else if (image.cloudCover < 20) {
return 'good';
} else if (image.cloudCover < 30) {
return 'fair';
} else {
return 'poor';
}
}
/**
* 获取NDVI等级描述
*/
static getNDVILabel(ndvi: number): string {
if (ndvi > NDVI_THRESHOLDS.excellent) return '长势旺盛';
if (ndvi > NDVI_THRESHOLDS.good) return '长势良好';
if (ndvi > NDVI_THRESHOLDS.fair) return '长势一般';
if (ndvi > NDVI_THRESHOLDS.poor) return '长势较弱';
return '无植被';
}
/**
* 获取NDVI颜色
*/
static getNDVIColor(ndvi: number): string {
if (ndvi > NDVI_THRESHOLDS.excellent) return '#22c55e'; // green-500
if (ndvi > NDVI_THRESHOLDS.good) return '#84cc16'; // lime-500
if (ndvi > NDVI_THRESHOLDS.fair) return '#eab308'; // yellow-500
if (ndvi > NDVI_THRESHOLDS.poor) return '#f97316'; // orange-500
return '#ef4444'; // red-500
}
/**
* 下载影像
*/
static async downloadImage(
image: SatelliteImage,
format: 'tif' | 'jpg' | 'png' = 'jpg'
): Promise<void> {
// 模拟下载
const filename = `${image.source}_${image.date}.${format}`;
console.log(`Downloading ${filename}...`);
// 实际应用中这里应该调用真实的下载API
// 创建下载链接
const link = document.createElement('a');
link.href = image.fullImageUrl;
link.download = filename;
link.click();
}
// ===== 私有辅助方法 =====
/**
* 生成模拟影像数据(用于演示)
*/
private static _generateMockImages(startDate: string, endDate: string): SatelliteImage[] {
const images: SatelliteImage[] = [];
const start = new Date(startDate);
const end = new Date(endDate);
// 生成每个月的影像
let current = new Date(start);
let id = 1;
while (current <= end) {
// Sentinel-2 影像5天一次
if (id % 3 === 0) {
const ndvi = 0.6 + Math.random() * 0.3;
images.push({
id: `sentinel-${id}`,
date: current.toISOString().split('T')[0],
source: 'Sentinel-2',
cloudCover: Math.random() * 40,
resolution: 10,
ndvi: ndvi,
evi: ndvi * 0.85,
savi: ndvi * 0.75,
lai: ndvi * 5,
thumbnail: `https://placeholder.com/sentinel-${id}`,
fullImageUrl: `https://api.sentinel.com/image/${id}`,
season: this._getSeason(current),
quality: 'good',
bands: {
red: 'B4',
green: 'B3',
blue: 'B2',
nir: 'B8'
}
});
}
// Landsat-8 影像16天一次
if (id % 5 === 0) {
const ndvi = 0.55 + Math.random() * 0.35;
images.push({
id: `landsat-${id}`,
date: current.toISOString().split('T')[0],
source: 'Landsat-8',
cloudCover: Math.random() * 35,
resolution: 30,
ndvi: ndvi,
evi: ndvi * 0.88,
savi: ndvi * 0.78,
lai: ndvi * 4.8,
thumbnail: `https://placeholder.com/landsat-${id}`,
fullImageUrl: `https://api.landsat.com/image/${id}`,
season: this._getSeason(current),
quality: 'good',
bands: {
red: 'B4',
green: 'B3',
blue: 'B2',
nir: 'B5'
}
});
}
current.setDate(current.getDate() + 5);
id++;
}
return images;
}
/**
* 获取季节
*/
private static _getSeason(date: Date): string {
const month = date.getMonth() + 1;
if (month >= 3 && month <= 5) return '春季';
if (month >= 6 && month <= 8) return '夏季';
if (month >= 9 && month <= 11) return '秋季';
return '冬季';
}
/**
* 生成变化描述
*/
private static _generateChangeDescription(ndviChange: number, eviChange: number): string {
if (ndviChange > 0.2) {
return '植被覆盖度显著增加,作物长势明显改善';
} else if (ndviChange > 0.1) {
return '植被覆盖度有所增加,作物长势良好';
} else if (ndviChange < -0.2) {
return '植被覆盖度显著下降,需要关注作物健康状况';
} else if (ndviChange < -0.1) {
return '植被覆盖度有所下降,建议加强管理';
} else {
return '植被覆盖度基本稳定,作物生长正常';
}
}
/**
* 生成建议
*/
private static _generateRecommendations(
changeType: 'improvement' | 'decline' | 'stable',
currentImage: SatelliteImage
): string[] {
const recommendations: string[] = [];
if (changeType === 'decline') {
recommendations.push('建议增加灌溉频次,确保水分供应');
recommendations.push('检查是否存在病虫害,及时防治');
recommendations.push('适当追施氮肥,促进作物生长');
} else if (changeType === 'improvement') {
recommendations.push('继续保持当前管理措施');
recommendations.push('适时进行田间观察,记录生长情况');
recommendations.push('注意防范极端天气影响');
} else {
recommendations.push('维持现有灌溉和施肥计划');
recommendations.push('定期监测作物生长状态');
}
// 根据NDVI值添加额外建议
if (currentImage.ndvi < 0.4) {
recommendations.push('⚠️ NDVI值偏低建议进行土壤检测');
}
// 根据云量添加建议
if (currentImage.cloudCover > 30) {
recommendations.push('当前云量较高,建议选择晴天影像进行分析');
}
return recommendations;
}
/**
* 计算趋势
*/
private static _calculateTrend(values: number[]): 'increasing' | 'decreasing' | 'fluctuating' | 'stable' {
if (values.length < 2) return 'stable';
// 简单线性回归
const n = values.length;
const sumX = (n * (n - 1)) / 2;
const sumY = values.reduce((a, b) => a + b, 0);
const sumXY = values.reduce((sum, y, x) => sum + x * y, 0);
const sumX2 = (n * (n - 1) * (2 * n - 1)) / 6;
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
// 计算波动性
const mean = sumY / n;
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / n;
const stdDev = Math.sqrt(variance);
if (Math.abs(slope) < 0.01 && stdDev < 0.05) {
return 'stable';
} else if (stdDev > 0.15) {
return 'fluctuating';
} else if (slope > 0.01) {
return 'increasing';
} else if (slope < -0.01) {
return 'decreasing';
}
return 'stable';
}
/**
* 判断生长阶段
*/
private static _determineGrowthStage(ndviValues: number[]): string {
if (ndviValues.length === 0) return '未知';
const latestNDVI = ndviValues[ndviValues.length - 1];
const avgNDVI = ndviValues.reduce((a, b) => a + b, 0) / ndviValues.length;
if (latestNDVI < 0.3) {
return '苗期';
} else if (latestNDVI < 0.5) {
return '生长期';
} else if (latestNDVI < 0.7) {
return '旺长期';
} else if (latestNDVI >= 0.7 && latestNDVI < 0.85) {
return '成熟前期';
} else {
return '成熟期';
}
}
/**
* 生成警报
*/
private static _generateAlerts(images: SatelliteImage[]): string[] {
const alerts: string[] = [];
if (images.length < 2) return alerts;
// 检查最近的NDVI变化
const recent = images.slice(-3);
const ndviValues = recent.map(img => img.ndvi);
// NDVI急剧下降
if (ndviValues.length >= 2) {
const change = ndviValues[ndviValues.length - 1] - ndviValues[0];
if (change < -0.2) {
alerts.push('⚠️ 警告:近期植被指数急剧下降,请检查作物健康状况');
}
}
// 云量过高
const highCloudImages = recent.filter(img => img.cloudCover > 40);
if (highCloudImages.length >= 2) {
alerts.push('☁️ 提示:近期云量较高,建议选择晴天影像进行分析');
}
// NDVI持续低迷
const lowNDVIImages = recent.filter(img => img.ndvi < 0.4);
if (lowNDVIImages.length >= 2) {
alerts.push('⚠️ 注意:植被指数持续偏低,建议加强田间管理');
}
return alerts;
}
}
// ===== 导出工具函数 =====
/**
* 获取云量颜色类
*/
export function getCloudCoverColorClass(cloudCover: number): string {
if (cloudCover < 10) return 'text-green-600 bg-green-100';
if (cloudCover < 20) return 'text-yellow-600 bg-yellow-100';
if (cloudCover < 30) return 'text-orange-600 bg-orange-100';
return 'text-red-600 bg-red-100';
}
/**
* 格式化日期
*/
export function formatImageDate(date: string): string {
const d = new Date(date);
return `${d.getFullYear()}${d.getMonth() + 1}${d.getDate()}`;
}
/**
* 获取数据源图标
*/
export function getDataSourceIcon(source: string): string {
const icons: Record<string, string> = {
'Sentinel-2': '🛰️',
'Landsat-8': '🛰️',
'Landsat-9': '🛰️',
'天地图': '🗺️',
'GF-1': '🛰️',
'GF-2': '🛰️'
};
return icons[source] || '📡';
}

View File

@@ -0,0 +1,937 @@
/**
* 空间数据服务API
* 提供PostGIS风格的空间查询、几何计算和数据导出功能
*/
// ===== 类型定义 =====
export interface Point {
lat: number;
lng: number;
alt?: number; // 海拔高度
}
export interface Polygon {
points: Point[];
holes?: Point[][]; // 多边形的孔洞
}
export interface Field {
id: string;
name: string;
code: string;
geometry: Polygon;
properties?: Record<string, any>;
}
export interface SpatialQueryResult<T = any> {
success: boolean;
data: T;
timestamp: string;
executionTime: number; // 毫秒
}
// ===== 常量定义 =====
// WGS-84椭球参数
const WGS84_A = 6378137.0; // 长半轴(米)
const WGS84_B = 6356752.314245; // 短半轴(米)
const WGS84_F = 1 / 298.257223563; // 扁率
// 1亩 = 666.67平方米
const MU_TO_SQUARE_METERS = 666.67;
// ===== 1. 空间查询API =====
/**
* 点面查询:判断点是否在多边形内
* 使用射线法Ray Casting Algorithm
*/
export class SpatialQuery {
/**
* 点在多边形内查询
* @param point 查询点
* @param fields 地块列表
* @returns 包含该点的地块列表
*/
static pointInPolygon(point: Point, fields: Field[]): SpatialQueryResult<{
matched: boolean;
fields: Array<{
field: Field;
distanceToBorder: number; // 到边界的最短距离(米)
}>;
}> {
const startTime = performance.now();
const results: Array<{ field: Field; distanceToBorder: number }> = [];
for (const field of fields) {
if (this._isPointInPolygon(point, field.geometry.points)) {
const distance = this._pointToPolygonDistance(point, field.geometry.points);
results.push({ field, distanceToBorder: distance });
}
}
const executionTime = performance.now() - startTime;
return {
success: true,
data: {
matched: results.length > 0,
fields: results,
},
timestamp: new Date().toISOString(),
executionTime,
};
}
/**
* 多边形相交查询
* @param sourceField 源地块
* @param targetFields 目标地块列表
* @returns 与源地块相交的地块列表
*/
static polygonIntersect(
sourceField: Field,
targetFields: Field[]
): SpatialQueryResult<{
intersections: Array<{
field: Field;
intersectArea: number; // 相交面积(亩)
intersectRatio: number; // 相交比例(%
intersectGeometry: Polygon; // 相交区域几何
}>;
}> {
const startTime = performance.now();
const intersections: Array<{
field: Field;
intersectArea: number;
intersectRatio: number;
intersectGeometry: Polygon;
}> = [];
const sourceArea = GeometryCalculator.calculateArea(sourceField.geometry);
for (const targetField of targetFields) {
if (targetField.id === sourceField.id) continue;
if (this._polygonsIntersect(sourceField.geometry.points, targetField.geometry.points)) {
const intersectGeometry = this._calculateIntersection(
sourceField.geometry,
targetField.geometry
);
const intersectArea = GeometryCalculator.calculateArea(intersectGeometry);
const intersectRatio = (intersectArea / sourceArea) * 100;
intersections.push({
field: targetField,
intersectArea,
intersectRatio,
intersectGeometry,
});
}
}
const executionTime = performance.now() - startTime;
return {
success: true,
data: { intersections },
timestamp: new Date().toISOString(),
executionTime,
};
}
/**
* 相邻地块查询
* @param sourceField 源地块
* @param targetFields 目标地块列表
* @returns 与源地块相邻的地块列表
*/
static adjacentPolygons(
sourceField: Field,
targetFields: Field[]
): SpatialQueryResult<{
adjacentFields: Array<{
field: Field;
sharedBorderLength: number; // 共享边界长度(米)
sharedBorderPoints: Point[]; // 共享边界点
}>;
}> {
const startTime = performance.now();
const adjacentFields: Array<{
field: Field;
sharedBorderLength: number;
sharedBorderPoints: Point[];
}> = [];
for (const targetField of targetFields) {
if (targetField.id === sourceField.id) continue;
const { isAdjacent, sharedBorder } = this._checkAdjacency(
sourceField.geometry.points,
targetField.geometry.points
);
if (isAdjacent && sharedBorder.length > 0) {
const sharedBorderLength = GeometryCalculator.calculatePerimeter({
points: sharedBorder,
});
adjacentFields.push({
field: targetField,
sharedBorderLength,
sharedBorderPoints: sharedBorder,
});
}
}
const executionTime = performance.now() - startTime;
return {
success: true,
data: { adjacentFields },
timestamp: new Date().toISOString(),
executionTime,
};
}
/**
* 缓冲区分析
* @param sourceField 源地块
* @param bufferDistance 缓冲区距离(米)
* @param targetFields 目标地块列表
* @returns 缓冲区内的地块列表
*/
static bufferAnalysis(
sourceField: Field,
bufferDistance: number,
targetFields: Field[]
): SpatialQueryResult<{
bufferGeometry: Polygon;
bufferArea: number; // 缓冲区面积(亩)
fieldsInBuffer: Array<{
field: Field;
distance: number; // 最短距离(米)
overlap: boolean; // 是否重叠
}>;
}> {
const startTime = performance.now();
// 生成缓冲区几何
const bufferGeometry = this._createBuffer(sourceField.geometry, bufferDistance);
const bufferArea = GeometryCalculator.calculateArea(bufferGeometry);
const fieldsInBuffer: Array<{
field: Field;
distance: number;
overlap: boolean;
}> = [];
for (const targetField of targetFields) {
if (targetField.id === sourceField.id) continue;
const distance = this._polygonToPolygonDistance(
sourceField.geometry.points,
targetField.geometry.points
);
if (distance <= bufferDistance) {
const overlap = this._polygonsIntersect(
bufferGeometry.points,
targetField.geometry.points
);
fieldsInBuffer.push({
field: targetField,
distance,
overlap,
});
}
}
const executionTime = performance.now() - startTime;
return {
success: true,
data: {
bufferGeometry,
bufferArea,
fieldsInBuffer,
},
timestamp: new Date().toISOString(),
executionTime,
};
}
// ===== 私有辅助方法 =====
/**
* 射线法判断点是否在多边形内
*/
private static _isPointInPolygon(point: Point, polygon: Point[]): boolean {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].lng,
yi = polygon[i].lat;
const xj = polygon[j].lng,
yj = polygon[j].lat;
const intersect =
yi > point.lat !== yj > point.lat &&
point.lng < ((xj - xi) * (point.lat - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
/**
* 计算点到多边形边界的最短距离
*/
private static _pointToPolygonDistance(point: Point, polygon: Point[]): number {
let minDistance = Infinity;
for (let i = 0; i < polygon.length; i++) {
const p1 = polygon[i];
const p2 = polygon[(i + 1) % polygon.length];
const distance = this._pointToSegmentDistance(point, p1, p2);
minDistance = Math.min(minDistance, distance);
}
return minDistance;
}
/**
* 计算点到线段的距离
*/
private static _pointToSegmentDistance(point: Point, p1: Point, p2: Point): number {
const dx = p2.lng - p1.lng;
const dy = p2.lat - p1.lat;
if (dx === 0 && dy === 0) {
return GeometryCalculator.haversineDistance(point, p1);
}
const t = Math.max(
0,
Math.min(
1,
((point.lng - p1.lng) * dx + (point.lat - p1.lat) * dy) / (dx * dx + dy * dy)
)
);
const nearestPoint: Point = {
lat: p1.lat + t * dy,
lng: p1.lng + t * dx,
};
return GeometryCalculator.haversineDistance(point, nearestPoint);
}
/**
* 判断两个多边形是否相交
*/
private static _polygonsIntersect(poly1: Point[], poly2: Point[]): boolean {
// 检查是否有顶点在另一个多边形内
for (const point of poly1) {
if (this._isPointInPolygon(point, poly2)) return true;
}
for (const point of poly2) {
if (this._isPointInPolygon(point, poly1)) return true;
}
// 检查边是否相交
for (let i = 0; i < poly1.length; i++) {
const p1 = poly1[i];
const p2 = poly1[(i + 1) % poly1.length];
for (let j = 0; j < poly2.length; j++) {
const p3 = poly2[j];
const p4 = poly2[(j + 1) % poly2.length];
if (this._segmentsIntersect(p1, p2, p3, p4)) return true;
}
}
return false;
}
/**
* 判断两条线段是否相交
*/
private static _segmentsIntersect(p1: Point, p2: Point, p3: Point, p4: Point): boolean {
const ccw = (A: Point, B: Point, C: Point) => {
return (C.lat - A.lat) * (B.lng - A.lng) > (B.lat - A.lat) * (C.lng - A.lng);
};
return ccw(p1, p3, p4) !== ccw(p2, p3, p4) && ccw(p1, p2, p3) !== ccw(p1, p2, p4);
}
/**
* 计算两个多边形的相交区域(简化实现)
*/
private static _calculateIntersection(poly1: Polygon, poly2: Polygon): Polygon {
// 这里使用简化算法实际应用中应使用Sutherland-Hodgman算法
const intersectPoints: Point[] = [];
// 收集在两个多边形内的点
for (const point of poly1.points) {
if (this._isPointInPolygon(point, poly2.points)) {
intersectPoints.push(point);
}
}
for (const point of poly2.points) {
if (this._isPointInPolygon(point, poly1.points)) {
intersectPoints.push(point);
}
}
// 如果没有交点,返回空多边形
if (intersectPoints.length === 0) {
return { points: [] };
}
// 计算凸包作为相交区域的近似
return { points: this._convexHull(intersectPoints) };
}
/**
* 计算凸包Graham扫描算法
*/
private static _convexHull(points: Point[]): Point[] {
if (points.length < 3) return points;
// 找到最下最左的点
let start = points[0];
points.forEach((p) => {
if (p.lat < start.lat || (p.lat === start.lat && p.lng < start.lng)) {
start = p;
}
});
// 按极角排序
const sorted = points
.filter((p) => p !== start)
.sort((a, b) => {
const angleA = Math.atan2(a.lat - start.lat, a.lng - start.lng);
const angleB = Math.atan2(b.lat - start.lat, b.lng - start.lng);
return angleA - angleB;
});
const hull: Point[] = [start];
for (const point of sorted) {
while (hull.length >= 2) {
const p2 = hull[hull.length - 1];
const p1 = hull[hull.length - 2];
const cross =
(p2.lng - p1.lng) * (point.lat - p1.lat) -
(p2.lat - p1.lat) * (point.lng - p1.lng);
if (cross <= 0) {
hull.pop();
} else {
break;
}
}
hull.push(point);
}
return hull;
}
/**
* 检查两个多边形是否相邻
*/
private static _checkAdjacency(
poly1: Point[],
poly2: Point[]
): { isAdjacent: boolean; sharedBorder: Point[] } {
const sharedBorder: Point[] = [];
const tolerance = 0.00001; // 约1米的容差
for (let i = 0; i < poly1.length; i++) {
const p1 = poly1[i];
const p2 = poly1[(i + 1) % poly1.length];
for (let j = 0; j < poly2.length; j++) {
const p3 = poly2[j];
const p4 = poly2[(j + 1) % poly2.length];
// 检查边是否重合
if (this._edgesOverlap(p1, p2, p3, p4, tolerance)) {
if (sharedBorder.length === 0 || !this._pointsEqual(sharedBorder[sharedBorder.length - 1], p1, tolerance)) {
sharedBorder.push(p1);
}
sharedBorder.push(p2);
}
}
}
return {
isAdjacent: sharedBorder.length >= 2,
sharedBorder,
};
}
/**
* 判断两条边是否重合
*/
private static _edgesOverlap(
p1: Point,
p2: Point,
p3: Point,
p4: Point,
tolerance: number
): boolean {
return (
(this._pointsEqual(p1, p3, tolerance) && this._pointsEqual(p2, p4, tolerance)) ||
(this._pointsEqual(p1, p4, tolerance) && this._pointsEqual(p2, p3, tolerance))
);
}
/**
* 判断两个点是否相等(在容差范围内)
*/
private static _pointsEqual(p1: Point, p2: Point, tolerance: number): boolean {
return (
Math.abs(p1.lat - p2.lat) < tolerance && Math.abs(p1.lng - p2.lng) < tolerance
);
}
/**
* 计算多边形到多边形的最短距离
*/
private static _polygonToPolygonDistance(poly1: Point[], poly2: Point[]): number {
let minDistance = Infinity;
for (const point of poly1) {
const distance = this._pointToPolygonDistance(point, poly2);
minDistance = Math.min(minDistance, distance);
}
for (const point of poly2) {
const distance = this._pointToPolygonDistance(point, poly1);
minDistance = Math.min(minDistance, distance);
}
return minDistance;
}
/**
* 创建缓冲区(简化实现)
*/
private static _createBuffer(geometry: Polygon, distance: number): Polygon {
const bufferPoints: Point[] = [];
const points = geometry.points;
// 简化算法对每个顶点在法线方向上偏移distance距离
for (let i = 0; i < points.length; i++) {
const prev = points[i === 0 ? points.length - 1 : i - 1];
const curr = points[i];
const next = points[(i + 1) % points.length];
// 计算法向量
const v1 = { lat: curr.lat - prev.lat, lng: curr.lng - prev.lng };
const v2 = { lat: next.lat - curr.lat, lng: next.lng - curr.lng };
// 计算平均法向量
const normal = {
lat: -(v1.lng + v2.lng),
lng: v1.lat + v2.lat,
};
// 归一化
const length = Math.sqrt(normal.lat * normal.lat + normal.lng * normal.lng);
if (length > 0) {
normal.lat /= length;
normal.lng /= length;
}
// 偏移顶点(简化:使用度数偏移,实际应转换为米)
const offsetDegrees = distance / 111320; // 约111.32km每度
bufferPoints.push({
lat: curr.lat + normal.lat * offsetDegrees,
lng: curr.lng + normal.lng * offsetDegrees,
});
}
return { points: bufferPoints };
}
}
// ===== 2. 几何计算API =====
export class GeometryCalculator {
/**
* 计算多边形精确面积(考虑地球曲率)
* 使用球面三角形面积公式
*/
static calculateArea(geometry: Polygon): number {
const points = geometry.points;
if (points.length < 3) return 0;
// 将多边形分解为三角形,计算球面三角形面积之和
let totalArea = 0;
const origin = points[0];
for (let i = 1; i < points.length - 1; i++) {
const area = this._sphericalTriangleArea(origin, points[i], points[i + 1]);
totalArea += area;
}
// 转换为亩
return totalArea / MU_TO_SQUARE_METERS;
}
/**
* 计算球面三角形面积L'Huilier定理
*/
private static _sphericalTriangleArea(p1: Point, p2: Point, p3: Point): number {
const R = WGS84_A; // 使用WGS-84长半轴
// 转换为弧度
const lat1 = this._toRadians(p1.lat);
const lng1 = this._toRadians(p1.lng);
const lat2 = this._toRadians(p2.lat);
const lng2 = this._toRadians(p2.lng);
const lat3 = this._toRadians(p3.lat);
const lng3 = this._toRadians(p3.lng);
// 计算边长(球面距离)
const a = this._sphericalDistance(lat2, lng2, lat3, lng3, R);
const b = this._sphericalDistance(lat3, lng3, lat1, lng1, R);
const c = this._sphericalDistance(lat1, lng1, lat2, lng2, R);
// 半周长
const s = (a + b + c) / 2;
// L'Huilier定理
const E = 4 * Math.atan(Math.sqrt(Math.tan(s / (2 * R)) * Math.tan((s - a) / (2 * R)) * Math.tan((s - b) / (2 * R)) * Math.tan((s - c) / (2 * R))));
// 面积 = R² * E
return R * R * E;
}
/**
* 计算球面距离
*/
private static _sphericalDistance(
lat1: number,
lng1: number,
lat2: number,
lng2: number,
radius: number
): number {
const dLat = lat2 - lat1;
const dLng = lng2 - lng1;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}
/**
* 计算多边形周长(考虑地球曲率)
*/
static calculatePerimeter(geometry: Polygon): number {
const points = geometry.points;
if (points.length < 2) return 0;
let perimeter = 0;
for (let i = 0; i < points.length; i++) {
const p1 = points[i];
const p2 = points[(i + 1) % points.length];
perimeter += this.haversineDistance(p1, p2);
}
return perimeter;
}
/**
* Haversine公式计算两点间距离
*/
static haversineDistance(p1: Point, p2: Point): number {
const R = WGS84_A;
const lat1 = this._toRadians(p1.lat);
const lng1 = this._toRadians(p1.lng);
const lat2 = this._toRadians(p2.lat);
const lng2 = this._toRadians(p2.lng);
return this._sphericalDistance(lat1, lng1, lat2, lng2, R);
}
/**
* 计算多边形中心点(几何中心)
*/
static calculateCentroid(geometry: Polygon): Point {
const points = geometry.points;
if (points.length === 0) return { lat: 0, lng: 0 };
let sumLat = 0;
let sumLng = 0;
let sumArea = 0;
for (let i = 0; i < points.length; i++) {
const p1 = points[i];
const p2 = points[(i + 1) % points.length];
const cross = p1.lng * p2.lat - p2.lng * p1.lat;
sumArea += cross;
sumLat += (p1.lat + p2.lat) * cross;
sumLng += (p1.lng + p2.lng) * cross;
}
sumArea /= 2;
if (Math.abs(sumArea) < 1e-10) {
// 如果面积接近0使用简单平均
const avgLat = points.reduce((sum, p) => sum + p.lat, 0) / points.length;
const avgLng = points.reduce((sum, p) => sum + p.lng, 0) / points.length;
return { lat: avgLat, lng: avgLng };
}
const centroidLat = sumLat / (6 * sumArea);
const centroidLng = sumLng / (6 * sumArea);
return { lat: centroidLat, lng: centroidLng };
}
/**
* 计算包围盒Bounding Box
*/
static calculateBoundingBox(geometry: Polygon): {
minLat: number;
maxLat: number;
minLng: number;
maxLng: number;
center: Point;
} {
const points = geometry.points;
if (points.length === 0) {
return {
minLat: 0,
maxLat: 0,
minLng: 0,
maxLng: 0,
center: { lat: 0, lng: 0 },
};
}
let minLat = points[0].lat;
let maxLat = points[0].lat;
let minLng = points[0].lng;
let maxLng = points[0].lng;
for (const point of points) {
minLat = Math.min(minLat, point.lat);
maxLat = Math.max(maxLat, point.lat);
minLng = Math.min(minLng, point.lng);
maxLng = Math.max(maxLng, point.lng);
}
return {
minLat,
maxLat,
minLng,
maxLng,
center: {
lat: (minLat + maxLat) / 2,
lng: (minLng + maxLng) / 2,
},
};
}
/**
* 角度转弧度
*/
private static _toRadians(degrees: number): number {
return (degrees * Math.PI) / 180;
}
/**
* 弧度转角度
*/
private static _toDegrees(radians: number): number {
return (radians * 180) / Math.PI;
}
}
// ===== 3. 数据导出API =====
export class DataExporter {
/**
* 导出为GeoJSON格式
*/
static exportToGeoJSON(fields: Field[]): string {
const features = fields.map((field) => ({
type: 'Feature',
id: field.id,
properties: {
name: field.name,
code: field.code,
area: GeometryCalculator.calculateArea(field.geometry),
perimeter: GeometryCalculator.calculatePerimeter(field.geometry),
centroid: GeometryCalculator.calculateCentroid(field.geometry),
...field.properties,
},
geometry: {
type: 'Polygon',
coordinates: [field.geometry.points.map((p) => [p.lng, p.lat])],
},
}));
const geoJSON = {
type: 'FeatureCollection',
crs: {
type: 'name',
properties: {
name: 'EPSG:4326', // WGS-84
},
},
features,
};
return JSON.stringify(geoJSON, null, 2);
}
/**
* 导出为KML格式
*/
static exportToKML(fields: Field[]): string {
const placemarks = fields
.map((field) => {
const coords = field.geometry.points.map((p) => `${p.lng},${p.lat},0`).join(' ');
return `
<Placemark>
<name>${field.name}</name>
<description>
编号: ${field.code}
面积: ${GeometryCalculator.calculateArea(field.geometry).toFixed(2)}
周长: ${GeometryCalculator.calculatePerimeter(field.geometry).toFixed(0)}
</description>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>${coords}</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>`;
})
.join('\n');
return `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>地块数据</name>
<description>智慧农业生产管理系统地块导出</description>
${placemarks}
</Document>
</kml>`;
}
/**
* 导出为Shapefile格式WKT格式
*/
static exportToWKT(field: Field): string {
const coords = field.geometry.points.map((p) => `${p.lng} ${p.lat}`).join(', ');
return `POLYGON((${coords}))`;
}
/**
* 导出为CSV格式
*/
static exportToCSV(fields: Field[]): string {
const headers = ['ID', '名称', '编号', '面积(亩)', '周长(米)', '中心点纬度', '中心点经度'];
const rows = fields.map((field) => {
const centroid = GeometryCalculator.calculateCentroid(field.geometry);
return [
field.id,
field.name,
field.code,
GeometryCalculator.calculateArea(field.geometry).toFixed(2),
GeometryCalculator.calculatePerimeter(field.geometry).toFixed(0),
centroid.lat.toFixed(6),
centroid.lng.toFixed(6),
];
});
const csvContent = [
headers.join(','),
...rows.map((row) => row.join(',')),
].join('\n');
return csvContent;
}
/**
* 下载文件
*/
static downloadFile(content: string, filename: string, mimeType: string): void {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
}
// ===== 4. 空间索引(用于性能优化) =====
export class SpatialIndex {
private rtree: Map<string, { bbox: any; field: Field }>;
constructor() {
this.rtree = new Map();
}
/**
* 插入地块
*/
insert(field: Field): void {
const bbox = GeometryCalculator.calculateBoundingBox(field.geometry);
this.rtree.set(field.id, { bbox, field });
}
/**
* 快速查询可能相交的地块
*/
query(bbox: {
minLat: number;
maxLat: number;
minLng: number;
maxLng: number;
}): Field[] {
const results: Field[] = [];
for (const [_, item] of this.rtree) {
if (this._bboxesIntersect(bbox, item.bbox)) {
results.push(item.field);
}
}
return results;
}
/**
* 判断两个包围盒是否相交
*/
private _bboxesIntersect(
bbox1: { minLat: number; maxLat: number; minLng: number; maxLng: number },
bbox2: { minLat: number; maxLat: number; minLng: number; maxLng: number }
): boolean {
return !(
bbox1.maxLat < bbox2.minLat ||
bbox1.minLat > bbox2.maxLat ||
bbox1.maxLng < bbox2.minLng ||
bbox1.minLng > bbox2.maxLng
);
}
}

78
src/lib/usePagination.ts Normal file
View File

@@ -0,0 +1,78 @@
import { useState, useMemo } from 'react';
export interface PaginationConfig {
currentPage: number;
pageSize: number;
total: number;
}
export interface UsePaginationReturn<T> {
currentPage: number;
pageSize: number;
totalPages: number;
paginatedData: T[];
goToPage: (page: number) => void;
nextPage: () => void;
previousPage: () => void;
setPageSize: (size: number) => void;
canPreviousPage: boolean;
canNextPage: boolean;
startIndex: number;
endIndex: number;
}
export function usePagination<T>(
data: T[],
initialPageSize: number = 10
): UsePaginationReturn<T> {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(initialPageSize);
const totalPages = Math.ceil(data.length / pageSize);
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return data.slice(startIndex, endIndex);
}, [data, currentPage, pageSize]);
const goToPage = (page: number) => {
const pageNumber = Math.max(1, Math.min(page, totalPages));
setCurrentPage(pageNumber);
};
const nextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
const previousPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const handleSetPageSize = (size: number) => {
setPageSize(size);
setCurrentPage(1); // 重置到第一页
};
const startIndex = (currentPage - 1) * pageSize + 1;
const endIndex = Math.min(currentPage * pageSize, data.length);
return {
currentPage,
pageSize,
totalPages,
paginatedData,
goToPage,
nextPage,
previousPage,
setPageSize: handleSetPageSize,
canPreviousPage: currentPage > 1,
canNextPage: currentPage < totalPages,
startIndex,
endIndex,
};
}

230
src/lib/workHourTracker.ts Normal file
View File

@@ -0,0 +1,230 @@
/**
* 工时跟踪器
* 用于自动从任务执行中记录和同步工时
*/
interface WorkHourRecord {
id: string;
staffId: string;
staffName: string;
taskId?: string;
taskName?: string;
date: string;
startTime: string;
endTime: string;
totalHours: number;
workType: string;
fieldName: string;
area: number;
notes: string;
status: '待审核' | '已审核' | '已驳回';
recordType: '自动记录' | '手动录入';
createdAt: string;
}
/**
* 从任务完成时自动创建工时记录
* 在任务状态更新为"已完成"时调用此函数
*/
export function createWorkHourFromTask(task: {
id: string;
taskName: string;
executor: string;
executorId?: string;
actualStartTime: string;
actualEndTime: string;
operationType?: string;
fieldName?: string;
area?: number;
}): WorkHourRecord | null {
try {
// 验证必要字段
if (!task.actualStartTime || !task.actualEndTime || !task.executor) {
console.warn('任务缺少必要的工时信息:', task);
return null;
}
// 计算工时
const start = new Date(task.actualStartTime);
const end = new Date(task.actualEndTime);
if (end <= start) {
console.warn('任务时间无效:', task);
return null;
}
const totalHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60);
// 创建工时记录
const workHour: WorkHourRecord = {
id: `hour-auto-${task.id}-${Date.now()}`,
staffId: task.executorId || 'unknown',
staffName: task.executor,
taskId: task.id,
taskName: task.taskName,
date: task.actualStartTime.split('T')[0],
startTime: start.toTimeString().slice(0, 5),
endTime: end.toTimeString().slice(0, 5),
totalHours: parseFloat(totalHours.toFixed(2)),
workType: task.operationType || '其他',
fieldName: task.fieldName || '',
area: task.area || 0,
notes: `从任务"${task.taskName}"自动生成`,
status: '已审核', // 从任务自动生成的工时默认已审核
recordType: '自动记录',
createdAt: new Date().toISOString(),
};
return workHour;
} catch (error) {
console.error('创建工时记录失败:', error);
return null;
}
}
/**
* 保存工时记录到 localStorage
*/
export function saveWorkHour(workHour: WorkHourRecord): boolean {
try {
const data = localStorage.getItem('smart_agriculture_work_hours');
let hours: WorkHourRecord[] = data ? JSON.parse(data) : [];
// 检查是否已存在相同任务的工时记录
if (workHour.taskId) {
const exists = hours.some(h => h.taskId === workHour.taskId);
if (exists) {
console.log('该任务的工时记录已存在,跳过创建');
return false;
}
}
// 添加新记录
hours.unshift(workHour);
localStorage.setItem('smart_agriculture_work_hours', JSON.stringify(hours));
console.log('工时记录已保存:', workHour);
return true;
} catch (error) {
console.error('保存工时记录失败:', error);
return false;
}
}
/**
* 批量从任务列表创建工时记录
*/
export function batchCreateWorkHoursFromTasks(tasks: any[]): number {
let count = 0;
tasks.forEach(task => {
// 只处理已完成的任务
if (task.status !== '已完成') return;
const workHour = createWorkHourFromTask(task);
if (workHour && saveWorkHour(workHour)) {
count++;
}
});
return count;
}
/**
* 获取指定员工的工时统计
*/
export function getStaffWorkHourStats(staffId: string, startDate?: string, endDate?: string): {
totalHours: number;
approvedHours: number;
pendingHours: number;
recordCount: number;
} {
try {
const data = localStorage.getItem('smart_agriculture_work_hours');
if (!data) {
return { totalHours: 0, approvedHours: 0, pendingHours: 0, recordCount: 0 };
}
let hours: WorkHourRecord[] = JSON.parse(data);
// 筛选指定员工的记录
hours = hours.filter(h => h.staffId === staffId);
// 日期范围筛选
if (startDate) {
hours = hours.filter(h => h.date >= startDate);
}
if (endDate) {
hours = hours.filter(h => h.date <= endDate);
}
const totalHours = hours.reduce((sum, h) => sum + h.totalHours, 0);
const approvedHours = hours
.filter(h => h.status === '已审核')
.reduce((sum, h) => sum + h.totalHours, 0);
const pendingHours = hours
.filter(h => h.status === '待审核')
.reduce((sum, h) => sum + h.totalHours, 0);
return {
totalHours: parseFloat(totalHours.toFixed(2)),
approvedHours: parseFloat(approvedHours.toFixed(2)),
pendingHours: parseFloat(pendingHours.toFixed(2)),
recordCount: hours.length,
};
} catch (error) {
console.error('获取工时统计失败:', error);
return { totalHours: 0, approvedHours: 0, pendingHours: 0, recordCount: 0 };
}
}
/**
* 导出工时记录(用于薪酬核算)
*/
export function exportWorkHoursForPayroll(startDate: string, endDate: string): any[] {
try {
const data = localStorage.getItem('smart_agriculture_work_hours');
if (!data) return [];
let hours: WorkHourRecord[] = JSON.parse(data);
// 筛选日期范围和已审核的记录
hours = hours.filter(h =>
h.date >= startDate &&
h.date <= endDate &&
h.status === '已审核'
);
// 按员工汇总
const payrollData = new Map<string, any>();
hours.forEach(h => {
if (!payrollData.has(h.staffId)) {
payrollData.set(h.staffId, {
staffId: h.staffId,
staffName: h.staffName,
totalHours: 0,
recordCount: 0,
workTypes: new Map<string, number>(),
});
}
const staff = payrollData.get(h.staffId);
staff.totalHours += h.totalHours;
staff.recordCount++;
const currentHours = staff.workTypes.get(h.workType) || 0;
staff.workTypes.set(h.workType, currentHours + h.totalHours);
});
// 转换为数组格式
return Array.from(payrollData.values()).map(staff => ({
...staff,
totalHours: parseFloat(staff.totalHours.toFixed(2)),
workTypes: Object.fromEntries(staff.workTypes),
}));
} catch (error) {
console.error('导出工时数据失败:', error);
return [];
}
}