| | <!DOCTYPE html> |
| | <html lang="ru"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Neurobox AI - Контроль Качества</title> |
| | <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> |
| | <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> |
| | <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <style> |
| | * { |
| | margin: 0; |
| | padding: 0; |
| | box-sizing: border-box; |
| | } |
| | body { |
| | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <div id="root"></div> |
| |
|
| | <script type="text/babel"> |
| | const { useState, useEffect, useRef } = React; |
| | |
| | |
| | const Camera = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/> |
| | <circle cx="12" cy="13" r="4"/> |
| | </svg> |
| | ); |
| | |
| | const AlertTriangle = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/> |
| | <line x1="12" y1="9" x2="12" y2="13"/> |
| | <line x1="12" y1="17" x2="12.01" y2="17"/> |
| | </svg> |
| | ); |
| | |
| | const CheckCircle = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> |
| | <polyline points="22 4 12 14.01 9 11.01"/> |
| | </svg> |
| | ); |
| | |
| | const Activity = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/> |
| | </svg> |
| | ); |
| | |
| | const Clock = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <circle cx="12" cy="12" r="10"/> |
| | <polyline points="12 6 12 12 16 14"/> |
| | </svg> |
| | ); |
| | |
| | const TrendingUp = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/> |
| | <polyline points="17 6 23 6 23 12"/> |
| | </svg> |
| | ); |
| | |
| | const Play = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <polygon points="5 3 19 12 5 21 5 3"/> |
| | </svg> |
| | ); |
| | |
| | const Pause = ({ size = 24, className = "" }) => ( |
| | <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}> |
| | <rect x="6" y="4" width="4" height="16"/> |
| | <rect x="14" y="4" width="4" height="16"/> |
| | </svg> |
| | ); |
| | |
| | const QualityControlDashboard = () => { |
| | const [currentTime, setCurrentTime] = useState(new Date()); |
| | const [inspectedCount, setInspectedCount] = useState(4512); |
| | const [defectsFound, setDefectsFound] = useState(28); |
| | const [isRunning, setIsRunning] = useState(true); |
| | const [events, setEvents] = useState([ |
| | { id: 77812, time: '10:32:04', type: 'defect', defectType: 'Царапина', severity: 'high' }, |
| | { id: 77811, time: '10:31:55', type: 'ok', defectType: null, severity: null }, |
| | { id: 77810, time: '10:31:48', type: 'defect', defectType: 'Непрокрас', severity: 'medium' }, |
| | { id: 77809, time: '10:31:41', type: 'ok', defectType: null, severity: null }, |
| | { id: 77808, time: '10:31:34', type: 'defect', defectType: 'Скол', severity: 'high' }, |
| | ]); |
| | |
| | const [currentItem, setCurrentItem] = useState({ id: 77813, status: 'checking' }); |
| | const [defectStats, setDefectStats] = useState({ |
| | 'Царапина': 15, |
| | 'Скол': 9, |
| | 'Непрокрас': 4 |
| | }); |
| | |
| | const canvasRef = useRef(null); |
| | |
| | useEffect(() => { |
| | const timer = setInterval(() => { |
| | setCurrentTime(new Date()); |
| | }, 1000); |
| | return () => clearInterval(timer); |
| | }, []); |
| | |
| | useEffect(() => { |
| | if (!isRunning) return; |
| | |
| | const interval = setInterval(() => { |
| | const newId = currentItem.id + 1; |
| | const isDefective = Math.random() < 0.0062; |
| | |
| | setInspectedCount(prev => prev + 1); |
| | |
| | if (isDefective) { |
| | setDefectsFound(prev => prev + 1); |
| | const defectTypes = ['Царапина', 'Скол', 'Непрокрас']; |
| | const severities = ['high', 'medium', 'low']; |
| | const defectType = defectTypes[Math.floor(Math.random() * defectTypes.length)]; |
| | const severity = severities[Math.floor(Math.random() * severities.length)]; |
| | |
| | setDefectStats(prev => ({ |
| | ...prev, |
| | [defectType]: (prev[defectType] || 0) + 1 |
| | })); |
| | |
| | const newEvent = { |
| | id: newId, |
| | time: new Date().toLocaleTimeString('ru-RU'), |
| | type: 'defect', |
| | defectType, |
| | severity |
| | }; |
| | |
| | setEvents(prev => [newEvent, ...prev.slice(0, 9)]); |
| | setCurrentItem({ id: newId, status: 'defect', defectType }); |
| | } else { |
| | const newEvent = { |
| | id: newId, |
| | time: new Date().toLocaleTimeString('ru-RU'), |
| | type: 'ok', |
| | defectType: null, |
| | severity: null |
| | }; |
| | |
| | setEvents(prev => [newEvent, ...prev.slice(0, 9)]); |
| | setCurrentItem({ id: newId, status: 'ok' }); |
| | } |
| | }, 3000); |
| | |
| | return () => clearInterval(interval); |
| | }, [isRunning, currentItem.id]); |
| | |
| | useEffect(() => { |
| | const canvas = canvasRef.current; |
| | if (!canvas) return; |
| | |
| | const ctx = canvas.getContext('2d'); |
| | let animationFrame; |
| | let offset = 0; |
| | |
| | const animate = () => { |
| | const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); |
| | gradient.addColorStop(0, '#ffffff'); |
| | gradient.addColorStop(1, '#f0f9ff'); |
| | ctx.fillStyle = gradient; |
| | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| | |
| | ctx.strokeStyle = 'rgba(59, 130, 246, 0.1)'; |
| | ctx.lineWidth = 1; |
| | for (let i = 0; i < 5; i++) { |
| | ctx.beginPath(); |
| | ctx.moveTo(0, 50 + i * 60); |
| | ctx.lineTo(canvas.width, 50 + i * 60); |
| | ctx.stroke(); |
| | } |
| | |
| | const lightGradient = ctx.createRadialGradient(canvas.width / 2, 100, 50, canvas.width / 2, 100, 300); |
| | lightGradient.addColorStop(0, 'rgba(59, 130, 246, 0.15)'); |
| | lightGradient.addColorStop(1, 'rgba(59, 130, 246, 0)'); |
| | ctx.fillStyle = lightGradient; |
| | ctx.fillRect(0, 0, canvas.width, 300); |
| | |
| | const beltGradient = ctx.createLinearGradient(0, canvas.height - 80, 0, canvas.height); |
| | beltGradient.addColorStop(0, '#64748b'); |
| | beltGradient.addColorStop(0.5, '#475569'); |
| | beltGradient.addColorStop(1, '#64748b'); |
| | ctx.fillStyle = beltGradient; |
| | ctx.fillRect(0, canvas.height - 80, canvas.width, 80); |
| | |
| | ctx.fillStyle = '#94a3b8'; |
| | ctx.fillRect(0, canvas.height - 82, canvas.width, 2); |
| | ctx.fillRect(0, canvas.height, canvas.width, 2); |
| | |
| | ctx.strokeStyle = '#cbd5e1'; |
| | ctx.lineWidth = 2; |
| | for (let i = -20; i < canvas.width; i += 40) { |
| | ctx.beginPath(); |
| | ctx.moveTo(i + offset, canvas.height - 80); |
| | ctx.lineTo(i + offset, canvas.height); |
| | ctx.stroke(); |
| | } |
| | |
| | for (let x of [50, canvas.width - 50]) { |
| | ctx.fillStyle = '#94a3b8'; |
| | ctx.beginPath(); |
| | ctx.arc(x, canvas.height - 40, 15, 0, Math.PI * 2); |
| | ctx.fill(); |
| | ctx.strokeStyle = '#475569'; |
| | ctx.lineWidth = 2; |
| | ctx.stroke(); |
| | } |
| | |
| | const itemX = canvas.width / 2 - 50; |
| | const itemY = canvas.height - 180; |
| | |
| | ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; |
| | ctx.beginPath(); |
| | ctx.ellipse(itemX + 50, itemY + 110, 55, 15, 0, 0, Math.PI * 2); |
| | ctx.fill(); |
| | |
| | const productGradient = ctx.createLinearGradient(itemX, itemY, itemX, itemY + 100); |
| | |
| | if (currentItem.status === 'defect') { |
| | productGradient.addColorStop(0, '#fca5a5'); |
| | productGradient.addColorStop(0.5, '#f87171'); |
| | productGradient.addColorStop(1, '#ef4444'); |
| | } else if (currentItem.status === 'ok') { |
| | productGradient.addColorStop(0, '#6ee7b7'); |
| | productGradient.addColorStop(0.5, '#34d399'); |
| | productGradient.addColorStop(1, '#10b981'); |
| | } else { |
| | productGradient.addColorStop(0, '#93c5fd'); |
| | productGradient.addColorStop(0.5, '#60a5fa'); |
| | productGradient.addColorStop(1, '#3b82f6'); |
| | } |
| | |
| | ctx.fillStyle = productGradient; |
| | |
| | ctx.fillRect(itemX + 35, itemY, 30, 15); |
| | ctx.fillRect(itemX + 20, itemY + 15, 60, 85); |
| | |
| | ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; |
| | ctx.fillRect(itemX + 25, itemY + 20, 15, 70); |
| | |
| | ctx.fillStyle = currentItem.status === 'defect' ? '#dc2626' : '#2563eb'; |
| | ctx.fillRect(itemX + 25, itemY + 40, 50, 30); |
| | ctx.fillStyle = '#ffffff'; |
| | ctx.font = 'bold 10px Arial'; |
| | ctx.fillText('NEURO', itemX + 32, itemY + 52); |
| | ctx.fillText('PRODUCT', itemX + 28, itemY + 64); |
| | |
| | ctx.fillStyle = '#334155'; |
| | ctx.fillRect(itemX + 32, itemY - 8, 36, 8); |
| | ctx.fillStyle = '#475569'; |
| | ctx.fillRect(itemX + 35, itemY - 5, 30, 5); |
| | |
| | ctx.strokeStyle = '#334155'; |
| | ctx.lineWidth = 2; |
| | ctx.strokeRect(itemX + 20, itemY + 15, 60, 85); |
| | ctx.strokeRect(itemX + 35, itemY, 30, 15); |
| | |
| | if (currentItem.status === 'defect') { |
| | ctx.strokeStyle = '#ef4444'; |
| | ctx.lineWidth = 5; |
| | ctx.setLineDash([10, 5]); |
| | ctx.strokeRect(itemX + 10, itemY - 20, 80, 130); |
| | ctx.setLineDash([]); |
| | |
| | if (currentItem.defectType === 'Царапина') { |
| | ctx.strokeStyle = '#dc2626'; |
| | ctx.lineWidth = 4; |
| | ctx.beginPath(); |
| | ctx.moveTo(itemX + 30, itemY + 35); |
| | ctx.lineTo(itemX + 45, itemY + 50); |
| | ctx.stroke(); |
| | } else if (currentItem.defectType === 'Скол') { |
| | ctx.fillStyle = '#dc2626'; |
| | ctx.beginPath(); |
| | ctx.moveTo(itemX + 75, itemY + 60); |
| | ctx.lineTo(itemX + 80, itemY + 55); |
| | ctx.lineTo(itemX + 80, itemY + 65); |
| | ctx.fill(); |
| | } else if (currentItem.defectType === 'Непрокрас') { |
| | ctx.fillStyle = 'rgba(220, 38, 38, 0.7)'; |
| | ctx.beginPath(); |
| | ctx.arc(itemX + 40, itemY + 70, 12, 0, Math.PI * 2); |
| | ctx.fill(); |
| | } |
| | |
| | ctx.fillStyle = '#ef4444'; |
| | ctx.beginPath(); |
| | ctx.arc(itemX + 85, itemY - 10, 20, 0, Math.PI * 2); |
| | ctx.fill(); |
| | |
| | ctx.fillStyle = '#ffffff'; |
| | ctx.font = 'bold 24px Arial'; |
| | ctx.fillText('!', itemX + 80, itemY - 1); |
| | |
| | ctx.fillStyle = '#dc2626'; |
| | ctx.font = 'bold 18px Arial'; |
| | ctx.fillText('БРАК ОБНАРУЖЕН', itemX - 30, itemY - 35); |
| | } else if (currentItem.status === 'ok') { |
| | ctx.strokeStyle = '#10b981'; |
| | ctx.lineWidth = 5; |
| | ctx.beginPath(); |
| | ctx.arc(itemX + 50, itemY - 30, 20, 0, Math.PI * 2); |
| | ctx.stroke(); |
| | |
| | ctx.strokeStyle = '#10b981'; |
| | ctx.lineWidth = 5; |
| | ctx.beginPath(); |
| | ctx.moveTo(itemX + 40, itemY - 30); |
| | ctx.lineTo(itemX + 47, itemY - 23); |
| | ctx.lineTo(itemX + 62, itemY - 38); |
| | ctx.stroke(); |
| | |
| | ctx.fillStyle = '#059669'; |
| | ctx.font = 'bold 16px Arial'; |
| | ctx.fillText('✓ ГОДЕН', itemX + 20, itemY - 35); |
| | } |
| | |
| | ctx.fillStyle = 'rgba(51, 65, 85, 0.9)'; |
| | ctx.fillRect(itemX + 15, itemY + 105, 70, 20); |
| | ctx.fillStyle = '#FFFFFF'; |
| | ctx.font = 'bold 13px monospace'; |
| | ctx.fillText(`ID: ${currentItem.id}`, itemX + 20, itemY + 118); |
| | |
| | const scanY = (Date.now() / 10) % 200 + itemY - 30; |
| | ctx.strokeStyle = 'rgba(59, 130, 246, 0.8)'; |
| | ctx.lineWidth = 3; |
| | ctx.beginPath(); |
| | ctx.moveTo(itemX - 20, scanY); |
| | ctx.lineTo(itemX + 120, scanY); |
| | ctx.stroke(); |
| | |
| | ctx.fillStyle = 'rgba(59, 130, 246, 0.3)'; |
| | ctx.fillRect(itemX - 20, scanY - 2, 140, 5); |
| | |
| | const drawBackgroundProduct = (x, y, scale, opacity) => { |
| | ctx.globalAlpha = opacity; |
| | ctx.fillStyle = '#94a3b8'; |
| | ctx.fillRect(x + 35 * scale, y, 30 * scale, 15 * scale); |
| | ctx.fillRect(x + 20 * scale, y + 15 * scale, 60 * scale, 85 * scale); |
| | ctx.strokeStyle = '#64748b'; |
| | ctx.lineWidth = 1; |
| | ctx.strokeRect(x + 20 * scale, y + 15 * scale, 60 * scale, 85 * scale); |
| | ctx.globalAlpha = 1; |
| | }; |
| | |
| | drawBackgroundProduct(150, itemY + 80, 0.6, 0.6); |
| | drawBackgroundProduct(550, itemY + 90, 0.5, 0.5); |
| | |
| | if (isRunning) { |
| | offset = (offset + 2) % 40; |
| | } |
| | |
| | animationFrame = requestAnimationFrame(animate); |
| | }; |
| | |
| | animate(); |
| | return () => cancelAnimationFrame(animationFrame); |
| | }, [currentItem, isRunning]); |
| | |
| | const defectRate = ((defectsFound / inspectedCount) * 100).toFixed(2); |
| | |
| | const getSeverityColor = (severity) => { |
| | switch(severity) { |
| | case 'high': return 'bg-red-100 text-red-700 border-red-400'; |
| | case 'medium': return 'bg-yellow-100 text-yellow-700 border-yellow-400'; |
| | case 'low': return 'bg-blue-100 text-blue-700 border-blue-400'; |
| | default: return 'bg-gray-100 text-gray-700 border-gray-400'; |
| | } |
| | }; |
| | |
| | const getSeverityText = (severity) => { |
| | switch(severity) { |
| | case 'high': return 'Высокая'; |
| | case 'medium': return 'Средняя'; |
| | case 'low': return 'Низкая'; |
| | default: return ''; |
| | } |
| | }; |
| | |
| | return ( |
| | <div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 p-6"> |
| | <div className="max-w-7xl mx-auto"> |
| | <div className="bg-gradient-to-r from-blue-500 to-indigo-600 rounded-xl shadow-xl p-6 mb-6 border-2 border-blue-300"> |
| | <div className="flex justify-between items-center"> |
| | <div className="flex items-center gap-4"> |
| | <div className="bg-white p-2 rounded-lg"> |
| | <Camera size={32} className="text-blue-600" /> |
| | </div> |
| | <div> |
| | <h1 className="text-3xl font-bold text-white">Дашборд контроля качества</h1> |
| | <p className="text-blue-50 text-sm font-medium">Линия 3 • Neurobox AI System</p> |
| | </div> |
| | </div> |
| | <div className="flex items-center gap-6"> |
| | <button |
| | onClick={() => setIsRunning(!isRunning)} |
| | className="flex items-center gap-2 bg-white text-blue-600 px-5 py-2.5 rounded-lg hover:bg-blue-50 transition-all font-bold shadow-lg hover:shadow-xl" |
| | > |
| | {isRunning ? <Pause size={20} /> : <Play size={20} />} |
| | {isRunning ? 'Пауза' : 'Запуск'} |
| | </button> |
| | <div className="flex items-center gap-2 bg-white px-4 py-2 rounded-lg shadow-md"> |
| | <Activity size={20} className={isRunning ? 'text-green-500 animate-pulse' : 'text-gray-400'} /> |
| | <span className={`font-bold ${isRunning ? 'text-green-600' : 'text-gray-600'}`}> |
| | {isRunning ? 'Работа' : 'Остановлено'} |
| | </span> |
| | </div> |
| | <div className="flex items-center gap-2 bg-white px-4 py-2 rounded-lg shadow-md"> |
| | <Clock size={20} className="text-indigo-600" /> |
| | <span className="font-mono text-lg text-gray-800 font-semibold"> |
| | {currentTime.toLocaleTimeString('ru-RU')} {currentTime.toLocaleDateString('ru-RU')} |
| | </span> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div className="grid grid-cols-3 gap-6"> |
| | <div className="col-span-2 space-y-6"> |
| | <div className="bg-white rounded-xl shadow-xl p-6 border-2 border-blue-200"> |
| | <div className="flex items-center justify-between mb-4"> |
| | <h2 className="text-xl font-bold text-gray-800 flex items-center gap-2"> |
| | <Camera className="text-blue-500" /> |
| | Прямая трансляция с камеры |
| | </h2> |
| | <span className="text-green-600 flex items-center gap-2 text-sm font-semibold bg-green-50 px-3 py-1 rounded-full border border-green-200"> |
| | <span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span> |
| | В эфире |
| | </span> |
| | </div> |
| | <div className="bg-gradient-to-br from-gray-50 to-blue-50 rounded-lg overflow-hidden border-2 border-gray-300 shadow-inner"> |
| | <canvas |
| | ref={canvasRef} |
| | width={800} |
| | height={400} |
| | className="w-full" |
| | /> |
| | </div> |
| | </div> |
| | |
| | <div className="bg-white rounded-xl shadow-xl p-6 border-2 border-blue-200"> |
| | <h2 className="text-xl font-bold text-gray-800 mb-4 flex items-center gap-2"> |
| | <TrendingUp className="text-green-500" /> |
| | Статистика за смену |
| | </h2> |
| | <div className="grid grid-cols-3 gap-4"> |
| | <div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl p-5 shadow-lg border-2 border-blue-300"> |
| | <p className="text-blue-50 text-sm mb-1 font-semibold">Проверено изделий</p> |
| | <p className="text-4xl font-bold text-white">{inspectedCount.toLocaleString('ru-RU')}</p> |
| | </div> |
| | <div className="bg-gradient-to-br from-red-500 to-red-600 rounded-xl p-5 shadow-lg border-2 border-red-300"> |
| | <p className="text-red-50 text-sm mb-1 font-semibold">Выявлено брака</p> |
| | <p className="text-4xl font-bold text-white">{defectsFound} <span className="text-xl">({defectRate}%)</span></p> |
| | </div> |
| | <div className="bg-gradient-to-br from-green-500 to-green-600 rounded-xl p-5 shadow-lg border-2 border-green-300"> |
| | <p className="text-green-50 text-sm mb-1 font-semibold">Годных изделий</p> |
| | <p className="text-4xl font-bold text-white">{(inspectedCount - defectsFound).toLocaleString('ru-RU')}</p> |
| | </div> |
| | </div> |
| | |
| | <div className="mt-6 bg-gradient-to-br from-gray-50 to-blue-50 rounded-xl p-5 border-2 border-gray-200 shadow-inner"> |
| | <h3 className="text-gray-800 font-bold mb-3 text-lg">Основные дефекты:</h3> |
| | <div className="space-y-3"> |
| | {Object.entries(defectStats).map(([defect, count]) => ( |
| | <div key={defect} className="flex items-center justify-between bg-white p-3 rounded-lg shadow-sm border border-gray-200"> |
| | <span className="text-gray-700 font-semibold">{defect}</span> |
| | <div className="flex items-center gap-3"> |
| | <div className="w-48 bg-gray-200 rounded-full h-3 overflow-hidden border border-gray-300"> |
| | <div |
| | className="bg-gradient-to-r from-red-500 to-red-600 h-full rounded-full transition-all duration-500 shadow-sm" |
| | style={{ width: `${(count / defectsFound) * 100}%` }} |
| | ></div> |
| | </div> |
| | <span className="text-gray-800 font-bold w-12 text-right text-lg">{count}</span> |
| | </div> |
| | </div> |
| | ))} |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div className="col-span-1"> |
| | <div className="bg-white rounded-xl shadow-xl p-6 h-full border-2 border-blue-200"> |
| | <h2 className="text-xl font-bold text-gray-800 mb-4 flex items-center gap-2"> |
| | <Activity className="text-yellow-500" /> |
| | Журнал событий |
| | <span className="text-sm text-gray-500 font-normal ml-auto">Последние 10 мин</span> |
| | </h2> |
| | <div className="space-y-3 overflow-y-auto max-h-[calc(100vh-200px)] pr-2"> |
| | {events.map((event, index) => ( |
| | <div |
| | key={`${event.id}-${index}`} |
| | className={`rounded-lg p-4 border-2 transition-all duration-300 shadow-md ${ |
| | event.type === 'defect' |
| | ? 'bg-red-50 border-red-400 hover:shadow-lg' |
| | : 'bg-green-50 border-green-400 hover:shadow-lg' |
| | } ${index === 0 ? 'ring-2 ring-blue-400 shadow-xl' : ''}`} |
| | > |
| | <div className="flex items-start justify-between mb-2"> |
| | <span className="text-gray-700 font-mono text-sm font-semibold bg-white px-2 py-1 rounded border border-gray-300">{event.time}</span> |
| | {event.type === 'defect' ? ( |
| | <AlertTriangle size={22} className="text-red-600" /> |
| | ) : ( |
| | <CheckCircle size={22} className="text-green-600" /> |
| | )} |
| | </div> |
| | <div className="flex items-center justify-between mb-2"> |
| | <span className={`font-bold text-lg ${event.type === 'defect' ? 'text-red-700' : 'text-green-700'}`}> |
| | {event.type === 'defect' ? `Дефект: ${event.defectType}` : 'OK'} |
| | </span> |
| | <span className="text-gray-600 text-sm font-semibold bg-white px-2 py-1 rounded border border-gray-300">ID: {event.id}</span> |
| | </div> |
| | {event.type === 'defect' && ( |
| | <div className="mt-2"> |
| | <span className={`inline-block px-3 py-1 rounded-full text-xs font-bold border-2 ${getSeverityColor(event.severity)}`}> |
| | Срочность: {getSeverityText(event.severity)} |
| | </span> |
| | </div> |
| | )} |
| | {event.type === 'defect' && ( |
| | <div className="mt-3 bg-white rounded-lg p-3 h-24 flex items-center justify-center border-2 border-gray-300 shadow-inner"> |
| | <div className="w-20 h-20 bg-gradient-to-br from-red-400 to-red-600 rounded-lg border-2 border-red-500 flex items-center justify-center shadow-md"> |
| | <span className="text-white text-xs font-bold">ФОТО</span> |
| | </div> |
| | </div> |
| | )} |
| | </div> |
| | ))} |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | <div className="mt-6 bg-white rounded-xl shadow-xl p-6 border-2 border-blue-200"> |
| | <div className="grid grid-cols-3 gap-6 text-center"> |
| | <div className="bg-gradient-to-br from-green-50 to-emerald-50 p-5 rounded-xl border-2 border-green-300 shadow-md"> |
| | <p className="text-gray-700 text-sm font-semibold mb-1">Снижение брака</p> |
| | <p className="text-4xl font-bold text-green-600">↓ 90%</p> |
| | </div> |
| | <div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-5 rounded-xl border-2 border-blue-300 shadow-md"> |
| | <p className="text-gray-700 text-sm font-semibold mb-1">Увеличение скорости линии</p> |
| | <p className="text-4xl font-bold text-blue-600">↑ 15%</p> |
| | </div> |
| | <div className="bg-gradient-to-br from-purple-50 to-pink-50 p-5 rounded-xl border-2 border-purple-300 shadow-md"> |
| | <p className="text-gray-700 text-sm font-semibold mb-1">Режим работы</p> |
| | <p className="text-4xl font-bold text-purple-600">24/7</p> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | ); |
| | }; |
| | |
| | const root = ReactDOM.createRoot(document.getElementById('root')); |
| | root.render(<QualityControlDashboard />); |
| | </script> |
| | </body> |
| | </html> |
| |
|