| <!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> |
|
|