feat:修改
This commit is contained in:
453
server/app.js
453
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) => {
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
<span class="navbar-title">{{ title }}</span>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<el-button link icon="Refresh" class="nav-btn" @click="handleRefresh" title="刷新数据">
|
||||
<span>{{ refreshing ? '刷新中...' : '' }}</span>
|
||||
</el-button>
|
||||
<el-button link :icon="isFullscreen ? 'FullScreenExit' : 'FullScreen'" class="nav-btn" @click="handleToggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'"></el-button>
|
||||
<button class="action-btn refresh-btn" @click="handleRefresh" title="刷新数据">
|
||||
<span class="btn-icon">🔄</span>
|
||||
<span class="btn-text">刷新</span>
|
||||
</button>
|
||||
<button class="action-btn fullscreen-btn" @click="handleToggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
|
||||
<span class="btn-icon">{{ isFullscreen ? '⛶' : '⛶' }}</span>
|
||||
<span class="btn-text">{{ isFullscreen ? '退出' : '全屏' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -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(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
height: 60px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.95) 0%, rgba(0, 212, 255, 0.9) 50%, rgba(0, 168, 204, 0.95) 100%);
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
.navbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #0a1428;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border: 2px solid rgba(10, 20, 40, 0.4);
|
||||
border-radius: 8px;
|
||||
background: rgba(10, 20, 40, 0.15);
|
||||
color: #0a1428;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 20, 40, 0.2);
|
||||
background: rgba(10, 20, 40, 0.3);
|
||||
border-color: rgba(10, 20, 40, 0.6);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
.btn-icon {
|
||||
font-size: 20px;
|
||||
animation: spin 2s linear infinite;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
&:hover .btn-icon {
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
.btn-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 212, 255, 0.3);
|
||||
box-shadow: 0 0 20px rgba(0, 212, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,47 +29,22 @@
|
||||
<div class="box-header">成本构成</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">成本对比</div>
|
||||
<div class="compare-list">
|
||||
<div class="compare-item" v-for="item in compareList" :key="item.name">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="bar-container">
|
||||
<div class="bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">成本类型对比</div>
|
||||
<div class="cost-grid">
|
||||
<div class="cost-item" v-for="item in costItems" :key="item.name">
|
||||
<div class="cost-header">
|
||||
<span class="cost-name">{{ item.name }}</span>
|
||||
<span class="cost-percent">{{ item.percent }}%</span>
|
||||
</div>
|
||||
<div class="cost-bar">
|
||||
<div class="cost-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<div class="cost-value">{{ item.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">月度成本对比</div>
|
||||
<div class="box-header">成本对比</div>
|
||||
<div ref="barChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">成本分析</div>
|
||||
<div class="analysis-list">
|
||||
<div class="analysis-item" v-for="item in analysisList" :key="item.label">
|
||||
<div v-for="item in analysisList" :key="item.label" class="analysis-item">
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<span class="value" :style="{ color: item.color }">{{ item.value }}</span>
|
||||
<span class="change" :class="item.change > 0 ? 'up' : 'down'">{{ item.change > 0 ? '↑' : '↓' }} {{ Math.abs(item.change) }}%</span>
|
||||
<span :class="['change', item.change >= 0 ? 'up' : 'down']">
|
||||
{{ item.change >= 0 ? '+' : '' }}{{ item.change }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,6 +57,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const currentTime = ref('')
|
||||
const isFullscreen = ref(false)
|
||||
@@ -95,35 +71,13 @@ let timeInterval = null
|
||||
let resizeObserver = null
|
||||
let fullscreenChangeHandler = null
|
||||
|
||||
const cards = ref([
|
||||
{ title: '总成本', value: '156.8', unit: '万', color: '#ff6b6b' },
|
||||
{ title: '材料成本', value: '98.5', unit: '万', color: '#00d4ff' },
|
||||
{ title: '人工成本', value: '23.4', unit: '万', color: '#7c63ff' },
|
||||
{ title: '能源成本', value: '18.9', unit: '万', color: '#ff9f43' }
|
||||
])
|
||||
const cards = ref([])
|
||||
|
||||
const compareList = ref([
|
||||
{ name: '材料成本', percent: 62.8, value: '98.5万', color: '#00d4ff' },
|
||||
{ name: '人工成本', percent: 14.9, value: '23.4万', color: '#7c63ff' },
|
||||
{ name: '能源成本', percent: 12.1, value: '18.9万', color: '#ff9f43' },
|
||||
{ name: '其他成本', percent: 10.2, value: '16.0万', color: '#ff6b6b' }
|
||||
])
|
||||
const costTrend = ref([])
|
||||
|
||||
const costItems = ref([
|
||||
{ name: '材料成本', percent: 62.8, value: '¥98.5万', color: '#00d4ff' },
|
||||
{ name: '人工成本', percent: 14.9, value: '¥23.4万', color: '#7c63ff' },
|
||||
{ name: '能源成本', percent: 12.1, value: '¥18.9万', color: '#ff9f43' },
|
||||
{ name: '其他成本', percent: 10.2, value: '¥16.0万', color: '#ff6b6b' }
|
||||
])
|
||||
const costComposition = ref([])
|
||||
|
||||
const analysisList = ref([
|
||||
{ label: '材料成本占比', value: '62.8%', change: 2.5, color: '#00d4ff' },
|
||||
{ label: '人工成本占比', value: '14.9%', change: -1.2, color: '#7c63ff' },
|
||||
{ label: '能源成本占比', value: '12.1%', change: 3.8, color: '#ff9f43' },
|
||||
{ label: '成本同比增长', value: '8.5%', change: 5.2, color: '#00ff88' },
|
||||
{ label: '预算执行率', value: '92.3%', change: -1.5, color: '#a0c4e8' },
|
||||
{ label: '成本节约率', value: '5.6%', change: 2.1, color: '#00ff88' }
|
||||
])
|
||||
const analysisList = ref([])
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
@@ -132,41 +86,29 @@ const updateTime = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().then(() => {
|
||||
isFullscreen.value = true
|
||||
}).catch(err => {
|
||||
console.error('全屏请求失败:', err)
|
||||
})
|
||||
} else {
|
||||
document.exitFullscreen().then(() => {
|
||||
isFullscreen.value = false
|
||||
}).catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (trendChartRef.value) {
|
||||
if (trendChart) {
|
||||
trendChart.dispose()
|
||||
}
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: { top: 30, right: 30, bottom: 30, left: 60 },
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
legend: {
|
||||
data: ['总成本', '材料成本', '人工成本', '能源成本'],
|
||||
textStyle: { color: '#a0c4e8' },
|
||||
top: 0
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
data: costTrend.value.map(item => item.month),
|
||||
axisLine: { lineStyle: { color: '#3a5a8a' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
@@ -176,27 +118,55 @@ const initCharts = () => {
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8', formatter: (v) => (v / 10000).toFixed(0) + '万' }
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
series: [{
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1420000, 1380000, 1520000, 1490000, 1568000, 1568000],
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(0, 212, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(0, 212, 255, 0.05)' }
|
||||
])
|
||||
series: [
|
||||
{
|
||||
name: '总成本',
|
||||
type: 'line',
|
||||
data: costTrend.value.map(item => item.total),
|
||||
smooth: true,
|
||||
lineStyle: { color: '#00d4ff', width: 3 },
|
||||
itemStyle: { color: '#00d4ff' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(0, 212, 255, 0.2)' },
|
||||
{ offset: 1, color: 'rgba(0, 212, 255, 0.05)' }
|
||||
])
|
||||
}
|
||||
},
|
||||
lineStyle: { color: '#00d4ff', width: 3 },
|
||||
itemStyle: { color: '#00d4ff' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 8
|
||||
}]
|
||||
{
|
||||
name: '材料成本',
|
||||
type: 'line',
|
||||
data: costTrend.value.map(item => item.material),
|
||||
smooth: true,
|
||||
lineStyle: { color: '#ff6b6b', width: 2 },
|
||||
itemStyle: { color: '#ff6b6b' }
|
||||
},
|
||||
{
|
||||
name: '人工成本',
|
||||
type: 'line',
|
||||
data: costTrend.value.map(item => item.labor),
|
||||
smooth: true,
|
||||
lineStyle: { color: '#00ff88', width: 2 },
|
||||
itemStyle: { color: '#00ff88' }
|
||||
},
|
||||
{
|
||||
name: '能源成本',
|
||||
type: 'line',
|
||||
data: costTrend.value.map(item => item.energy),
|
||||
smooth: true,
|
||||
lineStyle: { color: '#ffd43b', width: 2 },
|
||||
itemStyle: { color: '#ffd43b' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
if (pieChart) {
|
||||
pieChart.dispose()
|
||||
}
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
@@ -206,36 +176,43 @@ const initCharts = () => {
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
center: ['50%', '45%'],
|
||||
data: [
|
||||
{ value: 62.8, name: '材料成本', itemStyle: { color: '#00d4ff' } },
|
||||
{ value: 14.9, name: '人工成本', itemStyle: { color: '#7c63ff' } },
|
||||
{ value: 12.1, name: '能源成本', itemStyle: { color: '#ff9f43' } },
|
||||
{ value: 10.2, name: '其他成本', itemStyle: { color: '#ff6b6b' } }
|
||||
],
|
||||
label: { show: false }
|
||||
data: costComposition.value.map((item, index) => ({
|
||||
value: item.value,
|
||||
name: item.name,
|
||||
itemStyle: { color: ['#ff6b6b', '#00d4ff', '#00ff88', '#7c63ff'][index] }
|
||||
})),
|
||||
label: {
|
||||
show: true,
|
||||
color: '#fff',
|
||||
formatter: '{b}: {d}%'
|
||||
},
|
||||
labelLine: { show: true, lineStyle: { color: '#3a5a8a' } }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (barChartRef.value) {
|
||||
if (barChart) {
|
||||
barChart.dispose()
|
||||
}
|
||||
barChart = echarts.init(barChartRef.value)
|
||||
barChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
grid: { top: 20, right: 20, bottom: 40, left: 60 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
textStyle: { color: '#fff' },
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月'],
|
||||
data: costTrend.value.map(item => item.month),
|
||||
axisLine: { lineStyle: { color: '#3a5a8a' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
@@ -245,18 +222,19 @@ const initCharts = () => {
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8', formatter: (v) => (v / 10000).toFixed(0) + '万' }
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [142, 138, 152, 149, 156.8].map((value, index) => ({
|
||||
value,
|
||||
itemStyle: {
|
||||
color: ['#00d4ff', '#7c63ff', '#00ff88', '#ff9f43', '#ff6b6b'][index],
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
})),
|
||||
barWidth: '50%'
|
||||
data: costTrend.value.map(item => item.total),
|
||||
barWidth: '50%',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#7c63ff' },
|
||||
{ offset: 1, color: '#4a3a99' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
@@ -270,12 +248,60 @@ const handleResize = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/dashboard/cost',
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
const data = response.data
|
||||
|
||||
cards.value = [
|
||||
{ title: '总成本', value: data.totalCost?.toFixed(1) || '0', unit: '万', color: '#00d4ff' },
|
||||
{ title: '材料成本', value: data.materialCost?.toFixed(1) || '0', unit: '万', color: '#ff6b6b' },
|
||||
{ title: '人工成本', value: data.laborCost?.toFixed(1) || '0', unit: '万', color: '#00ff88' },
|
||||
{ title: '能源成本', value: data.energyCost?.toFixed(1) || '0', unit: '万', color: '#ffd43b' }
|
||||
]
|
||||
|
||||
if (data.costTrend) {
|
||||
costTrend.value = data.costTrend
|
||||
}
|
||||
|
||||
if (data.costComposition) {
|
||||
costComposition.value = data.costComposition
|
||||
}
|
||||
|
||||
if (data.analysisList) {
|
||||
analysisList.value = data.analysisList
|
||||
}
|
||||
|
||||
initCharts()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载成本数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
nextTick(() => {
|
||||
initCharts()
|
||||
loadData()
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
@@ -290,17 +316,15 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fullscreenChangeHandler = handleFullscreenChange
|
||||
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
|
||||
|
||||
window.addEventListener('refresh-data', handleRefresh)
|
||||
|
||||
fullscreenChangeHandler = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement
|
||||
}
|
||||
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
|
||||
})
|
||||
|
||||
const handleRefresh = () => {
|
||||
initCharts()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
@@ -338,33 +362,38 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
color: #0a1428;
|
||||
letter-spacing: 3px;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
@@ -378,33 +407,25 @@ onUnmounted(() => {
|
||||
|
||||
.time {
|
||||
font-size: 18px;
|
||||
color: #00d4ff;
|
||||
color: #0a1428;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
.exit-fullscreen-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.5);
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
color: #00d4ff;
|
||||
font-size: 18px;
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-color: rgba(0, 212, 255, 0.5);
|
||||
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
background: rgba(255, 107, 107, 0.3);
|
||||
border-color: #ff6b6b;
|
||||
box-shadow: 0 0 15px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,14 +461,14 @@ onUnmounted(() => {
|
||||
.kpi-value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin: 15px 0 5px;
|
||||
text-shadow: 0 0 15px currentColor;
|
||||
padding: 15px 0 5px;
|
||||
text-shadow: 0 0 20px currentColor;
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
font-size: 14px;
|
||||
color: #a0c4e8;
|
||||
margin-bottom: 15px;
|
||||
color: #8fa8cc;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,187 +476,76 @@ onUnmounted(() => {
|
||||
.chart-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-box {
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.flex-2 {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%);
|
||||
padding: 12px 18px;
|
||||
background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #0a1428;
|
||||
letter-spacing: 2px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 280px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.compare-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.compare-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.name {
|
||||
width: 80px;
|
||||
font-size: 13px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
color: #00d4ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cost-grid {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
.analysis-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
.cost-item {
|
||||
.cost-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.cost-name {
|
||||
font-size: 13px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
|
||||
.cost-percent {
|
||||
font-size: 14px;
|
||||
color: #00d4ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.cost-bar {
|
||||
height: 8px;
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.cost-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.cost-value {
|
||||
font-size: 13px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.analysis-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.analysis-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background: rgba(10, 20, 40, 0.8);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: rgba(0, 212, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #00d4ff;
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.change {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
&.up {
|
||||
background: rgba(0, 255, 136, 0.15);
|
||||
color: #00ff88;
|
||||
border: 1px solid rgba(0, 255, 136, 0.3);
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
&.down {
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
color: #ff6b6b;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
color: #00ff88;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -13,28 +13,20 @@
|
||||
|
||||
<main class="screen-body">
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card" v-for="card in energyOverview" :key="card.title">
|
||||
<div class="card-header">{{ card.title }}</div>
|
||||
<div class="kpi-value" :style="{ color: card.color }">{{ card.value }}</div>
|
||||
<div class="kpi-unit">{{ card.unit }}</div>
|
||||
<div class="kpi-trend" :class="card.trend > 0 ? 'positive' : 'negative'">
|
||||
<span>{{ card.trend > 0 ? '↑' : '↓' }}</span>
|
||||
{{ Math.abs(card.trend) }}%
|
||||
</div>
|
||||
<div class="kpi-card" v-for="(item, index) in energyOverview" :key="index">
|
||||
<div class="card-header">{{ item.title }}</div>
|
||||
<div class="kpi-value" :style="{ color: item.color }">{{ item.value }}</div>
|
||||
<div class="kpi-unit">{{ item.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">实时用电量</div>
|
||||
<div ref="gaugeChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">能源消耗趋势</div>
|
||||
<div class="box-header">能耗趋势</div>
|
||||
<div ref="lineChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">能源类型分布</div>
|
||||
<div class="box-header">能耗构成</div>
|
||||
<div ref="pieChartRef" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,30 +34,15 @@
|
||||
<div class="chart-row">
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">设备能耗排名</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in equipmentRanking" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="bar-wrapper">
|
||||
<div class="bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
<span class="value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">生产班次能耗</div>
|
||||
<div ref="barChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">实时告警</div>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="alarm in alarms" :key="alarm.time" :class="alarm.level">
|
||||
<span class="alarm-icon">{{ alarm.icon }}</span>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">{{ alarm.title }}</div>
|
||||
<div class="alarm-time">{{ alarm.time }}</div>
|
||||
</div>
|
||||
<div v-for="(alarm, index) in alarms" :key="index" :class="['alarm-item', alarm.level]">
|
||||
<span class="alarm-icon">{{ getAlarmIcon(alarm.level) }}</span>
|
||||
<span class="alarm-message">{{ alarm.message }}</span>
|
||||
<span class="alarm-time">{{ alarm.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,102 +55,72 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const currentTime = ref('')
|
||||
const isFullscreen = ref(false)
|
||||
const gaugeChartRef = ref(null)
|
||||
const lineChartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
const barChartRef = ref(null)
|
||||
let gaugeChart = null
|
||||
let lineChart = null
|
||||
let pieChart = null
|
||||
let fullscreenChangeHandler = null
|
||||
let barChart = null
|
||||
let timeInterval = null
|
||||
let resizeObserver = null
|
||||
let fullscreenChangeHandler = null
|
||||
|
||||
const energyOverview = ref([
|
||||
{ title: '今日用电', value: '25,600', unit: 'kWh', color: '#00d4ff', trend: 5.2 },
|
||||
{ title: '今日用气', value: '1,850', unit: 'm³', color: '#00ff88', trend: -2.1 },
|
||||
{ title: '今日用水', value: '320', unit: 't', color: '#7c63ff', trend: 1.8 },
|
||||
{ title: '综合能耗', value: '38.5', unit: 'kgce/t', color: '#ff9f43', trend: -3.5 }
|
||||
])
|
||||
const energyOverview = ref([])
|
||||
|
||||
const equipmentRanking = ref([
|
||||
{ name: '酸轧机组', value: '8,500', percent: 85, color: '#ff6b6b' },
|
||||
{ name: '退火炉', value: '5,200', percent: 52, color: '#ffd43b' },
|
||||
{ name: '轧机', value: '4,800', percent: 48, color: '#00d4ff' },
|
||||
{ name: '酸洗槽', value: '3,200', percent: 32, color: '#00ff88' },
|
||||
{ name: '剪切线', value: '2,600', percent: 26, color: '#7c63ff' }
|
||||
])
|
||||
const equipmentRanking = ref([])
|
||||
|
||||
const energyComposition = ref([])
|
||||
|
||||
const powerTrend = ref([])
|
||||
|
||||
const alarms = ref([
|
||||
{ icon: '⚠️', title: '能耗超标告警', time: '14:25:00', level: 'warning' },
|
||||
{ icon: '🔴', title: '设备异常告警', time: '13:15:00', level: 'danger' },
|
||||
{ icon: '✅', title: '系统运行正常', time: '08:00:00', level: 'success' }
|
||||
{ level: 'success', message: '能源系统运行正常', time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) }
|
||||
])
|
||||
|
||||
const getAlarmIcon = (level) => {
|
||||
const icons = {
|
||||
warning: '⚠️',
|
||||
danger: '🔴',
|
||||
success: '✅',
|
||||
info: 'ℹ️'
|
||||
}
|
||||
return icons[level] || 'ℹ️'
|
||||
}
|
||||
|
||||
const updateTime = () => {
|
||||
currentTime.value = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const initCharts = () => {
|
||||
if (gaugeChartRef.value) {
|
||||
gaugeChart = echarts.init(gaugeChartRef.value)
|
||||
gaugeChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
series: [{
|
||||
type: 'gauge',
|
||||
center: ['50%', '60%'],
|
||||
startAngle: 200,
|
||||
endAngle: -20,
|
||||
min: 0,
|
||||
max: 100,
|
||||
splitNumber: 10,
|
||||
itemStyle: { color: '#00d4ff' },
|
||||
progress: { show: true, width: 20 },
|
||||
pointer: { show: false },
|
||||
axisLine: { lineStyle: { width: 20, color: [[1, 'rgba(0, 212, 255, 0.2)']] } },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { show: false },
|
||||
title: { show: false },
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
color: '#00d4ff',
|
||||
formatter: '{value}%',
|
||||
offsetCenter: [0, '10%']
|
||||
},
|
||||
data: [{ value: 78 }]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (lineChartRef.value) {
|
||||
if (lineChart) {
|
||||
lineChart.dispose()
|
||||
}
|
||||
lineChart = echarts.init(lineChartRef.value)
|
||||
lineChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' },
|
||||
axisPointer: { type: 'line', lineStyle: { color: '#00d4ff' } }
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
legend: {
|
||||
data: ['电力', '水', '天然气', '蒸汽'],
|
||||
textStyle: { color: '#a0c4e8' },
|
||||
top: 0
|
||||
},
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
|
||||
data: powerTrend.value.map(item => item.hour),
|
||||
axisLine: { lineStyle: { color: '#3a5a8a' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
@@ -181,29 +128,46 @@ const initCharts = () => {
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用电',
|
||||
name: '电力',
|
||||
type: 'line',
|
||||
data: powerTrend.value.map(item => item.power),
|
||||
smooth: true,
|
||||
data: [620, 680, 650, 720, 780, 820],
|
||||
lineStyle: { color: '#00d4ff', width: 3 },
|
||||
itemStyle: { color: '#00d4ff' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(0, 212, 255, 0.3)' },
|
||||
{ offset: 0, color: 'rgba(0, 212, 255, 0.2)' },
|
||||
{ offset: 1, color: 'rgba(0, 212, 255, 0.05)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { color: '#00d4ff', width: 2 },
|
||||
itemStyle: { color: '#00d4ff' }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '用气',
|
||||
name: '水',
|
||||
type: 'line',
|
||||
data: powerTrend.value.map(item => item.water * 10),
|
||||
smooth: true,
|
||||
lineStyle: { color: '#7c63ff', width: 2 },
|
||||
itemStyle: { color: '#7c63ff' }
|
||||
},
|
||||
{
|
||||
name: '天然气',
|
||||
type: 'line',
|
||||
data: powerTrend.value.map(item => item.gas * 30),
|
||||
smooth: true,
|
||||
lineStyle: { color: '#ffd43b', width: 2 },
|
||||
itemStyle: { color: '#ffd43b' }
|
||||
},
|
||||
{
|
||||
name: '蒸汽',
|
||||
type: 'line',
|
||||
data: powerTrend.value.map(item => item.steam * 50),
|
||||
smooth: true,
|
||||
data: [45, 48, 46, 52, 55, 58],
|
||||
lineStyle: { color: '#00ff88', width: 2 },
|
||||
itemStyle: { color: '#00ff88' }
|
||||
}
|
||||
@@ -212,66 +176,84 @@ const initCharts = () => {
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
if (pieChart) {
|
||||
pieChart.dispose()
|
||||
}
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}% ({d}%)',
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
center: ['50%', '45%'],
|
||||
data: [
|
||||
{ value: 65, name: '电能', itemStyle: { color: '#00d4ff' } },
|
||||
{ value: 20, name: '天然气', itemStyle: { color: '#ff9f43' } },
|
||||
{ value: 10, name: '水', itemStyle: { color: '#7c63ff' } },
|
||||
{ value: 5, name: '其他', itemStyle: { color: '#a0c4e8' } }
|
||||
],
|
||||
label: { show: false }
|
||||
data: energyComposition.value.map((item, index) => ({
|
||||
value: item.value,
|
||||
name: item.name,
|
||||
itemStyle: { color: ['#00d4ff', '#7c63ff', '#ffd43b', '#00ff88'][index] }
|
||||
})),
|
||||
label: {
|
||||
show: true,
|
||||
color: '#fff',
|
||||
formatter: '{b}: {d}%'
|
||||
},
|
||||
labelLine: { show: true, lineStyle: { color: '#3a5a8a' } }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
if (barChartRef.value) {
|
||||
if (barChart) {
|
||||
barChart.dispose()
|
||||
}
|
||||
barChart = echarts.init(barChartRef.value)
|
||||
barChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: { top: 20, right: 80, bottom: 30, left: 80 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
textStyle: { color: '#fff' },
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: ['早班', '中班', '晚班'],
|
||||
data: equipmentRanking.value.map(item => item.name),
|
||||
inverse: true,
|
||||
axisLine: { lineStyle: { color: '#3a5a8a' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [9800, 12500, 8500].map((value, index) => ({
|
||||
value,
|
||||
itemStyle: {
|
||||
color: ['#00d4ff', '#7c63ff', '#00ff88'][index],
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
})),
|
||||
barWidth: '50%'
|
||||
data: equipmentRanking.value.map(item => item.power),
|
||||
barWidth: '60%',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#00d4ff' },
|
||||
{ offset: 1, color: '#0080aa' }
|
||||
]),
|
||||
borderRadius: [0, 4, 4, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
color: '#a0c4e8',
|
||||
formatter: '{c} kWh'
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
@@ -279,19 +261,70 @@ const initCharts = () => {
|
||||
|
||||
const handleResize = () => {
|
||||
nextTick(() => {
|
||||
gaugeChart?.resize()
|
||||
lineChart?.resize()
|
||||
pieChart?.resize()
|
||||
barChart?.resize()
|
||||
})
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/dashboard/energy',
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
const data = response.data
|
||||
|
||||
energyOverview.value = [
|
||||
{ title: '总用电量', value: data.totalPower?.toString() || '0', unit: 'kWh', color: '#00d4ff' },
|
||||
{ title: '用水量', value: data.waterUsage?.toString() || '0', unit: 'm³', color: '#7c63ff' },
|
||||
{ title: '用气量', value: data.gasUsage?.toString() || '0', unit: 'm³', color: '#ffd43b' },
|
||||
{ title: '蒸汽用量', value: data.steamUsage?.toString() || '0', unit: '吨', color: '#00ff88' }
|
||||
]
|
||||
|
||||
if (data.powerTrend) {
|
||||
powerTrend.value = data.powerTrend
|
||||
}
|
||||
|
||||
if (data.equipmentRanking && data.equipmentRanking.length > 0) {
|
||||
equipmentRanking.value = data.equipmentRanking
|
||||
}
|
||||
|
||||
if (data.energyComposition) {
|
||||
energyComposition.value = data.energyComposition
|
||||
}
|
||||
|
||||
if (data.alarms) {
|
||||
alarms.value = data.alarms
|
||||
}
|
||||
|
||||
initCharts()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载能源数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
nextTick(() => {
|
||||
initCharts()
|
||||
loadData()
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
@@ -315,18 +348,6 @@ onMounted(() => {
|
||||
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
|
||||
})
|
||||
|
||||
const handleRefresh = () => {
|
||||
initCharts()
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
@@ -342,10 +363,6 @@ onBeforeUnmount(() => {
|
||||
document.removeEventListener('fullscreenchange', fullscreenChangeHandler)
|
||||
fullscreenChangeHandler = null
|
||||
}
|
||||
if (gaugeChart) {
|
||||
gaugeChart.dispose()
|
||||
gaugeChart = null
|
||||
}
|
||||
if (lineChart) {
|
||||
lineChart.dispose()
|
||||
lineChart = null
|
||||
@@ -368,33 +385,38 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
color: #0a1428;
|
||||
letter-spacing: 3px;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
@@ -408,7 +430,7 @@ onUnmounted(() => {
|
||||
|
||||
.time {
|
||||
font-size: 18px;
|
||||
color: #00d4ff;
|
||||
color: #0a1428;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -462,35 +484,14 @@ onUnmounted(() => {
|
||||
.kpi-value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin: 15px 0 5px;
|
||||
text-shadow: 0 0 15px currentColor;
|
||||
padding: 15px 0 5px;
|
||||
text-shadow: 0 0 20px currentColor;
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
font-size: 14px;
|
||||
color: #a0c4e8;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kpi-trend {
|
||||
font-size: 13px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
display: inline-block;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
|
||||
&.positive {
|
||||
background: rgba(0, 255, 136, 0.15);
|
||||
color: #00ff88;
|
||||
border: 1px solid rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
&.negative {
|
||||
background: rgba(255, 107, 107, 0.15);
|
||||
color: #ff6b6b;
|
||||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
color: #8fa8cc;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,175 +499,86 @@ onUnmounted(() => {
|
||||
.chart-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-box {
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.flex-2 {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%);
|
||||
padding: 12px 18px;
|
||||
background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #0a1428;
|
||||
letter-spacing: 2px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 280px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: rgba(10, 20, 40, 0.8);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
|
||||
.rank {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-right: 12px;
|
||||
background: #3a5a8a;
|
||||
color: #a0c4e8;
|
||||
|
||||
&.rank-1 {
|
||||
background: linear-gradient(135deg, #ffd43b, #ff9f43);
|
||||
color: #0a1428;
|
||||
}
|
||||
&.rank-2 {
|
||||
background: linear-gradient(135deg, #a0c4e8, #74c0fc);
|
||||
color: #0a1428;
|
||||
}
|
||||
&.rank-3 {
|
||||
background: linear-gradient(135deg, #ffa94d, #ff6b6b);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
|
||||
.bar-wrapper {
|
||||
width: 80px;
|
||||
height: 8px;
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
padding: 12px 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid;
|
||||
background: rgba(10, 20, 40, 0.8);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
border-left-width: 4px;
|
||||
|
||||
&.danger {
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-color: #ffd43b;
|
||||
background: rgba(255, 212, 59, 0.1);
|
||||
border-left: 3px solid #ffd43b;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
border-left: 3px solid #ff6b6b;
|
||||
}
|
||||
|
||||
&.success {
|
||||
border-color: #00ff88;
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border-left: 3px solid #00ff88;
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
border-left: 3px solid #00d4ff;
|
||||
}
|
||||
|
||||
.alarm-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
.alarm-message {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.alarm-title {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 12px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
.alarm-time {
|
||||
font-size: 13px;
|
||||
color: #8fa8cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -257,28 +257,33 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
|
||||
@@ -145,10 +145,10 @@ const loadData = async () => {
|
||||
getOeeStoppageEvents({ pageSize: 10 })
|
||||
])
|
||||
|
||||
if (summaryRes?.data && summaryRes.data.length > 0) {
|
||||
summaryList.value = summaryRes.data
|
||||
const latest = summaryRes.data[0]
|
||||
const previous = summaryRes.data[1] || latest
|
||||
if (summaryRes && summaryRes.length > 0) {
|
||||
summaryList.value = summaryRes
|
||||
const latest = summaryRes[0]
|
||||
const previous = summaryRes[1] || latest
|
||||
|
||||
oeeData.value = {
|
||||
oee: latest.oee || 0,
|
||||
@@ -162,12 +162,12 @@ const loadData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (lossRes?.data) {
|
||||
lossList.value = lossRes.data
|
||||
if (lossRes) {
|
||||
lossList.value = lossRes
|
||||
}
|
||||
|
||||
if (eventsRes?.rows) {
|
||||
stoppageList.value = eventsRes.rows
|
||||
if (eventsRes) {
|
||||
stoppageList.value = eventsRes
|
||||
}
|
||||
|
||||
updateCharts()
|
||||
@@ -475,28 +475,33 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
|
||||
@@ -32,25 +32,29 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box flex-2">
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">订单列表</div>
|
||||
<div class="order-table">
|
||||
<div class="table-header">
|
||||
<span>订单号</span>
|
||||
<span>客户</span>
|
||||
<span>金额</span>
|
||||
<span>状态</span>
|
||||
<span>时间</span>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
<div class="table-row" v-for="order in orderList" :key="order.orderNo">
|
||||
<span class="order-no">{{ order.orderNo }}</span>
|
||||
<span>{{ order.customer }}</span>
|
||||
<span class="amount">{{ formatAmount(order.amount) }}</span>
|
||||
<span :class="['status', order.status]">{{ order.status }}</span>
|
||||
<span class="time">{{ order.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-list">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>客户</th>
|
||||
<th>金额(元)</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="order in orderList" :key="order.orderNo">
|
||||
<td>{{ order.orderNo }}</td>
|
||||
<td>{{ order.customer }}</td>
|
||||
<td>{{ order.amount.toLocaleString() }}</td>
|
||||
<td><span :class="getStatusClass(order.status)">{{ order.status }}</span></td>
|
||||
<td>{{ order.time }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,6 +66,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const currentTime = ref('')
|
||||
const isFullscreen = ref(false)
|
||||
@@ -73,23 +78,25 @@ let timeInterval = null
|
||||
let resizeObserver = null
|
||||
let fullscreenChangeHandler = null
|
||||
|
||||
const cards = ref([
|
||||
{ title: '今日订单', value: '45', unit: '单', color: '#00d4ff' },
|
||||
{ title: '待处理订单', value: '12', unit: '单', color: '#ffd43b' },
|
||||
{ title: '已完成订单', value: '156', unit: '单', color: '#00ff88' },
|
||||
{ title: '订单总额', value: '568', unit: '万', color: '#7c63ff' }
|
||||
])
|
||||
const cards = ref([])
|
||||
|
||||
const orderList = ref([
|
||||
{ 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' }
|
||||
])
|
||||
const orderList = ref([])
|
||||
|
||||
const orderTrend = ref([])
|
||||
|
||||
const statusDistribution = ref([])
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
const classMap = {
|
||||
'生产中': 'status-processing',
|
||||
'待生产': 'status-pending',
|
||||
'已完成': 'status-completed'
|
||||
}
|
||||
return classMap[status] || ''
|
||||
}
|
||||
|
||||
const formatAmount = (amount) => {
|
||||
return '¥' + (amount / 10000).toFixed(2) + '万'
|
||||
return (amount / 10000).toFixed(0)
|
||||
}
|
||||
|
||||
const updateTime = () => {
|
||||
@@ -101,6 +108,9 @@ const updateTime = () => {
|
||||
|
||||
const initCharts = () => {
|
||||
if (trendChartRef.value) {
|
||||
if (trendChart) {
|
||||
trendChart.dispose()
|
||||
}
|
||||
trendChart = echarts.init(trendChartRef.value)
|
||||
trendChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
@@ -111,35 +121,73 @@ const initCharts = () => {
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
legend: {
|
||||
data: ['订单数', '金额(万)'],
|
||||
textStyle: { color: '#a0c4e8' },
|
||||
top: 0
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
data: orderTrend.value.map(item => item.date),
|
||||
axisLine: { lineStyle: { color: '#3a5a8a' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [35, 42, 38, 45, 40, 48].map((value, index) => ({
|
||||
value,
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '订单数',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额(万)',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: '#a0c4e8' }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '订单数',
|
||||
type: 'bar',
|
||||
data: orderTrend.value.map(item => item.count),
|
||||
itemStyle: {
|
||||
color: ['#00d4ff', '#7c63ff', '#00ff88', '#ff9f43', '#ff6b6b', '#ffd43b'][index],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#00d4ff' },
|
||||
{ offset: 1, color: '#0080aa' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
barWidth: '40%'
|
||||
},
|
||||
{
|
||||
name: '金额(万)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: orderTrend.value.map(item => item.amount),
|
||||
smooth: true,
|
||||
lineStyle: { color: '#7c63ff', width: 3 },
|
||||
itemStyle: { color: '#7c63ff' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(124, 99, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(124, 99, 255, 0.05)' }
|
||||
])
|
||||
}
|
||||
})),
|
||||
barWidth: '50%'
|
||||
}]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (pieChartRef.value) {
|
||||
if (pieChart) {
|
||||
pieChart.dispose()
|
||||
}
|
||||
pieChart = echarts.init(pieChartRef.value)
|
||||
pieChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
@@ -149,17 +197,17 @@ const initCharts = () => {
|
||||
borderColor: '#1e3a5f',
|
||||
textStyle: { color: '#fff' }
|
||||
},
|
||||
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
center: ['50%', '45%'],
|
||||
data: [
|
||||
{ value: 45, name: '生产中', itemStyle: { color: '#00d4ff' } },
|
||||
{ value: 12, name: '待生产', itemStyle: { color: '#ffd43b' } },
|
||||
{ value: 156, name: '已完成', itemStyle: { color: '#00ff88' } }
|
||||
],
|
||||
label: { show: false }
|
||||
data: statusDistribution.value.map((item, index) => ({
|
||||
value: item.value,
|
||||
name: item.name,
|
||||
itemStyle: { color: ['#00d4ff', '#ffd43b', '#00ff88'][index] }
|
||||
})),
|
||||
label: { show: false },
|
||||
labelLine: { show: false }
|
||||
}]
|
||||
})
|
||||
}
|
||||
@@ -172,12 +220,60 @@ const handleResize = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/dashboard/order',
|
||||
method: 'get'
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
const data = response.data
|
||||
|
||||
cards.value = [
|
||||
{ title: '今日订单', value: data.todayOrderCount?.toString() || '0', unit: '单', color: '#00d4ff' },
|
||||
{ title: '待处理订单', value: data.pendingOrderCount?.toString() || '0', unit: '单', color: '#ffd43b' },
|
||||
{ title: '已完成订单', value: data.completedOrderCount?.toString() || '0', unit: '单', color: '#00ff88' },
|
||||
{ title: '订单总额', value: data.orderTotalAmount?.toFixed(0) || '0', unit: '万', color: '#7c63ff' }
|
||||
]
|
||||
|
||||
if (data.orderTrend) {
|
||||
orderTrend.value = data.orderTrend
|
||||
}
|
||||
|
||||
if (data.statusDistribution) {
|
||||
statusDistribution.value = data.statusDistribution
|
||||
}
|
||||
|
||||
if (data.recentOrders) {
|
||||
orderList.value = data.recentOrders
|
||||
}
|
||||
|
||||
initCharts()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载订单数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
nextTick(() => {
|
||||
initCharts()
|
||||
loadData()
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
@@ -201,18 +297,6 @@ onMounted(() => {
|
||||
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
|
||||
})
|
||||
|
||||
const handleRefresh = () => {
|
||||
initCharts()
|
||||
}
|
||||
|
||||
const exitFullscreen = () => {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval)
|
||||
@@ -246,33 +330,38 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
color: #0a1428;
|
||||
letter-spacing: 3px;
|
||||
margin: 0;
|
||||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||
@@ -286,7 +375,7 @@ onUnmounted(() => {
|
||||
|
||||
.time {
|
||||
font-size: 18px;
|
||||
color: #00d4ff;
|
||||
color: #0a1428;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -340,14 +429,14 @@ onUnmounted(() => {
|
||||
.kpi-value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin: 15px 0 5px;
|
||||
text-shadow: 0 0 15px currentColor;
|
||||
padding: 15px 0 5px;
|
||||
text-shadow: 0 0 20px currentColor;
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
font-size: 14px;
|
||||
color: #a0c4e8;
|
||||
margin-bottom: 15px;
|
||||
color: #8fa8cc;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,16 +444,13 @@ onUnmounted(() => {
|
||||
.chart-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.chart-box {
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.15);
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.flex-1 {
|
||||
@@ -376,117 +462,64 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.box-header {
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%);
|
||||
padding: 12px 18px;
|
||||
background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #0a1428;
|
||||
letter-spacing: 2px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.order-table {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.order-list {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 2fr 1.5fr 1fr 1fr;
|
||||
gap: 15px;
|
||||
padding: 12px 15px;
|
||||
background: rgba(0, 212, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #00d4ff;
|
||||
font-weight: bold;
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
.table-body {
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 2fr 1.5fr 1fr 1fr;
|
||||
gap: 15px;
|
||||
padding: 12px 15px;
|
||||
th {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
color: #00d4ff;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
|
||||
font-size: 13px;
|
||||
color: #a0c4e8;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background: rgba(0, 212, 255, 0.05);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 212, 255, 0.1);
|
||||
}
|
||||
|
||||
.order-no {
|
||||
color: #00d4ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.amount {
|
||||
color: #ff9f43;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
|
||||
&.已完成 {
|
||||
background: rgba(0, 255, 136, 0.15);
|
||||
color: #00ff88;
|
||||
border: 1px solid rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
&.生产中 {
|
||||
background: rgba(0, 212, 255, 0.15);
|
||||
color: #00d4ff;
|
||||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
&.待生产 {
|
||||
background: rgba(255, 212, 59, 0.15);
|
||||
color: #ffd43b;
|
||||
border: 1px solid rgba(255, 212, 59, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #a0c4e8;
|
||||
}
|
||||
tr:hover td {
|
||||
background: rgba(0, 212, 255, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
.status-processing {
|
||||
color: #ffd43b;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.status-pending {
|
||||
color: #00d4ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
.status-completed {
|
||||
color: #00ff88;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -162,15 +162,15 @@ const loadData = async () => {
|
||||
getDashboardOverview()
|
||||
])
|
||||
|
||||
if (outputRes?.data) {
|
||||
const data = outputRes.data
|
||||
if (outputRes) {
|
||||
const data = outputRes
|
||||
outputList.value = data.details || []
|
||||
|
||||
outputData.value = {
|
||||
todayCount: data.summary?.totalQuantity || 0,
|
||||
todayWeight: data.summary?.totalWeight || 0,
|
||||
monthCount: (overviewRes?.data?.monthTaskCount) || 156,
|
||||
yearCount: (overviewRes?.data?.yearTaskCount) || 1200
|
||||
monthCount: (overviewRes?.monthTaskCount) || 156,
|
||||
yearCount: (overviewRes?.yearTaskCount) || 1200
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,28 +484,33 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
|
||||
@@ -134,16 +134,28 @@ const loadData = async () => {
|
||||
try {
|
||||
const res = await getProductionStop()
|
||||
|
||||
if (res?.data) {
|
||||
const data = res.data
|
||||
if (res) {
|
||||
const data = res
|
||||
stoppageList.value = data.details || []
|
||||
typeDistribution.value = data.typeDistribution || []
|
||||
teamDistribution.value = data.teamDistribution || []
|
||||
|
||||
const typeMap = {}
|
||||
const teamMap = {}
|
||||
data.details?.forEach(item => {
|
||||
if (item.stopType) {
|
||||
typeMap[item.stopType] = (typeMap[item.stopType] || 0) + (item.duration || 0)
|
||||
}
|
||||
if (item.handler) {
|
||||
teamMap[item.handler] = (teamMap[item.handler] || 0) + 1
|
||||
}
|
||||
})
|
||||
|
||||
typeDistribution.value = Object.entries(typeMap).map(([name, value]) => ({ name, value }))
|
||||
teamDistribution.value = Object.entries(teamMap).map(([name, value]) => ({ name, value }))
|
||||
|
||||
stopData.value = {
|
||||
todayDuration: data.summary?.stopTime || 0,
|
||||
todayCount: data.summary?.stopCount || 0,
|
||||
runningRate: data.summary?.rate || 95.0,
|
||||
todayDuration: data.summary?.totalDuration || 0,
|
||||
todayCount: data.summary?.totalStops || 0,
|
||||
runningRate: 100 - ((data.summary?.totalDuration || 0) / 86400 * 100) || 95.0,
|
||||
avgDuration: data.summary?.avgDuration || 0
|
||||
}
|
||||
}
|
||||
@@ -433,28 +445,33 @@ onUnmounted(() => {
|
||||
<style lang="scss" scoped>
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.screen-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: '/api',
|
||||
baseURL: '',
|
||||
timeout: 15000
|
||||
})
|
||||
|
||||
|
||||
@@ -5,44 +5,61 @@
|
||||
<h1 class="title">酸轧数据大屏</h1>
|
||||
<div class="header-right">
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
<button v-if="isFullscreen" class="exit-fullscreen-btn" @click="exitFullscreen" title="退出全屏">
|
||||
<span>⛶ 退出全屏</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="screen-body">
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card" v-for="card in kpiCards" :key="card.title">
|
||||
<div class="card-header">{{ card.title }}</div>
|
||||
<div class="kpi-value" :style="{ color: '#00d4ff' }">{{ card.value }}</div>
|
||||
<div class="kpi-unit">{{ card.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<span class="loading-text">正在加载数据...</span>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">OEE趋势分析</div>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
<main class="screen-body" :class="{ 'loading': loading }">
|
||||
<div class="summary-section">
|
||||
<div class="summary-card main-kpi">
|
||||
<div class="card-header">今日产量</div>
|
||||
<div class="main-value">{{ kpiData.totalOutputTon.toLocaleString() }}</div>
|
||||
<div class="main-unit">吨</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: (kpiData.totalOutputTon / kpiData.targetOutputTon * 100) + '%' }"></div>
|
||||
</div>
|
||||
<div class="progress-text">目标: {{ kpiData.targetOutputTon.toLocaleString() }} 吨</div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">7大损失分布</div>
|
||||
<div ref="lossChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box flex-1">
|
||||
<div class="box-header">班组产量排名</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in rankingList" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="value">{{ item.value }} 吨</span>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card" v-for="card in kpiCards" :key="card.title">
|
||||
<div class="card-header">{{ card.title }}</div>
|
||||
<div class="kpi-value glow">{{ card.value }}</div>
|
||||
<div class="kpi-unit">{{ card.unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box full-width">
|
||||
<div class="chart-box large">
|
||||
<div class="box-header">
|
||||
<span>OEE趋势分析</span>
|
||||
<span class="sub-title">近7天变化</span>
|
||||
</div>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-header">7大损失分布</div>
|
||||
<div ref="lossChartRef" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-row">
|
||||
<div class="chart-box">
|
||||
<div class="box-header">班组产量排名</div>
|
||||
<div class="ranking-list">
|
||||
<div class="ranking-item" v-for="(item, index) in rankingList" :key="item.name">
|
||||
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<span class="value">{{ item.value.toLocaleString() }} 吨</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-box">
|
||||
<div class="box-header">实时告警</div>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="(alarm, index) in alarmList" :key="index" :class="alarm.level">
|
||||
@@ -63,6 +80,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const currentTime = ref('')
|
||||
const isFullscreen = ref(false)
|
||||
@@ -75,12 +93,14 @@ let dataInterval = null
|
||||
let resizeObserver = null
|
||||
let fullscreenChangeHandler = null
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const kpiData = ref({
|
||||
oee: 86.5,
|
||||
availability: 92.1,
|
||||
performanceTon: 89.8,
|
||||
quality: 97.5,
|
||||
totalOutputTon: 13100,
|
||||
oee: 0,
|
||||
availability: 0,
|
||||
performanceTon: 0,
|
||||
quality: 0,
|
||||
totalOutputTon: 0,
|
||||
targetOutputTon: 15000
|
||||
})
|
||||
|
||||
@@ -91,36 +111,14 @@ const kpiCards = computed(() => [
|
||||
{ title: '良品率', value: kpiData.value.quality.toFixed(1), unit: '%' }
|
||||
])
|
||||
|
||||
const summaryList = ref([
|
||||
{ statDate: '05-11', oee: 85.2, availability: 91.5, performanceTon: 88.7, quality: 97.2 },
|
||||
{ statDate: '05-12', oee: 86.8, availability: 92.8, performanceTon: 89.5, quality: 97.8 },
|
||||
{ statDate: '05-13', oee: 85.9, availability: 91.2, performanceTon: 89.2, quality: 97.5 },
|
||||
{ statDate: '05-14', oee: 87.2, availability: 93.5, performanceTon: 90.1, quality: 98.0 },
|
||||
{ statDate: '05-15', oee: 86.5, availability: 92.1, performanceTon: 89.8, quality: 97.5 }
|
||||
])
|
||||
const summaryList = ref([])
|
||||
|
||||
const loss7List = ref([
|
||||
{ lossName: '故障停机', lossRatio: 35 },
|
||||
{ lossName: '换模换线', lossRatio: 24 },
|
||||
{ lossName: '空转停机', lossRatio: 15 },
|
||||
{ lossName: '速度损失', lossRatio: 12 },
|
||||
{ lossName: '质量损失', lossRatio: 7 },
|
||||
{ lossName: '启动损失', lossRatio: 4 },
|
||||
{ lossName: '管理损失', lossRatio: 3 }
|
||||
])
|
||||
const loss7List = ref([])
|
||||
|
||||
const rankingList = ref([
|
||||
{ name: '甲班', value: 3200 },
|
||||
{ name: '乙班', value: 2980 },
|
||||
{ name: '丙班', value: 2850 },
|
||||
{ name: '丁班', value: 2720 },
|
||||
{ name: '戊班', value: 2580 }
|
||||
])
|
||||
const rankingList = ref([])
|
||||
|
||||
const alarmList = ref([
|
||||
{ icon: '⚠️', title: '速度损失告警', time: '14:25:00', level: 'warning' },
|
||||
{ icon: '🔴', title: '设备故障停机', time: '13:15:00', level: 'danger' },
|
||||
{ icon: '✅', title: '系统正常运行', time: '08:00:00', level: 'success' }
|
||||
{ icon: '✅', title: '系统正常运行', time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }), level: 'success' }
|
||||
])
|
||||
|
||||
const updateCurrentTime = () => {
|
||||
@@ -142,6 +140,21 @@ const exitFullscreen = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
const element = document.querySelector('.screen-wrapper')
|
||||
if (element) {
|
||||
if (!document.fullscreenElement) {
|
||||
element.requestFullscreen().catch(err => {
|
||||
console.error('进入全屏失败:', err)
|
||||
})
|
||||
} else {
|
||||
document.exitFullscreen().catch(err => {
|
||||
console.error('退出全屏失败:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement
|
||||
}
|
||||
@@ -162,7 +175,7 @@ const updateTrendChart = () => {
|
||||
|
||||
trendChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
grid: { top: 30, right: 20, bottom: 30, left: 60 },
|
||||
grid: { top: 50, right: 20, bottom: 40, left: 60 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(10, 20, 40, 0.9)',
|
||||
@@ -170,7 +183,7 @@ const updateTrendChart = () => {
|
||||
textStyle: { color: '#fff' },
|
||||
axisPointer: { type: 'line', lineStyle: { color: '#00d4ff' } }
|
||||
},
|
||||
legend: { data: ['OEE', '时间稼动率', '性能稼动率', '良品率'], bottom: 0, textStyle: { color: '#a0c4e8' } },
|
||||
legend: { data: ['OEE', '时间稼动率', '性能稼动率', '良品率'], top: 0, textStyle: { color: '#a0c4e8', fontSize: 11 } },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
@@ -244,11 +257,11 @@ const updateLossChart = () => {
|
||||
textStyle: { color: '#fff' },
|
||||
formatter: '{b}: {c}% ({d}%)'
|
||||
},
|
||||
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
|
||||
legend: { right: '5%', top: 'center', orient: 'vertical', textStyle: { color: '#a0c4e8', fontSize: 11 } },
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['45%', '75%'],
|
||||
center: ['50%', '45%'],
|
||||
radius: ['35%', '65%'],
|
||||
center: ['40%', '50%'],
|
||||
data: loss7List.value.map((item, index) => ({
|
||||
value: item.lossRatio || 10,
|
||||
name: item.lossName || '损失' + (index + 1),
|
||||
@@ -268,8 +281,84 @@ const handleResize = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
console.log('开始加载酸轧数据...')
|
||||
console.log('请求URL:', '/wms/acid-rolling/dashboard/overview')
|
||||
const response = await request({
|
||||
url: '/wms/acid-rolling/dashboard/overview',
|
||||
method: 'get'
|
||||
})
|
||||
console.log('数据加载成功:', response)
|
||||
console.log('数据类型:', typeof response)
|
||||
console.log('OEE值:', response.oee)
|
||||
|
||||
if (response) {
|
||||
const data = response
|
||||
|
||||
kpiData.value = {
|
||||
oee: data.oee || 0,
|
||||
availability: data.availability || 0,
|
||||
performanceTon: data.performance || 0,
|
||||
quality: parseFloat(data.quality) || 0,
|
||||
totalOutputTon: data.totalOutput || 0,
|
||||
targetOutputTon: data.targetOutput || 15000
|
||||
}
|
||||
|
||||
if (data.trendingData && data.trendingData.length > 0) {
|
||||
summaryList.value = data.trendingData.map(item => ({
|
||||
statDate: item.date || '',
|
||||
oee: item.oee || 0,
|
||||
availability: item.availability || 0,
|
||||
performanceTon: item.performance || 0,
|
||||
quality: item.quality || 0
|
||||
}))
|
||||
} else {
|
||||
summaryList.value = []
|
||||
}
|
||||
|
||||
if (data.lossData && data.lossData.length > 0) {
|
||||
const totalLoss = data.lossData.reduce((sum, item) => sum + (item.value || 0), 0)
|
||||
loss7List.value = data.lossData.map(item => ({
|
||||
lossName: item.name || '未知损失',
|
||||
lossRatio: totalLoss > 0 ? ((item.value || 0) / totalLoss * 100).toFixed(1) : 0
|
||||
}))
|
||||
} else {
|
||||
loss7List.value = []
|
||||
}
|
||||
|
||||
if (data.teamRanking && data.teamRanking.length > 0) {
|
||||
rankingList.value = data.teamRanking.map(item => ({
|
||||
name: item.name || '未知班组',
|
||||
value: item.output || 0
|
||||
}))
|
||||
} else {
|
||||
rankingList.value = []
|
||||
}
|
||||
|
||||
if (data.alarms && data.alarms.length > 0) {
|
||||
alarmList.value = data.alarms.map(alarm => ({
|
||||
icon: alarm.level === 'danger' ? '🔴' : alarm.level === 'warning' ? '⚠️' : '✅',
|
||||
title: alarm.message || '',
|
||||
time: alarm.time || '',
|
||||
level: alarm.level || 'success'
|
||||
}))
|
||||
} else {
|
||||
alarmList.value = [{ icon: '✅', title: '系统运行正常', time: new Date().toLocaleTimeString('zh-CN'), level: 'success' }]
|
||||
}
|
||||
|
||||
updateCharts()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载酸轧数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
updateCharts()
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -283,7 +372,8 @@ onMounted(() => {
|
||||
if (lossChartRef.value) {
|
||||
lossChart = echarts.init(lossChartRef.value)
|
||||
}
|
||||
updateCharts()
|
||||
|
||||
loadData()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
@@ -297,7 +387,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
dataInterval = setInterval(updateCharts, 30000)
|
||||
dataInterval = setInterval(loadData, 30000)
|
||||
})
|
||||
|
||||
fullscreenChangeHandler = handleFullscreenChange
|
||||
@@ -340,15 +430,20 @@ onBeforeUnmount(() => {
|
||||
.screen-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.screen-content {
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -357,11 +452,10 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%);
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
@@ -385,33 +479,166 @@ onBeforeUnmount(() => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.exit-fullscreen-btn {
|
||||
.refresh-btn, .fullscreen-btn, .exit-fullscreen-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.5);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-color: rgba(0, 212, 255, 0.5);
|
||||
color: #00d4ff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 212, 255, 0.3);
|
||||
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
background: rgba(0, 255, 136, 0.2);
|
||||
border-color: rgba(0, 255, 136, 0.5);
|
||||
color: #00ff88;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 255, 136, 0.3);
|
||||
box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.exit-fullscreen-btn {
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
border-color: rgba(255, 107, 107, 0.5);
|
||||
color: #ff6b6b;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 107, 107, 0.3);
|
||||
border-color: #ff6b6b;
|
||||
box-shadow: 0 0 15px rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(10, 20, 40, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 4px solid rgba(0, 212, 255, 0.2);
|
||||
border-top-color: #00d4ff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
color: #00d4ff;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.screen-body {
|
||||
padding: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 3fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%);
|
||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&.main-kpi {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #0a1428;
|
||||
letter-spacing: 2px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.main-value {
|
||||
font-size: 56px;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
text-shadow: 0 0 30px rgba(0, 212, 255, 0.7);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.main-unit {
|
||||
font-size: 18px;
|
||||
color: #a0c4e8;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00d4ff, #00ff88);
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #8899aa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
gap: 12px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.kpi-card {
|
||||
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%);
|
||||
@@ -436,6 +663,10 @@ onBeforeUnmount(() => {
|
||||
font-weight: bold;
|
||||
margin: 15px 0 5px;
|
||||
text-shadow: 0 0 15px currentColor;
|
||||
|
||||
&.glow {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-unit {
|
||||
@@ -446,6 +677,15 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
text-shadow: 0 0 15px currentColor;
|
||||
}
|
||||
50% {
|
||||
text-shadow: 0 0 25px currentColor, 0 0 35px currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
@@ -461,6 +701,11 @@ onBeforeUnmount(() => {
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
min-height: 320px;
|
||||
flex: 1;
|
||||
|
||||
&.large {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
&.flex-1 {
|
||||
flex: 1;
|
||||
@@ -479,6 +724,15 @@ onBeforeUnmount(() => {
|
||||
letter-spacing: 2px;
|
||||
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: rgba(10, 20, 40, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
|
||||
@@ -26,6 +26,14 @@ export default defineConfig({
|
||||
'/l2': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/wms': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/oee': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user