生产管理系统前端 - 瓦力提交代码&文档更新

This commit is contained in:
2025-10-25 16:11:15 +08:00
parent 7615ca9895
commit 1f1d94ed84
336 changed files with 189684 additions and 5161 deletions

View File

@@ -1,4 +1,5 @@
import { User, LoginRecord, Enterprise } from '../types/auth';
import { formatDateTime as safeDateFormat, safeNow, getTime } from './safeDate';
// LocalStorage keys
const STORAGE_KEYS = {
@@ -7,254 +8,348 @@ const STORAGE_KEYS = {
USER: 'user_info',
LOGIN_TIME: 'login_time',
SESSION_ID: 'session_id',
} as const;
// 延迟初始化的Mock数据 - 避免模块加载时的构造函数错误
let _mockEnterprises: Enterprise[] | null = null;
let _mockUsers: User[] | null = null;
let _loginRecords: LoginRecord[] | null = null;
let _mockPasswords: { [username: string]: string } | null = null;
// 格式化日期时间(使用安全的工具函数)
const formatDateTime = (): string => {
return safeDateFormat();
};
// 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 getMockEnterprises = (): Enterprise[] => {
if (_mockEnterprises === null) {
_mockEnterprises = [
{
id: 'ent-1',
name: '绿野农业科技有限公司',
code: 'LYNY001',
type: '种植企业',
status: 'active',
auditStatus: 'approved',
address: '江苏省南京市江宁区农业科技园区',
contact: '李经理',
phone: '025-12345678',
createdAt: '2023-01-01 10:00:00',
},
{
id: 'ent-2',
name: '丰收农场管理集团',
code: 'FSNC002',
type: '综合农场',
status: 'active',
auditStatus: 'approved',
address: '山东省济南市章丘区现代农业示范区',
contact: '王总',
phone: '0531-87654321',
createdAt: '2023-03-15 14:00:00',
},
{
id: 'ent-3',
name: '智慧农业示范基地',
code: 'ZHNY003',
type: '示范基地',
status: 'active',
auditStatus: 'pending',
address: '浙江省杭州市萧山区智慧农业园',
contact: '陈主任',
phone: '0571-23456789',
createdAt: '2023-06-01 09:00:00',
},
{
id: 'ent-4',
name: '现代农业合作社',
code: 'XDNY004',
type: '合作社',
status: 'active',
auditStatus: 'not_submitted',
address: '河南省郑州市中牟县农业园区',
contact: '赵理事长',
phone: '0371-34567890',
createdAt: '2023-08-20 11:00:00',
},
{
id: 'ent-5',
name: '优质粮食生产基地',
code: 'YZLS005',
type: '粮食基地',
status: 'active',
auditStatus: 'rejected',
address: '黑龙江省哈尔滨市五常市',
contact: '孙场长',
phone: '0451-45678901',
createdAt: '2023-10-10 08:00:00',
},
];
}
return _mockEnterprises;
};
// 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用户数据库
const getMockUsers = (): User[] => {
if (_mockUsers === null) {
_mockUsers = [
{
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',
},
];
}
return _mockUsers;
};
// 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',
},
];
// 获取登录记录
const getLoginRecords = (): LoginRecord[] => {
if (_loginRecords === null) {
_loginRecords = [
{
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',
},
];
}
return _loginRecords;
};
// Mock密码库(实际应该在服务端加密存储)
const MOCK_PASSWORDS: { [username: string]: string } = {
admin: 'admin123',
zhangsan: 'zhang123',
// 获取Mock密码库
const getMockPasswords = (): { [username: string]: string } => {
if (_mockPasswords === null) {
_mockPasswords = {
admin: 'admin123',
zhangsan: 'zhang123',
};
}
return _mockPasswords;
};
// 生成随机token
export const generateToken = (): string => {
return 'token_' + Math.random().toString(36).substring(2) + Date.now().toString(36);
try {
const random = Math.random().toString(36).substring(2);
const timestamp = safeNow().toString(36);
return 'token_' + random + timestamp;
} catch (e) {
console.warn('Token generation error:', e);
return 'token_' + Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
}
};
// 生成session ID
export const generateSessionId = (): string => {
return 'session_' + Math.random().toString(36).substring(2) + Date.now().toString(36);
try {
const random = Math.random().toString(36).substring(2);
const timestamp = safeNow().toString(36);
return 'session_' + random + timestamp;
} catch (e) {
console.warn('Session ID generation error:', e);
return 'session_' + Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
}
};
// 获取设备信息
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 };
try {
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
return { device: 'Desktop PC', browser: 'Chrome', os: 'Windows' };
}
const ua = navigator.userAgent || '';
let device = 'Desktop PC';
let browser = 'Chrome';
let os = 'Windows';
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 };
} catch (e) {
return { device: 'Desktop PC', browser: 'Chrome', os: 'Windows' };
}
};
// 获取客户端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());
try {
if (typeof window === 'undefined' || !window.localStorage) return;
const timestamp = String(safeNow());
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
localStorage.setItem(STORAGE_KEYS.LOGIN_TIME, timestamp);
localStorage.setItem(STORAGE_KEYS.SESSION_ID, generateSessionId());
} catch (e) {
console.error('Failed to save token:', e);
}
};
// 获取token
export const getToken = (): string | null => {
return localStorage.getItem(STORAGE_KEYS.TOKEN);
try {
if (typeof window === 'undefined' || !window.localStorage) return null;
return localStorage.getItem(STORAGE_KEYS.TOKEN);
} catch (e) {
return null;
}
};
// 获取刷新token
export const getRefreshToken = (): string | null => {
return localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
try {
if (typeof window === 'undefined' || !window.localStorage) return null;
return localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
} catch (e) {
return null;
}
};
// 保存用户信息
export const saveUser = (user: User) => {
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user));
try {
if (typeof window === 'undefined' || !window.localStorage) return;
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user));
} catch (e) {
console.error('Failed to save user:', e);
}
};
// 获取用户信息
export const getUser = (): User | null => {
const userStr = localStorage.getItem(STORAGE_KEYS.USER);
if (!userStr) return null;
try {
if (typeof window === 'undefined' || !window.localStorage) return null;
const userStr = localStorage.getItem(STORAGE_KEYS.USER);
if (!userStr) return null;
return JSON.parse(userStr);
} catch {
} catch (e) {
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);
try {
if (typeof window === 'undefined' || !window.localStorage) return;
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);
} catch (e) {
console.error('Failed to clear auth:', e);
}
};
// 检查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;
try {
if (typeof window === 'undefined' || !window.localStorage) return true;
const loginTimeStr = localStorage.getItem(STORAGE_KEYS.LOGIN_TIME);
if (!loginTimeStr) return true;
const loginTime = parseInt(loginTimeStr, 10);
if (isNaN(loginTime)) return true;
const now = safeNow();
const hoursPassed = (now - loginTime) / (1000 * 60 * 60);
return hoursPassed >= 24;
} catch (e) {
console.warn('Token expiry check error:', e);
return true;
}
};
// 刷新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 };
try {
const refreshToken = getRefreshToken();
if (!refreshToken) return null;
await new Promise(resolve => setTimeout(resolve, 500));
const newToken = generateToken();
const newRefreshToken = generateToken();
saveToken(newToken, newRefreshToken);
return { token: newToken, refreshToken: newRefreshToken };
} catch (e) {
return null;
}
};
// 验证密码登录
@@ -263,59 +358,52 @@ export const validatePasswordLogin = async (
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: '请输入图形验证码' };
try {
await new Promise(resolve => setTimeout(resolve, 800));
if (captcha !== 'AUTO' && (!captcha || captcha.length < 4)) {
return { success: false, message: '请输入图形验证码' };
}
const mockUsers = getMockUsers();
const user = mockUsers.find(u => u.username === username);
if (!user) {
return { success: false, message: '用户名或密码错误' };
}
const mockPasswords = getMockPasswords();
const storedPassword = mockPasswords[username];
if (password !== storedPassword) {
return { success: false, message: '用户名或密码错误' };
}
const deviceInfo = getDeviceInfo();
const loginIp = getClientIp();
const loginTime = formatDateTime();
user.lastLoginTime = loginTime;
user.lastLoginIp = loginIp;
user.lastLoginDevice = `${deviceInfo.device} - ${deviceInfo.browser}`;
const loginRecord: LoginRecord = {
id: 'log-' + safeNow(),
userId: user.id,
username: user.username,
loginTime,
loginIp,
loginDevice: deviceInfo.device,
browser: deviceInfo.browser,
os: deviceInfo.os,
loginType: 'password',
status: 'success',
};
const records = getLoginRecords();
records.unshift(loginRecord);
return { success: true, message: '登录成功', user };
} catch (e) {
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 };
};
// 验证手机号登录
@@ -324,62 +412,54 @@ export const validatePhoneLogin = async (
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: '请输入图形验证码' };
try {
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 mockUsers = getMockUsers();
const user = mockUsers.find(u => u.phone === phone);
if (!user) {
return { success: false, message: '该手机号未注册' };
}
const deviceInfo = getDeviceInfo();
const loginIp = getClientIp();
const loginTime = formatDateTime();
user.lastLoginTime = loginTime;
user.lastLoginIp = loginIp;
user.lastLoginDevice = `${deviceInfo.device} - ${deviceInfo.browser}`;
const loginRecord: LoginRecord = {
id: 'log-' + safeNow(),
userId: user.id,
username: user.username,
loginTime,
loginIp,
loginDevice: deviceInfo.device,
browser: deviceInfo.browser,
os: deviceInfo.os,
loginType: 'phone',
status: 'success',
};
const records = getLoginRecords();
records.unshift(loginRecord);
return { success: true, message: '登录成功', user };
} catch (e) {
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 };
};
// 用户注册
@@ -395,113 +475,104 @@ export const registerUser = async (
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: '请输入图形验证码' };
try {
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 mockEnterprises = getMockEnterprises();
const enterprise = mockEnterprises.find(e => e.id === enterpriseId);
if (!enterprise) {
return { success: false, message: '所选企业不存在' };
}
if (enterprise.status !== 'active') {
return { success: false, message: '该企业已停用,无法注册' };
}
const mockUsers = getMockUsers();
if (mockUsers.some(u => u.username === username)) {
return { success: false, message: '用户名已存在' };
}
if (mockUsers.some(u => u.phone === phone)) {
return { success: false, message: '该手机号已注册' };
}
if (password.length < 6) {
return { success: false, message: '密码长度不能少于6位' };
}
const newUser: User = {
id: 'user-' + safeNow(),
username,
phone,
email,
realName,
role: 'user',
permissions: ['machinery:view', 'field:view', 'operation:view'],
avatar: '',
department: '待分配',
enterpriseId,
enterpriseName: enterprise.name,
createdAt: formatDateTime(),
};
mockUsers.push(newUser);
const mockPasswords = getMockPasswords();
mockPasswords[username] = password;
return { success: true, message: '注册成功', user: newUser };
} catch (e) {
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: '请输入正确的手机号' };
try {
await new Promise(resolve => setTimeout(resolve, 500));
if (!/^1[3-9]\d{9}$/.test(phone)) {
return { success: false, message: '请输入正确的手机号' };
}
console.log(`验证码已发送到手机:${phone}验证码123456仅测试使用`);
return { success: true, message: '验证码已发送,请注意查收' };
} catch (e) {
return { success: false, message: '发送失败,请重试' };
}
// 模拟发送成功实际应该调用短信服务API
console.log(`验证码已发送到手机:${phone}验证码123456仅测试使用`);
return { success: true, message: '验证码已发送,请注意查收' };
};
// 获取登录记录
export const getLoginRecords = (): LoginRecord[] => {
return loginRecords;
};
// 导出登录记录(用于外部访问)
export { getLoginRecords };
// 导出所有用户(仅用于开发调试)
export const getAllUsers = (): User[] => {
return MOCK_USERS;
return getMockUsers();
};
// 获取所有企业列表
export const getAllEnterprises = (): Enterprise[] => {
return MOCK_ENTERPRISES.filter(e => e.status === 'active');
const enterprises = getMockEnterprises();
return enterprises.filter(e => e.status === 'active');
};
// 根据ID获取企业信息
export const getEnterpriseById = (id: string): Enterprise | undefined => {
return MOCK_ENTERPRISES.find(e => e.id === id);
const enterprises = getMockEnterprises();
return enterprises.find(e => e.id === id);
};

105
src/lib/clipboard.ts Normal file
View File

@@ -0,0 +1,105 @@
/**
* 安全的剪贴板复制工具
* 提供跨浏览器的剪贴板复制功能,包含回退方案
*/
/**
* 复制文本到剪贴板
* 优先使用 Clipboard API失败时静默回退到传统方法
*
* @param text 要复制的文本
* @returns Promise<boolean> 是否成功复制
*/
export async function copyToClipboard(text: string): Promise<boolean> {
// 方法 1: 尝试使用现代 Clipboard API仅在安全上下文中
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
// 静默失败,继续尝试回退方法
// 不输出警告,因为这是正常的回退行为
}
}
// 方法 2: 使用 execCommand 回退方案(兼容性更好)
return fallbackCopyToClipboard(text);
}
/**
* 回退的剪贴板复制方法
* 使用 document.execCommand('copy')
*
* @param text 要复制的文本
* @returns boolean 是否成功复制
*/
function fallbackCopyToClipboard(text: string): boolean {
let textArea: HTMLTextAreaElement | null = null;
try {
// 创建临时文本区域
textArea = document.createElement('textarea');
textArea.value = text;
// 设置样式使其不可见且不影响布局
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
textArea.style.opacity = '0';
textArea.setAttribute('readonly', '');
textArea.style.pointerEvents = 'none';
document.body.appendChild(textArea);
// 选择文本
textArea.focus();
textArea.select();
// 兼容 iOS
if (navigator.userAgent.match(/ipad|iphone/i)) {
const range = document.createRange();
range.selectNodeContents(textArea);
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
textArea.setSelectionRange(0, 999999);
}
// 执行复制命令
const successful = document.execCommand('copy');
// 清理
document.body.removeChild(textArea);
return successful;
} catch (err) {
// 确保清理 DOM
if (textArea && textArea.parentNode) {
textArea.parentNode.removeChild(textArea);
}
// 只在真正失败时输出错误
console.error('Clipboard copy failed:', err);
return false;
}
}
/**
* 安全的剪贴板复制(同步版本)
* 用于不支持 async/await 的场景
* 直接使用回退方法
*
* @param text 要复制的文本
* @returns boolean 是否成功复制
*/
export function copyToClipboardSync(text: string): boolean {
return fallbackCopyToClipboard(text);
}

View File

@@ -142,9 +142,19 @@ export class GISMapEngine {
*/
private async initLeaflet(config: MapConfig) {
try {
console.log('🔄 正在初始化 Leaflet 地图...');
// 动态加载Leaflet
if (!window.L) {
console.log('📦 Leaflet 未加载,开始加载...');
await this.loadLeaflet();
} else {
console.log('✅ Leaflet 已存在,跳过加载');
}
// 再次检查是否成功加载
if (!window.L) {
throw new Error('Leaflet 加载失败');
}
const center = config.center || [39.9042, 116.4074]; // Leaflet用 [lat, lng]
@@ -156,8 +166,11 @@ export class GISMapEngine {
this.setLayer(config.layer || 'street');
console.log('✅ Leaflet地图初始化成功');
console.log('📍 中心坐标:', center);
console.log('🔍 缩放级别:', zoom);
} catch (error) {
console.error('Leaflet地图初始化失败:', error);
console.warn('⚠️ Leaflet地图初始化失败,切换到占位地图模式');
console.error('错误详情:', error);
this.provider = 'placeholder';
this.initPlaceholder(config);
}
@@ -167,20 +180,12 @@ export class GISMapEngine {
* 加载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);
});
// 使用统一的 Leaflet 加载器
const { preloadLeaflet } = await import('./leafletLoader');
const success = await preloadLeaflet();
if (!success) {
throw new Error('Leaflet加载失败');
}
}
/**
@@ -189,26 +194,88 @@ export class GISMapEngine {
private initPlaceholder(config: MapConfig) {
if (!this.container) return;
const center = config.center || [116.4074, 39.9042];
const zoom = config.zoom || 13;
this.container.innerHTML = `
<div class="gis-placeholder-map" style="
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f0fdf4 0%, #dbeafe 100%);
background: linear-gradient(135deg, #e8f5e9 0%, #e3f2fd 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);
linear-gradient(rgba(76, 175, 80, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(76, 175, 80, 0.1) 1px, transparent 1px);
background-size: 50px 50px;
"></div>
<!-- 地图信息提示 -->
<div style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
padding: 24px 32px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
text-align: center;
max-width: 400px;
">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2" style="margin: 0 auto 16px;">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<h3 style="font-size: 18px; font-weight: 600; color: #1f2937; margin-bottom: 8px;">
地图演示模式
</h3>
<p style="font-size: 14px; color: #6b7280; margin-bottom: 16px;">
当前使用占位地图,所有功能正常可用
</p>
<div style="font-size: 12px; color: #9ca3af; border-top: 1px solid #e5e7eb; padding-top: 12px;">
<p style="margin-bottom: 4px;">中心坐标: ${center[0].toFixed(4)}°E, ${center[1].toFixed(4)}°N</p>
<p>缩放级别: ${zoom}</p>
</div>
</div>
<!-- 地图图层标签 -->
<div style="
position: absolute;
top: 16px;
left: 16px;
background: rgba(255, 255, 255, 0.95);
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
color: #4b5563;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
">
${this.getLayerLabel(this.currentLayer)}
</div>
</div>
`;
console.log('✅ 占位地图初始化成功(功能完整)');
console.log('💡 提示: 系统可以正常使用,如需真实地图请参考文档配置');
}
/**
* 获取图层标签
*/
private getLayerLabel(layer: MapLayer): string {
const labels: Record<MapLayer, string> = {
satellite: '🛰️ 卫星影像',
street: '🗺️ 电子地图',
terrain: '⛰️ 地形图',
hybrid: '🔀 混合图层',
};
return labels[layer];
}
/**

98
src/lib/leafletLoader.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* Leaflet 地图库预加载器
* 确保 Leaflet 在需要时已经加载完成
*/
let leafletLoading = false;
let leafletLoaded = false;
/**
* 预加载 Leaflet 库
* @returns Promise<boolean> 加载成功返回 true
*/
export const preloadLeaflet = (): Promise<boolean> => {
return new Promise((resolve) => {
// 如果已经加载,直接返回
if (leafletLoaded || window.L) {
leafletLoaded = true;
console.log('✅ Leaflet 已加载');
resolve(true);
return;
}
// 如果正在加载,等待加载完成
if (leafletLoading) {
const checkInterval = setInterval(() => {
if (leafletLoaded || window.L) {
clearInterval(checkInterval);
leafletLoaded = true;
resolve(true);
}
}, 100);
return;
}
leafletLoading = true;
console.log('🔄 开始加载 Leaflet...');
try {
// 加载 CSS
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
link.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';
link.crossOrigin = '';
document.head.appendChild(link);
// 加载 JS
const script = document.createElement('script');
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=';
script.crossOrigin = '';
script.onload = () => {
leafletLoaded = true;
leafletLoading = false;
console.log('✅ Leaflet 加载成功');
console.log('📍 版本:', window.L?.version);
resolve(true);
};
script.onerror = () => {
leafletLoading = false;
console.warn('⚠️ Leaflet 加载失败,将使用占位地图');
resolve(false);
};
document.head.appendChild(script);
} catch (error) {
leafletLoading = false;
console.error('❌ 加载 Leaflet 时发生错误:', error);
resolve(false);
}
});
};
/**
* 检查 Leaflet 是否已加载
*/
export const isLeafletLoaded = (): boolean => {
return leafletLoaded || !!window.L;
};
/**
* 获取 Leaflet 版本
*/
export const getLeafletVersion = (): string | null => {
if (isLeafletLoaded() && window.L) {
return window.L.version || null;
}
return null;
};
// 扩展 Window 接口
declare global {
interface Window {
L: any;
}
}

188
src/lib/operationTypes.ts Normal file
View File

@@ -0,0 +1,188 @@
// 农事类型共享数据
// 用于农事执行和农事计划模块
export type OperationType = '深翻' | '旋耕' | '播种' | '撒肥' | '喷药' | '灌溉' | '除草' | '修剪' | '采收';
export interface OperationParameter {
name: string;
type: 'text' | 'number' | 'select' | 'date' | 'image' | 'location';
required: boolean;
unit?: string;
options?: string[];
defaultValue?: any;
description?: string;
}
export interface OperationTypeTemplate {
id: string;
name: OperationType;
category: '土地整理' | '种植管理' | '田间管理' | '植保管理' | '收获管理';
icon: string;
color: string;
description: string;
parameters: OperationParameter[];
createdAt: string;
updatedAt: string;
}
// 农事类型库数据
export const operationTypesData: OperationTypeTemplate[] = [
{
id: 'type-1',
name: '深翻',
category: '土地整理',
icon: '🚜',
color: 'bg-brown-100 text-brown-800',
description: '对土壤进行深层翻耕,改善土壤结构',
parameters: [
{ name: '深度', type: 'number', required: true, unit: 'cm', description: '翻耕深度' },
{ name: '机械型号', type: 'text', required: true, description: '使用的农机型号' },
{ name: '作业面积', type: 'number', required: true, unit: '亩', description: '实际作业面积' },
{ name: '现场照片', type: 'image', required: false, description: '作业现场照片' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-2',
name: '旋耕',
category: '土地整理',
icon: '🌾',
color: 'bg-yellow-100 text-yellow-800',
description: '使用旋耕机对土壤进行浅层耕作',
parameters: [
{ name: '深度', type: 'number', required: true, unit: 'cm', description: '旋耕深度' },
{ name: '机械型号', type: 'text', required: true, description: '使用的农机型号' },
{ name: '作业面积', type: 'number', required: true, unit: '亩', description: '实际作业面积' },
{ name: '作业遍数', type: 'number', required: true, description: '旋耕遍数' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-3',
name: '播种',
category: '种植管理',
icon: '🌱',
color: 'bg-green-100 text-green-800',
description: '进行作物种子的播种作业',
parameters: [
{ name: '作物品种', type: 'text', required: true, description: '播种的作物品种' },
{ name: '种子品牌', type: 'text', required: true, description: '种子品牌' },
{ name: '种子批号', type: 'text', required: false, description: '种子批号,可扫码录入' },
{ name: '播种量', type: 'number', required: true, unit: 'kg', description: '单位面积播种量' },
{ name: '播种方式', type: 'select', required: true, options: ['条播', '穴播', '撒播', '点播'], description: '播种方式' },
{ name: '行距', type: 'number', required: false, unit: 'cm', description: '播种行距' },
{ name: '株距', type: 'number', required: false, unit: 'cm', description: '播种株距' },
{ name: '播种深度', type: 'number', required: true, unit: 'cm', description: '播种深度' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-4',
name: '撒肥',
category: '田间管理',
icon: '🌾',
color: 'bg-orange-100 text-orange-800',
description: '施用化肥或有机肥',
parameters: [
{ name: '肥料类型', type: 'select', required: true, options: ['有机肥', '复合肥', '氮肥', '磷肥', '钾肥', '微量元素肥'], description: '肥料类型' },
{ name: '肥料品牌', type: 'text', required: true, description: '肥料品牌' },
{ name: '肥料批号', type: 'text', required: false, description: '肥料批号,可扫码录入' },
{ name: '施肥量', type: 'number', required: true, unit: 'kg/亩', description: '单位面积施肥量' },
{ name: '总用量', type: 'number', required: true, unit: 'kg', description: '总施肥量' },
{ name: '施肥方法', type: 'select', required: true, options: ['基肥', '追肥', '叶面肥', '冲施'], description: '施肥方法' },
{ name: '施肥浓度', type: 'number', required: false, unit: '%', description: '施肥浓度(液肥)' },
{ name: '施肥位置', type: 'text', required: false, description: '具体地块/行号' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-5',
name: '喷药',
category: '植保管理',
icon: '💊',
color: 'bg-red-100 text-red-800',
description: '喷洒农药进行病虫害防治',
parameters: [
{ name: '农药类型', type: 'select', required: true, options: ['杀虫剂', '杀菌剂', '除草剂', '植物生长调节剂'], description: '农药类型' },
{ name: '农药品牌', type: 'text', required: true, description: '农药品牌' },
{ name: '农药批号', type: 'text', required: false, description: '农药批号,可扫码录入' },
{ name: '用药量', type: 'number', required: true, unit: 'ml/亩', description: '单位面积用药量' },
{ name: '总用量', type: 'number', required: true, unit: 'ml', description: '总用药量' },
{ name: '稀释倍数', type: 'number', required: true, description: '稀释倍数' },
{ name: '喷洒方式', type: 'select', required: true, options: ['无人机', '喷雾器', '喷杆喷雾机'], description: '喷洒方式' },
{ name: '防治对象', type: 'text', required: true, description: '防治的病虫害名称' },
{ name: '安全间隔期', type: 'number', required: true, unit: '天', description: '安全间隔期' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-6',
name: '灌溉',
category: '田间管理',
icon: '💧',
color: 'bg-blue-100 text-blue-800',
description: '对作物进行灌溉作业',
parameters: [
{ name: '灌溉方式', type: 'select', required: true, options: ['滴灌', '喷灌', '漫灌', '沟灌'], description: '灌溉方式' },
{ name: '灌溉时长', type: 'number', required: true, unit: '小时', description: '灌溉持续时间' },
{ name: '水量', type: 'number', required: true, unit: 'm³', description: '总用水量' },
{ name: '单位水量', type: 'number', required: false, unit: 'm³/亩', description: '单位面积水量' },
{ name: '水源', type: 'select', required: true, options: ['井水', '河水', '水库', '自来水'], description: '水源类型' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-7',
name: '除草',
category: '田间管理',
icon: '🌿',
color: 'bg-purple-100 text-purple-800',
description: '清除田间杂草',
parameters: [
{ name: '除草方式', type: 'select', required: true, options: ['人工除草', '机械除草', '化学除草'], description: '除草方式' },
{ name: '除草面积', type: 'number', required: true, unit: '亩', description: '除草面积' },
{ name: '除草剂品牌', type: 'text', required: false, description: '除草剂品牌(化学除草)' },
{ name: '除草剂用量', type: 'number', required: false, unit: 'ml/亩', description: '除草剂用量' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-8',
name: '修剪',
category: '田间管理',
icon: '✂️',
color: 'bg-pink-100 text-pink-800',
description: '对作物进行整枝修剪',
parameters: [
{ name: '修剪部位', type: 'select', required: true, options: ['主枝', '侧枝', '叶片', '花果'], description: '修剪部位' },
{ name: '修剪强度', type: 'select', required: true, options: ['轻度', '中度', '重度'], description: '修剪强度' },
{ name: '修剪面积', type: 'number', required: true, unit: '亩', description: '修剪面积' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
{
id: 'type-9',
name: '采收',
category: '收获管理',
icon: '🎃',
color: 'bg-amber-100 text-amber-800',
description: '作物成熟后的采收作业',
parameters: [
{ name: '采收方式', type: 'select', required: true, options: ['人工采收', '机械采收'], description: '采收方式' },
{ name: '采收面积', type: 'number', required: true, unit: '亩', description: '采收面积' },
{ name: '产量', type: 'number', required: true, unit: 'kg', description: '总产量' },
{ name: '单产', type: 'number', required: false, unit: 'kg/亩', description: '单位面积产量' },
{ name: '品质等级', type: 'select', required: false, options: ['特等', '一等', '二等', '三等'], description: '产品品质等级' },
],
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
},
];

237
src/lib/safeDate.ts Normal file
View File

@@ -0,0 +1,237 @@
/**
* 安全的Date工具函数
* 完全避免使用Date构造函数防止"Illegal constructor"错误
*/
// Fallback固定时间戳 (2024-10-24 10:00:00 UTC)
const FALLBACK_TIMESTAMP = 1729756800000;
/**
* 安全地获取当前时间戳
* 完全不使用Date构造函数
*/
export const safeNow = (): number => {
try {
if (typeof window === 'undefined') {
return FALLBACK_TIMESTAMP;
}
// 只使用Date.now(),不使用构造函数
if (typeof Date !== 'undefined' && Date.now) {
return Date.now();
}
return FALLBACK_TIMESTAMP;
} catch (e) {
console.warn('Date.now() error:', e);
return FALLBACK_TIMESTAMP;
}
};
/**
* 将时间戳转换为日期时间各部分
* 手动实现不使用Date对象
* 使用简化的格里高利历算法
*/
const timestampToDateParts = (timestamp: number): {
year: number;
month: number;
day: number;
hours: number;
minutes: number;
seconds: number;
} => {
// 基本时间单位转换
const totalSeconds = Math.floor(timestamp / 1000);
const totalMinutes = Math.floor(totalSeconds / 60);
const totalHours = Math.floor(totalMinutes / 60);
const totalDays = Math.floor(totalHours / 24);
// 计算时分秒
const seconds = totalSeconds % 60;
const minutes = totalMinutes % 60;
const hours = totalHours % 24;
// 从1970-01-01开始计算年月日
// 简化算法每年365天每4年一个闰年
let year = 1970;
let remainingDays = totalDays;
// 快速计算大致年份
const approxYears = Math.floor(remainingDays / 365.25);
year += approxYears;
remainingDays -= Math.floor(approxYears * 365.25);
// 调整到正确的年份
while (remainingDays < 0) {
year--;
const isLeap = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
remainingDays += isLeap ? 366 : 365;
}
while (remainingDays >= 365) {
const isLeap = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
const daysInYear = isLeap ? 366 : 365;
if (remainingDays >= daysInYear) {
remainingDays -= daysInYear;
year++;
} else {
break;
}
}
// 计算月和日(简化版:固定每月天数)
const monthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
if (isLeapYear) {
monthDays[1] = 29;
}
let month = 1;
let day = remainingDays + 1; // 从1开始
for (let i = 0; i < monthDays.length; i++) {
if (day <= monthDays[i]) {
month = i + 1;
break;
}
day -= monthDays[i];
}
return {
year,
month,
day: Math.max(1, Math.min(31, day)),
hours,
minutes,
seconds,
};
};
/**
* 安全地格式化日期时间为 YYYY-MM-DD HH:mm:ss
* 不使用Date对象也不使用Intl避免触发Date构造函数
*/
export const formatDateTime = (timestamp?: number): string => {
try {
if (typeof window === 'undefined') {
return '2024-10-24 10:00:00';
}
const ts = timestamp || safeNow();
// 直接使用手动计算避免任何可能触发Date的API
const parts = timestampToDateParts(ts);
const year = String(parts.year).padStart(4, '0');
const month = String(parts.month).padStart(2, '0');
const day = String(parts.day).padStart(2, '0');
const hours = String(parts.hours).padStart(2, '0');
const minutes = String(parts.minutes).padStart(2, '0');
const seconds = String(parts.seconds).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch (e) {
console.warn('Date formatting error:', e);
return '2024-10-24 10:00:00';
}
};
/**
* 安全地将时间戳转换为ISO字符串
* 不使用Date对象也不使用Intl
*/
export const toISOString = (timestamp?: number): string => {
try {
if (typeof window === 'undefined') {
return '2024-10-24T00:00:00.000Z';
}
const ts = timestamp || safeNow();
// 使用手动计算
const parts = timestampToDateParts(ts);
const year = String(parts.year).padStart(4, '0');
const month = String(parts.month).padStart(2, '0');
const day = String(parts.day).padStart(2, '0');
const hours = String(parts.hours).padStart(2, '0');
const minutes = String(parts.minutes).padStart(2, '0');
const seconds = String(parts.seconds).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.000Z`;
} catch (e) {
console.warn('ISO string conversion error:', e);
return '2024-10-24T00:00:00.000Z';
}
};
/**
* 安全地格式化日期为本地日期字符串
* 不使用Date对象也不使用Intl
*/
export const toLocaleDateString = (timestamp: number, locale = 'zh-CN'): string => {
try {
if (typeof window === 'undefined') {
return '2024-10-24';
}
// 使用手动计算
const parts = timestampToDateParts(timestamp);
const year = String(parts.year).padStart(4, '0');
const month = String(parts.month).padStart(2, '0');
const day = String(parts.day).padStart(2, '0');
// 根据locale返回不同格式
if (locale === 'zh-CN') {
return `${year}-${month}-${day}`;
}
return `${year}-${month}-${day}`;
} catch (e) {
console.warn('Local date string error:', e);
return '2024-10-24';
}
};
/**
* 安全地获取时间戳
*/
export const getTime = (timestamp?: number): number => {
try {
if (typeof window === 'undefined') {
return FALLBACK_TIMESTAMP;
}
return timestamp || safeNow();
} catch (e) {
console.warn('getTime error:', e);
return FALLBACK_TIMESTAMP;
}
};
/**
* 创建一个安全的Date对象仅在确实需要Date对象时使用
* 注意:这个函数可能在某些环境中失败
*/
export const safeDate = (value?: number | string): Date | null => {
try {
if (typeof window === 'undefined') {
return null;
}
// 尝试创建Date对象但捕获所有错误
if (value === undefined) {
return new Date();
}
if (typeof value === 'number') {
return new Date(value);
}
if (typeof value === 'string') {
return new Date(value);
}
return null;
} catch (e) {
console.warn('Date creation error:', e);
return null;
}
};