feat:修改

This commit is contained in:
2026-05-19 19:26:41 +08:00
parent 4434480f32
commit 2030e68ff9
12 changed files with 1554 additions and 921 deletions

View File

@@ -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) => {

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,7 +1,7 @@
import axios from 'axios'
const service = axios.create({
baseURL: '/api',
baseURL: '',
timeout: 15000
})

View File

@@ -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 {

View File

@@ -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
}
}
}