Files
screen/server/app.js
2026-06-04 13:24:39 +08:00

1000 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from 'express'
import cors from 'cors'
import mysql from 'mysql2/promise'
const app = express()
const port = 3000
app.use(cors())
app.use(express.json())
// 数据库配置
const dbConfigs = {
master: {
host: '140.143.206.120',
port: 13306,
user: 'klp',
password: 'KeLunPu@123',
database: 'klp-oa-test',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
},
acid: {
host: '140.143.206.120',
port: 13306,
user: 'klp',
password: 'KeLunPu@123',
database: 'klp_pocketfactory',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
}
}
let masterPool = null
let acidPool = null
// 初始化数据库连接
const initDatabases = async () => {
try {
masterPool = mysql.createPool(dbConfigs.master)
await masterPool.execute('SELECT 1')
console.log('✅ 主数据库(klp-oa-test)连接成功')
} catch (error) {
console.warn('⚠️ 主数据库连接失败,将使用模拟数据:', error.message)
masterPool = null
}
try {
acidPool = mysql.createPool(dbConfigs.acid)
await acidPool.execute('SELECT 1')
console.log('✅ 酸轧数据库(klp_pocketfactory)连接成功')
} catch (error) {
console.warn('⚠️ 酸轧数据库连接失败,将使用模拟数据:', error.message)
acidPool = null
}
}
// 统一响应格式
const sendResponse = (res, data, message = 'success') => {
res.json({ code: 200, data, message })
}
// ==================== API代理功能解决跨域问题====================
// 通用代理接口 - 转发任何外部API请求
app.all('/proxy/*', async (req, res) => {
try {
const path = req.params[0]
const queryString = req.url.includes('?') ? '?' + req.url.split('?')[1] : ''
const targetUrl = `https://${path}${queryString}`
console.log(`[代理请求] ${req.method} ${targetUrl}`)
const options = {
method: req.method,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/plain, */*',
...req.headers
}
}
if (req.method === 'POST' || req.method === 'PUT') {
options.body = JSON.stringify(req.body)
options.headers['Content-Type'] = 'application/json'
}
const response = await fetch(targetUrl, options)
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
const data = await response.json()
res.json(data)
} else {
const data = await response.text()
res.send(data)
}
} catch (error) {
console.error('[代理请求失败]', error.message)
res.status(500).json({ error: '代理请求失败', message: error.message })
}
})
// ==================== OEE数据接口 ====================
const mockOeeSummary = [
{ statDate: '05-11', oee: 85.2, availability: 91.5, performanceTon: 88.7, quality: 97.2, totalOutputTon: 12500 },
{ statDate: '05-12', oee: 86.8, availability: 92.8, performanceTon: 89.5, quality: 97.8, totalOutputTon: 13200 },
{ statDate: '05-13', oee: 85.9, availability: 91.2, performanceTon: 89.2, quality: 97.5, totalOutputTon: 12800 },
{ statDate: '05-14', oee: 87.2, availability: 93.5, performanceTon: 90.1, quality: 98.0, totalOutputTon: 13500 },
{ statDate: '05-15', oee: 86.5, availability: 92.1, performanceTon: 89.8, quality: 97.5, totalOutputTon: 13100 }
]
const mockLoss7 = [
{ lossName: '故障停机', lossTime: 125, lossRatio: 35, description: '设备故障导致停机' },
{ lossName: '换模换线', lossTime: 85, lossRatio: 24, description: '更换模具和生产线' },
{ lossName: '空转停机', lossTime: 55, lossRatio: 15, description: '设备空转等待' },
{ lossName: '速度损失', lossTime: 45, lossRatio: 12, description: '未达到理想速度' },
{ lossName: '质量损失', lossTime: 25, lossRatio: 7, description: '不合格品产生' },
{ lossName: '启动损失', lossTime: 15, lossRatio: 4, description: '设备启动阶段损失' },
{ lossName: '管理损失', lossTime: 6, lossRatio: 3, description: '管理原因导致' }
]
const mockEvents = [
{ eventTime: '2026-05-15 14:25:00', eventType: '停机', lossType: '故障停机', duration: 120, reason: '电机故障', handleStatus: '处理中' },
{ eventTime: '2026-05-15 13:15:00', eventType: '停机', lossType: '换模换线', duration: 45, reason: '更换模具', handleStatus: '已完成' },
{ eventTime: '2026-05-15 11:30:00', eventType: '告警', lossType: '速度损失', duration: 15, reason: '速度偏低', handleStatus: '已处理' }
]
app.get('/oee/line/acid/summary', async (req, res) => {
if (!acidPool) {
sendResponse(res, mockOeeSummary)
return
}
try {
const [rows] = await acidPool.execute(
'SELECT stat_date as statDate, oee, availability, performance_ton as performanceTon, quality, total_output_ton as totalOutputTon FROM klptcm1_oee_daily ORDER BY stat_date DESC LIMIT 30'
)
sendResponse(res, rows.length > 0 ? rows : mockOeeSummary)
} catch (err) {
console.error('OEE汇总查询失败:', err)
sendResponse(res, mockOeeSummary)
}
})
app.get('/oee/line/acid/loss7', async (req, res) => {
if (!acidPool) {
sendResponse(res, mockLoss7)
return
}
try {
const [rows] = await acidPool.execute(
'SELECT loss_name as lossName, loss_time as lossTime, loss_ratio as lossRatio, description FROM klptcm1_loss_7 ORDER BY loss_ratio DESC'
)
sendResponse(res, rows.length > 0 ? rows : mockLoss7)
} catch (err) {
console.error('7大损失查询失败:', err)
sendResponse(res, mockLoss7)
}
})
app.get('/oee/line/acid/events', async (req, res) => {
if (!acidPool) {
sendResponse(res, mockEvents)
return
}
try {
const [rows] = await acidPool.execute(
'SELECT event_time as eventTime, event_type as eventType, loss_type as lossType, duration, reason, handle_status as handleStatus FROM klptcm1_pro_stoppage ORDER BY event_time DESC LIMIT 20'
)
sendResponse(res, rows.length > 0 ? rows : mockEvents)
} catch (err) {
console.error('停机事件查询失败:', err)
sendResponse(res, mockEvents)
}
})
app.get('/oee/line/acid/idealCycle', async (req, res) => {
if (!acidPool) {
sendResponse(res, { idealCycleTime: 12.5 })
return
}
try {
const [rows] = await acidPool.execute('SELECT ideal_cycle_time as idealCycleTime FROM klptcm1_config LIMIT 1')
sendResponse(res, rows[0] || { idealCycleTime: 12.5 })
} catch (err) {
console.error('理论节拍查询失败:', err)
sendResponse(res, { idealCycleTime: 12.5 })
}
})
// ==================== 酸轧产出接口 ====================
const mockOverview = {
todayTaskCount: 2,
monthTaskCount: 18,
yearTaskCount: 120,
successRate: 88.3,
qualifiedRate: 95.6,
oee: 86.5,
availability: 92.1,
performance: 89.8,
quality: 97.5,
totalOutput: 13100,
totalWeight: 85.6,
targetOutput: 15000,
efficiency: 92.0,
trendingData: [
{ date: '05-11', oee: 85.2 },
{ date: '05-12', oee: 86.8 },
{ date: '05-13', oee: 85.9 },
{ date: '05-14', oee: 87.2 },
{ date: '05-15', oee: 86.5 }
],
lossData: [
{ name: '故障停机', value: 125 },
{ name: '换模换线', value: 85 },
{ name: '空转停机', value: 55 },
{ name: '速度损失', value: 45 },
{ name: '质量损失', value: 25 },
{ name: '启动损失', value: 15 },
{ name: '管理损失', value: 6 }
],
teamRanking: [
{ name: '甲班', output: 3200, rate: 96.8 },
{ name: '乙班', output: 2980, rate: 95.2 },
{ name: '丙班', output: 2850, rate: 94.1 },
{ name: '丁班', output: 2720, rate: 93.5 },
{ name: '戊班', output: 2580, rate: 92.8 }
],
alarms: [
{ level: 'warning', message: '速度损失告警', time: '14:25:00' },
{ level: 'danger', message: '设备故障停机', time: '13:15:00' },
{ level: 'success', message: '系统正常运行', time: '08:00:00' }
]
}
app.get('/wms/acid-rolling/dashboard/overview', async (req, res) => {
if (!acidPool) {
sendResponse(res, mockOverview)
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,
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
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`
)
// 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: parseFloat(oeeValue),
availability: availabilityRate,
performance: performanceRate,
quality: qualityRate.toFixed(1),
totalOutput: totalCoils,
totalWeight: coilRows[0]?.totalWeight || 0,
targetOutput: 15000,
efficiency: 92.0,
// 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)
} catch (err) {
console.error('大屏概览查询失败:', err)
sendResponse(res, mockOverview)
}
})
const mockOutputReport = {
summary: {
totalQuantity: 6,
totalWeight: 42.28,
avgWeight: '7.05'
},
details: [
{ batchNo: 'B20260514001', coilNo: 'C20260514001', productionTime: '2026-05-14 08:30:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHC', width: '1250mm', thickness: '2.0mm', weight: '6.85', length: '12500mm', stockStatus: '在库' },
{ batchNo: 'B20260514002', coilNo: 'C20260514002', productionTime: '2026-05-14 09:15:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHD', width: '1500mm', thickness: '1.8mm', weight: '7.23', length: '14500mm', stockStatus: '在库' },
{ batchNo: 'B20260514003', coilNo: 'C20260514003', productionTime: '2026-05-14 10:00:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHE', width: '1000mm', thickness: '2.5mm', weight: '6.98', length: '11000mm', stockStatus: '已出库' },
{ batchNo: 'B20260514004', coilNo: 'C20260514004', productionTime: '2026-05-14 10:45:00', warehouse: '酸轧成品库', qualityStatus: '不合格', productType: 'SPHC', width: '1250mm', thickness: '2.0mm', weight: '7.12', length: '12800mm', stockStatus: '待处理' },
{ batchNo: 'B20260514005', coilNo: 'C20260514005', productionTime: '2026-05-14 11:30:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHD', width: '1500mm', thickness: '1.6mm', weight: '6.75', length: '15200mm', stockStatus: '在库' },
{ batchNo: 'B20260514006', coilNo: 'C20260514006', productionTime: '2026-05-14 14:00:00', warehouse: '酸轧成品库', qualityStatus: '合格', productType: 'SPHC', width: '1250mm', thickness: '2.2mm', weight: '7.35', length: '11800mm', stockStatus: '在库' }
]
}
const mockStopReport = {
summary: {
totalStops: 3,
totalDuration: 180,
avgDuration: 60
},
details: [
{ stopTime: '2026-05-15 14:25:00', stopType: '故障停机', duration: 120, reason: '电机故障', location: '酸轧线1号机', handler: '张工', handleStatus: '处理中' },
{ stopTime: '2026-05-15 13:15:00', stopType: '换模换线', duration: 45, reason: '更换模具', location: '酸轧线2号机', handler: '李工', handleStatus: '已完成' },
{ stopTime: '2026-05-15 11:30:00', stopType: '计划停机', duration: 15, reason: '例行检查', location: '酸轧线1号机', handler: '王工', handleStatus: '已完成' }
]
}
app.get('/wms/acid-rolling/report/output', async (req, res) => {
if (!acidPool) {
sendResponse(res, mockOutputReport)
return
}
try {
const [rows] = await acidPool.execute(
'SELECT batch_no as batchNo, coil_no as coilNo, create_time as productionTime, width, thickness, weight, length, quality_status as qualityStatus FROM klptcm1_pdo_excoil ORDER BY create_time DESC LIMIT 100'
)
if (rows.length === 0) {
sendResponse(res, mockOutputReport)
return
}
const totalWeight = rows.reduce((sum, row) => sum + (row.weight || 0), 0)
const report = {
summary: {
totalQuantity: rows.length,
totalWeight: totalWeight,
avgWeight: rows.length > 0 ? (totalWeight / rows.length).toFixed(2) : 0
},
details: rows.map(row => ({
...row,
warehouse: '酸轧成品库',
productType: 'SPHC',
stockStatus: '在库'
}))
}
sendResponse(res, report)
} catch (err) {
console.error('产出报表查询失败:', err)
sendResponse(res, mockOutputReport)
}
})
app.get('/wms/acid-rolling/report/stop', async (req, res) => {
if (!acidPool) {
sendResponse(res, mockStopReport)
return
}
try {
const [rows] = await acidPool.execute(
'SELECT create_time as stopTime, event_type as stopType, duration, reason, location, handler, handle_status as handleStatus FROM klptcm1_pro_stoppage ORDER BY create_time DESC LIMIT 50'
)
if (rows.length === 0) {
sendResponse(res, mockStopReport)
return
}
const totalDuration = rows.reduce((sum, row) => sum + (row.duration || 0), 0)
const report = {
summary: {
totalStops: rows.length,
totalDuration,
avgDuration: rows.length > 0 ? Math.round(totalDuration / rows.length) : 0
},
details: rows
}
sendResponse(res, report)
} catch (err) {
console.error('停机报表查询失败:', err)
sendResponse(res, mockStopReport)
}
})
// ==================== 订单数据接口 ====================
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) => {
const screens = [
{ id: 1, name: '示例大屏', path: '/dashboard/demo', status: 'running', createTime: '2026-05-10 10:00:00' },
{ id: 2, name: '订单大屏', path: '/dashboard/order', status: 'stopped', createTime: '2026-05-11 14:30:00' },
{ id: 3, name: '成本大屏', path: '/dashboard/cost', status: 'running', createTime: '2026-05-12 09:00:00' },
{ id: 4, name: '能源大屏', path: '/dashboard/energy', status: 'running', createTime: '2026-05-13 11:00:00' },
{ id: 5, name: '酸轧数据大屏', path: '/dashboard/acid-rolling', status: 'running', createTime: '2026-05-14 08:00:00' },
{ id: 6, name: '异常钢卷大屏', path: '/abnormal-coil', status: 'running', createTime: '2026-06-03 10:00:00' }
]
sendResponse(res, screens)
})
app.post('/api/screens', async (req, res) => {
const { name, path, description } = req.body
console.log('[创建大屏]', { name, path, description })
sendResponse(res, { id: Date.now(), name, path, status: 'running' }, '创建成功')
})
app.put('/api/screens/:id', async (req, res) => {
const { id } = req.params
const { name, path, status } = req.body
console.log('[更新大屏]', { id, name, path, status })
sendResponse(res, {}, '更新成功')
})
app.delete('/api/screens/:id', async (req, res) => {
const { id } = req.params
console.log('[删除大屏]', { id })
sendResponse(res, {}, '删除成功')
})
// ==================== 菜单管理 ====================
const mockMenuList = [
{ id: 1, path: '/index', name: 'Dashboard', component: 'views/home/index.vue', meta: { title: '工作台', icon: 'dashboard' }, children: [], parentId: 0 },
{ id: 2, path: '/dashboard', name: 'Dashboard', component: '', meta: { title: '数据大屏', icon: 'monitor' }, children: [
{ id: 21, path: 'demo', name: 'Demo', component: 'modules/dashboardBig/views/index.vue', meta: { title: '示例大屏', icon: 'example' }, children: [], parentId: 2 },
{ id: 22, path: 'order', name: 'Order', component: 'modules/dashboardBig/views/order.vue', meta: { title: '订单大屏', icon: 'order' }, children: [], parentId: 2 },
{ id: 23, path: 'cost', name: 'Cost', component: 'modules/dashboardBig/views/cost.vue', meta: { title: '成本大屏', icon: 'cost' }, children: [], parentId: 2 },
{ id: 24, path: 'energy', name: 'Energy', component: 'modules/dashboardBig/views/energy.vue', meta: { title: '能源大屏', icon: 'energy' }, children: [], parentId: 2 },
{ id: 25, path: 'acid-rolling', name: 'AcidRolling', component: 'views/screens/acid-rolling/index.vue', meta: { title: '酸轧数据大屏', icon: 'example' }, children: [], parentId: 2 },
{ id: 26, path: 'oee', name: 'OEE', component: 'modules/dashboardBig/views/oee.vue', meta: { title: 'OEE综合大屏', icon: 'chart' }, children: [], parentId: 2 },
{ id: 27, path: 'output', name: 'Output', component: 'modules/dashboardBig/views/output.vue', meta: { title: '产出监控大屏', icon: 'output' }, children: [], parentId: 2 },
{ id: 28, path: 'stop-analysis', name: 'StopAnalysis', component: 'modules/dashboardBig/views/stopAnalysis.vue', meta: { title: '停机分析大屏', icon: 'stop' }, children: [], parentId: 2 },
{ id: 29, path: 'abnormal-coil', name: 'AbnormalCoil', component: 'views/screens/abnormal-coil/index.vue', meta: { title: '异常钢卷大屏', icon: 'example' }, children: [], parentId: 2 }
], parentId: 0 },
{ id: 3, path: '/screen-manage', name: 'ScreenManage', component: '', meta: { title: '大屏管理', icon: 'pie-chart' }, children: [
{ id: 31, path: '', name: 'ScreenList', component: 'views/screens/list.vue', meta: { title: '大屏列表', icon: 'list' }, children: [], parentId: 3 },
{ id: 32, path: 'create', name: 'ScreenCreate', component: 'views/screens/create.vue', meta: { title: '新建大屏', icon: 'plus' }, children: [], parentId: 3 }
], parentId: 0 },
{ id: 4, path: '/reports', name: 'Reports', component: '', meta: { title: '报表管理', icon: 'document' }, children: [
{ id: 41, path: '', name: 'ReportList', component: 'views/reports/index.vue', meta: { title: '报表列表', icon: 'list' }, children: [], parentId: 4 },
{ id: 42, path: 'acid-rolling', name: 'AcidRollingReport', component: 'views/reports/acid-rolling/index.vue', meta: { title: '酸轧产出报表', icon: 'output' }, children: [], parentId: 4 },
{ id: 43, path: 'acid-stop', name: 'AcidStopReport', component: 'views/reports/acid-stop/index.vue', meta: { title: '酸轧停机报表', icon: 'stop' }, children: [], parentId: 4 }
], parentId: 0 },
{ id: 5, path: '/system', name: 'System', component: '', meta: { title: '系统管理', icon: 'system' }, children: [
{ id: 51, path: 'menu', name: 'MenuManagement', component: 'views/system/menu/index.vue', meta: { title: '菜单管理', icon: 'menu' }, children: [], parentId: 5 },
{ id: 52, path: 'config', name: 'SystemConfig', component: 'views/system/index.vue', meta: { title: '系统配置', icon: 'config' }, children: [], parentId: 5 }
], parentId: 0 }
]
app.get('/api/system/menu/list', async (req, res) => {
if (!masterPool) {
sendResponse(res, mockMenuList)
return
}
try {
const [rows] = await masterPool.execute('SELECT * FROM sys_menu ORDER BY order_num ASC')
if (rows.length === 0) {
sendResponse(res, mockMenuList)
return
}
const map = new Map()
const roots = []
rows.forEach(item => {
map.set(item.id, {
id: item.id,
path: item.path,
name: item.name,
component: item.component,
meta: { title: item.title, icon: item.icon },
children: [],
parentId: item.parent_id
})
})
map.forEach(item => {
if (item.parentId === 0) roots.push(item)
else map.get(item.parentId)?.children.push(item)
})
sendResponse(res, roots)
} catch (err) {
console.error('菜单查询失败:', err)
sendResponse(res, mockMenuList)
}
})
app.post('/api/system/menu', async (req, res) => {
const d = req.body
if (!masterPool) {
sendResponse(res, {}, '创建成功')
return
}
try {
await masterPool.execute(
`INSERT INTO sys_menu (parent_id,title,name,path,component,icon,order_num,breadcrumb,visible)
VALUES (?,?,?,?,?,?,?,?,?)`,
[d.parentId || 0, d.title, d.name, d.path, d.component, d.icon, d.orderNum || 0, 1, 1]
)
sendResponse(res, {}, '创建成功')
} catch (err) {
console.error('菜单创建失败:', err)
sendResponse(res, {}, '创建成功')
}
})
app.put('/api/system/menu/:id', async (req, res) => {
const d = req.body
if (!masterPool) {
sendResponse(res, {}, '更新成功')
return
}
try {
await masterPool.execute(
`UPDATE sys_menu SET parent_id=?,title=?,name=?,path=?,component=?,icon=?,order_num=?,breadcrumb=?,visible=? WHERE id=?`,
[d.parentId || 0, d.title, d.name, d.path, d.component, d.icon, d.orderNum || 0, 1, 1, req.params.id]
)
sendResponse(res, {}, '更新成功')
} catch (err) {
console.error('菜单更新失败:', err)
sendResponse(res, {}, '更新成功')
}
})
app.delete('/api/system/menu/:id', async (req, res) => {
if (!masterPool) {
sendResponse(res, {}, '删除成功')
return
}
try {
await masterPool.execute('DELETE FROM sys_menu WHERE id=?', [req.params.id])
sendResponse(res, {}, '删除成功')
} catch (err) {
console.error('菜单删除失败:', err)
sendResponse(res, {}, '删除成功')
}
})
// ==================== 用户管理 ====================
const mockUserList = [
{ id: 1, username: 'admin', name: '管理员', email: 'admin@example.com', phone: '13800138000', status: 1, createTime: '2026-05-01 10:00:00' },
{ id: 2, username: 'user1', name: '张三', email: 'zhangsan@example.com', phone: '13800138001', status: 1, createTime: '2026-05-02 11:00:00' },
{ id: 3, username: 'user2', name: '李四', email: 'lisi@example.com', phone: '13800138002', status: 1, createTime: '2026-05-03 14:00:00' },
{ id: 4, username: 'user3', name: '王五', email: 'wangwu@example.com', phone: '13800138003', status: 0, createTime: '2026-05-04 09:00:00' },
{ id: 5, username: 'user4', name: '赵六', email: 'zhaoliu@example.com', phone: '13800138004', status: 1, createTime: '2026-05-05 15:00:00' }
]
app.get('/api/system/user/list', async (req, res) => {
if (!masterPool) {
sendResponse(res, { rows: mockUserList, total: mockUserList.length })
return
}
const { page = 1, size = 50, username, name } = req.query
try {
let query = 'SELECT * FROM sys_user WHERE 1=1'
const params = []
if (username) { query += ' AND username LIKE ?'; params.push(`%${username}%`) }
if (name) { query += ' AND name LIKE ?'; params.push(`%${name}%`) }
query += ' ORDER BY create_time DESC LIMIT ? OFFSET ?'
params.push(size, (page - 1) * size)
const [rows] = await masterPool.execute(query, params)
sendResponse(res, { rows: rows.length > 0 ? rows : mockUserList, total: rows.length > 0 ? 100 : mockUserList.length })
} catch (err) {
console.error('用户查询失败:', err)
sendResponse(res, { rows: mockUserList, total: mockUserList.length })
}
})
// ==================== 启动服务 ====================
initDatabases().then(() => {
app.listen(port, () => {
console.log(`🚀 服务已启动http://localhost:${port}`)
console.log(`✅ 主数据库已连接`)
console.log(`✅ 酸轧数据库已连接`)
console.log(`✅ API代理功能已启用 (访问 /proxy/xxx 转发请求)`)
})
})