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

1 month ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
2 weeks ago
  1. <template>
  2. <div class="overview-page">
  3. <div class="overview-header">
  4. <div class="header-left"><img class="site-navbar__brand-logo" src="~@/assets/img/lc.png" alt="龙闯电梯"></div>
  5. <div class="header-center">
  6. <div class="header-title">龙闯电梯 MES - 工厂综合运营大屏</div>
  7. </div>
  8. <div class="right-tools">
  9. <div class="time-text">{{ currentTime }}</div>
  10. </div>
  11. </div>
  12. <div class="kpi-grid">
  13. <div class="kpi-card">
  14. <div class="kpi-label">近一月整梯排产</div>
  15. <div class="kpi-value">{{ kpi.monthWhole }}</div>
  16. </div>
  17. <div class="kpi-card">
  18. <div class="kpi-label">近一月VL2.5升级排产</div>
  19. <div class="kpi-value">{{ kpi.monthRenovation }}</div>
  20. </div>
  21. <div class="kpi-card">
  22. <div class="kpi-label">近一月线缆/COP任务</div>
  23. <div class="kpi-value">{{ kpi.monthTask }}</div>
  24. </div>
  25. <div class="kpi-card">
  26. <div class="kpi-label">近一月机加工任务</div>
  27. <div class="kpi-value">{{ kpi.monthMachining }}</div>
  28. </div>
  29. <div class="kpi-card">
  30. <div class="kpi-label">近一月完工达成率</div>
  31. <div class="kpi-value highlight">{{ kpi.finishRate }}%</div>
  32. </div>
  33. <div class="kpi-card">
  34. <div class="kpi-label">当前在制总量</div>
  35. <div class="kpi-value warning">{{ kpi.wipTotal }}</div>
  36. </div>
  37. <div class="kpi-card">
  38. <div class="kpi-label">订单准交率</div>
  39. <div class="kpi-value success">{{ kpi.onTimeRate }}%</div>
  40. </div>
  41. </div>
  42. <div class="chart-grid">
  43. <div class="panel">
  44. <div class="panel-title">近一月排产/完工对比</div>
  45. <div id="overviewBarChart" class="chart-box"></div>
  46. </div>
  47. <div class="panel">
  48. <div class="panel-title">生产结构分布</div>
  49. <div id="overviewPieChart" class="chart-box"></div>
  50. </div>
  51. <div class="panel wide">
  52. <div class="panel-title">最近 7 天完工趋势</div>
  53. <div id="overviewLineChart" class="chart-box line-box"></div>
  54. </div>
  55. <div class="panel">
  56. <div class="panel-title">关键环节达成率</div>
  57. <div class="node-rate-grid">
  58. <div v-for="item in nodeRates" :key="item.name" class="node-rate-card">
  59. <div class="node-rate-name">{{ item.name }}</div>
  60. <div class="node-rate-meta">
  61. <span class="node-rate-count">{{ item.done }}/{{ item.total }}</span>
  62. <span class="node-rate-badge" :class="getNodeRateClass(item.rate)">{{ item.rate }}%</span>
  63. </div>
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. </template>
  70. <script>
  71. import echarts from 'echarts'
  72. import { getFactoryOverviewBoardData } from '@/api/longchuang/productionPlan'
  73. const STATUS_ALLOW_LIST = ['已排产', '进行中', '已完成']
  74. export default {
  75. name: 'ScreenFactoryOverview',
  76. data() {
  77. return {
  78. currentTime: '',
  79. timerId: null,
  80. boardTimerId: null,
  81. loading: false,
  82. chartBar: null,
  83. chartPie: null,
  84. chartLine: null,
  85. wholeList: [],
  86. taskList: [],
  87. renovationList: [],
  88. machiningList: [],
  89. kpi: {
  90. monthWhole: 0,
  91. monthRenovation: 0,
  92. monthTask: 0,
  93. monthMachining: 0,
  94. finishRate: 0,
  95. wipTotal: 0,
  96. onTimeRate: 0
  97. },
  98. nodeRates: []
  99. }
  100. },
  101. mounted() {
  102. this.loadBoardData()
  103. this.updateTime()
  104. this.timerId = setInterval(this.updateTime, 1000)
  105. this.boardTimerId = setInterval(() => {
  106. this.loadBoardData()
  107. }, 60000)
  108. window.addEventListener('resize', this.resizeCharts)
  109. },
  110. beforeDestroy() {
  111. if (this.timerId) clearInterval(this.timerId)
  112. if (this.boardTimerId) clearInterval(this.boardTimerId)
  113. window.removeEventListener('resize', this.resizeCharts)
  114. this.disposeCharts()
  115. },
  116. methods: {
  117. loadBoardData() {
  118. if (this.loading) return
  119. this.loading = true
  120. getFactoryOverviewBoardData({ page: 1, limit: 500, statusList: STATUS_ALLOW_LIST }).then(({ data }) => {
  121. this.loading = false
  122. const boardData = data && data.code === 0 && data.boardData ? data.boardData : {}
  123. this.wholeList = this.normalizeBoardList(boardData.homeLiftList)
  124. this.taskList = this.normalizeBoardList(boardData.cableCopTaskList)
  125. this.renovationList = this.normalizeBoardList(boardData.renovationOrderList)
  126. this.machiningList = this.normalizeBoardList(boardData.machiningTaskList)
  127. this.buildDashboard()
  128. this.$nextTick(() => {
  129. setTimeout(() => {
  130. this.resizeCharts()
  131. }, 120)
  132. })
  133. }).catch(() => {
  134. this.loading = false
  135. this.wholeList = []
  136. this.taskList = []
  137. this.renovationList = []
  138. this.machiningList = []
  139. this.buildDashboard()
  140. })
  141. },
  142. normalizeBoardList(sourceList) {
  143. return (sourceList || [])
  144. .filter(item => STATUS_ALLOW_LIST.includes(item.status))
  145. .map(item => ({
  146. ...item,
  147. nodeList: item.nodeList || []
  148. }))
  149. },
  150. buildDashboard() {
  151. const periodStart = this.dayjs().subtract(1, 'month').startOf('day')
  152. const allOrderList = this.wholeList
  153. .concat(this.renovationList)
  154. .concat(this.taskList)
  155. .concat(this.machiningList)
  156. const recentOrderList = this.filterByDateRange(allOrderList, periodStart)
  157. const finishedAll = recentOrderList.filter(item => item.status === '已完成').length
  158. const inScheduleAll = recentOrderList.filter(item => STATUS_ALLOW_LIST.includes(item.status)).length
  159. const wipTotal = this.wholeList.filter(item => item.status === '进行中').length +
  160. this.taskList.filter(item => item.status === '进行中').length +
  161. this.renovationList.filter(item => item.status === '进行中').length +
  162. this.machiningList.filter(item => item.status === '进行中').length
  163. const monthWhole = this.countByMonth(this.wholeList, periodStart)
  164. const monthRenovation = this.countByMonth(this.renovationList, periodStart)
  165. const monthTask = this.countByMonth(this.taskList, periodStart)
  166. const monthMachining = this.countByMonth(this.machiningList, periodStart)
  167. const onTimeTotal = allOrderList.filter(item => item.status === '已完成').length
  168. const onTimeCount = allOrderList.filter(item => item.status === '已完成' && this.isOnTime(item)).length
  169. this.kpi.monthWhole = monthWhole
  170. this.kpi.monthRenovation = monthRenovation
  171. this.kpi.monthTask = monthTask
  172. this.kpi.monthMachining = monthMachining
  173. this.kpi.finishRate = inScheduleAll ? Math.round((finishedAll / inScheduleAll) * 100) : 0
  174. this.kpi.wipTotal = wipTotal
  175. this.kpi.onTimeRate = onTimeTotal ? Math.round((onTimeCount / onTimeTotal) * 100) : 0
  176. this.nodeRates = this.buildNodeRates()
  177. this.$nextTick(() => {
  178. this.renderBarChart()
  179. this.renderPieChart()
  180. this.renderLineChart()
  181. })
  182. },
  183. countByMonth(list, monthStart) {
  184. const startValue = monthStart.valueOf()
  185. return list.filter(item => {
  186. const dateVal = item.planDeliveryDate || item.planFinishDate || item.createTime
  187. const dateObj = this.toDayjs(dateVal)
  188. return dateObj && dateObj.valueOf() >= startValue
  189. }).length
  190. },
  191. filterByDateRange(list, startDate) {
  192. return (list || []).filter(item => {
  193. const dateVal = item.planDeliveryDate || item.planFinishDate || item.createTime
  194. const dateObj = this.toDayjs(dateVal)
  195. return dateObj && dateObj.valueOf() >= startDate.valueOf()
  196. })
  197. },
  198. countFinishedInRange(list, startDate) {
  199. return (list || []).filter(item => {
  200. if (item.status !== '已完成') return false
  201. const dateVal = item.finishDate || item.planDeliveryDate || item.planFinishDate || item.createTime
  202. const dateObj = this.toDayjs(dateVal)
  203. return dateObj && dateObj.valueOf() >= startDate.valueOf()
  204. }).length
  205. },
  206. isOnTime(item) {
  207. const finishDate = this.toDayjs(item.finishDate)
  208. const planDate = this.toDayjs(item.planDeliveryDate || item.planFinishDate)
  209. if (!finishDate || !planDate) return false
  210. return finishDate.valueOf() <= planDate.valueOf()
  211. },
  212. toDayjs(dateVal) {
  213. if (!dateVal) return null
  214. const d = this.dayjs(dateVal)
  215. return d && d.isValid() ? d : null
  216. },
  217. buildNodeRates() {
  218. const nodeMap = {}
  219. const all = this.wholeList.concat(this.taskList).concat(this.renovationList).concat(this.machiningList)
  220. all.forEach(item => {
  221. const list = item.nodeList || []
  222. list.forEach(node => {
  223. if (!nodeMap[node.nodeName]) nodeMap[node.nodeName] = { total: 0, done: 0 }
  224. nodeMap[node.nodeName].total += 1
  225. if (node.status === '已完成') nodeMap[node.nodeName].done += 1
  226. })
  227. })
  228. return Object.keys(nodeMap).map((name) => {
  229. const total = nodeMap[name].total
  230. const done = nodeMap[name].done
  231. return {
  232. name,
  233. total,
  234. done,
  235. rate: total ? Math.round((done / total) * 100) : 0,
  236. }
  237. }).sort((a, b) => b.rate - a.rate)
  238. },
  239. getNodeRateClass(rate) {
  240. if (rate >= 85) return 'is-high'
  241. if (rate >= 60) return 'is-mid'
  242. return 'is-low'
  243. },
  244. renderBarChart() {
  245. const el = document.getElementById('overviewBarChart')
  246. if (!el) return
  247. if (!this.chartBar) this.chartBar = echarts.init(el)
  248. const periodStart = this.dayjs().subtract(1, 'month').startOf('day')
  249. const scheduledWhole = this.countByMonth(this.wholeList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
  250. const scheduledRenovation = this.countByMonth(this.renovationList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
  251. const scheduledTask = this.countByMonth(this.taskList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
  252. const scheduledMachining = this.countByMonth(this.machiningList.filter(item => STATUS_ALLOW_LIST.includes(item.status)), periodStart)
  253. const finishedWhole = this.countFinishedInRange(this.wholeList, periodStart)
  254. const finishedRenovation = this.countFinishedInRange(this.renovationList, periodStart)
  255. const finishedTask = this.countFinishedInRange(this.taskList, periodStart)
  256. const finishedMachining = this.countFinishedInRange(this.machiningList, periodStart)
  257. this.chartBar.setOption({
  258. color: ['#4fa8ff', '#6ed3a6'],
  259. grid: { left: 30, right: 20, top: 40, bottom: 20, containLabel: true },
  260. tooltip: { trigger: 'axis' },
  261. legend: { textStyle: { color: '#cbe7ff' }, data: ['排产量', '完工量'] },
  262. xAxis: { type: 'category', axisLabel: { color: '#cbe7ff' }, data: ['整梯', 'VL2.5升级', '线缆/COP', '机加工'] },
  263. yAxis: { type: 'value', axisLabel: { color: '#cbe7ff' }, splitLine: { lineStyle: { color: 'rgba(160,200,240,0.15)' } } },
  264. series: [
  265. {
  266. name: '排产量',
  267. type: 'bar',
  268. barWidth: 24,
  269. data: [scheduledWhole, scheduledRenovation, scheduledTask, scheduledMachining],
  270. itemStyle: { color: '#4fa8ff' },
  271. label: {
  272. normal: {
  273. show: true,
  274. position: 'top',
  275. color: '#d4ebff',
  276. fontSize: 12,
  277. formatter: '{c}'
  278. }
  279. }
  280. },
  281. {
  282. name: '完工量',
  283. type: 'bar',
  284. barWidth: 24,
  285. data: [finishedWhole, finishedRenovation, finishedTask, finishedMachining],
  286. itemStyle: { color: '#6ed3a6' },
  287. label: {
  288. normal: {
  289. show: true,
  290. position: 'top',
  291. color: '#d4ebff',
  292. fontSize: 12,
  293. formatter: '{c}'
  294. }
  295. }
  296. }
  297. ]
  298. }, true)
  299. },
  300. renderPieChart() {
  301. const el = document.getElementById('overviewPieChart')
  302. if (!el) return
  303. if (!this.chartPie) this.chartPie = echarts.init(el)
  304. const wholeCount = this.wholeList.length
  305. const renovationCount = this.renovationList.length
  306. const taskCount = this.taskList.length
  307. const machiningCount = this.machiningList.length
  308. const pieData = [
  309. { value: wholeCount, name: '整梯订单' },
  310. { value: renovationCount, name: 'VL2.5升级订单' },
  311. { value: taskCount, name: '线缆/COP任务' },
  312. { value: machiningCount, name: '机加工任务' }
  313. ]
  314. this.chartPie.setOption({
  315. color: ['#57b8ff', '#5dd4b0', '#7fa6d9', '#f5c15e'],
  316. tooltip: {
  317. trigger: 'item',
  318. formatter: (params) => `${params.name}<br/>数量:${params.value}<br/>占比:${params.percent || 0}%`
  319. },
  320. legend: { bottom: 2, textStyle: { color: '#cbe7ff' } },
  321. series: [{
  322. type: 'pie',
  323. radius: ['42%', '72%'],
  324. center: ['50%', '45%'],
  325. avoidLabelOverlap: false,
  326. minShowLabelAngle: 0,
  327. label: {
  328. normal: {
  329. show: true,
  330. position: 'outside',
  331. color: '#d4ebff',
  332. fontSize: 14,
  333. formatter: '{b} {d}%'
  334. },
  335. emphasis: {
  336. show: true,
  337. formatter: '{b} {d}%'
  338. }
  339. },
  340. labelLine: {
  341. normal: {
  342. show: true,
  343. length: 14,
  344. length2: 10,
  345. lineStyle: {
  346. color: '#7fb4de'
  347. }
  348. }
  349. },
  350. data: pieData
  351. }]
  352. }, true)
  353. },
  354. renderLineChart() {
  355. const el = document.getElementById('overviewLineChart')
  356. if (!el) return
  357. if (!this.chartLine) this.chartLine = echarts.init(el)
  358. const xAxis = []
  359. const wholeSeries = []
  360. const renovationSeries = []
  361. const taskSeries = []
  362. const machiningSeries = []
  363. for (let i = 6; i >= 0; i--) {
  364. const day = this.dayjs().subtract(i, 'day')
  365. xAxis.push(day.format('MM-DD'))
  366. wholeSeries.push(this.countFinishByDay(this.wholeList, day))
  367. renovationSeries.push(this.countFinishByDay(this.renovationList, day))
  368. taskSeries.push(this.countFinishByDay(this.taskList, day))
  369. machiningSeries.push(this.countFinishByDay(this.machiningList, day))
  370. }
  371. this.chartLine.setOption({
  372. color: ['#6cc7ff', '#74e0b3', '#7fa6d9', '#f5c15e'],
  373. grid: { left: 35, right: 20, top: 34, bottom: 20, containLabel: true },
  374. tooltip: { trigger: 'axis' },
  375. legend: { textStyle: { color: '#cbe7ff' }, data: ['整梯完工', 'VL2.5升级完工', '线缆/COP完工', '机加工完工'] },
  376. xAxis: { type: 'category', axisLabel: { color: '#cbe7ff' }, data: xAxis },
  377. yAxis: { type: 'value', axisLabel: { color: '#cbe7ff' }, splitLine: { lineStyle: { color: 'rgba(160,200,240,0.15)' } } },
  378. series: [
  379. { 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)' } },
  380. { 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)' } },
  381. { 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)' } },
  382. { 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)' } }
  383. ]
  384. }, true)
  385. },
  386. countFinishByDay(list, day) {
  387. return list.filter(item => {
  388. const finishDate = this.toDayjs(item.finishDate)
  389. return finishDate && finishDate.format('YYYY-MM-DD') === day.format('YYYY-MM-DD')
  390. }).length
  391. },
  392. updateTime() {
  393. const now = new Date()
  394. const weekList = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  395. const d = this.dayjs(now)
  396. this.currentTime = `${d.format('YYYY/MM/DD')} ${weekList[now.getDay()]} ${d.format('HH:mm:ss')}`
  397. },
  398. resizeCharts() {
  399. if (this.chartBar) this.chartBar.resize()
  400. if (this.chartPie) this.chartPie.resize()
  401. if (this.chartLine) this.chartLine.resize()
  402. },
  403. disposeCharts() {
  404. if (this.chartBar) { this.chartBar.dispose(); this.chartBar = null }
  405. if (this.chartPie) { this.chartPie.dispose(); this.chartPie = null }
  406. if (this.chartLine) { this.chartLine.dispose(); this.chartLine = null }
  407. }
  408. }
  409. }
  410. </script>
  411. <style scoped>
  412. .overview-page {
  413. height: 100vh;
  414. padding: 16px;
  415. background: radial-gradient(circle at 20% 20%, #1b3352 0%, #102338 45%, #0a1523 100%);
  416. color: #e7f3ff;
  417. overflow: hidden;
  418. display: flex;
  419. flex-direction: column;
  420. box-sizing: border-box;
  421. }
  422. .overview-header {
  423. display: flex;
  424. justify-content: space-between;
  425. align-items: center;
  426. margin-bottom: 14px;
  427. padding: 12px 16px;
  428. border-radius: 10px;
  429. border: 1px solid rgba(100, 187, 255, 0.28);
  430. background: rgba(12, 36, 58, 0.78);
  431. position: relative;
  432. }
  433. .left-logo {
  434. display: flex;
  435. align-items: center;
  436. min-width: 110px;
  437. z-index: 2;
  438. }
  439. .logo-box {
  440. width: 86px;
  441. height: 30px;
  442. border-radius: 6px;
  443. border: 1px solid rgba(128, 198, 255, 0.45);
  444. color: #d8edff;
  445. font-size: 14px;
  446. font-weight: 700;
  447. display: flex;
  448. align-items: center;
  449. justify-content: center;
  450. background: rgba(36, 82, 122, 0.35);
  451. }
  452. .overview-header .header-center {
  453. position: absolute;
  454. left: 50%;
  455. top: 50%;
  456. transform: translate(-50%, -50%);
  457. text-align: center;
  458. pointer-events: none;
  459. max-width: 70%;
  460. width: auto;
  461. padding: 6px 24px 8px;
  462. border-radius: 8px;
  463. border: 1px solid rgba(96, 170, 232, 0.34);
  464. background: linear-gradient(180deg, rgba(33, 73, 116, 0.52), rgba(16, 44, 77, 0.42));
  465. }
  466. .overview-header .header-center::before {
  467. content: '';
  468. position: absolute;
  469. left: 50%;
  470. transform: translateX(-50%);
  471. top: -8px;
  472. width: 70%;
  473. height: 1px;
  474. background: linear-gradient(90deg, rgba(87,164,230,0), rgba(87,164,230,.9), rgba(87,164,230,0));
  475. }
  476. .header-title {
  477. font-size: 30px;
  478. font-weight: 800;
  479. letter-spacing: 3px;
  480. line-height: 1.05;
  481. color: #8fe7ff;
  482. text-shadow: 0 0 14px rgba(79, 179, 255, 0.36);
  483. }
  484. .header-subtitle {
  485. margin-top: 4px;
  486. font-size: 12px;
  487. color: #c7e8ff;
  488. letter-spacing: 1px;
  489. }
  490. .right-tools {
  491. display: flex;
  492. align-items: center;
  493. gap: 12px;
  494. margin-left: auto;
  495. z-index: 2;
  496. }
  497. .time-text {
  498. font-size: 18px;
  499. font-weight: 600;
  500. color: #7bd9ff;
  501. }
  502. .refresh-btn {
  503. background: #ecf5ff;
  504. border-color: #b3d8ff;
  505. color: #409eff;
  506. }
  507. .refresh-btn:hover {
  508. background: #409eff;
  509. border-color: #409eff;
  510. color: #fff;
  511. }
  512. .kpi-grid {
  513. display: grid;
  514. grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  515. gap: 10px;
  516. margin-bottom: 14px;
  517. }
  518. .kpi-card {
  519. border-radius: 10px;
  520. padding: 12px 14px;
  521. background: linear-gradient(135deg, rgba(29, 66, 106, 0.95), rgba(17, 43, 69, 0.92));
  522. border: 1px solid rgba(95, 180, 255, 0.24);
  523. }
  524. .kpi-label {
  525. font-size: 12px;
  526. color: #b9d8f8;
  527. }
  528. .kpi-value {
  529. margin-top: 6px;
  530. font-size: 28px;
  531. font-weight: 700;
  532. color: #dff0ff;
  533. }
  534. .kpi-value.highlight {
  535. color: #67d2ff;
  536. }
  537. .kpi-value.warning {
  538. color: #f5c15e;
  539. }
  540. .kpi-value.success {
  541. color: #4ade80;
  542. }
  543. .chart-grid {
  544. display: grid;
  545. grid-template-columns: 1fr 1fr;
  546. grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
  547. gap: 12px;
  548. flex: 1;
  549. min-height: 0;
  550. }
  551. .panel {
  552. border-radius: 10px;
  553. background: rgba(13, 34, 54, 0.86);
  554. border: 1px solid rgba(95, 180, 255, 0.24);
  555. padding: 10px 12px;
  556. display: flex;
  557. flex-direction: column;
  558. min-height: 0;
  559. }
  560. .panel.wide {
  561. grid-column: span 1;
  562. }
  563. .panel-title {
  564. font-size: 14px;
  565. font-weight: 600;
  566. color: #d9ebff;
  567. margin-bottom: 8px;
  568. }
  569. .chart-box {
  570. flex: 1;
  571. min-height: 160px;
  572. height: auto;
  573. }
  574. .line-box {
  575. min-height: 160px;
  576. }
  577. .node-rate-grid {
  578. flex: 1;
  579. min-height: 0;
  580. overflow: hidden;
  581. display: grid;
  582. grid-template-columns: repeat(2, minmax(0, 1fr));
  583. gap: 8px;
  584. }
  585. .node-rate-card {
  586. padding: 8px 10px;
  587. border-radius: 8px;
  588. border: 1px solid rgba(103, 168, 227, 0.25);
  589. background: rgba(29, 58, 86, 0.5);
  590. }
  591. .node-rate-name {
  592. color: #d1e7ff;
  593. font-size: 12px;
  594. white-space: nowrap;
  595. overflow: hidden;
  596. text-overflow: ellipsis;
  597. }
  598. .node-rate-meta {
  599. display: flex;
  600. justify-content: space-between;
  601. align-items: center;
  602. margin-top: 6px;
  603. }
  604. .node-rate-count {
  605. color: #9cc5ea;
  606. font-size: 12px;
  607. }
  608. .node-rate-badge {
  609. min-width: 50px;
  610. text-align: center;
  611. padding: 2px 8px;
  612. border-radius: 10px;
  613. font-size: 12px;
  614. font-weight: 700;
  615. }
  616. .node-rate-badge.is-high {
  617. color: #81f3bd;
  618. background: rgba(23, 108, 80, 0.35);
  619. }
  620. .node-rate-badge.is-mid {
  621. color: #ffd978;
  622. background: rgba(122, 92, 25, 0.35);
  623. }
  624. .node-rate-badge.is-low {
  625. color: #ff9fa3;
  626. background: rgba(126, 49, 53, 0.35);
  627. }
  628. </style>