生产管理系统 - 登录,二维码功能集成

This commit is contained in:
2025-10-31 11:49:11 +08:00
parent 2fa64e66c9
commit 46ff61eaed
11 changed files with 1867 additions and 185 deletions

View File

@@ -0,0 +1,70 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface User {
id: string;
username: string;
realName: string;
email?: string;
phone?: string;
enterpriseId?: string;
}
interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const login = (userData: User) => {
setUser(userData);
// 存储到 localStorage
localStorage.setItem('user', JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
// 初始化时检查 localStorage
React.useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
try {
setUser(JSON.parse(storedUser));
} catch (error) {
console.error('Failed to parse stored user data:', error);
localStorage.removeItem('user');
}
}
}, []);
const value: AuthContextType = {
user,
login,
logout,
isAuthenticated: !!user,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,190 @@
'use client';
import { useState, useEffect } from 'react';
import { RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { getCaptchaApiV1AuthCaptchaGet } from '@/lib/api/sdk.gen';
import type { CaptchaResponse } from '@/lib/api/types.gen';
interface CaptchaInputProps {
value: string;
onChange: (value: string) => void;
onCaptchaChange?: (captchaData: CaptchaResponse | null) => void;
className?: string;
}
export function CaptchaInput({ value, onChange, onCaptchaChange, className = '' }: CaptchaInputProps) {
const [captchaData, setCaptchaData] = useState<CaptchaResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const fetchCaptcha = async () => {
setLoading(true);
setError('');
onChange(''); // 清空验证码输入
try {
const response = await getCaptchaApiV1AuthCaptchaGet();
console.log('API验证码获取成功:', response);
setCaptchaData(response.data);
if (onCaptchaChange) {
onCaptchaChange(response.data);
}
} catch (err) {
console.error('验证码获取失败:', err);
// 如果API失败使用备用验证码
const fallbackCaptcha = generateFallbackCaptcha();
console.log('生成备用验证码:', fallbackCaptcha);
setCaptchaData(fallbackCaptcha);
if (onCaptchaChange) {
onCaptchaChange(fallbackCaptcha);
}
setError(''); // 清除错误状态,因为备用验证码已生成
} finally {
setLoading(false);
}
};
const generateFallbackCaptcha = (): CaptchaResponse => {
// 备用验证码生成使用Canvas
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let text = '';
for (let i = 0; i < 4; i++) {
text += chars.charAt(Math.floor(Math.random() * chars.length));
}
const canvas = document.createElement('canvas');
canvas.width = 120;
canvas.height = 40;
const ctx = canvas.getContext('2d');
if (!ctx) {
return {
captcha_id: 'fallback-' + Date.now(),
image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
};
}
// 背景
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 干扰线
for (let i = 0; i < 3; i++) {
ctx.strokeStyle = `rgba(${Math.random() * 100}, ${Math.random() * 100}, ${Math.random() * 100}, 0.3)`;
ctx.beginPath();
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.stroke();
}
// 干扰点
for (let i = 0; i < 30; i++) {
ctx.fillStyle = `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.3)`;
ctx.beginPath();
ctx.arc(
Math.random() * canvas.width,
Math.random() * canvas.height,
1,
0,
2 * Math.PI
);
ctx.fill();
}
// 验证码文字
ctx.font = 'bold 24px Arial';
ctx.textBaseline = 'middle';
for (let i = 0; i < text.length; i++) {
const char = text[i];
const x = 20 + i * 25;
const y = 20 + (Math.random() - 0.5) * 6;
const angle = (Math.random() - 0.5) * 0.4;
ctx.save();
ctx.translate(x, y);
ctx.rotate(angle);
// 随机颜色
const colors = ['#16a34a', '#2563eb', '#dc2626', '#ea580c', '#8b5cf6'];
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
ctx.fillText(char, 0, 0);
ctx.restore();
}
return {
captcha_id: 'fallback-' + Date.now(),
image: canvas.toDataURL()
};
};
useEffect(() => {
fetchCaptcha();
}, []);
const handleRefresh = () => {
fetchCaptcha();
};
const handleCaptchaChange = (inputValue: string) => {
onChange(inputValue);
};
return (
<div className={`flex gap-2 ${className}`}>
<Input
type="text"
placeholder="请输入验证码"
value={value}
onChange={(e) => handleCaptchaChange(e.target.value.toUpperCase())}
maxLength={4}
className="flex-1"
disabled={loading}
/>
<div className="flex gap-2 items-center">
{loading ? (
<div className="w-[120px] h-10 border border-gray-300 rounded flex items-center justify-center bg-gray-100">
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
</div>
) : error ? (
<div className="w-[120px] h-10 border border-red-300 rounded flex items-center justify-center bg-red-50">
<span className="text-xs text-red-500"></span>
</div>
) : (
<div
className="h-10 w-[120px] border border-gray-300 rounded cursor-pointer hover:opacity-80 transition-opacity overflow-hidden"
onClick={handleRefresh}
title="点击刷新验证码"
>
{captchaData && (
<img
src={captchaData.image}
alt="验证码"
className="w-full h-full object-cover"
onError={(e) => {
// 如果图片加载失败,隐藏错误图片
e.currentTarget.style.display = 'none';
setError('图片加载失败');
}}
/>
)}
</div>
)}
<Button
type="button"
variant="outline"
size="icon"
onClick={handleRefresh}
disabled={loading}
title="刷新验证码"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,441 @@
'use client';
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="fixed inset-0 w-screen h-screen"
style={{
background: 'linear-gradient(135deg, #064e3b 0%, #065f46 20%, #047857 40%, #059669 60%, #10b981 80%, #34d399 100%)'
}}
/>
{/* Canvas动态背景 - 固定定位覆盖整个视口 */}
<canvas
ref={canvasRef}
className="fixed inset-0 w-screen h-screen"
style={{ mixBlendMode: 'screen', opacity: 0.7 }}
/>
{/* 顶部光效 - 固定定位 */}
<div className="fixed inset-0 w-screen h-screen bg-gradient-to-b from-green-900/30 via-transparent to-green-900/30"></div>
{/* 底部装饰光 - 固定定位 */}
<div className="fixed bottom-0 left-0 right-0 w-screen h-32 bg-gradient-to-t from-green-950/50 to-transparent"></div>
</>
);
}