提交1 bmad搭建与项目启动 - ok
This commit is contained in:
507
src/lib/authStorage.ts
Normal file
507
src/lib/authStorage.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
import { User, LoginRecord, Enterprise } from '../types/auth';
|
||||
|
||||
// LocalStorage keys
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: 'auth_token',
|
||||
REFRESH_TOKEN: 'refresh_token',
|
||||
USER: 'user_info',
|
||||
LOGIN_TIME: 'login_time',
|
||||
SESSION_ID: 'session_id',
|
||||
};
|
||||
|
||||
// Mock企业数据库
|
||||
const MOCK_ENTERPRISES: Enterprise[] = [
|
||||
{
|
||||
id: 'ent-1',
|
||||
name: '绿野农业科技有限公司',
|
||||
code: 'LYNY001',
|
||||
type: '种植企业',
|
||||
status: 'active',
|
||||
address: '江苏省南京市江宁区农业科技园区',
|
||||
contact: '李经理',
|
||||
phone: '025-12345678',
|
||||
createdAt: '2023-01-01 10:00:00',
|
||||
},
|
||||
{
|
||||
id: 'ent-2',
|
||||
name: '丰收农场管理集团',
|
||||
code: 'FSNC002',
|
||||
type: '综合农场',
|
||||
status: 'active',
|
||||
address: '山东省济南市章丘区现代农业示范区',
|
||||
contact: '王总',
|
||||
phone: '0531-87654321',
|
||||
createdAt: '2023-03-15 14:00:00',
|
||||
},
|
||||
{
|
||||
id: 'ent-3',
|
||||
name: '智慧农业示范基地',
|
||||
code: 'ZHNY003',
|
||||
type: '示范基地',
|
||||
status: 'active',
|
||||
address: '浙江省杭州市萧山区智慧农业园',
|
||||
contact: '陈主任',
|
||||
phone: '0571-23456789',
|
||||
createdAt: '2023-06-01 09:00:00',
|
||||
},
|
||||
{
|
||||
id: 'ent-4',
|
||||
name: '现代农业合作社',
|
||||
code: 'XDNY004',
|
||||
type: '合作社',
|
||||
status: 'active',
|
||||
address: '河南省郑州市中牟县农业园区',
|
||||
contact: '赵理事长',
|
||||
phone: '0371-34567890',
|
||||
createdAt: '2023-08-20 11:00:00',
|
||||
},
|
||||
{
|
||||
id: 'ent-5',
|
||||
name: '优质粮食生产基地',
|
||||
code: 'YZLS005',
|
||||
type: '粮食基地',
|
||||
status: 'active',
|
||||
address: '黑龙江省哈尔滨市五常市',
|
||||
contact: '孙场长',
|
||||
phone: '0451-45678901',
|
||||
createdAt: '2023-10-10 08:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
// Mock用户数据库
|
||||
const MOCK_USERS: User[] = [
|
||||
{
|
||||
id: 'user-1',
|
||||
username: 'admin',
|
||||
phone: '13800138000',
|
||||
email: 'admin@agriculture.com',
|
||||
realName: '系统管理员',
|
||||
role: 'admin',
|
||||
permissions: ['*'],
|
||||
avatar: '',
|
||||
department: '技术部',
|
||||
enterpriseId: 'ent-1',
|
||||
enterpriseName: '绿野农业科技有限公司',
|
||||
createdAt: '2024-01-01 10:00:00',
|
||||
lastLoginTime: '2024-10-15 09:30:00',
|
||||
lastLoginIp: '192.168.1.100',
|
||||
lastLoginDevice: 'Windows PC - Chrome',
|
||||
},
|
||||
{
|
||||
id: 'user-2',
|
||||
username: 'zhangsan',
|
||||
phone: '13800138001',
|
||||
email: 'zhangsan@agriculture.com',
|
||||
realName: '张三',
|
||||
role: 'manager',
|
||||
permissions: ['machinery:*', 'field:*', 'operation:*'],
|
||||
avatar: '',
|
||||
department: '运营部',
|
||||
enterpriseId: 'ent-1',
|
||||
enterpriseName: '绿野农业科技有限公司',
|
||||
createdAt: '2024-01-15 14:00:00',
|
||||
lastLoginTime: '2024-10-14 16:20:00',
|
||||
lastLoginIp: '192.168.1.101',
|
||||
lastLoginDevice: 'iPhone 14 - Safari',
|
||||
},
|
||||
];
|
||||
|
||||
// Mock登录记录
|
||||
let loginRecords: LoginRecord[] = [
|
||||
{
|
||||
id: 'log-1',
|
||||
userId: 'user-1',
|
||||
username: 'admin',
|
||||
loginTime: '2024-10-15 09:30:00',
|
||||
loginIp: '192.168.1.100',
|
||||
loginDevice: 'Windows PC',
|
||||
browser: 'Chrome 118',
|
||||
os: 'Windows 10',
|
||||
loginType: 'password',
|
||||
status: 'success',
|
||||
},
|
||||
{
|
||||
id: 'log-2',
|
||||
userId: 'user-2',
|
||||
username: 'zhangsan',
|
||||
loginTime: '2024-10-14 16:20:00',
|
||||
loginIp: '192.168.1.101',
|
||||
loginDevice: 'iPhone 14',
|
||||
browser: 'Safari 17',
|
||||
os: 'iOS 17',
|
||||
loginType: 'phone',
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
|
||||
// Mock密码库(实际应该在服务端加密存储)
|
||||
const MOCK_PASSWORDS: { [username: string]: string } = {
|
||||
admin: 'admin123',
|
||||
zhangsan: 'zhang123',
|
||||
};
|
||||
|
||||
// 生成随机token
|
||||
export const generateToken = (): string => {
|
||||
return 'token_' + Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||
};
|
||||
|
||||
// 生成session ID
|
||||
export const generateSessionId = (): string => {
|
||||
return 'session_' + Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||
};
|
||||
|
||||
// 获取设备信息
|
||||
export const getDeviceInfo = (): { device: string; browser: string; os: string } => {
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
let device = 'Unknown Device';
|
||||
let browser = 'Unknown Browser';
|
||||
let os = 'Unknown OS';
|
||||
|
||||
// 检测操作系统
|
||||
if (ua.indexOf('Win') !== -1) os = 'Windows';
|
||||
else if (ua.indexOf('Mac') !== -1) os = 'macOS';
|
||||
else if (ua.indexOf('Linux') !== -1) os = 'Linux';
|
||||
else if (ua.indexOf('Android') !== -1) os = 'Android';
|
||||
else if (ua.indexOf('iOS') !== -1 || ua.indexOf('iPhone') !== -1) os = 'iOS';
|
||||
|
||||
// 检测浏览器
|
||||
if (ua.indexOf('Chrome') !== -1) browser = 'Chrome';
|
||||
else if (ua.indexOf('Safari') !== -1) browser = 'Safari';
|
||||
else if (ua.indexOf('Firefox') !== -1) browser = 'Firefox';
|
||||
else if (ua.indexOf('Edge') !== -1) browser = 'Edge';
|
||||
|
||||
// 检测设备类型
|
||||
if (ua.indexOf('Mobile') !== -1) device = 'Mobile Device';
|
||||
else if (ua.indexOf('Tablet') !== -1) device = 'Tablet';
|
||||
else device = 'Desktop PC';
|
||||
|
||||
return { device, browser, os };
|
||||
};
|
||||
|
||||
// 获取客户端IP(模拟)
|
||||
export const getClientIp = (): string => {
|
||||
// 实际应该从服务端获取
|
||||
return '192.168.1.' + Math.floor(Math.random() * 255);
|
||||
};
|
||||
|
||||
// 保存token
|
||||
export const saveToken = (token: string, refreshToken: string) => {
|
||||
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
|
||||
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
||||
localStorage.setItem(STORAGE_KEYS.LOGIN_TIME, new Date().toISOString());
|
||||
localStorage.setItem(STORAGE_KEYS.SESSION_ID, generateSessionId());
|
||||
};
|
||||
|
||||
// 获取token
|
||||
export const getToken = (): string | null => {
|
||||
return localStorage.getItem(STORAGE_KEYS.TOKEN);
|
||||
};
|
||||
|
||||
// 获取刷新token
|
||||
export const getRefreshToken = (): string | null => {
|
||||
return localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
};
|
||||
|
||||
// 保存用户信息
|
||||
export const saveUser = (user: User) => {
|
||||
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user));
|
||||
};
|
||||
|
||||
// 获取用户信息
|
||||
export const getUser = (): User | null => {
|
||||
const userStr = localStorage.getItem(STORAGE_KEYS.USER);
|
||||
if (!userStr) return null;
|
||||
try {
|
||||
return JSON.parse(userStr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 清除认证信息
|
||||
export const clearAuth = () => {
|
||||
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
||||
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
localStorage.removeItem(STORAGE_KEYS.USER);
|
||||
localStorage.removeItem(STORAGE_KEYS.LOGIN_TIME);
|
||||
localStorage.removeItem(STORAGE_KEYS.SESSION_ID);
|
||||
};
|
||||
|
||||
// 检查token是否过期(模拟)
|
||||
export const isTokenExpired = (): boolean => {
|
||||
const loginTime = localStorage.getItem(STORAGE_KEYS.LOGIN_TIME);
|
||||
if (!loginTime) return true;
|
||||
|
||||
const loginDate = new Date(loginTime);
|
||||
const now = new Date();
|
||||
const hoursPassed = (now.getTime() - loginDate.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
// 假设token有效期为24小时
|
||||
return hoursPassed >= 24;
|
||||
};
|
||||
|
||||
// 刷新token(模拟)
|
||||
export const refreshAuthToken = async (): Promise<{ token: string; refreshToken: string } | null> => {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) return null;
|
||||
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const newToken = generateToken();
|
||||
const newRefreshToken = generateToken();
|
||||
|
||||
saveToken(newToken, newRefreshToken);
|
||||
|
||||
return { token: newToken, refreshToken: newRefreshToken };
|
||||
};
|
||||
|
||||
// 验证密码登录
|
||||
export const validatePasswordLogin = async (
|
||||
username: string,
|
||||
password: string,
|
||||
captcha: string
|
||||
): Promise<{ success: boolean; message: string; user?: User }> => {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
// 验证图形验证码(简单模拟)
|
||||
// AUTO标识为系统自动登录,跳过验证码验证
|
||||
if (captcha !== 'AUTO' && (!captcha || captcha.length < 4)) {
|
||||
return { success: false, message: '请输入图形验证码' };
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = MOCK_USERS.find(u => u.username === username);
|
||||
if (!user) {
|
||||
return { success: false, message: '用户名或密码错误' };
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const storedPassword = MOCK_PASSWORDS[username];
|
||||
if (password !== storedPassword) {
|
||||
return { success: false, message: '用户名或密码错误' };
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
const deviceInfo = getDeviceInfo();
|
||||
const loginIp = getClientIp();
|
||||
const loginTime = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
user.lastLoginTime = loginTime;
|
||||
user.lastLoginIp = loginIp;
|
||||
user.lastLoginDevice = `${deviceInfo.device} - ${deviceInfo.browser}`;
|
||||
|
||||
// 记录登录日志
|
||||
const loginRecord: LoginRecord = {
|
||||
id: 'log-' + Date.now(),
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
loginTime,
|
||||
loginIp,
|
||||
loginDevice: deviceInfo.device,
|
||||
browser: deviceInfo.browser,
|
||||
os: deviceInfo.os,
|
||||
loginType: 'password',
|
||||
status: 'success',
|
||||
};
|
||||
loginRecords.unshift(loginRecord);
|
||||
|
||||
return { success: true, message: '登录成功', user };
|
||||
};
|
||||
|
||||
// 验证手机号登录
|
||||
export const validatePhoneLogin = async (
|
||||
phone: string,
|
||||
code: string,
|
||||
captcha: string
|
||||
): Promise<{ success: boolean; message: string; user?: User }> => {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
// 验证图形验证码
|
||||
if (!captcha || captcha.length < 4) {
|
||||
return { success: false, message: '请输入图形验证码' };
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
return { success: false, message: '请输入正确的手机号' };
|
||||
}
|
||||
|
||||
// 验证验证码(模拟,实际应该验证服务端发送的验证码)
|
||||
if (code !== '123456') {
|
||||
return { success: false, message: '验证码错误' };
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
const user = MOCK_USERS.find(u => u.phone === phone);
|
||||
if (!user) {
|
||||
return { success: false, message: '该手机号未注册' };
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
const deviceInfo = getDeviceInfo();
|
||||
const loginIp = getClientIp();
|
||||
const loginTime = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
user.lastLoginTime = loginTime;
|
||||
user.lastLoginIp = loginIp;
|
||||
user.lastLoginDevice = `${deviceInfo.device} - ${deviceInfo.browser}`;
|
||||
|
||||
// 记录登录日志
|
||||
const loginRecord: LoginRecord = {
|
||||
id: 'log-' + Date.now(),
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
loginTime,
|
||||
loginIp,
|
||||
loginDevice: deviceInfo.device,
|
||||
browser: deviceInfo.browser,
|
||||
os: deviceInfo.os,
|
||||
loginType: 'phone',
|
||||
status: 'success',
|
||||
};
|
||||
loginRecords.unshift(loginRecord);
|
||||
|
||||
return { success: true, message: '登录成功', user };
|
||||
};
|
||||
|
||||
// 用户注册
|
||||
export const registerUser = async (
|
||||
registerForm: {
|
||||
username: string;
|
||||
password: string;
|
||||
phone: string;
|
||||
code: string;
|
||||
realName: string;
|
||||
email?: string;
|
||||
enterpriseId: string;
|
||||
captcha: string;
|
||||
}
|
||||
): Promise<{ success: boolean; message: string; user?: User }> => {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const { username, password, phone, code, realName, email, enterpriseId, captcha } = registerForm;
|
||||
|
||||
// 验证图形验证码
|
||||
if (!captcha || captcha.length < 4) {
|
||||
return { success: false, message: '请输入图形验证码' };
|
||||
}
|
||||
|
||||
// 验证手机验证码
|
||||
if (code !== '123456') {
|
||||
return { success: false, message: '手机验证码错误' };
|
||||
}
|
||||
|
||||
// 验证企业选择
|
||||
if (!enterpriseId) {
|
||||
return { success: false, message: '请选择所属企业' };
|
||||
}
|
||||
|
||||
// 检查企业是否存在
|
||||
const enterprise = MOCK_ENTERPRISES.find(e => e.id === enterpriseId);
|
||||
if (!enterprise) {
|
||||
return { success: false, message: '所选企业不存在' };
|
||||
}
|
||||
|
||||
if (enterprise.status !== 'active') {
|
||||
return { success: false, message: '该企业已停用,无法注册' };
|
||||
}
|
||||
|
||||
// 检查用户名唯一性
|
||||
if (MOCK_USERS.some(u => u.username === username)) {
|
||||
return { success: false, message: '用户名已存在' };
|
||||
}
|
||||
|
||||
// 检查手机号唯一性
|
||||
if (MOCK_USERS.some(u => u.phone === phone)) {
|
||||
return { success: false, message: '该手机号已注册' };
|
||||
}
|
||||
|
||||
// 验证密码强度(至少6位)
|
||||
if (password.length < 6) {
|
||||
return { success: false, message: '密码长度不能少于6位' };
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
const newUser: User = {
|
||||
id: 'user-' + Date.now(),
|
||||
username,
|
||||
phone,
|
||||
email,
|
||||
realName,
|
||||
role: 'user',
|
||||
permissions: ['machinery:view', 'field:view', 'operation:view'],
|
||||
avatar: '',
|
||||
department: '待分配',
|
||||
enterpriseId,
|
||||
enterpriseName: enterprise.name,
|
||||
createdAt: new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}),
|
||||
};
|
||||
|
||||
// 保存用户(添加到mock数据库)
|
||||
MOCK_USERS.push(newUser);
|
||||
MOCK_PASSWORDS[username] = password;
|
||||
|
||||
return { success: true, message: '注册成功', user: newUser };
|
||||
};
|
||||
|
||||
// 发送短信验证码(模拟)
|
||||
export const sendSmsCode = async (phone: string): Promise<{ success: boolean; message: string }> => {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 验证手机号格式
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
return { success: false, message: '请输入正确的手机号' };
|
||||
}
|
||||
|
||||
// 模拟发送成功(实际应该调用短信服务API)
|
||||
console.log(`验证码已发送到手机:${phone},验证码:123456(仅测试使用)`);
|
||||
|
||||
return { success: true, message: '验证码已发送,请注意查收' };
|
||||
};
|
||||
|
||||
// 获取登录记录
|
||||
export const getLoginRecords = (): LoginRecord[] => {
|
||||
return loginRecords;
|
||||
};
|
||||
|
||||
// 导出所有用户(仅用于开发调试)
|
||||
export const getAllUsers = (): User[] => {
|
||||
return MOCK_USERS;
|
||||
};
|
||||
|
||||
// 获取所有企业列表
|
||||
export const getAllEnterprises = (): Enterprise[] => {
|
||||
return MOCK_ENTERPRISES.filter(e => e.status === 'active');
|
||||
};
|
||||
|
||||
// 根据ID获取企业信息
|
||||
export const getEnterpriseById = (id: string): Enterprise | undefined => {
|
||||
return MOCK_ENTERPRISES.find(e => e.id === id);
|
||||
};
|
||||
273
src/lib/geoFenceTestData.ts
Normal file
273
src/lib/geoFenceTestData.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { GeoFence, GeoFenceAlert } from '../types/equipment';
|
||||
|
||||
/**
|
||||
* 生成测试用的电子围栏数据
|
||||
*/
|
||||
export function generateTestGeoFences(machineryIds: string[]): GeoFence[] {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return [
|
||||
// 矩形作业区
|
||||
{
|
||||
id: 'fence-test-1',
|
||||
name: '1号作业区',
|
||||
type: 'polygon',
|
||||
points: [
|
||||
{ latitude: 36.6512, longitude: 117.1201 },
|
||||
{ latitude: 36.6532, longitude: 117.1201 },
|
||||
{ latitude: 36.6532, longitude: 117.1251 },
|
||||
{ latitude: 36.6512, longitude: 117.1251 },
|
||||
],
|
||||
machineryIds: machineryIds.slice(0, 2),
|
||||
alertOnExit: true,
|
||||
alertOnEnter: false,
|
||||
countWorkHours: true,
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: '系统管理员',
|
||||
},
|
||||
|
||||
// 圆形禁入区
|
||||
{
|
||||
id: 'fence-test-2',
|
||||
name: '东侧水塘禁入区',
|
||||
type: 'circle',
|
||||
center: { latitude: 36.6500, longitude: 117.1200 },
|
||||
radius: 200,
|
||||
machineryIds: machineryIds,
|
||||
alertOnExit: false,
|
||||
alertOnEnter: true,
|
||||
countWorkHours: false,
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: '系统管理员',
|
||||
},
|
||||
|
||||
// 大型多边形作业区
|
||||
{
|
||||
id: 'fence-test-3',
|
||||
name: '2号地块-西区',
|
||||
type: 'polygon',
|
||||
points: [
|
||||
{ latitude: 36.6480, longitude: 117.1150 },
|
||||
{ latitude: 36.6500, longitude: 117.1150 },
|
||||
{ latitude: 36.6510, longitude: 117.1180 },
|
||||
{ latitude: 36.6500, longitude: 117.1200 },
|
||||
{ latitude: 36.6480, longitude: 117.1190 },
|
||||
],
|
||||
machineryIds: machineryIds.slice(0, 1),
|
||||
alertOnExit: true,
|
||||
alertOnEnter: false,
|
||||
countWorkHours: true,
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: '系统管理员',
|
||||
},
|
||||
|
||||
// 休息区(圆形)
|
||||
{
|
||||
id: 'fence-test-4',
|
||||
name: '农机停放区',
|
||||
type: 'circle',
|
||||
center: { latitude: 36.6550, longitude: 117.1100 },
|
||||
radius: 100,
|
||||
machineryIds: machineryIds,
|
||||
alertOnExit: false,
|
||||
alertOnEnter: false,
|
||||
countWorkHours: false,
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: '系统管理员',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成测试用的围栏报警数据
|
||||
*/
|
||||
export function generateTestGeoFenceAlerts(
|
||||
fences: GeoFence[],
|
||||
machineryData: { id: string; name: string }[]
|
||||
): GeoFenceAlert[] {
|
||||
const alerts: GeoFenceAlert[] = [];
|
||||
|
||||
// 生成一些历史报警
|
||||
if (fences.length > 0 && machineryData.length > 0) {
|
||||
const fence1 = fences[0];
|
||||
const machinery1 = machineryData[0];
|
||||
|
||||
alerts.push({
|
||||
id: 'alert-test-1',
|
||||
fenceId: fence1.id,
|
||||
fenceName: fence1.name,
|
||||
machineryId: machinery1.id,
|
||||
machineryName: machinery1.name,
|
||||
alertType: 'exit',
|
||||
location: { latitude: 36.6510, longitude: 117.1252 },
|
||||
alertedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), // 2小时前
|
||||
acknowledged: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (fences.length > 1 && machineryData.length > 1) {
|
||||
const fence2 = fences[1];
|
||||
const machinery2 = machineryData[1];
|
||||
|
||||
alerts.push({
|
||||
id: 'alert-test-2',
|
||||
fenceId: fence2.id,
|
||||
fenceName: fence2.name,
|
||||
machineryId: machinery2.id,
|
||||
machineryName: machinery2.name,
|
||||
alertType: 'enter',
|
||||
location: { latitude: 36.6501, longitude: 117.1201 },
|
||||
alertedAt: new Date(Date.now() - 30 * 60 * 1000).toISOString(), // 30分钟前
|
||||
acknowledged: true,
|
||||
acknowledgedBy: '值班员张三',
|
||||
acknowledgedAt: new Date(Date.now() - 25 * 60 * 1000).toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证围栏坐标是否有效
|
||||
*/
|
||||
export function validateGeoFence(fence: Partial<GeoFence>): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!fence.name || fence.name.trim() === '') {
|
||||
errors.push('围栏名称不能为空');
|
||||
}
|
||||
|
||||
if (!fence.type) {
|
||||
errors.push('必须选择围栏类型');
|
||||
}
|
||||
|
||||
if (fence.type === 'circle') {
|
||||
if (!fence.center) {
|
||||
errors.push('圆形围栏必须设置中心点');
|
||||
} else {
|
||||
if (!isValidLatitude(fence.center.latitude)) {
|
||||
errors.push('中心点纬度无效(应在-90到90之间)');
|
||||
}
|
||||
if (!isValidLongitude(fence.center.longitude)) {
|
||||
errors.push('中心点经度无效(应在-180到180之间)');
|
||||
}
|
||||
}
|
||||
|
||||
if (!fence.radius || fence.radius <= 0) {
|
||||
errors.push('圆形围栏半径必须大于0');
|
||||
}
|
||||
}
|
||||
|
||||
if (fence.type === 'polygon') {
|
||||
if (!fence.points || fence.points.length < 3) {
|
||||
errors.push('多边形围栏至少需要3个顶点');
|
||||
} else {
|
||||
fence.points.forEach((point, index) => {
|
||||
if (!isValidLatitude(point.latitude)) {
|
||||
errors.push(`顶点${index + 1}的纬度无效`);
|
||||
}
|
||||
if (!isValidLongitude(point.longitude)) {
|
||||
errors.push(`顶点${index + 1}的经度无效`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!fence.machineryIds || fence.machineryIds.length === 0) {
|
||||
errors.push('必须至少关联一台农机');
|
||||
}
|
||||
|
||||
if (!fence.alertOnEnter && !fence.alertOnExit) {
|
||||
errors.push('建议至少启用一种报警方式(进入或离开)');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证纬度是否有效
|
||||
*/
|
||||
function isValidLatitude(lat: number): boolean {
|
||||
return !isNaN(lat) && lat >= -90 && lat <= 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证经度是否有效
|
||||
*/
|
||||
function isValidLongitude(lng: number): boolean {
|
||||
return !isNaN(lng) && lng >= -180 && lng <= 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的距离(米)
|
||||
* 使用 Haversine 公式
|
||||
*/
|
||||
export function calculateDistance(
|
||||
point1: { latitude: number; longitude: number },
|
||||
point2: { latitude: number; longitude: number }
|
||||
): number {
|
||||
const R = 6371000; // 地球半径,米
|
||||
const lat1 = (point1.latitude * Math.PI) / 180;
|
||||
const lat2 = (point2.latitude * Math.PI) / 180;
|
||||
const deltaLat = ((point2.latitude - point1.latitude) * Math.PI) / 180;
|
||||
const deltaLng = ((point2.longitude - point1.longitude) * Math.PI) / 180;
|
||||
|
||||
const a =
|
||||
Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
|
||||
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断点是否在圆形围栏内
|
||||
*/
|
||||
export function isPointInCircle(
|
||||
point: { latitude: number; longitude: number },
|
||||
center: { latitude: number; longitude: number },
|
||||
radius: number
|
||||
): boolean {
|
||||
const distance = calculateDistance(point, center);
|
||||
return distance <= radius;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断点是否在多边形围栏内
|
||||
* 使用射线法(Ray Casting Algorithm)
|
||||
*/
|
||||
export function isPointInPolygon(
|
||||
point: { latitude: number; longitude: number },
|
||||
polygon: { latitude: number; longitude: number }[]
|
||||
): boolean {
|
||||
let inside = false;
|
||||
const x = point.longitude;
|
||||
const y = point.latitude;
|
||||
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const xi = polygon[i].longitude;
|
||||
const yi = polygon[i].latitude;
|
||||
const xj = polygon[j].longitude;
|
||||
const yj = polygon[j].latitude;
|
||||
|
||||
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
||||
|
||||
if (intersect) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
85
src/lib/machineryStorage.ts
Normal file
85
src/lib/machineryStorage.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
// 农机档案数据存储管理
|
||||
import { MachineryRecord, MachineryChangeHistory, MachineryTag } from '../types/machinery';
|
||||
|
||||
const MACHINERY_KEY = 'smart_agriculture_machinery';
|
||||
const HISTORY_KEY = 'smart_agriculture_machinery_history';
|
||||
const TAGS_KEY = 'smart_agriculture_machinery_tags';
|
||||
|
||||
export const machineryStorage = {
|
||||
// 获取所有农机档案
|
||||
getAllMachinery(): MachineryRecord[] {
|
||||
const data = localStorage.getItem(MACHINERY_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
},
|
||||
|
||||
// 获取单个农机档案
|
||||
getMachinery(id: string): MachineryRecord | undefined {
|
||||
const all = this.getAllMachinery();
|
||||
return all.find(m => m.id === id);
|
||||
},
|
||||
|
||||
// 保存农机档案
|
||||
saveMachinery(machinery: MachineryRecord): void {
|
||||
const all = this.getAllMachinery();
|
||||
const index = all.findIndex(m => m.id === machinery.id);
|
||||
if (index >= 0) {
|
||||
all[index] = machinery;
|
||||
} else {
|
||||
all.push(machinery);
|
||||
}
|
||||
localStorage.setItem(MACHINERY_KEY, JSON.stringify(all));
|
||||
},
|
||||
|
||||
// 删除农机档案
|
||||
deleteMachinery(id: string): void {
|
||||
const all = this.getAllMachinery();
|
||||
const filtered = all.filter(m => m.id !== id);
|
||||
localStorage.setItem(MACHINERY_KEY, JSON.stringify(filtered));
|
||||
},
|
||||
|
||||
// 获取变更历史
|
||||
getChangeHistory(machineryId: string): MachineryChangeHistory[] {
|
||||
const data = localStorage.getItem(HISTORY_KEY);
|
||||
const all: MachineryChangeHistory[] = data ? JSON.parse(data) : [];
|
||||
return all.filter(h => h.machineryId === machineryId);
|
||||
},
|
||||
|
||||
// 保存变更历史
|
||||
saveChangeHistory(history: MachineryChangeHistory): void {
|
||||
const data = localStorage.getItem(HISTORY_KEY);
|
||||
const all: MachineryChangeHistory[] = data ? JSON.parse(data) : [];
|
||||
all.push(history);
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(all));
|
||||
},
|
||||
|
||||
// 获取所有标签
|
||||
getAllTags(): MachineryTag[] {
|
||||
const data = localStorage.getItem(TAGS_KEY);
|
||||
return data ? JSON.parse(data) : [];
|
||||
},
|
||||
|
||||
// 保存标签
|
||||
saveTag(tag: MachineryTag): void {
|
||||
const all = this.getAllTags();
|
||||
const index = all.findIndex(t => t.id === tag.id);
|
||||
if (index >= 0) {
|
||||
all[index] = tag;
|
||||
} else {
|
||||
all.push(tag);
|
||||
}
|
||||
localStorage.setItem(TAGS_KEY, JSON.stringify(all));
|
||||
},
|
||||
|
||||
// 删除标签
|
||||
deleteTag(id: string): void {
|
||||
const all = this.getAllTags();
|
||||
const filtered = all.filter(t => t.id !== id);
|
||||
localStorage.setItem(TAGS_KEY, JSON.stringify(filtered));
|
||||
},
|
||||
|
||||
// 根据二维码获取农机
|
||||
getMachineryByQRCode(qrCode: string): MachineryRecord | undefined {
|
||||
const all = this.getAllMachinery();
|
||||
return all.find(m => m.qrCode === qrCode);
|
||||
}
|
||||
};
|
||||
197
src/lib/mockData.ts
Normal file
197
src/lib/mockData.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
// 初始化示例数据
|
||||
import { MachineryRecord, MachineryTag } from '../types/machinery';
|
||||
import { machineryStorage } from './machineryStorage';
|
||||
|
||||
export function initializeMockData() {
|
||||
// 检查是否已有数据
|
||||
const existingMachinery = machineryStorage.getAllMachinery();
|
||||
if (existingMachinery.length > 0) {
|
||||
return; // 已有数据,不重复初始化
|
||||
}
|
||||
|
||||
// 初始化标签
|
||||
const mockTags: MachineryTag[] = [
|
||||
{ id: 'tag-1', name: '重点设备', color: '#ef4444', createdAt: '2025-01-01T00:00:00.000Z' },
|
||||
{ id: 'tag-2', name: '高效节能', color: '#10b981', createdAt: '2025-01-01T00:00:00.000Z' },
|
||||
{ id: 'tag-3', name: '进口设备', color: '#3b82f6', createdAt: '2025-01-01T00:00:00.000Z' },
|
||||
{ id: 'tag-4', name: '需保养', color: '#f59e0b', createdAt: '2025-01-01T00:00:00.000Z' },
|
||||
];
|
||||
|
||||
mockTags.forEach(tag => machineryStorage.saveTag(tag));
|
||||
|
||||
// 初始化农机档案
|
||||
const mockMachinery: MachineryRecord[] = [
|
||||
{
|
||||
id: 'machinery-1',
|
||||
name: '约翰迪尔6B-1404拖拉机',
|
||||
model: '6B-1404',
|
||||
category: '耕地机械',
|
||||
usage: '旱地',
|
||||
manufacturer: '约翰迪尔(天津)有限公司',
|
||||
manufactureDate: '2024-03-15',
|
||||
purchaseDate: '2024-04-01',
|
||||
engineNumber: 'JD20240315001',
|
||||
chassisNumber: 'JDTJ2024040001',
|
||||
power: '103',
|
||||
weight: '4200',
|
||||
workingWidth: '2.8',
|
||||
purchasePrice: 350000,
|
||||
supplier: '天津农机销售公司',
|
||||
invoiceNumber: 'INV-2024-001',
|
||||
insuranceCompany: '中国人民财产保险',
|
||||
insurancePolicyNumber: 'PICC-2024-001',
|
||||
insuranceStartDate: '2024-04-01',
|
||||
insuranceEndDate: '2025-03-31',
|
||||
insuranceAmount: 300000,
|
||||
status: '运行中',
|
||||
currentLocation: '1号地块',
|
||||
operator: '张三',
|
||||
department: '第一生产队',
|
||||
remarks: '主力耕作设备,状态良好',
|
||||
tags: ['tag-1', 'tag-2'],
|
||||
qrCode: 'machinery-1',
|
||||
createdAt: '2024-04-01T08:00:00.000Z',
|
||||
updatedAt: '2024-04-01T08:00:00.000Z',
|
||||
createdBy: '系统管理员',
|
||||
updatedBy: '系统管理员',
|
||||
},
|
||||
{
|
||||
id: 'machinery-2',
|
||||
name: '久保田PRO688Q收割机',
|
||||
model: 'PRO688Q',
|
||||
category: '收获机械',
|
||||
usage: '水田',
|
||||
manufacturer: '久保田农业机械(苏州)有限公司',
|
||||
manufactureDate: '2024-05-20',
|
||||
purchaseDate: '2024-06-01',
|
||||
engineNumber: 'KB20240520001',
|
||||
chassisNumber: 'KBSZ2024060001',
|
||||
power: '68',
|
||||
weight: '2850',
|
||||
workingWidth: '2.0',
|
||||
purchasePrice: 280000,
|
||||
supplier: '苏州农机经销商',
|
||||
invoiceNumber: 'INV-2024-002',
|
||||
insuranceCompany: '太平洋保险',
|
||||
insurancePolicyNumber: 'CPIC-2024-002',
|
||||
insuranceStartDate: '2024-06-01',
|
||||
insuranceEndDate: '2025-05-31',
|
||||
insuranceAmount: 250000,
|
||||
status: '空闲中',
|
||||
currentLocation: '机库A区',
|
||||
operator: '李四',
|
||||
department: '第二生产队',
|
||||
remarks: '水稻收割专用,效率高',
|
||||
tags: ['tag-2', 'tag-3'],
|
||||
qrCode: 'machinery-2',
|
||||
createdAt: '2024-06-01T08:00:00.000Z',
|
||||
updatedAt: '2024-06-01T08:00:00.000Z',
|
||||
createdBy: '系统管理员',
|
||||
updatedBy: '系统管理员',
|
||||
},
|
||||
{
|
||||
id: 'machinery-3',
|
||||
name: '丰疆智能无人播种机',
|
||||
model: 'FJ-BS2024',
|
||||
category: '播种机械',
|
||||
usage: '通用',
|
||||
manufacturer: '丰疆智能科技股份有限公司',
|
||||
manufactureDate: '2024-02-10',
|
||||
purchaseDate: '2024-03-01',
|
||||
engineNumber: 'FJ20240210001',
|
||||
chassisNumber: 'FJZN2024030001',
|
||||
power: '45',
|
||||
weight: '1500',
|
||||
workingWidth: '3.0',
|
||||
purchasePrice: 180000,
|
||||
supplier: '智能农机专营店',
|
||||
invoiceNumber: 'INV-2024-003',
|
||||
insuranceCompany: '中国人寿保险',
|
||||
insurancePolicyNumber: 'CLIC-2024-003',
|
||||
insuranceStartDate: '2024-03-01',
|
||||
insuranceEndDate: '2025-02-28',
|
||||
insuranceAmount: 150000,
|
||||
status: '待维护',
|
||||
currentLocation: '维修车间',
|
||||
operator: '王五',
|
||||
department: '第一生产队',
|
||||
remarks: '智能播种,需要定期校准',
|
||||
tags: ['tag-2', 'tag-4'],
|
||||
qrCode: 'machinery-3',
|
||||
createdAt: '2024-03-01T08:00:00.000Z',
|
||||
updatedAt: '2024-03-01T08:00:00.000Z',
|
||||
createdBy: '系统管理员',
|
||||
updatedBy: '系统管理员',
|
||||
},
|
||||
{
|
||||
id: 'machinery-4',
|
||||
name: '大疆T40植保无人机',
|
||||
model: 'T40',
|
||||
category: '植保机械',
|
||||
usage: '通用',
|
||||
manufacturer: '深圳大疆创新科技有限公司',
|
||||
manufactureDate: '2024-01-15',
|
||||
purchaseDate: '2024-02-01',
|
||||
engineNumber: 'DJI20240115001',
|
||||
chassisNumber: 'DJISZ2024020001',
|
||||
power: '电动',
|
||||
weight: '50',
|
||||
workingWidth: '7.0',
|
||||
purchasePrice: 58000,
|
||||
supplier: '大疆农业授权经销商',
|
||||
invoiceNumber: 'INV-2024-004',
|
||||
insuranceCompany: '平安保险',
|
||||
insurancePolicyNumber: 'PINGAN-2024-004',
|
||||
insuranceStartDate: '2024-02-01',
|
||||
insuranceEndDate: '2025-01-31',
|
||||
insuranceAmount: 50000,
|
||||
status: '运行中',
|
||||
currentLocation: '5号地块',
|
||||
operator: '赵六',
|
||||
department: '植保组',
|
||||
remarks: '高效植保,续航40分钟',
|
||||
tags: ['tag-1', 'tag-2', 'tag-3'],
|
||||
qrCode: 'machinery-4',
|
||||
createdAt: '2024-02-01T08:00:00.000Z',
|
||||
updatedAt: '2024-02-01T08:00:00.000Z',
|
||||
createdBy: '系统管理员',
|
||||
updatedBy: '系统管理员',
|
||||
},
|
||||
{
|
||||
id: 'machinery-5',
|
||||
name: '雷沃欧豹1604拖拉机',
|
||||
model: '1604',
|
||||
category: '耕地机械',
|
||||
usage: '旱地',
|
||||
manufacturer: '雷沃重工股份有限公司',
|
||||
manufactureDate: '2023-11-20',
|
||||
purchaseDate: '2023-12-01',
|
||||
engineNumber: 'LW20231120001',
|
||||
chassisNumber: 'LWSD2023120001',
|
||||
power: '118',
|
||||
weight: '4800',
|
||||
workingWidth: '3.2',
|
||||
purchasePrice: 320000,
|
||||
supplier: '山东农机销售中心',
|
||||
invoiceNumber: 'INV-2023-088',
|
||||
insuranceCompany: '中国人民财产保险',
|
||||
insurancePolicyNumber: 'PICC-2023-088',
|
||||
insuranceStartDate: '2023-12-01',
|
||||
insuranceEndDate: '2024-11-30',
|
||||
insuranceAmount: 280000,
|
||||
status: '运行中',
|
||||
currentLocation: '3号地块',
|
||||
operator: '孙七',
|
||||
department: '第三生产队',
|
||||
remarks: '国产品牌,性价比高',
|
||||
tags: ['tag-2'],
|
||||
qrCode: 'machinery-5',
|
||||
createdAt: '2023-12-01T08:00:00.000Z',
|
||||
updatedAt: '2023-12-01T08:00:00.000Z',
|
||||
createdBy: '系统管理员',
|
||||
updatedBy: '系统管理员',
|
||||
},
|
||||
];
|
||||
|
||||
mockMachinery.forEach(machinery => machineryStorage.saveMachinery(machinery));
|
||||
}
|
||||
Reference in New Issue
Block a user