From 2030e68ff951111926dac800d48a62da898f2b39 Mon Sep 17 00:00:00 2001 From: zuqijia <2924963185@qq.com> Date: Tue, 19 May 2026 19:26:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/app.js | 453 ++++++++++++++- src/layout/components/Navbar/index.vue | 101 +++- src/modules/dashboardBig/views/cost.vue | 478 +++++++--------- src/modules/dashboardBig/views/energy.vue | 526 ++++++++---------- src/modules/dashboardBig/views/index.vue | 15 +- src/modules/dashboardBig/views/oee.vue | 31 +- src/modules/dashboardBig/views/order.vue | 387 +++++++------ src/modules/dashboardBig/views/output.vue | 23 +- .../dashboardBig/views/stopAnalysis.vue | 41 +- src/utils/request.js | 2 +- src/views/screens/acid-rolling/index.vue | 410 +++++++++++--- vite.config.js | 8 + 12 files changed, 1554 insertions(+), 921 deletions(-) diff --git a/server/app.js b/server/app.js index 2b5277f..90f1b11 100644 --- a/server/app.js +++ b/server/app.js @@ -242,33 +242,135 @@ app.get('/wms/acid-rolling/dashboard/overview', async (req, res) => { return } try { + // 1. 获取当前班次OEE数据 const [shiftRows] = await acidPool.execute( 'SELECT * FROM klptcm1_shift_current ORDER BY create_time DESC LIMIT 1' ) + const currentShift = shiftRows[0] || {} + // 2. 获取今日产出统计 const [coilRows] = await acidPool.execute( - 'SELECT COUNT(*) as count, SUM(weight) as totalWeight FROM klptcm1_pdo_excoil WHERE DATE(create_time) = CURDATE()' + `SELECT + COUNT(*) as count, + SUM(weight) as totalWeight, + SUM(CASE WHEN quality_status = 'A' THEN 1 ELSE 0 END) as qualifiedCount + FROM klptcm1_pdo_excoil + WHERE DATE(create_time) = CURDATE()` ) + // 3. 计算OEE指标 + const totalCoils = coilRows[0]?.count || 0 + const qualifiedCoils = coilRows[0]?.qualifiedCount || 0 + const qualityRate = totalCoils > 0 ? (qualifiedCoils / totalCoils * 100) : 0 + const availabilityRate = currentShift.availability || 92.1 + const performanceRate = currentShift.performance || 89.8 + const oeeValue = (availabilityRate * performanceRate * qualityRate / 10000).toFixed(1) + + // 4. 获取OEE趋势数据(最近7天) + const [trendingRows] = await acidPool.execute( + `SELECT + DATE(create_time) as date, + AVG(oee) as oee, + AVG(availability) as availability, + AVG(performance) as performance, + COUNT(*) as coilCount + FROM klptcm1_shift_current + WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY DATE(create_time) + ORDER BY date ASC` + ) + + // 5. 获取7大损失分布(按停机类型统计) const [lossRows] = await acidPool.execute( - 'SELECT loss_name as name, SUM(loss_time) as value FROM klptcm1_pro_stoppage WHERE DATE(create_time) = CURDATE() GROUP BY loss_name' + `SELECT + stop_type as name, + SUM(duration) as value + FROM klptcm1_pro_stoppage + WHERE create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) + GROUP BY stop_type + ORDER BY value DESC` ) - const latest = shiftRows[0] || {} + // 6. 获取班组产量排名 + const [teamRows] = await acidPool.execute( + `SELECT + crew as name, + COUNT(*) as coilCount, + SUM(weight) as output + FROM klptcm1_pdo_excoil + WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) + GROUP BY crew + ORDER BY output DESC` + ) + + // 7. 获取实时告警(停机异常、质量不合格) + const [alarmRows] = await acidPool.execute( + `SELECT + '停机告警' as type, + stop_type as message, + DATE_FORMAT(create_time, '%H:%i:%s') as time, + CASE + WHEN duration > 3600 THEN 'danger' + WHEN duration > 1800 THEN 'warning' + ELSE 'info' + END as level + FROM klptcm1_pro_stoppage + WHERE create_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR) + ORDER BY create_time DESC + LIMIT 5` + ) + + // 8. 获取质量告警 + const [qualityAlarmRows] = await acidPool.execute( + `SELECT + '质量告警' as type, + CONCAT('钢卷 ', coil_id, ' 质量异常') as message, + DATE_FORMAT(in_date, '%H:%i:%s') as time, + 'warning' as level + FROM klptcm1_pdo_excoil + WHERE quality_status IN ('B', 'C', 'D') + AND DATE(in_date) = CURDATE() + ORDER BY in_date DESC + LIMIT 3` + ) const overview = { - oee: latest.oee || 86.5, - availability: latest.availability || 92.1, - performance: latest.performance || 89.8, - quality: latest.quality || 97.5, - totalOutput: coilRows[0]?.count || mockOverview.totalOutput, - totalWeight: coilRows[0]?.totalWeight || mockOverview.totalWeight, + // 核心指标 + oee: parseFloat(oeeValue), + availability: availabilityRate, + performance: performanceRate, + quality: qualityRate.toFixed(1), + totalOutput: totalCoils, + totalWeight: coilRows[0]?.totalWeight || 0, targetOutput: 15000, efficiency: 92.0, - trendingData: mockOverview.trendingData, - lossData: lossRows.length > 0 ? lossRows : mockOverview.lossData, - teamRanking: mockOverview.teamRanking, - alarms: mockOverview.alarms + // OEE趋势数据 + trendingData: trendingRows.length > 0 ? trendingRows.map(row => ({ + date: row.date ? row.date.toString().substring(5) : '', + oee: parseFloat((row.oee || 0).toFixed(1)), + availability: parseFloat((row.availability || 0).toFixed(1)), + performance: parseFloat((row.performance || 0).toFixed(1)), + coilCount: row.coilCount || 0 + })) : mockOverview.trendingData, + // 7大损失分布 + lossData: lossRows.length > 0 ? lossRows.map(row => ({ + name: row.name || '未知损失', + value: row.value || 0 + })) : mockOverview.lossData, + // 班组产量排名 + teamRanking: teamRows.length > 0 ? teamRows.map(row => ({ + name: row.name || '未知班组', + output: Math.round(row.output || 0), + coilCount: row.coilCount || 0, + rate: 96.5 + })) : mockOverview.teamRanking, + // 告警信息 + alarms: [...alarmRows, ...qualityAlarmRows].map(row => ({ + type: row.type, + message: row.message, + time: row.time, + level: row.level + })) } sendResponse(res, overview) @@ -378,6 +480,331 @@ app.get('/wms/acid-rolling/report/stop', async (req, res) => { } }) +// ==================== 订单数据接口 ==================== + +app.get('/api/dashboard/order', async (req, res) => { + if (!masterPool) { + sendResponse(res, { + todayOrderCount: 45, + pendingOrderCount: 12, + completedOrderCount: 156, + orderTotalAmount: 568, + orderTrend: [ + { date: '05-11', count: 38, amount: 420 }, + { date: '05-12', count: 42, amount: 480 }, + { date: '05-13', count: 35, amount: 390 }, + { date: '05-14', count: 48, amount: 520 }, + { date: '05-15', count: 45, amount: 568 } + ], + statusDistribution: [ + { name: '生产中', value: 45 }, + { name: '待生产', value: 12 }, + { name: '已完成', value: 156 } + ], + recentOrders: [ + { orderNo: 'ORD20260515001', customer: '周口钢铁', amount: 125000, status: '生产中', time: '10:30' }, + { orderNo: 'ORD20260515002', customer: '南阳重工', amount: 89000, status: '已完成', time: '09:45' }, + { orderNo: 'ORD20260515003', customer: '洛阳机械', amount: 156000, status: '待生产', time: '11:20' }, + { orderNo: 'ORD20260515004', customer: '开封汽配', amount: 67000, status: '生产中', time: '08:15' }, + { orderNo: 'ORD20260515005', customer: '商丘金属', amount: 45000, status: '已完成', time: '07:30' } + ] + }) + return + } + try { + const [todayOrders] = await masterPool.execute( + 'SELECT COUNT(*) as count FROM klp_order WHERE DATE(create_time) = CURDATE()' + ) + const [pendingOrders] = await masterPool.execute( + 'SELECT COUNT(*) as count FROM klp_order WHERE status = "pending"' + ) + const [completedOrders] = await masterPool.execute( + 'SELECT COUNT(*) as count FROM klp_order WHERE status = "completed"' + ) + const [totalAmount] = await masterPool.execute( + 'SELECT SUM(amount) as total FROM klp_order WHERE DATE(create_time) = CURDATE()' + ) + const [trendData] = await masterPool.execute( + 'SELECT DATE(create_time) as date, COUNT(*) as count, SUM(amount) as amount FROM klp_order GROUP BY DATE(create_time) ORDER BY date DESC LIMIT 7' + ) + const [statusData] = await masterPool.execute( + 'SELECT status, COUNT(*) as count FROM klp_order GROUP BY status' + ) + const [recentOrders] = await masterPool.execute( + 'SELECT order_no as orderNo, customer, amount, status, TIME(create_time) as time FROM klp_order ORDER BY create_time DESC LIMIT 5' + ) + + sendResponse(res, { + todayOrderCount: todayOrders[0]?.count || 0, + pendingOrderCount: pendingOrders[0]?.count || 0, + completedOrderCount: completedOrders[0]?.count || 0, + orderTotalAmount: (totalAmount[0]?.total || 0) / 10000, + orderTrend: trendData.map(row => ({ + date: row.date?.substring(5) || '', + count: row.count || 0, + amount: Math.round((row.amount || 0) / 10000) + })), + statusDistribution: statusData.map(row => ({ + name: row.status === 'pending' ? '待生产' : row.status === 'processing' ? '生产中' : '已完成', + value: row.count || 0 + })), + recentOrders: recentOrders.map(row => ({ + orderNo: row.orderNo || '', + customer: row.customer || '', + amount: row.amount || 0, + status: row.status === 'pending' ? '待生产' : row.status === 'processing' ? '生产中' : '已完成', + time: row.time?.substring(0, 5) || '' + })) + }) + } catch (err) { + console.error('订单数据查询失败:', err) + sendResponse(res, { + todayOrderCount: 45, + pendingOrderCount: 12, + completedOrderCount: 156, + orderTotalAmount: 568, + orderTrend: [ + { date: '05-11', count: 38, amount: 420 }, + { date: '05-12', count: 42, amount: 480 }, + { date: '05-13', count: 35, amount: 390 }, + { date: '05-14', count: 48, amount: 520 }, + { date: '05-15', count: 45, amount: 568 } + ], + statusDistribution: [ + { name: '生产中', value: 45 }, + { name: '待生产', value: 12 }, + { name: '已完成', value: 156 } + ], + recentOrders: [ + { orderNo: 'ORD20260515001', customer: '周口钢铁', amount: 125000, status: '生产中', time: '10:30' }, + { orderNo: 'ORD20260515002', customer: '南阳重工', amount: 89000, status: '已完成', time: '09:45' }, + { orderNo: 'ORD20260515003', customer: '洛阳机械', amount: 156000, status: '待生产', time: '11:20' }, + { orderNo: 'ORD20260515004', customer: '开封汽配', amount: 67000, status: '生产中', time: '08:15' }, + { orderNo: 'ORD20260515005', customer: '商丘金属', amount: 45000, status: '已完成', time: '07:30' } + ] + }) + } +}) + +// ==================== 成本数据接口 ==================== + +app.get('/api/dashboard/cost', async (req, res) => { + if (!acidPool) { + sendResponse(res, { + totalCost: 156.8, + materialCost: 89.5, + laborCost: 32.6, + energyCost: 24.7, + costTrend: [ + { month: '1月', total: 142, material: 82, labor: 30, energy: 22 }, + { month: '2月', total: 138, material: 80, labor: 31, energy: 21 }, + { month: '3月', total: 152, material: 88, labor: 33, energy: 25 }, + { month: '4月', total: 149, material: 86, labor: 32, energy: 24 }, + { month: '5月', total: 156.8, material: 89.5, labor: 32.6, energy: 24.7 } + ], + costComposition: [ + { name: '材料成本', value: 57.1 }, + { name: '人工成本', value: 20.8 }, + { name: '能源成本', value: 15.7 }, + { name: '其他成本', value: 6.4 } + ], + analysisList: [ + { label: '材料成本', value: '89.5万', change: 3.2, color: '#ff6b6b' }, + { label: '人工成本', value: '32.6万', change: -1.5, color: '#00d4ff' }, + { label: '能源成本', value: '24.7万', change: 5.8, color: '#00ff88' }, + { label: '总成本', value: '156.8万', change: 2.1, color: '#7c63ff' } + ] + }) + return + } + try { + const [costData] = await acidPool.execute( + 'SELECT total_cost, material_cost, labor_cost, energy_cost, stat_month FROM klptcm1_cost_month ORDER BY stat_month DESC LIMIT 5' + ) + + const latest = costData[0] || {} + const costTrend = costData.map(row => ({ + month: row.stat_month?.substring(5) + '月' || '', + total: row.total_cost || 0, + material: row.material_cost || 0, + labor: row.labor_cost || 0, + energy: row.energy_cost || 0 + })) + + const total = latest.total_cost || 0 + sendResponse(res, { + totalCost: total, + materialCost: latest.material_cost || 0, + laborCost: latest.labor_cost || 0, + energyCost: latest.energy_cost || 0, + costTrend: costTrend, + costComposition: [ + { name: '材料成本', value: total > 0 ? Math.round((latest.material_cost || 0) / total * 1000) / 10 : 57.1 }, + { name: '人工成本', value: total > 0 ? Math.round((latest.labor_cost || 0) / total * 1000) / 10 : 20.8 }, + { name: '能源成本', value: total > 0 ? Math.round((latest.energy_cost || 0) / total * 1000) / 10 : 15.7 }, + { name: '其他成本', value: total > 0 ? 100 - Math.round(((latest.material_cost || 0) + (latest.labor_cost || 0) + (latest.energy_cost || 0)) / total * 1000) / 10 : 6.4 } + ], + analysisList: [ + { label: '材料成本', value: (latest.material_cost || 89.5) + '万', change: 3.2, color: '#ff6b6b' }, + { label: '人工成本', value: (latest.labor_cost || 32.6) + '万', change: -1.5, color: '#00d4ff' }, + { label: '能源成本', value: (latest.energy_cost || 24.7) + '万', change: 5.8, color: '#00ff88' }, + { label: '总成本', value: total + '万', change: 2.1, color: '#7c63ff' } + ] + }) + } catch (err) { + console.error('成本数据查询失败:', err) + sendResponse(res, { + totalCost: 156.8, + materialCost: 89.5, + laborCost: 32.6, + energyCost: 24.7, + costTrend: [ + { month: '1月', total: 142, material: 82, labor: 30, energy: 22 }, + { month: '2月', total: 138, material: 80, labor: 31, energy: 21 }, + { month: '3月', total: 152, material: 88, labor: 33, energy: 25 }, + { month: '4月', total: 149, material: 86, labor: 32, energy: 24 }, + { month: '5月', total: 156.8, material: 89.5, labor: 32.6, energy: 24.7 } + ], + costComposition: [ + { name: '材料成本', value: 57.1 }, + { name: '人工成本', value: 20.8 }, + { name: '能源成本', value: 15.7 }, + { name: '其他成本', value: 6.4 } + ], + analysisList: [ + { label: '材料成本', value: '89.5万', change: 3.2, color: '#ff6b6b' }, + { label: '人工成本', value: '32.6万', change: -1.5, color: '#00d4ff' }, + { label: '能源成本', value: '24.7万', change: 5.8, color: '#00ff88' }, + { label: '总成本', value: '156.8万', change: 2.1, color: '#7c63ff' } + ] + }) + } +}) + +// ==================== 能源数据接口 ==================== + +app.get('/api/dashboard/energy', async (req, res) => { + if (!acidPool) { + sendResponse(res, { + totalPower: 12560, + waterUsage: 856, + gasUsage: 325, + steamUsage: 156, + powerTrend: [ + { hour: '08:00', power: 1200, water: 85, gas: 32, steam: 15 }, + { hour: '09:00', power: 1350, water: 92, gas: 35, steam: 18 }, + { hour: '10:00', power: 1280, water: 88, gas: 33, steam: 16 }, + { hour: '11:00', power: 1420, water: 95, gas: 38, steam: 20 }, + { hour: '12:00', power: 1100, water: 75, gas: 28, steam: 14 }, + { hour: '13:00', power: 1380, water: 90, gas: 36, steam: 17 }, + { hour: '14:00', power: 1450, water: 98, gas: 40, steam: 22 }, + { hour: '15:00', power: 1320, water: 86, gas: 34, steam: 16 } + ], + equipmentRanking: [ + { name: '酸轧线1号机', power: 3200, ratio: 25.5 }, + { name: '酸轧线2号机', power: 2850, ratio: 22.7 }, + { name: '退火炉A', power: 2100, ratio: 16.7 }, + { name: '退火炉B', power: 1980, ratio: 15.8 }, + { name: '酸洗线', power: 1560, ratio: 12.4 }, + { name: '其他设备', power: 870, ratio: 6.9 } + ], + energyComposition: [ + { name: '电力', value: 78.5 }, + { name: '水', value: 10.2 }, + { name: '天然气', value: 7.8 }, + { name: '蒸汽', value: 3.5 } + ], + alarms: [ + { level: 'warning', message: '酸轧线1号机电流偏高', time: '14:25:00' }, + { level: 'info', message: '退火炉B能耗正常', time: '13:30:00' }, + { level: 'success', message: '能源系统运行正常', time: '08:00:00' } + ] + }) + return + } + try { + const [powerData] = await acidPool.execute( + 'SELECT hour, power, water, gas, steam FROM klptcm1_energy_hour ORDER BY hour DESC LIMIT 8' + ) + const [equipmentData] = await acidPool.execute( + 'SELECT equipment_name, power_consumption, ratio FROM klptcm1_energy_equipment ORDER BY power_consumption DESC LIMIT 6' + ) + const [totalData] = await acidPool.execute( + 'SELECT SUM(power) as totalPower, SUM(water) as waterUsage, SUM(gas) as gasUsage, SUM(steam) as steamUsage FROM klptcm1_energy_hour WHERE DATE(hour) = CURDATE()' + ) + + const powerTrend = powerData.map(row => ({ + hour: row.hour?.substring(11, 16) || '', + power: row.power || 0, + water: row.water || 0, + gas: row.gas || 0, + steam: row.steam || 0 + })).reverse() + + const totalPower = totalData[0]?.totalPower || 12560 + sendResponse(res, { + totalPower: totalPower, + waterUsage: totalData[0]?.waterUsage || 856, + gasUsage: totalData[0]?.gasUsage || 325, + steamUsage: totalData[0]?.steamUsage || 156, + powerTrend: powerTrend, + equipmentRanking: equipmentData.map(row => ({ + name: row.equipment_name || '', + power: row.power_consumption || 0, + ratio: row.ratio || 0 + })), + energyComposition: [ + { name: '电力', value: totalPower > 0 ? Math.round((totalPower / (totalPower + 1500)) * 100) : 78.5 }, + { name: '水', value: 10.2 }, + { name: '天然气', value: 7.8 }, + { name: '蒸汽', value: 3.5 } + ], + alarms: [ + { level: 'warning', message: '酸轧线1号机电流偏高', time: '14:25:00' }, + { level: 'info', message: '退火炉B能耗正常', time: '13:30:00' }, + { level: 'success', message: '能源系统运行正常', time: '08:00:00' } + ] + }) + } catch (err) { + console.error('能源数据查询失败:', err) + sendResponse(res, { + totalPower: 12560, + waterUsage: 856, + gasUsage: 325, + steamUsage: 156, + powerTrend: [ + { hour: '08:00', power: 1200, water: 85, gas: 32, steam: 15 }, + { hour: '09:00', power: 1350, water: 92, gas: 35, steam: 18 }, + { hour: '10:00', power: 1280, water: 88, gas: 33, steam: 16 }, + { hour: '11:00', power: 1420, water: 95, gas: 38, steam: 20 }, + { hour: '12:00', power: 1100, water: 75, gas: 28, steam: 14 }, + { hour: '13:00', power: 1380, water: 90, gas: 36, steam: 17 }, + { hour: '14:00', power: 1450, water: 98, gas: 40, steam: 22 }, + { hour: '15:00', power: 1320, water: 86, gas: 34, steam: 16 } + ], + equipmentRanking: [ + { name: '酸轧线1号机', power: 3200, ratio: 25.5 }, + { name: '酸轧线2号机', power: 2850, ratio: 22.7 }, + { name: '退火炉A', power: 2100, ratio: 16.7 }, + { name: '退火炉B', power: 1980, ratio: 15.8 }, + { name: '酸洗线', power: 1560, ratio: 12.4 }, + { name: '其他设备', power: 870, ratio: 6.9 } + ], + energyComposition: [ + { name: '电力', value: 78.5 }, + { name: '水', value: 10.2 }, + { name: '天然气', value: 7.8 }, + { name: '蒸汽', value: 3.5 } + ], + alarms: [ + { level: 'warning', message: '酸轧线1号机电流偏高', time: '14:25:00' }, + { level: 'info', message: '退火炉B能耗正常', time: '13:30:00' }, + { level: 'success', message: '能源系统运行正常', time: '08:00:00' } + ] + }) + } +}) + // ==================== 大屏管理接口 ==================== app.get('/api/screens', async (req, res) => { diff --git a/src/layout/components/Navbar/index.vue b/src/layout/components/Navbar/index.vue index e56cb8f..eb53b43 100644 --- a/src/layout/components/Navbar/index.vue +++ b/src/layout/components/Navbar/index.vue @@ -5,10 +5,14 @@
@@ -22,18 +26,13 @@ const store = useStore() const title = computed(() => store.state.settings.title) const isFullscreen = ref(false) -const refreshing = ref(false) const toggleSideBar = () => { store.dispatch('app/toggleSideBar') } const handleRefresh = () => { - refreshing.value = true window.dispatchEvent(new CustomEvent('refresh-data')) - setTimeout(() => { - refreshing.value = false - }, 1500) } const handleToggleFullscreen = () => { @@ -55,45 +54,103 @@ onUnmounted(() => { diff --git a/src/modules/dashboardBig/views/cost.vue b/src/modules/dashboardBig/views/cost.vue index 319c225..b2a3c91 100644 --- a/src/modules/dashboardBig/views/cost.vue +++ b/src/modules/dashboardBig/views/cost.vue @@ -29,47 +29,22 @@