Files
screen/server/app.js

571 lines
23 KiB
JavaScript
Raw 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 {
const [shiftRows] = await acidPool.execute(
'SELECT * FROM klptcm1_shift_current ORDER BY create_time DESC LIMIT 1'
)
const [coilRows] = await acidPool.execute(
'SELECT COUNT(*) as count, SUM(weight) as totalWeight FROM klptcm1_pdo_excoil WHERE DATE(create_time) = CURDATE()'
)
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'
)
const latest = shiftRows[0] || {}
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,
targetOutput: 15000,
efficiency: 92.0,
trendingData: mockOverview.trendingData,
lossData: lossRows.length > 0 ? lossRows : mockOverview.lossData,
teamRanking: mockOverview.teamRanking,
alarms: mockOverview.alarms
}
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/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' }
]
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 }
], 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 转发请求)`)
})
})