You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
671 lines
21 KiB
671 lines
21 KiB
<template>
|
|
<div class="overview-page">
|
|
<div class="overview-header">
|
|
<div class="header-left"><img class="site-navbar__brand-logo" src="~@/assets/img/lc.png" alt="龙闯电梯"></div>
|
|
<div class="header-center">
|
|
<div class="header-title">龙闯电梯 MES - 工厂综合运营大屏</div>
|
|
</div>
|
|
<div class="right-tools">
|
|
<div class="time-text">{{ currentTime }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kpi-grid">
|
|
<div class="kpi-card">
|
|
<div class="kpi-label">近一月整梯排产</div>
|
|
<div class="kpi-value">{{ kpi.monthWhole }}</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-label">近一月VL2.5升级排产</div>
|
|
<div class="kpi-value">{{ kpi.monthRenovation }}</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-label">近一月线缆/COP任务</div>
|
|
<div class="kpi-value">{{ kpi.monthTask }}</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-label">近一月机加工任务</div>
|
|
<div class="kpi-value">{{ kpi.monthMachining }}</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-label">近一月完工达成率</div>
|
|
<div class="kpi-value highlight">{{ kpi.finishRate }}%</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-label">当前在制总量</div>
|
|
<div class="kpi-value warning">{{ kpi.wipTotal }}</div>
|
|
</div>
|
|
<div class="kpi-card">
|
|
<div class="kpi-label">订单准交率</div>
|
|
<div class="kpi-value success">{{ kpi.onTimeRate }}%</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-grid">
|
|
<div class="panel">
|
|
<div class="panel-title">近一月排产/完工对比</div>
|
|
<div id="overviewBarChart" class="chart-box"></div>
|
|
</div>
|
|
<div class="panel">
|
|
<div class="panel-title">生产结构分布</div>
|
|
<div id="overviewPieChart" class="chart-box"></div>
|
|
</div>
|
|
<div class="panel wide">
|
|
<div class="panel-title">最近 7 天完工趋势</div>
|
|
<div id="overviewLineChart" class="chart-box line-box"></div>
|
|
</div>
|
|
<div class="panel">
|
|
<div class="panel-title">关键环节达成率</div>
|
|
<div class="node-rate-grid">
|
|
<div v-for="item in nodeRates" :key="item.name" class="node-rate-card">
|
|
<div class="node-rate-name">{{ item.name }}</div>
|
|
<div class="node-rate-meta">
|
|
<span class="node-rate-count">{{ item.done }}/{{ item.total }}</span>
|
|
<span class="node-rate-badge" :class="getNodeRateClass(item.rate)">{{ item.rate }}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import echarts from 'echarts'
|
|
import { getFactoryOverviewBoardData } from '@/api/longchuang/productionPlan'
|
|
|
|
const STATUS_ALLOW_LIST = ['已排产', '进行中', '已完成']
|
|
|
|
export default {
|
|
name: 'ScreenFactoryOverview',
|
|
data() {
|
|
return {
|
|
currentTime: '',
|
|
timerId: null,
|
|
boardTimerId: null,
|
|
loading: false,
|
|
chartBar: null,
|
|
chartPie: null,
|
|
chartLine: null,
|
|
wholeList: [],
|
|
taskList: [],
|
|
renovationList: [],
|
|
machiningList: [],
|
|
kpi: {
|
|
monthWhole: 0,
|
|
monthRenovation: 0,
|
|
monthTask: 0,
|
|
monthMachining: 0,
|
|
finishRate: 0,
|
|
wipTotal: 0,
|
|
onTimeRate: 0
|
|
},
|
|
nodeRates: []
|
|
}
|
|
},
|
|
mounted() {
|
|
this.loadBoardData()
|
|
this.updateTime()
|
|
this.timerId = setInterval(this.updateTime, 1000)
|
|
this.boardTimerId = setInterval(() => {
|
|
this.loadBoardData()
|
|
}, 60000)
|
|
window.addEventListener('resize', this.resizeCharts)
|
|
},
|
|
beforeDestroy() {
|
|
if (this.timerId) clearInterval(this.timerId)
|
|
if (this.boardTimerId) clearInterval(this.boardTimerId)
|
|
window.removeEventListener('resize', this.resizeCharts)
|
|
this.disposeCharts()
|
|
},
|
|
methods: {
|
|
loadBoardData() {
|
|
if (this.loading) return
|
|
this.loading = true
|
|
getFactoryOverviewBoardData({ page: 1, limit: 500, statusList: STATUS_ALLOW_LIST }).then(({ data }) => {
|
|
this.loading = false
|
|
const boardData = data && data.code === 0 && data.boardData ? data.boardData : {}
|
|
this.wholeList = this.normalizeBoardList(boardData.homeLiftList)
|
|
this.taskList = this.normalizeBoardList(boardData.cableCopTaskList)
|
|
this.renovationList = this.normalizeBoardList(boardData.renovationOrderList)
|
|
this.machiningList = this.normalizeBoardList(boardData.machiningTaskList)
|
|
this.buildDashboard()
|
|
this.$nextTick(() => {
|
|
setTimeout(() => {
|
|
this.resizeCharts()
|
|
}, 120)
|
|
})
|
|
}).catch(() => {
|
|
this.loading = false
|
|
this.wholeList = []
|
|
this.taskList = []
|
|
this.renovationList = []
|
|
this.machiningList = []
|
|
this.buildDashboard()
|
|
})
|
|
},
|
|
normalizeBoardList(sourceList) {
|
|
return (sourceList || [])
|
|
.filter(item => STATUS_ALLOW_LIST.includes(item.status))
|
|
.map(item => ({
|
|
...item,
|
|
nodeList: item.nodeList || []
|
|
}))
|
|
},
|
|
buildDashboard() {
|
|
const periodStart = this.dayjs().subtract(1, 'month').startOf('day')
|
|
const allOrderList = this.wholeList
|
|
.concat(this.renovationList)
|
|
.concat(this.taskList)
|
|
.concat(this.machiningList)
|
|
const recentOrderList = this.filterByDateRange(allOrderList, periodStart)
|
|
const finishedAll = recentOrderList.filter(item => item.status === '已完成').length
|
|
const inScheduleAll = recentOrderList.filter(item => STATUS_ALLOW_LIST.includes(item.status)).length
|
|
const wipTotal = this.wholeList.filter(item => item.status === '进行中').length +
|
|
this.taskList.filter(item => item.status === '进行中').length +
|
|
this.renovationList.filter(item => item.status === '进行中').length +
|
|
this.machiningList.filter(item => item.status === '进行中').length
|
|
|
|
const monthWhole = this.countByMonth(this.wholeList, periodStart)
|
|
const monthRenovation = this.countByMonth(this.renovationList, periodStart)
|
|
const monthTask = this.countByMonth(this.taskList, periodStart)
|
|
const monthMachining = this.countByMonth(this.machiningList, periodStart)
|
|
|
|
const onTimeTotal = allOrderList.filter(item => item.status === '已完成').length
|
|
const onTimeCount = allOrderList.filter(item => item.status === '已完成' && this.isOnTime(item)).length
|
|
|
|
this.kpi.monthWhole = monthWhole
|
|
this.kpi.monthRenovation = monthRenovation
|
|
this.kpi.monthTask = monthTask
|
|
this.kpi.monthMachining = monthMachining
|
|
this.kpi.finishRate = inScheduleAll ? Math.round((finishedAll / inScheduleAll) * 100) : 0
|
|
this.kpi.wipTotal = wipTotal
|
|
this.kpi.onTimeRate = onTimeTotal ? Math.round((onTimeCount / onTimeTotal) * 100) : 0
|
|
|
|
this.nodeRates = this.buildNodeRates()
|
|
this.$nextTick(() => {
|
|
this.renderBarChart()
|
|
this.renderPieChart()
|
|
this.renderLineChart()
|
|
})
|
|
},
|
|
countByMonth(list, monthStart) {
|
|
const startValue = monthStart.valueOf()
|
|
return list.filter(item => {
|
|
const dateVal = item.planDeliveryDate || item.planFinishDate || item.createTime
|
|
const dateObj = this.toDayjs(dateVal)
|
|
return dateObj && dateObj.valueOf() >= startValue
|
|
}).length
|
|
},
|
|
filterByDateRange(list, startDate) {
|
|
return (list || []).filter(item => {
|
|
const dateVal = item.planDeliveryDate || item.planFinishDate || item.createTime
|
|
const dateObj = this.toDayjs(dateVal)
|
|
return dateObj && dateObj.valueOf() >= startDate.valueOf()
|
|
})
|
|
},
|
|
countFinishedInRange(list, startDate) {
|
|
return (list || []).filter(item => {
|
|
if (item.status !== '已完成') return false
|
|
const dateVal = item.finishDate || item.planDeliveryDate || item.planFinishDate || item.createTime
|
|
const dateObj = this.toDayjs(dateVal)
|
|
return dateObj && dateObj.valueOf() >= startDate.valueOf()
|
|
}).length
|
|
},
|
|
isOnTime(item) {
|
|
const finishDate = this.toDayjs(item.finishDate)
|
|
const planDate = this.toDayjs(item.planDeliveryDate || item.planFinishDate)
|
|
if (!finishDate || !planDate) return false
|
|
return finishDate.valueOf() <= planDate.valueOf()
|
|
},
|
|
toDayjs(dateVal) {
|
|
if (!dateVal) return null
|
|
const d = this.dayjs(dateVal)
|
|
return d && d.isValid() ? d : null
|
|
},
|
|
buildNodeRates() {
|
|
const nodeMap = {}
|
|
const all = this.wholeList.concat(this.taskList).concat(this.renovationList).concat(this.machiningList)
|
|
all.forEach(item => {
|
|
const list = item.nodeList || []
|
|
list.forEach(node => {
|
|
if (!nodeMap[node.nodeName]) nodeMap[node.nodeName] = { total: 0, done: 0 }
|
|
nodeMap[node.nodeName].total += 1
|
|
if (node.status === '已完成') nodeMap[node.nodeName].done += 1
|
|
})
|
|
})
|
|
return Object.keys(nodeMap).map((name) => {
|
|
const total = nodeMap[name].total
|
|
const done = nodeMap[name].done
|
|
return {
|
|
name,
|
|
total,
|
|
done,
|
|
rate: total ? Math.round((done / total) * 100) : 0,
|
|
}
|
|
}).sort((a, b) => b.rate - a.rate)
|
|
},
|
|
getNodeRateClass(rate) {
|
|
if (rate >= 85) return 'is-high'
|
|
if (rate >= 60) return 'is-mid'
|
|
return 'is-low'
|
|
},
|
|
renderBarChart() {
|
|
const el = document.getElementById('overviewBarChart')
|
|
if (!el) return
|
|
if (!this.chartBar) this.chartBar = echarts.init(el)
|
|
const periodStart = this.dayjs().subtract(1, 'month').startOf('day')
|
|
const scheduledWhole = this.countByMonth(this.wholeList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
|
|
const scheduledRenovation = this.countByMonth(this.renovationList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
|
|
const scheduledTask = this.countByMonth(this.taskList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
|
|
const scheduledMachining = this.countByMonth(this.machiningList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
|
|
const finishedWhole = this.countFinishedInRange(this.wholeList, periodStart)
|
|
const finishedRenovation = this.countFinishedInRange(this.renovationList, periodStart)
|
|
const finishedTask = this.countFinishedInRange(this.taskList, periodStart)
|
|
const finishedMachining = this.countFinishedInRange(this.machiningList, periodStart)
|
|
this.chartBar.setOption({
|
|
color: ['#4fa8ff', '#6ed3a6'],
|
|
grid: { left: 30, right: 20, top: 40, bottom: 20, containLabel: true },
|
|
tooltip: { trigger: 'axis' },
|
|
legend: { textStyle: { color: '#cbe7ff' }, data: ['排产量', '完工量'] },
|
|
xAxis: { type: 'category', axisLabel: { color: '#cbe7ff' }, data: ['整梯', 'VL2.5升级', '线缆/COP', '机加工'] },
|
|
yAxis: { type: 'value', axisLabel: { color: '#cbe7ff' }, splitLine: { lineStyle: { color: 'rgba(160,200,240,0.15)' } } },
|
|
series: [
|
|
{
|
|
name: '排产量',
|
|
type: 'bar',
|
|
barWidth: 24,
|
|
data: [scheduledWhole, scheduledRenovation, scheduledTask, scheduledMachining],
|
|
itemStyle: { color: '#4fa8ff' },
|
|
label: {
|
|
normal: {
|
|
show: true,
|
|
position: 'top',
|
|
color: '#d4ebff',
|
|
fontSize: 12,
|
|
formatter: '{c}'
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: '完工量',
|
|
type: 'bar',
|
|
barWidth: 24,
|
|
data: [finishedWhole, finishedRenovation, finishedTask, finishedMachining],
|
|
itemStyle: { color: '#6ed3a6' },
|
|
label: {
|
|
normal: {
|
|
show: true,
|
|
position: 'top',
|
|
color: '#d4ebff',
|
|
fontSize: 12,
|
|
formatter: '{c}'
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}, true)
|
|
},
|
|
renderPieChart() {
|
|
const el = document.getElementById('overviewPieChart')
|
|
if (!el) return
|
|
if (!this.chartPie) this.chartPie = echarts.init(el)
|
|
const wholeCount = this.wholeList.length
|
|
const renovationCount = this.renovationList.length
|
|
const taskCount = this.taskList.length
|
|
const machiningCount = this.machiningList.length
|
|
const pieData = [
|
|
{ value: wholeCount, name: '整梯订单' },
|
|
{ value: renovationCount, name: 'VL2.5升级订单' },
|
|
{ value: taskCount, name: '线缆/COP任务' },
|
|
{ value: machiningCount, name: '机加工任务' }
|
|
]
|
|
this.chartPie.setOption({
|
|
color: ['#57b8ff', '#5dd4b0', '#7fa6d9', '#f5c15e'],
|
|
tooltip: {
|
|
trigger: 'item',
|
|
formatter: (params) => `${params.name}<br/>数量:${params.value}<br/>占比:${params.percent || 0}%`
|
|
},
|
|
legend: { bottom: 2, textStyle: { color: '#cbe7ff' } },
|
|
series: [{
|
|
type: 'pie',
|
|
radius: ['42%', '72%'],
|
|
center: ['50%', '45%'],
|
|
avoidLabelOverlap: false,
|
|
minShowLabelAngle: 0,
|
|
label: {
|
|
normal: {
|
|
show: true,
|
|
position: 'outside',
|
|
color: '#d4ebff',
|
|
fontSize: 14,
|
|
formatter: '{b} {d}%'
|
|
},
|
|
emphasis: {
|
|
show: true,
|
|
formatter: '{b} {d}%'
|
|
}
|
|
},
|
|
labelLine: {
|
|
normal: {
|
|
show: true,
|
|
length: 14,
|
|
length2: 10,
|
|
lineStyle: {
|
|
color: '#7fb4de'
|
|
}
|
|
}
|
|
},
|
|
data: pieData
|
|
}]
|
|
}, true)
|
|
},
|
|
renderLineChart() {
|
|
const el = document.getElementById('overviewLineChart')
|
|
if (!el) return
|
|
if (!this.chartLine) this.chartLine = echarts.init(el)
|
|
const xAxis = []
|
|
const wholeSeries = []
|
|
const renovationSeries = []
|
|
const taskSeries = []
|
|
const machiningSeries = []
|
|
for (let i = 6; i >= 0; i--) {
|
|
const day = this.dayjs().subtract(i, 'day')
|
|
xAxis.push(day.format('MM-DD'))
|
|
wholeSeries.push(this.countFinishByDay(this.wholeList, day))
|
|
renovationSeries.push(this.countFinishByDay(this.renovationList, day))
|
|
taskSeries.push(this.countFinishByDay(this.taskList, day))
|
|
machiningSeries.push(this.countFinishByDay(this.machiningList, day))
|
|
}
|
|
this.chartLine.setOption({
|
|
color: ['#6cc7ff', '#74e0b3', '#7fa6d9', '#f5c15e'],
|
|
grid: { left: 35, right: 20, top: 34, bottom: 20, containLabel: true },
|
|
tooltip: { trigger: 'axis' },
|
|
legend: { textStyle: { color: '#cbe7ff' }, data: ['整梯完工', 'VL2.5升级完工', '线缆/COP完工', '机加工完工'] },
|
|
xAxis: { type: 'category', axisLabel: { color: '#cbe7ff' }, data: xAxis },
|
|
yAxis: { type: 'value', axisLabel: { color: '#cbe7ff' }, splitLine: { lineStyle: { color: 'rgba(160,200,240,0.15)' } } },
|
|
series: [
|
|
{ name: '整梯完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: wholeSeries, itemStyle: { color: '#6cc7ff' }, lineStyle: { color: '#6cc7ff' }, areaStyle: { color: 'rgba(108,199,255,0.18)' } },
|
|
{ name: 'VL2.5升级完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: renovationSeries, itemStyle: { color: '#74e0b3' }, lineStyle: { color: '#74e0b3' }, areaStyle: { color: 'rgba(116,224,179,0.18)' } },
|
|
{ name: '线缆/COP完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: taskSeries, itemStyle: { color: '#7fa6d9' }, lineStyle: { color: '#7fa6d9' }, areaStyle: { color: 'rgba(127,166,217,0.16)' } },
|
|
{ name: '机加工完工', type: 'line', smooth: true, symbol: 'circle', symbolSize: 7, data: machiningSeries, itemStyle: { color: '#f5c15e' }, lineStyle: { color: '#f5c15e' }, areaStyle: { color: 'rgba(245,193,94,0.15)' } }
|
|
]
|
|
}, true)
|
|
},
|
|
countFinishByDay(list, day) {
|
|
return list.filter(item => {
|
|
const finishDate = this.toDayjs(item.finishDate)
|
|
return finishDate && finishDate.format('YYYY-MM-DD') === day.format('YYYY-MM-DD')
|
|
}).length
|
|
},
|
|
updateTime() {
|
|
const now = new Date()
|
|
const weekList = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
|
const d = this.dayjs(now)
|
|
this.currentTime = `${d.format('YYYY/MM/DD')} ${weekList[now.getDay()]} ${d.format('HH:mm:ss')}`
|
|
},
|
|
resizeCharts() {
|
|
if (this.chartBar) this.chartBar.resize()
|
|
if (this.chartPie) this.chartPie.resize()
|
|
if (this.chartLine) this.chartLine.resize()
|
|
},
|
|
disposeCharts() {
|
|
if (this.chartBar) { this.chartBar.dispose(); this.chartBar = null }
|
|
if (this.chartPie) { this.chartPie.dispose(); this.chartPie = null }
|
|
if (this.chartLine) { this.chartLine.dispose(); this.chartLine = null }
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.overview-page {
|
|
height: 100vh;
|
|
padding: 16px;
|
|
background: radial-gradient(circle at 20% 20%, #1b3352 0%, #102338 45%, #0a1523 100%);
|
|
color: #e7f3ff;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.overview-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 14px;
|
|
padding: 12px 16px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(100, 187, 255, 0.28);
|
|
background: rgba(12, 36, 58, 0.78);
|
|
position: relative;
|
|
}
|
|
|
|
.left-logo {
|
|
display: flex;
|
|
align-items: center;
|
|
min-width: 110px;
|
|
z-index: 2;
|
|
}
|
|
|
|
.logo-box {
|
|
width: 86px;
|
|
height: 30px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(128, 198, 255, 0.45);
|
|
color: #d8edff;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(36, 82, 122, 0.35);
|
|
}
|
|
|
|
.overview-header .header-center {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -50%);
|
|
text-align: center;
|
|
pointer-events: none;
|
|
max-width: 70%;
|
|
width: auto;
|
|
padding: 6px 24px 8px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(96, 170, 232, 0.34);
|
|
background: linear-gradient(180deg, rgba(33, 73, 116, 0.52), rgba(16, 44, 77, 0.42));
|
|
}
|
|
|
|
.overview-header .header-center::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
top: -8px;
|
|
width: 70%;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, rgba(87,164,230,0), rgba(87,164,230,.9), rgba(87,164,230,0));
|
|
}
|
|
|
|
.header-title {
|
|
font-size: 30px;
|
|
font-weight: 800;
|
|
letter-spacing: 3px;
|
|
line-height: 1.05;
|
|
color: #8fe7ff;
|
|
text-shadow: 0 0 14px rgba(79, 179, 255, 0.36);
|
|
}
|
|
|
|
.header-subtitle {
|
|
margin-top: 4px;
|
|
font-size: 12px;
|
|
color: #c7e8ff;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.right-tools {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-left: auto;
|
|
z-index: 2;
|
|
}
|
|
|
|
.time-text {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #7bd9ff;
|
|
}
|
|
|
|
.refresh-btn {
|
|
background: #ecf5ff;
|
|
border-color: #b3d8ff;
|
|
color: #409eff;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
background: #409eff;
|
|
border-color: #409eff;
|
|
color: #fff;
|
|
}
|
|
|
|
.kpi-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
gap: 10px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.kpi-card {
|
|
border-radius: 10px;
|
|
padding: 12px 14px;
|
|
background: linear-gradient(135deg, rgba(29, 66, 106, 0.95), rgba(17, 43, 69, 0.92));
|
|
border: 1px solid rgba(95, 180, 255, 0.24);
|
|
}
|
|
|
|
.kpi-label {
|
|
font-size: 12px;
|
|
color: #b9d8f8;
|
|
}
|
|
|
|
.kpi-value {
|
|
margin-top: 6px;
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: #dff0ff;
|
|
}
|
|
|
|
.kpi-value.highlight {
|
|
color: #67d2ff;
|
|
}
|
|
|
|
.kpi-value.warning {
|
|
color: #f5c15e;
|
|
}
|
|
|
|
.kpi-value.success {
|
|
color: #4ade80;
|
|
}
|
|
|
|
.chart-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
|
gap: 12px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
.panel {
|
|
border-radius: 10px;
|
|
background: rgba(13, 34, 54, 0.86);
|
|
border: 1px solid rgba(95, 180, 255, 0.24);
|
|
padding: 10px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.panel.wide {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #d9ebff;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.chart-box {
|
|
flex: 1;
|
|
min-height: 160px;
|
|
height: auto;
|
|
}
|
|
|
|
.line-box {
|
|
min-height: 160px;
|
|
}
|
|
|
|
.node-rate-grid {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 8px;
|
|
}
|
|
|
|
.node-rate-card {
|
|
padding: 8px 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(103, 168, 227, 0.25);
|
|
background: rgba(29, 58, 86, 0.5);
|
|
}
|
|
|
|
.node-rate-name {
|
|
color: #d1e7ff;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.node-rate-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.node-rate-count {
|
|
color: #9cc5ea;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.node-rate-badge {
|
|
min-width: 50px;
|
|
text-align: center;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.node-rate-badge.is-high {
|
|
color: #81f3bd;
|
|
background: rgba(23, 108, 80, 0.35);
|
|
}
|
|
|
|
.node-rate-badge.is-mid {
|
|
color: #ffd978;
|
|
background: rgba(122, 92, 25, 0.35);
|
|
}
|
|
|
|
.node-rate-badge.is-low {
|
|
color: #ff9fa3;
|
|
background: rgba(126, 49, 53, 0.35);
|
|
}
|
|
</style>
|