feat:修改

This commit is contained in:
zuqijia
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 return
} }
try { try {
// 1. 获取当前班次OEE数据
const [shiftRows] = await acidPool.execute( const [shiftRows] = await acidPool.execute(
'SELECT * FROM klptcm1_shift_current ORDER BY create_time DESC LIMIT 1' 'SELECT * FROM klptcm1_shift_current ORDER BY create_time DESC LIMIT 1'
) )
const currentShift = shiftRows[0] || {}
// 2. 获取今日产出统计
const [coilRows] = await acidPool.execute( 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( 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 = { const overview = {
oee: latest.oee || 86.5, // 核心指标
availability: latest.availability || 92.1, oee: parseFloat(oeeValue),
performance: latest.performance || 89.8, availability: availabilityRate,
quality: latest.quality || 97.5, performance: performanceRate,
totalOutput: coilRows[0]?.count || mockOverview.totalOutput, quality: qualityRate.toFixed(1),
totalWeight: coilRows[0]?.totalWeight || mockOverview.totalWeight, totalOutput: totalCoils,
totalWeight: coilRows[0]?.totalWeight || 0,
targetOutput: 15000, targetOutput: 15000,
efficiency: 92.0, efficiency: 92.0,
trendingData: mockOverview.trendingData, // OEE趋势数据
lossData: lossRows.length > 0 ? lossRows : mockOverview.lossData, trendingData: trendingRows.length > 0 ? trendingRows.map(row => ({
teamRanking: mockOverview.teamRanking, date: row.date ? row.date.toString().substring(5) : '',
alarms: mockOverview.alarms 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) 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) => { app.get('/api/screens', async (req, res) => {

View File

@@ -5,10 +5,14 @@
<span class="navbar-title">{{ title }}</span> <span class="navbar-title">{{ title }}</span>
</div> </div>
<div class="navbar-right"> <div class="navbar-right">
<el-button link icon="Refresh" class="nav-btn" @click="handleRefresh" title="刷新数据"> <button class="action-btn refresh-btn" @click="handleRefresh" title="刷新数据">
<span>{{ refreshing ? '刷新中...' : '' }}</span> <span class="btn-icon">🔄</span>
</el-button> <span class="btn-text">刷新</span>
<el-button link :icon="isFullscreen ? 'FullScreenExit' : 'FullScreen'" class="nav-btn" @click="handleToggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'"></el-button> </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> </div>
</nav> </nav>
</template> </template>
@@ -22,18 +26,13 @@ const store = useStore()
const title = computed(() => store.state.settings.title) const title = computed(() => store.state.settings.title)
const isFullscreen = ref(false) const isFullscreen = ref(false)
const refreshing = ref(false)
const toggleSideBar = () => { const toggleSideBar = () => {
store.dispatch('app/toggleSideBar') store.dispatch('app/toggleSideBar')
} }
const handleRefresh = () => { const handleRefresh = () => {
refreshing.value = true
window.dispatchEvent(new CustomEvent('refresh-data')) window.dispatchEvent(new CustomEvent('refresh-data'))
setTimeout(() => {
refreshing.value = false
}, 1500)
} }
const handleToggleFullscreen = () => { const handleToggleFullscreen = () => {
@@ -55,45 +54,103 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.navbar { .navbar {
height: 50px; height: 60px;
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%); 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%; width: 100%;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 20px; padding: 0 24px;
box-sizing: border-box; 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 { .navbar-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 20px;
} }
.navbar-title { .navbar-title {
font-size: 18px; font-size: 20px;
font-weight: bold; font-weight: bold;
color: #0a1428; color: #0a1428;
letter-spacing: 2px;
} }
.navbar-right { .navbar-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 16px;
} }
.nav-btn { .action-btn {
width: 36px; display: flex;
height: 36px; align-items: center;
border-radius: 6px; 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; color: #0a1428;
font-size: 16px; font-size: 15px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
&:hover { &: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> </style>

View File

@@ -29,47 +29,22 @@
<div class="box-header">成本构成</div> <div class="box-header">成本构成</div>
<div ref="pieChartRef" class="chart"></div> <div ref="pieChartRef" class="chart"></div>
</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>
<div class="chart-row"> <div class="chart-row">
<div class="chart-box flex-1"> <div class="chart-box flex-1">
<div class="box-header">成本类型对比</div> <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 ref="barChartRef" class="chart"></div> <div ref="barChartRef" class="chart"></div>
</div> </div>
<div class="chart-box flex-1"> <div class="chart-box flex-1">
<div class="box-header">成本分析</div> <div class="box-header">成本分析</div>
<div class="analysis-list"> <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="label">{{ item.label }}</span>
<span class="value" :style="{ color: item.color }">{{ item.value }}</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> </div>
</div> </div>
@@ -82,6 +57,7 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue' import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import request from '@/utils/request'
const currentTime = ref('') const currentTime = ref('')
const isFullscreen = ref(false) const isFullscreen = ref(false)
@@ -95,35 +71,13 @@ let timeInterval = null
let resizeObserver = null let resizeObserver = null
let fullscreenChangeHandler = null let fullscreenChangeHandler = null
const cards = ref([ 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 compareList = ref([ const costTrend = 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 costItems = ref([ const costComposition = 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 analysisList = 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 updateTime = () => { const updateTime = () => {
currentTime.value = new Date().toLocaleString('zh-CN', { 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 = () => { const initCharts = () => {
if (trendChartRef.value) { if (trendChartRef.value) {
if (trendChart) {
trendChart.dispose()
}
trendChart = echarts.init(trendChartRef.value) trendChart = echarts.init(trendChartRef.value)
trendChart.setOption({ trendChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
grid: { top: 30, right: 30, bottom: 30, left: 60 }, grid: { top: 20, right: 20, bottom: 30, left: 50 },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: 'rgba(10, 20, 40, 0.9)', backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' } textStyle: { color: '#fff' }
}, },
legend: {
data: ['总成本', '材料成本', '人工成本', '能源成本'],
textStyle: { color: '#a0c4e8' },
top: 0
},
xAxis: { xAxis: {
type: 'category', type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月'], data: costTrend.value.map(item => item.month),
axisLine: { lineStyle: { color: '#3a5a8a' } }, axisLine: { lineStyle: { color: '#3a5a8a' } },
axisTick: { show: false }, axisTick: { show: false },
axisLabel: { color: '#a0c4e8' } axisLabel: { color: '#a0c4e8' }
@@ -176,27 +118,55 @@ const initCharts = () => {
axisLine: { show: false }, axisLine: { show: false },
axisTick: { show: false }, axisTick: { show: false },
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } }, splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
axisLabel: { color: '#a0c4e8', formatter: (v) => (v / 10000).toFixed(0) + '万' } axisLabel: { color: '#a0c4e8' }
}, },
series: [{ series: [
type: 'line', {
smooth: true, name: '总成本',
data: [1420000, 1380000, 1520000, 1490000, 1568000, 1568000], type: 'line',
areaStyle: { data: costTrend.value.map(item => item.total),
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ smooth: true,
{ offset: 0, color: 'rgba(0, 212, 255, 0.3)' }, lineStyle: { color: '#00d4ff', width: 3 },
{ offset: 1, color: 'rgba(0, 212, 255, 0.05)' } 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' }, name: '材料成本',
symbol: 'circle', type: 'line',
symbolSize: 8 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 (pieChartRef.value) {
if (pieChart) {
pieChart.dispose()
}
pieChart = echarts.init(pieChartRef.value) pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({ pieChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
@@ -206,36 +176,43 @@ const initCharts = () => {
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' } textStyle: { color: '#fff' }
}, },
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
series: [{ series: [{
type: 'pie', type: 'pie',
radius: ['45%', '75%'], radius: ['45%', '75%'],
center: ['50%', '45%'], center: ['50%', '45%'],
data: [ data: costComposition.value.map((item, index) => ({
{ value: 62.8, name: '材料成本', itemStyle: { color: '#00d4ff' } }, value: item.value,
{ value: 14.9, name: '人工成本', itemStyle: { color: '#7c63ff' } }, name: item.name,
{ value: 12.1, name: '能源成本', itemStyle: { color: '#ff9f43' } }, itemStyle: { color: ['#ff6b6b', '#00d4ff', '#00ff88', '#7c63ff'][index] }
{ value: 10.2, name: '其他成本', itemStyle: { color: '#ff6b6b' } } })),
], label: {
label: { show: false } show: true,
color: '#fff',
formatter: '{b}: {d}%'
},
labelLine: { show: true, lineStyle: { color: '#3a5a8a' } }
}] }]
}) })
} }
if (barChartRef.value) { if (barChartRef.value) {
if (barChart) {
barChart.dispose()
}
barChart = echarts.init(barChartRef.value) barChart = echarts.init(barChartRef.value)
barChart.setOption({ barChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
grid: { top: 20, right: 20, bottom: 30, left: 50 }, grid: { top: 20, right: 20, bottom: 40, left: 60 },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: 'rgba(10, 20, 40, 0.9)', backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' } textStyle: { color: '#fff' },
axisPointer: { type: 'shadow' }
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
data: ['1月', '2月', '3月', '4月', '5月'], data: costTrend.value.map(item => item.month),
axisLine: { lineStyle: { color: '#3a5a8a' } }, axisLine: { lineStyle: { color: '#3a5a8a' } },
axisTick: { show: false }, axisTick: { show: false },
axisLabel: { color: '#a0c4e8' } axisLabel: { color: '#a0c4e8' }
@@ -245,18 +222,19 @@ const initCharts = () => {
axisLine: { show: false }, axisLine: { show: false },
axisTick: { show: false }, axisTick: { show: false },
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } }, splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
axisLabel: { color: '#a0c4e8', formatter: (v) => (v / 10000).toFixed(0) + '万' } axisLabel: { color: '#a0c4e8' }
}, },
series: [{ series: [{
type: 'bar', type: 'bar',
data: [142, 138, 152, 149, 156.8].map((value, index) => ({ data: costTrend.value.map(item => item.total),
value, barWidth: '50%',
itemStyle: { itemStyle: {
color: ['#00d4ff', '#7c63ff', '#00ff88', '#ff9f43', '#ff6b6b'][index], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
borderRadius: [4, 4, 0, 0] { offset: 0, color: '#7c63ff' },
} { offset: 1, color: '#4a3a99' }
})), ]),
barWidth: '50%' 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(() => { onMounted(() => {
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
nextTick(() => { nextTick(() => {
initCharts() loadData()
handleResize() handleResize()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
@@ -290,17 +316,15 @@ onMounted(() => {
} }
} }
}) })
fullscreenChangeHandler = handleFullscreenChange
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
window.addEventListener('refresh-data', handleRefresh) window.addEventListener('refresh-data', handleRefresh)
fullscreenChangeHandler = () => {
isFullscreen.value = !!document.fullscreenElement
}
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
}) })
const handleRefresh = () => {
initCharts()
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
@@ -338,33 +362,38 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100%; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.screen-header { .screen-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;
font-weight: bold; font-weight: bold;
color: #00d4ff; color: #0a1428;
letter-spacing: 3px; letter-spacing: 3px;
margin: 0; margin: 0;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5); text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
@@ -378,33 +407,25 @@ onUnmounted(() => {
.time { .time {
font-size: 18px; font-size: 18px;
color: #00d4ff; color: #0a1428;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-weight: bold; font-weight: bold;
} }
.fullscreen-btn { .exit-fullscreen-btn {
width: 36px; padding: 8px 16px;
height: 36px; border: 1px solid rgba(255, 107, 107, 0.5);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 6px; border-radius: 6px;
background: rgba(0, 212, 255, 0.1); background: rgba(255, 107, 107, 0.2);
color: #00d4ff; color: #ff6b6b;
font-size: 18px; font-size: 14px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
&:hover { &:hover {
background: rgba(0, 212, 255, 0.2); background: rgba(255, 107, 107, 0.3);
border-color: rgba(0, 212, 255, 0.5); border-color: #ff6b6b;
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3); box-shadow: 0 0 15px rgba(255, 107, 107, 0.3);
}
&:active {
transform: scale(0.95);
} }
} }
} }
@@ -440,14 +461,14 @@ onUnmounted(() => {
.kpi-value { .kpi-value {
font-size: 36px; font-size: 36px;
font-weight: bold; font-weight: bold;
margin: 15px 0 5px; padding: 15px 0 5px;
text-shadow: 0 0 15px currentColor; text-shadow: 0 0 20px currentColor;
} }
.kpi-unit { .kpi-unit {
font-size: 14px; font-size: 14px;
color: #a0c4e8; color: #8fa8cc;
margin-bottom: 15px; padding-bottom: 10px;
} }
} }
} }
@@ -455,187 +476,76 @@ onUnmounted(() => {
.chart-row { .chart-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
margin-bottom: 16px; margin-bottom: 20px;
.chart-box { .chart-box {
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%); 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.15); border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 8px; border-radius: 8px;
padding: 0; padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
&.flex-1 { &.flex-1 {
flex: 1; flex: 1;
} }
&.flex-2 {
flex: 2;
}
.box-header { .box-header {
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%); background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
padding: 12px 18px; padding: 10px 15px;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
color: #0a1428; color: #0a1428;
letter-spacing: 2px; letter-spacing: 2px;
border-bottom: 1px solid rgba(0, 212, 255, 0.3); border-radius: 6px;
margin-bottom: 15px;
} }
.chart { .chart {
height: 280px; height: 280px;
width: 100%;
padding: 15px;
} }
.compare-list { .analysis-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;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; 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 { .analysis-item {
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 10px; align-items: center;
background: rgba(10, 20, 40, 0.8); padding: 15px;
border: 1px solid rgba(0, 212, 255, 0.15); background: rgba(0, 212, 255, 0.05);
border-radius: 6px; border-radius: 6px;
border-left: 3px solid #00d4ff;
.label { .label {
font-size: 13px; font-size: 14px;
color: #a0c4e8; color: #a0c4e8;
} }
.value { .value {
font-size: 14px; font-size: 18px;
font-weight: bold; font-weight: bold;
} }
.change { .change {
font-size: 12px; font-size: 14px;
padding: 2px 6px; font-weight: bold;
border-radius: 4px;
&.up { &.up {
background: rgba(0, 255, 136, 0.15); color: #ff6b6b;
color: #00ff88;
border: 1px solid rgba(0, 255, 136, 0.3);
} }
&.down { &.down {
background: rgba(255, 107, 107, 0.15); color: #00ff88;
color: #ff6b6b;
border: 1px solid rgba(255, 107, 107, 0.3);
} }
} }
} }
} }
} }
} }
</style>
@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>

View File

@@ -13,28 +13,20 @@
<main class="screen-body"> <main class="screen-body">
<div class="kpi-grid"> <div class="kpi-grid">
<div class="kpi-card" v-for="card in energyOverview" :key="card.title"> <div class="kpi-card" v-for="(item, index) in energyOverview" :key="index">
<div class="card-header">{{ card.title }}</div> <div class="card-header">{{ item.title }}</div>
<div class="kpi-value" :style="{ color: card.color }">{{ card.value }}</div> <div class="kpi-value" :style="{ color: item.color }">{{ item.value }}</div>
<div class="kpi-unit">{{ card.unit }}</div> <div class="kpi-unit">{{ item.unit }}</div>
<div class="kpi-trend" :class="card.trend > 0 ? 'positive' : 'negative'">
<span>{{ card.trend > 0 ? '↑' : '↓' }}</span>
{{ Math.abs(card.trend) }}%
</div>
</div> </div>
</div> </div>
<div class="chart-row"> <div class="chart-row">
<div class="chart-box flex-1"> <div class="chart-box flex-1">
<div class="box-header">实时用电量</div> <div class="box-header">能耗趋势</div>
<div ref="gaugeChartRef" class="chart"></div>
</div>
<div class="chart-box flex-1">
<div class="box-header">能源消耗趋势</div>
<div ref="lineChartRef" class="chart"></div> <div ref="lineChartRef" class="chart"></div>
</div> </div>
<div class="chart-box flex-1"> <div class="chart-box flex-1">
<div class="box-header">源类型分布</div> <div class="box-header">耗构成</div>
<div ref="pieChartRef" class="chart"></div> <div ref="pieChartRef" class="chart"></div>
</div> </div>
</div> </div>
@@ -42,30 +34,15 @@
<div class="chart-row"> <div class="chart-row">
<div class="chart-box flex-1"> <div class="chart-box flex-1">
<div class="box-header">设备能耗排名</div> <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 ref="barChartRef" class="chart"></div>
</div> </div>
<div class="chart-box flex-1"> <div class="chart-box flex-1">
<div class="box-header">实时告警</div> <div class="box-header">实时告警</div>
<div class="alarm-list"> <div class="alarm-list">
<div class="alarm-item" v-for="alarm in alarms" :key="alarm.time" :class="alarm.level"> <div v-for="(alarm, index) in alarms" :key="index" :class="['alarm-item', alarm.level]">
<span class="alarm-icon">{{ alarm.icon }}</span> <span class="alarm-icon">{{ getAlarmIcon(alarm.level) }}</span>
<div class="alarm-content"> <span class="alarm-message">{{ alarm.message }}</span>
<div class="alarm-title">{{ alarm.title }}</div> <span class="alarm-time">{{ alarm.time }}</span>
<div class="alarm-time">{{ alarm.time }}</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -78,102 +55,72 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue' import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import request from '@/utils/request'
const currentTime = ref('') const currentTime = ref('')
const isFullscreen = ref(false) const isFullscreen = ref(false)
const gaugeChartRef = ref(null)
const lineChartRef = ref(null) const lineChartRef = ref(null)
const pieChartRef = ref(null) const pieChartRef = ref(null)
const barChartRef = ref(null) const barChartRef = ref(null)
let gaugeChart = null
let lineChart = null let lineChart = null
let pieChart = null let pieChart = null
let fullscreenChangeHandler = null
let barChart = null let barChart = null
let timeInterval = null let timeInterval = null
let resizeObserver = null let resizeObserver = null
let fullscreenChangeHandler = null
const energyOverview = ref([ 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 equipmentRanking = ref([ const equipmentRanking = ref([])
{ name: '酸轧机组', value: '8,500', percent: 85, color: '#ff6b6b' },
{ name: '退火炉', value: '5,200', percent: 52, color: '#ffd43b' }, const energyComposition = ref([])
{ name: '轧机', value: '4,800', percent: 48, color: '#00d4ff' },
{ name: '酸洗槽', value: '3,200', percent: 32, color: '#00ff88' }, const powerTrend = ref([])
{ name: '剪切线', value: '2,600', percent: 26, color: '#7c63ff' }
])
const alarms = ref([ const alarms = ref([
{ icon: '⚠️', title: '能耗超标告警', time: '14:25:00', level: 'warning' }, { level: 'success', message: '能源系统运行正常', time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) }
{ icon: '🔴', title: '设备异常告警', time: '13:15:00', level: 'danger' },
{ icon: '✅', title: '系统运行正常', time: '08:00:00', level: 'success' }
]) ])
const getAlarmIcon = (level) => {
const icons = {
warning: '⚠️',
danger: '🔴',
success: '✅',
info: ''
}
return icons[level] || ''
}
const updateTime = () => { const updateTime = () => {
currentTime.value = new Date().toLocaleString('zh-CN', { currentTime.value = new Date().toLocaleString('zh-CN', {
year: 'numeric', year: 'numeric', month: '2-digit', day: '2-digit',
month: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}) })
} }
const initCharts = () => { 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 (lineChartRef.value) {
if (lineChart) {
lineChart.dispose()
}
lineChart = echarts.init(lineChartRef.value) lineChart = echarts.init(lineChartRef.value)
lineChart.setOption({ lineChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
grid: { top: 20, right: 20, bottom: 30, left: 50 },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: 'rgba(10, 20, 40, 0.9)', backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' }, textStyle: { color: '#fff' }
axisPointer: { type: 'line', lineStyle: { color: '#00d4ff' } } },
legend: {
data: ['电力', '水', '天然气', '蒸汽'],
textStyle: { color: '#a0c4e8' },
top: 0
}, },
grid: { top: 20, right: 20, bottom: 30, left: 50 },
xAxis: { xAxis: {
type: 'category', 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' } }, axisLine: { lineStyle: { color: '#3a5a8a' } },
axisTick: { show: false }, axisTick: { show: false },
axisLabel: { color: '#a0c4e8' } axisLabel: { color: '#a0c4e8' }
@@ -181,29 +128,46 @@ const initCharts = () => {
yAxis: { yAxis: {
type: 'value', type: 'value',
axisLine: { show: false }, axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } }, splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
axisLabel: { color: '#a0c4e8' } axisLabel: { color: '#a0c4e8' }
}, },
series: [ series: [
{ {
name: '电', name: '电',
type: 'line', type: 'line',
data: powerTrend.value.map(item => item.power),
smooth: true, smooth: true,
data: [620, 680, 650, 720, 780, 820], lineStyle: { color: '#00d4ff', width: 3 },
itemStyle: { color: '#00d4ff' },
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ 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)' } { offset: 1, color: 'rgba(0, 212, 255, 0.05)' }
]) ])
}, }
lineStyle: { color: '#00d4ff', width: 2 },
itemStyle: { color: '#00d4ff' }
}, },
{ {
name: '用气', name: '',
type: 'line', 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, smooth: true,
data: [45, 48, 46, 52, 55, 58],
lineStyle: { color: '#00ff88', width: 2 }, lineStyle: { color: '#00ff88', width: 2 },
itemStyle: { color: '#00ff88' } itemStyle: { color: '#00ff88' }
} }
@@ -212,66 +176,84 @@ const initCharts = () => {
} }
if (pieChartRef.value) { if (pieChartRef.value) {
if (pieChart) {
pieChart.dispose()
}
pieChart = echarts.init(pieChartRef.value) pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({ pieChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
formatter: '{b}: {c}% ({d}%)',
backgroundColor: 'rgba(10, 20, 40, 0.9)', backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' } textStyle: { color: '#fff' }
}, },
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
series: [{ series: [{
type: 'pie', type: 'pie',
radius: ['45%', '75%'], radius: ['45%', '75%'],
center: ['50%', '45%'], center: ['50%', '45%'],
data: [ data: energyComposition.value.map((item, index) => ({
{ value: 65, name: '电能', itemStyle: { color: '#00d4ff' } }, value: item.value,
{ value: 20, name: '天然气', itemStyle: { color: '#ff9f43' } }, name: item.name,
{ value: 10, name: '水', itemStyle: { color: '#7c63ff' } }, itemStyle: { color: ['#00d4ff', '#7c63ff', '#ffd43b', '#00ff88'][index] }
{ value: 5, name: '其他', itemStyle: { color: '#a0c4e8' } } })),
], label: {
label: { show: false } show: true,
color: '#fff',
formatter: '{b}: {d}%'
},
labelLine: { show: true, lineStyle: { color: '#3a5a8a' } }
}] }]
}) })
} }
if (barChartRef.value) { if (barChartRef.value) {
if (barChart) {
barChart.dispose()
}
barChart = echarts.init(barChartRef.value) barChart = echarts.init(barChartRef.value)
barChart.setOption({ barChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
grid: { top: 20, right: 80, bottom: 30, left: 80 },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: 'rgba(10, 20, 40, 0.9)', backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' } textStyle: { color: '#fff' },
axisPointer: { type: 'shadow' }
}, },
grid: { top: 20, right: 20, bottom: 30, left: 50 },
xAxis: { xAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
axisLabel: { color: '#a0c4e8' }
},
yAxis: {
type: 'category', type: 'category',
data: ['早班', '中班', '晚班'], data: equipmentRanking.value.map(item => item.name),
inverse: true,
axisLine: { lineStyle: { color: '#3a5a8a' } }, axisLine: { lineStyle: { color: '#3a5a8a' } },
axisTick: { show: false }, axisTick: { show: false },
axisLabel: { color: '#a0c4e8' } axisLabel: { color: '#a0c4e8' }
}, },
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
axisLabel: { color: '#a0c4e8' }
},
series: [{ series: [{
type: 'bar', type: 'bar',
data: [9800, 12500, 8500].map((value, index) => ({ data: equipmentRanking.value.map(item => item.power),
value, barWidth: '60%',
itemStyle: { itemStyle: {
color: ['#00d4ff', '#7c63ff', '#00ff88'][index], color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
borderRadius: [4, 4, 0, 0] { offset: 0, color: '#00d4ff' },
} { offset: 1, color: '#0080aa' }
})), ]),
barWidth: '50%' borderRadius: [0, 4, 4, 0]
},
label: {
show: true,
position: 'right',
color: '#a0c4e8',
formatter: '{c} kWh'
}
}] }]
}) })
} }
@@ -279,19 +261,70 @@ const initCharts = () => {
const handleResize = () => { const handleResize = () => {
nextTick(() => { nextTick(() => {
gaugeChart?.resize()
lineChart?.resize() lineChart?.resize()
pieChart?.resize() pieChart?.resize()
barChart?.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(() => { onMounted(() => {
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
nextTick(() => { nextTick(() => {
initCharts() loadData()
handleResize() handleResize()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
@@ -315,18 +348,6 @@ onMounted(() => {
document.addEventListener('fullscreenchange', fullscreenChangeHandler) document.addEventListener('fullscreenchange', fullscreenChangeHandler)
}) })
const handleRefresh = () => {
initCharts()
}
const exitFullscreen = () => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(err => {
console.error('退出全屏失败:', err)
})
}
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
@@ -342,10 +363,6 @@ onBeforeUnmount(() => {
document.removeEventListener('fullscreenchange', fullscreenChangeHandler) document.removeEventListener('fullscreenchange', fullscreenChangeHandler)
fullscreenChangeHandler = null fullscreenChangeHandler = null
} }
if (gaugeChart) {
gaugeChart.dispose()
gaugeChart = null
}
if (lineChart) { if (lineChart) {
lineChart.dispose() lineChart.dispose()
lineChart = null lineChart = null
@@ -368,33 +385,38 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100%; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.screen-header { .screen-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;
font-weight: bold; font-weight: bold;
color: #00d4ff; color: #0a1428;
letter-spacing: 3px; letter-spacing: 3px;
margin: 0; margin: 0;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5); text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
@@ -408,7 +430,7 @@ onUnmounted(() => {
.time { .time {
font-size: 18px; font-size: 18px;
color: #00d4ff; color: #0a1428;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-weight: bold; font-weight: bold;
} }
@@ -462,35 +484,14 @@ onUnmounted(() => {
.kpi-value { .kpi-value {
font-size: 36px; font-size: 36px;
font-weight: bold; font-weight: bold;
margin: 15px 0 5px; padding: 15px 0 5px;
text-shadow: 0 0 15px currentColor; text-shadow: 0 0 20px currentColor;
} }
.kpi-unit { .kpi-unit {
font-size: 14px; font-size: 14px;
color: #a0c4e8; color: #8fa8cc;
margin-bottom: 10px; padding-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);
}
} }
} }
} }
@@ -498,175 +499,86 @@ onUnmounted(() => {
.chart-row { .chart-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
margin-bottom: 16px; margin-bottom: 20px;
.chart-box { .chart-box {
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%); 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.15); border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 8px; border-radius: 8px;
padding: 0; padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
&.flex-1 { &.flex-1 {
flex: 1; flex: 1;
} }
&.flex-2 {
flex: 2;
}
.box-header { .box-header {
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%); background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
padding: 12px 18px; padding: 10px 15px;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
color: #0a1428; color: #0a1428;
letter-spacing: 2px; letter-spacing: 2px;
border-bottom: 1px solid rgba(0, 212, 255, 0.3); border-radius: 6px;
margin-bottom: 15px;
} }
.chart { .chart {
height: 280px; 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 { .alarm-list {
flex: 1;
padding: 15px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 12px;
.alarm-item { .alarm-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px; gap: 12px;
padding: 12px 15px;
border-radius: 6px; 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 { &.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 { &.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 { .alarm-icon {
font-size: 18px; font-size: 18px;
margin-right: 12px;
} }
.alarm-content { .alarm-message {
flex: 1; flex: 1;
font-size: 14px;
color: #fff;
}
.alarm-title { .alarm-time {
font-size: 14px; font-size: 13px;
color: #ffffff; color: #8fa8cc;
margin-bottom: 4px;
}
.alarm-time {
font-size: 12px;
color: #a0c4e8;
}
} }
} }
} }
} }
} }
</style>
@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>

View File

@@ -257,28 +257,33 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100%; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.screen-header { .screen-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;

View File

@@ -145,10 +145,10 @@ const loadData = async () => {
getOeeStoppageEvents({ pageSize: 10 }) getOeeStoppageEvents({ pageSize: 10 })
]) ])
if (summaryRes?.data && summaryRes.data.length > 0) { if (summaryRes && summaryRes.length > 0) {
summaryList.value = summaryRes.data summaryList.value = summaryRes
const latest = summaryRes.data[0] const latest = summaryRes[0]
const previous = summaryRes.data[1] || latest const previous = summaryRes[1] || latest
oeeData.value = { oeeData.value = {
oee: latest.oee || 0, oee: latest.oee || 0,
@@ -162,12 +162,12 @@ const loadData = async () => {
} }
} }
if (lossRes?.data) { if (lossRes) {
lossList.value = lossRes.data lossList.value = lossRes
} }
if (eventsRes?.rows) { if (eventsRes) {
stoppageList.value = eventsRes.rows stoppageList.value = eventsRes
} }
updateCharts() updateCharts()
@@ -475,28 +475,33 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100%; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.screen-header { .screen-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;

View File

@@ -32,25 +32,29 @@
</div> </div>
<div class="chart-row"> <div class="chart-row">
<div class="chart-box flex-2"> <div class="chart-box flex-1">
<div class="box-header">订单列表</div> <div class="box-header">订单列表</div>
<div class="order-table"> <div class="order-list">
<div class="table-header"> <table>
<span>订单号</span> <thead>
<span>客户</span> <tr>
<span>金额</span> <th>订单号</th>
<span>状态</span> <th>客户</th>
<span>时间</span> <th>金额()</th>
</div> <th>状态</th>
<div class="table-body"> <th>时间</th>
<div class="table-row" v-for="order in orderList" :key="order.orderNo"> </tr>
<span class="order-no">{{ order.orderNo }}</span> </thead>
<span>{{ order.customer }}</span> <tbody>
<span class="amount">{{ formatAmount(order.amount) }}</span> <tr v-for="order in orderList" :key="order.orderNo">
<span :class="['status', order.status]">{{ order.status }}</span> <td>{{ order.orderNo }}</td>
<span class="time">{{ order.time }}</span> <td>{{ order.customer }}</td>
</div> <td>{{ order.amount.toLocaleString() }}</td>
</div> <td><span :class="getStatusClass(order.status)">{{ order.status }}</span></td>
<td>{{ order.time }}</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
@@ -62,6 +66,7 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue' import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import request from '@/utils/request'
const currentTime = ref('') const currentTime = ref('')
const isFullscreen = ref(false) const isFullscreen = ref(false)
@@ -73,23 +78,25 @@ let timeInterval = null
let resizeObserver = null let resizeObserver = null
let fullscreenChangeHandler = null let fullscreenChangeHandler = null
const cards = ref([ 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 orderList = ref([ const orderList = ref([])
{ orderNo: 'ORD20260515001', customer: '周口钢铁', amount: 125000, status: '生产中', time: '10:30' },
{ orderNo: 'ORD20260515002', customer: '南阳重工', amount: 89000, status: '已完成', time: '09:45' }, const orderTrend = ref([])
{ orderNo: 'ORD20260515003', customer: '洛阳机械', amount: 156000, status: '待生产', time: '11:20' },
{ orderNo: 'ORD20260515004', customer: '开封汽配', amount: 67000, status: '生产中', time: '08:15' }, const statusDistribution = ref([])
{ orderNo: 'ORD20260515005', customer: '商丘金属', amount: 45000, status: '已完成', time: '07:30' }
]) const getStatusClass = (status) => {
const classMap = {
'生产中': 'status-processing',
'待生产': 'status-pending',
'已完成': 'status-completed'
}
return classMap[status] || ''
}
const formatAmount = (amount) => { const formatAmount = (amount) => {
return '¥' + (amount / 10000).toFixed(2) + '万' return (amount / 10000).toFixed(0)
} }
const updateTime = () => { const updateTime = () => {
@@ -101,6 +108,9 @@ const updateTime = () => {
const initCharts = () => { const initCharts = () => {
if (trendChartRef.value) { if (trendChartRef.value) {
if (trendChart) {
trendChart.dispose()
}
trendChart = echarts.init(trendChartRef.value) trendChart = echarts.init(trendChartRef.value)
trendChart.setOption({ trendChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
@@ -111,35 +121,73 @@ const initCharts = () => {
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' } textStyle: { color: '#fff' }
}, },
legend: {
data: ['订单数', '金额(万)'],
textStyle: { color: '#a0c4e8' },
top: 0
},
xAxis: { xAxis: {
type: 'category', type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月'], data: orderTrend.value.map(item => item.date),
axisLine: { lineStyle: { color: '#3a5a8a' } }, axisLine: { lineStyle: { color: '#3a5a8a' } },
axisTick: { show: false }, axisTick: { show: false },
axisLabel: { color: '#a0c4e8' } axisLabel: { color: '#a0c4e8' }
}, },
yAxis: { yAxis: [
type: 'value', {
axisLine: { show: false }, type: 'value',
axisTick: { show: false }, name: '订单数',
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } }, axisLine: { show: false },
axisLabel: { color: '#a0c4e8' } axisTick: { show: false },
}, splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
series: [{ axisLabel: { color: '#a0c4e8' }
type: 'bar', },
data: [35, 42, 38, 45, 40, 48].map((value, index) => ({ {
value, 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: { 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] 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 (pieChartRef.value) {
if (pieChart) {
pieChart.dispose()
}
pieChart = echarts.init(pieChartRef.value) pieChart = echarts.init(pieChartRef.value)
pieChart.setOption({ pieChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
@@ -149,17 +197,17 @@ const initCharts = () => {
borderColor: '#1e3a5f', borderColor: '#1e3a5f',
textStyle: { color: '#fff' } textStyle: { color: '#fff' }
}, },
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
series: [{ series: [{
type: 'pie', type: 'pie',
radius: ['45%', '75%'], radius: ['45%', '75%'],
center: ['50%', '45%'], center: ['50%', '45%'],
data: [ data: statusDistribution.value.map((item, index) => ({
{ value: 45, name: '生产中', itemStyle: { color: '#00d4ff' } }, value: item.value,
{ value: 12, name: '待生产', itemStyle: { color: '#ffd43b' } }, name: item.name,
{ value: 156, name: '已完成', itemStyle: { color: '#00ff88' } } itemStyle: { color: ['#00d4ff', '#ffd43b', '#00ff88'][index] }
], })),
label: { show: false } 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(() => { onMounted(() => {
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
nextTick(() => { nextTick(() => {
initCharts() loadData()
handleResize() handleResize()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
@@ -201,18 +297,6 @@ onMounted(() => {
document.addEventListener('fullscreenchange', fullscreenChangeHandler) document.addEventListener('fullscreenchange', fullscreenChangeHandler)
}) })
const handleRefresh = () => {
initCharts()
}
const exitFullscreen = () => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(err => {
console.error('退出全屏失败:', err)
})
}
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
@@ -246,33 +330,38 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100%; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.screen-header { .screen-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;
font-weight: bold; font-weight: bold;
color: #00d4ff; color: #0a1428;
letter-spacing: 3px; letter-spacing: 3px;
margin: 0; margin: 0;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5); text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
@@ -286,7 +375,7 @@ onUnmounted(() => {
.time { .time {
font-size: 18px; font-size: 18px;
color: #00d4ff; color: #0a1428;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
font-weight: bold; font-weight: bold;
} }
@@ -340,14 +429,14 @@ onUnmounted(() => {
.kpi-value { .kpi-value {
font-size: 36px; font-size: 36px;
font-weight: bold; font-weight: bold;
margin: 15px 0 5px; padding: 15px 0 5px;
text-shadow: 0 0 15px currentColor; text-shadow: 0 0 20px currentColor;
} }
.kpi-unit { .kpi-unit {
font-size: 14px; font-size: 14px;
color: #a0c4e8; color: #8fa8cc;
margin-bottom: 15px; padding-bottom: 10px;
} }
} }
} }
@@ -355,16 +444,13 @@ onUnmounted(() => {
.chart-row { .chart-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
margin-bottom: 16px; margin-bottom: 20px;
.chart-box { .chart-box {
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%); 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.15); border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 8px; border-radius: 8px;
padding: 0; padding: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
&.flex-1 { &.flex-1 {
@@ -376,117 +462,64 @@ onUnmounted(() => {
} }
.box-header { .box-header {
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%); background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
padding: 12px 18px; padding: 10px 15px;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
color: #0a1428; color: #0a1428;
letter-spacing: 2px; letter-spacing: 2px;
border-bottom: 1px solid rgba(0, 212, 255, 0.3); border-radius: 6px;
margin-bottom: 15px;
} }
.chart { .chart {
height: 300px; height: 280px;
width: 100%;
padding: 15px;
} }
.order-table { .order-list {
flex: 1; max-height: 280px;
padding: 15px; overflow-y: auto;
display: flex;
flex-direction: column;
.table-header { table {
display: grid; width: 100%;
grid-template-columns: 2fr 2fr 1.5fr 1fr 1fr; border-collapse: collapse;
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-body { th {
.table-row { background: rgba(0, 212, 255, 0.1);
display: grid; color: #00d4ff;
grid-template-columns: 2fr 2fr 1.5fr 1fr 1fr; padding: 10px;
gap: 15px; text-align: left;
padding: 12px 15px; font-weight: bold;
font-size: 13px;
}
td {
padding: 10px;
border-bottom: 1px solid rgba(0, 212, 255, 0.1); border-bottom: 1px solid rgba(0, 212, 255, 0.1);
font-size: 13px; font-size: 13px;
color: #a0c4e8;
}
&:nth-child(even) { tr:hover td {
background: rgba(0, 212, 255, 0.05); 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;
}
} }
} }
} }
} }
} }
@media screen and (max-width: 1200px) { .status-processing {
.kpi-grid { color: #ffd43b;
grid-template-columns: repeat(2, 1fr); font-weight: bold;
}
.chart-row {
flex-direction: column;
}
} }
@media screen and (max-width: 768px) { .status-pending {
.kpi-grid { color: #00d4ff;
grid-template-columns: 1fr; font-weight: bold;
}
} }
</style>
.status-completed {
color: #00ff88;
font-weight: bold;
}
</style>

View File

@@ -162,15 +162,15 @@ const loadData = async () => {
getDashboardOverview() getDashboardOverview()
]) ])
if (outputRes?.data) { if (outputRes) {
const data = outputRes.data const data = outputRes
outputList.value = data.details || [] outputList.value = data.details || []
outputData.value = { outputData.value = {
todayCount: data.summary?.totalQuantity || 0, todayCount: data.summary?.totalQuantity || 0,
todayWeight: data.summary?.totalWeight || 0, todayWeight: data.summary?.totalWeight || 0,
monthCount: (overviewRes?.data?.monthTaskCount) || 156, monthCount: (overviewRes?.monthTaskCount) || 156,
yearCount: (overviewRes?.data?.yearTaskCount) || 1200 yearCount: (overviewRes?.yearTaskCount) || 1200
} }
} }
@@ -484,28 +484,33 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100%; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.screen-header { .screen-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;

View File

@@ -134,16 +134,28 @@ const loadData = async () => {
try { try {
const res = await getProductionStop() const res = await getProductionStop()
if (res?.data) { if (res) {
const data = res.data const data = res
stoppageList.value = data.details || [] 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 = { stopData.value = {
todayDuration: data.summary?.stopTime || 0, todayDuration: data.summary?.totalDuration || 0,
todayCount: data.summary?.stopCount || 0, todayCount: data.summary?.totalStops || 0,
runningRate: data.summary?.rate || 95.0, runningRate: 100 - ((data.summary?.totalDuration || 0) / 86400 * 100) || 95.0,
avgDuration: data.summary?.avgDuration || 0 avgDuration: data.summary?.avgDuration || 0
} }
} }
@@ -433,28 +445,33 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100%; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
.screen-header { .screen-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;

View File

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

View File

@@ -5,44 +5,61 @@
<h1 class="title">酸轧数据大屏</h1> <h1 class="title">酸轧数据大屏</h1>
<div class="header-right"> <div class="header-right">
<span class="time">{{ currentTime }}</span> <span class="time">{{ currentTime }}</span>
<button v-if="isFullscreen" class="exit-fullscreen-btn" @click="exitFullscreen" title="退出全屏">
<span> 退出全屏</span>
</button>
</div> </div>
</header> </header>
<main class="screen-body"> <div v-if="loading" class="loading-overlay">
<div class="kpi-grid"> <div class="loading-spinner"></div>
<div class="kpi-card" v-for="card in kpiCards" :key="card.title"> <span class="loading-text">正在加载数据...</span>
<div class="card-header">{{ card.title }}</div> </div>
<div class="kpi-value" :style="{ color: '#00d4ff' }">{{ card.value }}</div>
<div class="kpi-unit">{{ card.unit }}</div>
</div>
</div>
<div class="chart-row"> <main class="screen-body" :class="{ 'loading': loading }">
<div class="chart-box flex-1"> <div class="summary-section">
<div class="box-header">OEE趋势分析</div> <div class="summary-card main-kpi">
<div ref="trendChartRef" class="chart"></div> <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>
<div class="chart-box flex-1">
<div class="box-header">7大损失分布</div> <div class="kpi-grid">
<div ref="lossChartRef" class="chart"></div> <div class="kpi-card" v-for="card in kpiCards" :key="card.title">
</div> <div class="card-header">{{ card.title }}</div>
<div class="chart-box flex-1"> <div class="kpi-value glow">{{ card.value }}</div>
<div class="box-header">班组产量排名</div> <div class="kpi-unit">{{ card.unit }}</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> </div>
</div> </div>
</div> </div>
<div class="chart-row"> <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="box-header">实时告警</div>
<div class="alarm-list"> <div class="alarm-list">
<div class="alarm-item" v-for="(alarm, index) in alarmList" :key="index" :class="alarm.level"> <div class="alarm-item" v-for="(alarm, index) in alarmList" :key="index" :class="alarm.level">
@@ -63,6 +80,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue' import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import request from '@/utils/request'
const currentTime = ref('') const currentTime = ref('')
const isFullscreen = ref(false) const isFullscreen = ref(false)
@@ -75,12 +93,14 @@ let dataInterval = null
let resizeObserver = null let resizeObserver = null
let fullscreenChangeHandler = null let fullscreenChangeHandler = null
const loading = ref(true)
const kpiData = ref({ const kpiData = ref({
oee: 86.5, oee: 0,
availability: 92.1, availability: 0,
performanceTon: 89.8, performanceTon: 0,
quality: 97.5, quality: 0,
totalOutputTon: 13100, totalOutputTon: 0,
targetOutputTon: 15000 targetOutputTon: 15000
}) })
@@ -91,36 +111,14 @@ const kpiCards = computed(() => [
{ title: '良品率', value: kpiData.value.quality.toFixed(1), unit: '%' } { title: '良品率', value: kpiData.value.quality.toFixed(1), unit: '%' }
]) ])
const summaryList = ref([ 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 loss7List = 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 rankingList = ref([ const rankingList = ref([])
{ name: '甲班', value: 3200 },
{ name: '乙班', value: 2980 },
{ name: '丙班', value: 2850 },
{ name: '丁班', value: 2720 },
{ name: '戊班', value: 2580 }
])
const alarmList = ref([ const alarmList = ref([
{ icon: '⚠️', title: '速度损失告警', time: '14:25:00', level: 'warning' }, { icon: '', title: '系统正常运行', time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }), level: 'success' }
{ icon: '🔴', title: '设备故障停机', time: '13:15:00', level: 'danger' },
{ icon: '✅', title: '系统正常运行', time: '08:00:00', level: 'success' }
]) ])
const updateCurrentTime = () => { 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 = () => { const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement isFullscreen.value = !!document.fullscreenElement
} }
@@ -162,7 +175,7 @@ const updateTrendChart = () => {
trendChart.setOption({ trendChart.setOption({
backgroundColor: 'transparent', backgroundColor: 'transparent',
grid: { top: 30, right: 20, bottom: 30, left: 60 }, grid: { top: 50, right: 20, bottom: 40, left: 60 },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
backgroundColor: 'rgba(10, 20, 40, 0.9)', backgroundColor: 'rgba(10, 20, 40, 0.9)',
@@ -170,7 +183,7 @@ const updateTrendChart = () => {
textStyle: { color: '#fff' }, textStyle: { color: '#fff' },
axisPointer: { type: 'line', lineStyle: { color: '#00d4ff' } } 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: { xAxis: {
type: 'category', type: 'category',
data: dates, data: dates,
@@ -244,11 +257,11 @@ const updateLossChart = () => {
textStyle: { color: '#fff' }, textStyle: { color: '#fff' },
formatter: '{b}: {c}% ({d}%)' formatter: '{b}: {c}% ({d}%)'
}, },
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } }, legend: { right: '5%', top: 'center', orient: 'vertical', textStyle: { color: '#a0c4e8', fontSize: 11 } },
series: [{ series: [{
type: 'pie', type: 'pie',
radius: ['45%', '75%'], radius: ['35%', '65%'],
center: ['50%', '45%'], center: ['40%', '50%'],
data: loss7List.value.map((item, index) => ({ data: loss7List.value.map((item, index) => ({
value: item.lossRatio || 10, value: item.lossRatio || 10,
name: item.lossName || '损失' + (index + 1), 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 = () => { const handleRefresh = () => {
updateCharts() loadData()
} }
onMounted(() => { onMounted(() => {
@@ -283,7 +372,8 @@ onMounted(() => {
if (lossChartRef.value) { if (lossChartRef.value) {
lossChart = echarts.init(lossChartRef.value) lossChart = echarts.init(lossChartRef.value)
} }
updateCharts()
loadData()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
@@ -297,7 +387,7 @@ onMounted(() => {
} }
} }
dataInterval = setInterval(updateCharts, 30000) dataInterval = setInterval(loadData, 30000)
}) })
fullscreenChangeHandler = handleFullscreenChange fullscreenChangeHandler = handleFullscreenChange
@@ -340,15 +430,20 @@ onBeforeUnmount(() => {
.screen-wrapper { .screen-wrapper {
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%); background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
margin: 0;
padding: 0;
} }
.screen-content { .screen-content {
background: transparent; background: transparent;
color: #ffffff; color: #ffffff;
width: 100%; width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -357,11 +452,10 @@ onBeforeUnmount(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 30px; padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.1) 0%, rgba(12, 30, 60, 0.8) 100%); 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%);
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid rgba(0, 212, 255, 0.2); border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title { .title {
font-size: 26px; font-size: 26px;
@@ -385,33 +479,166 @@ onBeforeUnmount(() => {
font-weight: bold; font-weight: bold;
} }
.exit-fullscreen-btn { .refresh-btn, .fullscreen-btn, .exit-fullscreen-btn {
padding: 8px 16px; padding: 8px 16px;
border: 1px solid rgba(255, 107, 107, 0.5);
border-radius: 6px; border-radius: 6px;
background: rgba(255, 107, 107, 0.2);
color: #ff6b6b;
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; 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 { &:hover {
background: rgba(255, 107, 107, 0.3); background: rgba(255, 107, 107, 0.3);
border-color: #ff6b6b;
box-shadow: 0 0 15px rgba(255, 107, 107, 0.3); 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 { .screen-body {
padding: 0; 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 { .kpi-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 16px; gap: 12px;
margin-bottom: 20px; margin-bottom: 0;
.kpi-card { .kpi-card {
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%); 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; font-weight: bold;
margin: 15px 0 5px; margin: 15px 0 5px;
text-shadow: 0 0 15px currentColor; text-shadow: 0 0 15px currentColor;
&.glow {
animation: pulse 2s ease-in-out infinite;
}
} }
.kpi-unit { .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 { .chart-row {
display: flex; display: flex;
gap: 16px; gap: 16px;
@@ -461,6 +701,11 @@ onBeforeUnmount(() => {
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-height: 320px; min-height: 320px;
flex: 1;
&.large {
flex: 2;
}
&.flex-1 { &.flex-1 {
flex: 1; flex: 1;
@@ -479,6 +724,15 @@ onBeforeUnmount(() => {
letter-spacing: 2px; letter-spacing: 2px;
border-bottom: 1px solid rgba(0, 212, 255, 0.3); border-bottom: 1px solid rgba(0, 212, 255, 0.3);
flex-shrink: 0; 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 { .chart {

View File

@@ -26,6 +26,14 @@ export default defineConfig({
'/l2': { '/l2': {
target: 'http://localhost:3000', target: 'http://localhost:3000',
changeOrigin: true changeOrigin: true
},
'/wms': {
target: 'http://localhost:3000',
changeOrigin: true
},
'/oee': {
target: 'http://localhost:3000',
changeOrigin: true
} }
} }
} }