生产管理系统前端 - 瓦力提交代码&文档更新
This commit is contained in:
@@ -1,15 +1,5 @@
|
||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { User, AuthState } from '../../types/auth';
|
||||
import {
|
||||
getToken,
|
||||
getUser,
|
||||
saveToken,
|
||||
saveUser,
|
||||
clearAuth,
|
||||
isTokenExpired,
|
||||
refreshAuthToken,
|
||||
generateToken,
|
||||
} from '../../lib/authStorage';
|
||||
|
||||
interface AuthContextType {
|
||||
authState: AuthState;
|
||||
@@ -28,18 +18,38 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
});
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// 初始化时检查登录状态
|
||||
useEffect(() => {
|
||||
const initAuth = async () => {
|
||||
const token = getToken();
|
||||
const user = getUser();
|
||||
// 确保在浏览器环境中执行
|
||||
if (typeof window === 'undefined') {
|
||||
setIsInitialized(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 延迟执行以避免构造函数错误
|
||||
const timer = setTimeout(() => {
|
||||
initAuth();
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const initAuth = async () => {
|
||||
try {
|
||||
// 动态导入以避免模块初始化时的错误
|
||||
const authStorage = await import('../../lib/authStorage');
|
||||
|
||||
const token = authStorage.getToken();
|
||||
const user = authStorage.getUser();
|
||||
|
||||
if (token && user) {
|
||||
// 检查token是否过期
|
||||
if (isTokenExpired()) {
|
||||
if (authStorage.isTokenExpired()) {
|
||||
// 尝试刷新token
|
||||
const refreshed = await refreshAuthToken();
|
||||
const refreshed = await authStorage.refreshAuthToken();
|
||||
if (refreshed) {
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
@@ -56,29 +66,42 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
isAuthenticated: true,
|
||||
user,
|
||||
token,
|
||||
refreshToken: getToken(),
|
||||
refreshToken: authStorage.getRefreshToken() || token,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 没有登录信息,自动使用默认账号登录
|
||||
await autoLoginWithDefaultAccount();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Auth initialization error:', error);
|
||||
// 初始化失败时设置为未登录状态
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
});
|
||||
} finally {
|
||||
setIsInitialized(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动登录默认账号
|
||||
const autoLoginWithDefaultAccount = async () => {
|
||||
// 动态导入避免循环依赖
|
||||
const { validatePasswordLogin } = await import('../../lib/authStorage');
|
||||
// 自动登录默认账号
|
||||
const autoLoginWithDefaultAccount = async () => {
|
||||
try {
|
||||
// 动态导入避免循环依赖和模块初始化错误
|
||||
const authStorage = await import('../../lib/authStorage');
|
||||
|
||||
// 使用默认管理员账号自动登录
|
||||
const result = await validatePasswordLogin('admin', 'admin123', 'AUTO');
|
||||
const result = await authStorage.validatePasswordLogin('admin', 'admin123', 'AUTO');
|
||||
|
||||
if (result.success && result.user) {
|
||||
const newToken = generateToken();
|
||||
const newRefreshToken = generateToken();
|
||||
const newToken = authStorage.generateToken();
|
||||
const newRefreshToken = authStorage.generateToken();
|
||||
|
||||
saveToken(newToken, newRefreshToken);
|
||||
saveUser(result.user);
|
||||
authStorage.saveToken(newToken, newRefreshToken);
|
||||
authStorage.saveUser(result.user);
|
||||
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
@@ -95,70 +118,111 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
refreshToken: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
initAuth();
|
||||
}, []);
|
||||
} catch (error) {
|
||||
console.error('Auto login error:', error);
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 定期检查token有效性
|
||||
useEffect(() => {
|
||||
if (!authState.isAuthenticated) return;
|
||||
if (!authState.isAuthenticated || !isInitialized) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
if (isTokenExpired()) {
|
||||
const refreshed = await refreshAuthToken();
|
||||
if (refreshed) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
token: refreshed.token,
|
||||
refreshToken: refreshed.refreshToken,
|
||||
}));
|
||||
} else {
|
||||
logout();
|
||||
try {
|
||||
const authStorage = await import('../../lib/authStorage');
|
||||
|
||||
if (authStorage.isTokenExpired()) {
|
||||
const refreshed = await authStorage.refreshAuthToken();
|
||||
if (refreshed) {
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
token: refreshed.token,
|
||||
refreshToken: refreshed.refreshToken,
|
||||
}));
|
||||
} else {
|
||||
logout();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token refresh error:', error);
|
||||
}
|
||||
}, 5 * 60 * 1000); // 每5分钟检查一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [authState.isAuthenticated]);
|
||||
}, [authState.isAuthenticated, isInitialized]);
|
||||
|
||||
const login = (user: User) => {
|
||||
const token = generateToken();
|
||||
const refreshToken = generateToken();
|
||||
const login = async (user: User) => {
|
||||
try {
|
||||
const authStorage = await import('../../lib/authStorage');
|
||||
const token = authStorage.generateToken();
|
||||
const refreshToken = authStorage.generateToken();
|
||||
|
||||
saveToken(token, refreshToken);
|
||||
saveUser(user);
|
||||
authStorage.saveToken(token, refreshToken);
|
||||
authStorage.saveUser(user);
|
||||
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
user,
|
||||
token,
|
||||
refreshToken,
|
||||
});
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
user,
|
||||
token,
|
||||
refreshToken,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
clearAuth();
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
});
|
||||
const logout = async () => {
|
||||
try {
|
||||
const authStorage = await import('../../lib/authStorage');
|
||||
authStorage.clearAuth();
|
||||
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const updateUser = (user: User) => {
|
||||
saveUser(user);
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
user,
|
||||
}));
|
||||
const updateUser = async (user: User) => {
|
||||
try {
|
||||
const authStorage = await import('../../lib/authStorage');
|
||||
authStorage.saveUser(user);
|
||||
|
||||
setAuthState(prev => ({
|
||||
...prev,
|
||||
user,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Update user error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkAuth = (): boolean => {
|
||||
return authState.isAuthenticated && !isTokenExpired();
|
||||
return authState.isAuthenticated;
|
||||
};
|
||||
|
||||
// 在初始化完成前显示加载状态
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-12 w-12 animate-spin rounded-full border-4 border-solid border-green-600 border-r-transparent mb-4"></div>
|
||||
<p className="text-gray-600">正在加载...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ authState, login, logout, updateUser, checkAuth }}>
|
||||
{children}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
import { Alert, AlertDescription } from '../ui/alert';
|
||||
import { CaptchaInput } from './CaptchaInput';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { validatePasswordLogin, validatePhoneLogin, sendSmsCode } from '../../lib/authStorage';
|
||||
import { SmartFieldBackground } from './SmartFieldBackground';
|
||||
import {
|
||||
Lock,
|
||||
User,
|
||||
@@ -67,6 +67,7 @@ export function Login({ onRegisterClick }: LoginProps) {
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const { sendSmsCode } = await import('../../lib/authStorage');
|
||||
const result = await sendSmsCode(phoneForm.phone);
|
||||
if (result.success) {
|
||||
toast.success(result.message + '(测试验证码:123456)');
|
||||
@@ -109,6 +110,7 @@ export function Login({ onRegisterClick }: LoginProps) {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const { validatePasswordLogin } = await import('../../lib/authStorage');
|
||||
const result = await validatePasswordLogin(
|
||||
passwordForm.username,
|
||||
passwordForm.password,
|
||||
@@ -148,6 +150,7 @@ export function Login({ onRegisterClick }: LoginProps) {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const { validatePhoneLogin } = await import('../../lib/authStorage');
|
||||
const result = await validatePhoneLogin(
|
||||
phoneForm.phone,
|
||||
phoneForm.code,
|
||||
@@ -170,18 +173,22 @@ export function Login({ onRegisterClick }: LoginProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 via-blue-50 to-cyan-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* 智慧大田动态背景 */}
|
||||
<SmartFieldBackground />
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
{/* Logo和标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-600 rounded-2xl mb-4">
|
||||
<Shield className="w-8 h-8 text-white" />
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-white/90 backdrop-blur-sm rounded-2xl mb-4 shadow-lg">
|
||||
<Shield className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h1 className="text-green-900 mb-2">智慧农业生产管理系统</h1>
|
||||
<p className="text-sm text-muted-foreground">安全、智能、高效的农业管理平台</p>
|
||||
<h1 className="text-white mb-2 drop-shadow-lg">智慧农业生产管理系统</h1>
|
||||
<p className="text-sm text-white/90 drop-shadow-md">安全、智能、高效的农业管理平台</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 shadow-xl">
|
||||
<Card className="p-8 shadow-2xl bg-white/95 backdrop-blur-md border-white/20">
|
||||
<Tabs value={loginType} onValueChange={(v) => setLoginType(v as 'password' | 'phone')}>
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="password" className="flex items-center gap-2">
|
||||
@@ -386,7 +393,7 @@ export function Login({ onRegisterClick }: LoginProps) {
|
||||
</Card>
|
||||
|
||||
{/* 页脚 */}
|
||||
<div className="mt-6 text-center text-xs text-muted-foreground">
|
||||
<div className="mt-6 text-center text-xs text-white/80 drop-shadow-md">
|
||||
<p>© 2024 智慧农业生产管理系统. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Alert, AlertDescription } from '../ui/alert';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import { CaptchaInput } from './CaptchaInput';
|
||||
import { useAuth } from './AuthContext';
|
||||
import { registerUser, sendSmsCode, getAllEnterprises } from '../../lib/authStorage';
|
||||
import { Enterprise } from '../../types/auth';
|
||||
import { SmartFieldBackground } from './SmartFieldBackground';
|
||||
import {
|
||||
User,
|
||||
Lock,
|
||||
@@ -56,8 +56,12 @@ export function Register({ onBackToLogin }: RegisterProps) {
|
||||
|
||||
// 加载企业列表
|
||||
useEffect(() => {
|
||||
const enterpriseList = getAllEnterprises();
|
||||
setEnterprises(enterpriseList);
|
||||
const loadEnterprises = async () => {
|
||||
const { getAllEnterprises } = await import('../../lib/authStorage');
|
||||
const enterpriseList = getAllEnterprises();
|
||||
setEnterprises(enterpriseList);
|
||||
};
|
||||
loadEnterprises();
|
||||
}, []);
|
||||
|
||||
const [validation, setValidation] = useState({
|
||||
@@ -120,6 +124,7 @@ export function Register({ onBackToLogin }: RegisterProps) {
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const { sendSmsCode } = await import('../../lib/authStorage');
|
||||
const result = await sendSmsCode(form.phone);
|
||||
if (result.success) {
|
||||
toast.success(result.message + '(测试验证码:123456)');
|
||||
@@ -184,6 +189,7 @@ export function Register({ onBackToLogin }: RegisterProps) {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const { registerUser } = await import('../../lib/authStorage');
|
||||
const result = await registerUser({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
@@ -216,18 +222,22 @@ export function Register({ onBackToLogin }: RegisterProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-green-50 via-blue-50 to-cyan-50 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
|
||||
{/* 智慧大田动态背景 */}
|
||||
<SmartFieldBackground />
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
{/* Logo和标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-600 rounded-2xl mb-4">
|
||||
<Shield className="w-8 h-8 text-white" />
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-white/90 backdrop-blur-sm rounded-2xl mb-4 shadow-lg">
|
||||
<Shield className="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h1 className="text-green-900 mb-2">用户注册</h1>
|
||||
<p className="text-sm text-muted-foreground">创建您的智慧农业管理账号</p>
|
||||
<h1 className="text-white mb-2 drop-shadow-lg">用户注册</h1>
|
||||
<p className="text-sm text-white/90 drop-shadow-md">创建您的智慧农业管理账号</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 shadow-xl">
|
||||
<Card className="p-8 shadow-2xl bg-white/95 backdrop-blur-md border-white/20">
|
||||
{error && (
|
||||
<Alert className="mb-4 bg-red-50 border-red-200">
|
||||
<AlertCircle className="w-4 h-4 text-red-600" />
|
||||
@@ -510,7 +520,7 @@ export function Register({ onBackToLogin }: RegisterProps) {
|
||||
</Card>
|
||||
|
||||
{/* 页脚 */}
|
||||
<div className="mt-6 text-center text-xs text-muted-foreground">
|
||||
<div className="mt-6 text-center text-xs text-white/80 drop-shadow-md">
|
||||
<p>© 2024 智慧农业生产管理系统. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
439
src/components/auth/SmartFieldBackground.tsx
Normal file
439
src/components/auth/SmartFieldBackground.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export function SmartFieldBackground() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// 设置canvas尺寸
|
||||
const resizeCanvas = () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
};
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
// 田间传感器节点
|
||||
class SensorNode {
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
pulsePhase: number;
|
||||
pulseSpeed: number;
|
||||
connections: SensorNode[];
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.radius = 4;
|
||||
this.pulsePhase = Math.random() * Math.PI * 2;
|
||||
this.pulseSpeed = 0.03 + Math.random() * 0.02;
|
||||
this.connections = [];
|
||||
}
|
||||
|
||||
update() {
|
||||
this.pulsePhase += this.pulseSpeed;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
const pulse = Math.sin(this.pulsePhase) * 0.5 + 0.5;
|
||||
|
||||
// 节点外圈
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius + pulse * 4, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = `rgba(34, 197, 94, ${0.3 + pulse * 0.3})`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// 节点核心
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(34, 197, 94, ${0.8 + pulse * 0.2})`;
|
||||
ctx.fill();
|
||||
|
||||
// 光晕
|
||||
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius * 6);
|
||||
gradient.addColorStop(0, `rgba(34, 197, 94, ${0.4 * pulse})`);
|
||||
gradient.addColorStop(1, 'rgba(34, 197, 94, 0)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius * 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
drawConnections(ctx: CanvasRenderingContext2D) {
|
||||
this.connections.forEach(node => {
|
||||
const distance = Math.hypot(node.x - this.x, node.y - this.y);
|
||||
const opacity = Math.max(0, 1 - distance / 250);
|
||||
|
||||
if (opacity > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.x, this.y);
|
||||
ctx.lineTo(node.x, node.y);
|
||||
ctx.strokeStyle = `rgba(34, 197, 94, ${opacity * 0.2})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// 数据流动效果
|
||||
const dataFlowPhase = (Date.now() / 1000) % 1;
|
||||
const flowX = this.x + (node.x - this.x) * dataFlowPhase;
|
||||
const flowY = this.y + (node.y - this.y) * dataFlowPhase;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(flowX, flowY, 2, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(34, 197, 94, ${opacity * 0.6})`;
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 无人机
|
||||
class Drone {
|
||||
x: number;
|
||||
y: number;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
speed: number;
|
||||
size: number;
|
||||
rotorPhase: number;
|
||||
trail: { x: number; y: number; alpha: number }[];
|
||||
|
||||
constructor() {
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.targetX = this.x;
|
||||
this.targetY = this.y;
|
||||
this.speed = 1.5;
|
||||
this.size = 12;
|
||||
this.rotorPhase = 0;
|
||||
this.trail = [];
|
||||
this.setNewTarget();
|
||||
}
|
||||
|
||||
setNewTarget() {
|
||||
this.targetX = Math.random() * canvas.width;
|
||||
this.targetY = Math.random() * canvas.height;
|
||||
}
|
||||
|
||||
update() {
|
||||
const dx = this.targetX - this.x;
|
||||
const dy = this.targetY - this.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 10) {
|
||||
this.setNewTarget();
|
||||
} else {
|
||||
this.x += (dx / distance) * this.speed;
|
||||
this.y += (dy / distance) * this.speed;
|
||||
}
|
||||
|
||||
this.rotorPhase += 0.3;
|
||||
|
||||
// 更新轨迹
|
||||
this.trail.push({ x: this.x, y: this.y, alpha: 1 });
|
||||
if (this.trail.length > 30) {
|
||||
this.trail.shift();
|
||||
}
|
||||
this.trail.forEach((point, index) => {
|
||||
point.alpha = index / this.trail.length;
|
||||
});
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
// 绘制轨迹
|
||||
this.trail.forEach((point, index) => {
|
||||
if (index > 0) {
|
||||
const prev = this.trail[index - 1];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(prev.x, prev.y);
|
||||
ctx.lineTo(point.x, point.y);
|
||||
ctx.strokeStyle = `rgba(59, 130, 246, ${point.alpha * 0.3})`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
|
||||
// 无人机机身
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
|
||||
// 机身
|
||||
ctx.fillStyle = 'rgba(59, 130, 246, 0.9)';
|
||||
ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size);
|
||||
|
||||
// 四个螺旋桨
|
||||
const rotorPositions = [
|
||||
{ x: -this.size * 0.7, y: -this.size * 0.7 },
|
||||
{ x: this.size * 0.7, y: -this.size * 0.7 },
|
||||
{ x: -this.size * 0.7, y: this.size * 0.7 },
|
||||
{ x: this.size * 0.7, y: this.size * 0.7 },
|
||||
];
|
||||
|
||||
rotorPositions.forEach(pos => {
|
||||
ctx.save();
|
||||
ctx.translate(pos.x, pos.y);
|
||||
ctx.rotate(this.rotorPhase);
|
||||
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-6, 0);
|
||||
ctx.lineTo(6, 0);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -6);
|
||||
ctx.lineTo(0, 6);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// 扫描光束
|
||||
const scanRadius = 40;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, scanRadius, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = 'rgba(59, 130, 246, 0.3)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// 数据上传指示
|
||||
if (Math.random() < 0.1) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(34, 197, 94, 0.8)';
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 田地波浪效果
|
||||
class FieldWave {
|
||||
offset: number;
|
||||
speed: number;
|
||||
amplitude: number;
|
||||
frequency: number;
|
||||
y: number;
|
||||
|
||||
constructor(y: number) {
|
||||
this.offset = Math.random() * 1000;
|
||||
this.speed = 0.5 + Math.random() * 0.5;
|
||||
this.amplitude = 15 + Math.random() * 10;
|
||||
this.frequency = 0.01 + Math.random() * 0.01;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.offset += this.speed;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, this.y);
|
||||
|
||||
for (let x = 0; x <= canvas.width; x += 5) {
|
||||
const waveY = this.y + Math.sin((x + this.offset) * this.frequency) * this.amplitude;
|
||||
ctx.lineTo(x, waveY);
|
||||
}
|
||||
|
||||
ctx.strokeStyle = 'rgba(34, 197, 94, 0.15)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// 数据流粒子
|
||||
class DataParticle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
life: number;
|
||||
maxLife: number;
|
||||
size: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
const angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.5;
|
||||
const speed = 2 + Math.random() * 2;
|
||||
this.vx = Math.cos(angle) * speed;
|
||||
this.vy = Math.sin(angle) * speed;
|
||||
this.life = 0;
|
||||
this.maxLife = 60 + Math.random() * 40;
|
||||
this.size = 2 + Math.random() * 2;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
this.life++;
|
||||
}
|
||||
|
||||
draw(ctx: CanvasRenderingContext2D) {
|
||||
const alpha = 1 - this.life / this.maxLife;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(34, 197, 94, ${alpha * 0.8})`;
|
||||
ctx.fill();
|
||||
|
||||
// 光晕
|
||||
const gradient = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size * 3);
|
||||
gradient.addColorStop(0, `rgba(34, 197, 94, ${alpha * 0.4})`);
|
||||
gradient.addColorStop(1, 'rgba(34, 197, 94, 0)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size * 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
isDead() {
|
||||
return this.life >= this.maxLife;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化传感器网络
|
||||
const sensors: SensorNode[] = [];
|
||||
const gridSize = 150;
|
||||
for (let x = gridSize; x < canvas.width; x += gridSize) {
|
||||
for (let y = gridSize; y < canvas.height; y += gridSize) {
|
||||
const offsetX = (Math.random() - 0.5) * 40;
|
||||
const offsetY = (Math.random() - 0.5) * 40;
|
||||
sensors.push(new SensorNode(x + offsetX, y + offsetY));
|
||||
}
|
||||
}
|
||||
|
||||
// 建立传感器连接
|
||||
sensors.forEach(sensor => {
|
||||
sensors.forEach(other => {
|
||||
if (sensor !== other) {
|
||||
const distance = Math.hypot(other.x - sensor.x, other.y - sensor.y);
|
||||
if (distance < 250 && sensor.connections.length < 3) {
|
||||
sensor.connections.push(other);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化无人机
|
||||
const drones: Drone[] = [];
|
||||
for (let i = 0; i < 2; i++) {
|
||||
drones.push(new Drone());
|
||||
}
|
||||
|
||||
// 初始化田地波浪
|
||||
const waves: FieldWave[] = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
waves.push(new FieldWave((canvas.height / 8) * i));
|
||||
}
|
||||
|
||||
// 数据粒子
|
||||
const dataParticles: DataParticle[] = [];
|
||||
|
||||
// 动画循环
|
||||
let animationId: number;
|
||||
const animate = () => {
|
||||
// 渐变背景(不完全清除,产生拖尾效果)
|
||||
ctx.fillStyle = 'rgba(15, 118, 110, 0.08)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 绘制田地波浪
|
||||
waves.forEach(wave => {
|
||||
wave.update();
|
||||
wave.draw(ctx);
|
||||
});
|
||||
|
||||
// 绘制传感器连接线
|
||||
sensors.forEach(sensor => {
|
||||
sensor.drawConnections(ctx);
|
||||
});
|
||||
|
||||
// 绘制传感器节点
|
||||
sensors.forEach(sensor => {
|
||||
sensor.update();
|
||||
sensor.draw(ctx);
|
||||
});
|
||||
|
||||
// 随机生成数据粒子(从传感器节点)
|
||||
if (Math.random() < 0.3 && sensors.length > 0) {
|
||||
const randomSensor = sensors[Math.floor(Math.random() * sensors.length)];
|
||||
dataParticles.push(new DataParticle(randomSensor.x, randomSensor.y));
|
||||
}
|
||||
|
||||
// 更新和绘制数据粒子
|
||||
for (let i = dataParticles.length - 1; i >= 0; i--) {
|
||||
const particle = dataParticles[i];
|
||||
particle.update();
|
||||
particle.draw(ctx);
|
||||
|
||||
if (particle.isDead()) {
|
||||
dataParticles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制无人机
|
||||
drones.forEach(drone => {
|
||||
drone.update();
|
||||
drone.draw(ctx);
|
||||
});
|
||||
|
||||
// 绘制网格线(农田分界)
|
||||
ctx.strokeStyle = 'rgba(34, 197, 94, 0.08)';
|
||||
ctx.lineWidth = 1;
|
||||
const gridSpacing = 100;
|
||||
for (let x = 0; x < canvas.width; x += gridSpacing) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < canvas.height; y += gridSpacing) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(canvas.width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
cancelAnimationFrame(animationId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 静态渐变背景 */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #064e3b 0%, #065f46 20%, #047857 40%, #059669 60%, #10b981 80%, #34d399 100%)'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Canvas动态背景 */}
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0"
|
||||
style={{ mixBlendMode: 'screen', opacity: 0.7 }}
|
||||
/>
|
||||
|
||||
{/* 顶部光效 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-green-900/30 via-transparent to-green-900/30"></div>
|
||||
|
||||
{/* 底部装饰光 */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-green-950/50 to-transparent"></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user