|
|
|
|
@ -1,7 +1,7 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="dashboard-page">
|
|
|
|
|
<!-- 统计卡片 -->
|
|
|
|
|
<el-row :gutter="20" class="stat-row">
|
|
|
|
|
<!-- 统计卡片 第1行 -->
|
|
|
|
|
<el-row :gutter="16" class="stat-row">
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
@ -29,20 +29,10 @@
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-sub" v-if="collectorStatus.status === 'running'">运行 {{ formatUptime(collectorStatus.uptimeSeconds) }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 采集服务控制按钮 -->
|
|
|
|
|
<div class="collector-actions" style="margin-top: 12px; display: flex; gap: 8px; justify-content: flex-start; flex-wrap: wrap;">
|
|
|
|
|
<!-- 当采集服务未运行时,显示启动按钮 -->
|
|
|
|
|
<el-button v-if="collectorStatus.status !== 'running'" size="small" type="success" :loading="startLoading" @click="startCollector">
|
|
|
|
|
启动采集
|
|
|
|
|
</el-button>
|
|
|
|
|
<!-- 当采集服务运行中,显示停止按钮 -->
|
|
|
|
|
<el-button v-if="collectorStatus.status === 'running'" size="small" type="danger" :loading="stopLoading" @click="stopCollector">
|
|
|
|
|
停止采集
|
|
|
|
|
</el-button>
|
|
|
|
|
<!-- 始终显示刷新配置按钮 -->
|
|
|
|
|
<el-button size="small" type="warning" :loading="refreshLoading" @click="refreshCollectorConfig">
|
|
|
|
|
刷新配置
|
|
|
|
|
</el-button>
|
|
|
|
|
<div class="collector-actions">
|
|
|
|
|
<el-button v-if="collectorStatus.status !== 'running'" size="small" type="success" :loading="startLoading" @click="startCollector">启动采集</el-button>
|
|
|
|
|
<el-button v-if="collectorStatus.status === 'running'" size="small" type="danger" :loading="stopLoading" @click="stopCollector">停止采集</el-button>
|
|
|
|
|
<el-button size="small" type="warning" :loading="refreshLoading" @click="refreshCollectorConfig">刷新配置</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
@ -58,22 +48,108 @@
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 统计卡片 第2行 -->
|
|
|
|
|
<el-row :gutter="16" class="stat-row">
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-label">采集成功率</div>
|
|
|
|
|
<div class="stat-value">{{ summary.collectSuccessRate }}<span class="stat-unit">%</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-label">今日切削总时</div>
|
|
|
|
|
<div class="stat-value">{{ summary.todayCuttingTime }}<span class="stat-unit"> h</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-label">运行机床</div>
|
|
|
|
|
<div class="stat-value">{{ summary.runningMachines }}<span class="stat-unit"> 台</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-label">数据缺失</div>
|
|
|
|
|
<div class="stat-value" :class="{ 'alert-value': summary.dataMissingMachines > 0 }">{{ summary.dataMissingMachines }}<span class="stat-unit"> 台</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 图表区:产量趋势 + 车间对比 -->
|
|
|
|
|
<el-row :gutter="16" class="chart-row">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header><span class="card-title">产量趋势(近7天)</span></template>
|
|
|
|
|
<div ref="trendChartRef" style="height: 260px"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header><span class="card-title">车间产量对比(今日)</span></template>
|
|
|
|
|
<div ref="workshopChartRef" style="height: 260px"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 信息区:机床状态分布 + 最新告警 -->
|
|
|
|
|
<el-row :gutter="16" class="info-row">
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header><span class="card-title">机床状态分布</span></template>
|
|
|
|
|
<div ref="statusPieRef" style="height: 260px"></div>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="16">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span class="card-title">最新告警</span>
|
|
|
|
|
<el-button link type="primary" @click="$router.push(isMock ? '/mock/alert' : '/alert')">查看全部</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table :data="recentAlerts" stripe size="small" style="width: 100%">
|
|
|
|
|
<el-table-column prop="createdAt" label="时间" width="160" />
|
|
|
|
|
<el-table-column label="类型" width="100" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="alertTypeTag(row.alertType)" size="small">{{ alertTypeLabel(row.alertType) }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="machineName" label="机床" width="100" />
|
|
|
|
|
<el-table-column prop="title" label="告警内容" show-overflow-tooltip />
|
|
|
|
|
<el-table-column label="状态" width="80" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.isResolved ? 'success' : 'danger'" size="small">{{ row.isResolved ? '已处理' : '未处理' }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 排行表格 -->
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 20px;">
|
|
|
|
|
<el-row :gutter="16" class="rank-row">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>机床产量排行 TOP10</span>
|
|
|
|
|
<span class="card-title">机床产量排行 TOP10</span>
|
|
|
|
|
<span class="card-sub">今日</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table :data="machineRank" stripe size="small" style="width: 100%">
|
|
|
|
|
<el-table-column prop="rank" label="排名" width="60" align="center" />
|
|
|
|
|
<!-- 机床名称:跳转链接,mock模式跳转到/mock/machine/{id} -->
|
|
|
|
|
<el-table-column label="机床名称" width="120">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<router-link :to="isMock ? '/mock/machine/' + row.id : '/machine/' + row.id" class="machine-link">{{ row.machineName }}</router-link>
|
|
|
|
|
<router-link :to="isMock ? '/mock/machine/' + row.machineId : '/machine/' + row.machineId" class="machine-link">{{ row.machineName }}</router-link>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="program" label="当前程序" show-overflow-tooltip />
|
|
|
|
|
@ -90,7 +166,7 @@
|
|
|
|
|
<el-card shadow="hover">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
<span>工人产量排行 TOP10</span>
|
|
|
|
|
<span class="card-title">工人产量排行 TOP10</span>
|
|
|
|
|
<span class="card-sub">今日</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
@ -107,61 +183,53 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
|
|
import request from '@/utils/request'
|
|
|
|
|
import { useMockMode } from '@/composables/useMockMode'
|
|
|
|
|
import type { ApiResponse, DashboardSummary, CollectorStatus, MachineRankRow, WorkerRankRow } from '@/types'
|
|
|
|
|
import echarts from '@/utils/echarts'
|
|
|
|
|
import type { ECharts } from 'echarts/core'
|
|
|
|
|
import type { ApiResponse, DashboardSummary, CollectorStatus, MachineRankRow, WorkerRankRow, DashboardTrendItem, WorkshopProduction, MachineStatusDistribution, RecentAlert } from '@/types'
|
|
|
|
|
|
|
|
|
|
const { isMock } = useMockMode()
|
|
|
|
|
|
|
|
|
|
const summary = ref<DashboardSummary>({ onlineCount: 0, totalMachines: 0, todayProduction: 0, activeAlerts: 0 } as DashboardSummary)
|
|
|
|
|
const summary = ref<DashboardSummary>({ onlineCount: 0, totalMachines: 0, todayProduction: 0, activeAlerts: 0, collectSuccessRate: 0, todayCuttingTime: 0, runningMachines: 0, dataMissingMachines: 0 })
|
|
|
|
|
const collectorStatus = ref<CollectorStatus>({ status: 'stopped', uptimeSeconds: 0 })
|
|
|
|
|
const machineRank = ref<MachineRankRow[]>([])
|
|
|
|
|
const workerRank = ref<WorkerRankRow[]>([])
|
|
|
|
|
// 采集服务按钮加载状态,防止重复点击
|
|
|
|
|
const trendData = ref<DashboardTrendItem[]>([])
|
|
|
|
|
const workshopData = ref<WorkshopProduction[]>([])
|
|
|
|
|
const statusDist = ref<MachineStatusDistribution>({ online: 0, offline: 0, disabled: 0 })
|
|
|
|
|
const recentAlerts = ref<RecentAlert[]>([])
|
|
|
|
|
|
|
|
|
|
const startLoading = ref(false)
|
|
|
|
|
const stopLoading = ref(false)
|
|
|
|
|
const refreshLoading = ref(false)
|
|
|
|
|
let refreshTimer: number | undefined
|
|
|
|
|
|
|
|
|
|
// 启动采集
|
|
|
|
|
// ECharts refs
|
|
|
|
|
const trendChartRef = ref<HTMLElement>()
|
|
|
|
|
const workshopChartRef = ref<HTMLElement>()
|
|
|
|
|
const statusPieRef = ref<HTMLElement>()
|
|
|
|
|
let trendChart: ECharts | null = null
|
|
|
|
|
let workshopChart: ECharts | null = null
|
|
|
|
|
let statusPie: ECharts | null = null
|
|
|
|
|
|
|
|
|
|
async function startCollector() {
|
|
|
|
|
if (startLoading.value) return
|
|
|
|
|
startLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
await request.post('/admin/collector/start')
|
|
|
|
|
await loadData()
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// 这里可以统一错误处理,保持接口不暴露细节
|
|
|
|
|
} finally {
|
|
|
|
|
startLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
try { await request.post('/admin/collector/start'); await loadData() } catch { /* */ } finally { startLoading.value = false }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 停止采集
|
|
|
|
|
async function stopCollector() {
|
|
|
|
|
if (stopLoading.value) return
|
|
|
|
|
stopLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
await request.post('/admin/collector/stop')
|
|
|
|
|
await loadData()
|
|
|
|
|
} catch {
|
|
|
|
|
} finally {
|
|
|
|
|
stopLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
try { await request.post('/admin/collector/stop'); await loadData() } catch { /* */ } finally { stopLoading.value = false }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 刷新配置
|
|
|
|
|
async function refreshCollectorConfig() {
|
|
|
|
|
if (refreshLoading.value) return
|
|
|
|
|
refreshLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
await request.post('/admin/collector/refresh')
|
|
|
|
|
await loadData()
|
|
|
|
|
} catch {
|
|
|
|
|
} finally {
|
|
|
|
|
refreshLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
try { await request.post('/admin/collector/refresh'); await loadData() } catch { /* */ } finally { refreshLoading.value = false }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatUptime(seconds: number): string {
|
|
|
|
|
@ -172,75 +240,182 @@ function formatUptime(seconds: number): string {
|
|
|
|
|
return `${hours}时`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function alertTypeTag(type: string): string {
|
|
|
|
|
const map: Record<string, string> = { collect_fail: 'danger', data_missing: 'warning', device_offline: 'danger', new_device: 'info' }
|
|
|
|
|
return map[type] || 'warning'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function alertTypeLabel(type: string): string {
|
|
|
|
|
const map: Record<string, string> = { collect_fail: '采集失败', data_missing: '数据缺失', device_offline: '设备离线', new_device: '新设备' }
|
|
|
|
|
return map[type] || type
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initCharts() {
|
|
|
|
|
// 产量趋势折线图
|
|
|
|
|
if (trendChartRef.value && trendData.value.length) {
|
|
|
|
|
trendChart = echarts.init(trendChartRef.value)
|
|
|
|
|
trendChart.setOption({
|
|
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
|
|
grid: { left: 50, right: 20, top: 20, bottom: 30 },
|
|
|
|
|
xAxis: { type: 'category', data: trendData.value.map(i => i.date.slice(5)), axisLabel: { fontSize: 12 } },
|
|
|
|
|
yAxis: { type: 'value', axisLabel: { fontSize: 12 } },
|
|
|
|
|
series: [{
|
|
|
|
|
type: 'line', data: trendData.value.map(i => i.quantity), smooth: true,
|
|
|
|
|
areaStyle: { opacity: 0.15 }, itemStyle: { color: '#409EFF' },
|
|
|
|
|
}],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 车间产量柱状图
|
|
|
|
|
if (workshopChartRef.value && workshopData.value.length) {
|
|
|
|
|
workshopChart = echarts.init(workshopChartRef.value)
|
|
|
|
|
workshopChart.setOption({
|
|
|
|
|
tooltip: { trigger: 'axis' },
|
|
|
|
|
grid: { left: 50, right: 20, top: 20, bottom: 30 },
|
|
|
|
|
xAxis: { type: 'category', data: workshopData.value.map(i => i.workshopName), axisLabel: { fontSize: 12 } },
|
|
|
|
|
yAxis: { type: 'value', axisLabel: { fontSize: 12 } },
|
|
|
|
|
series: [{
|
|
|
|
|
type: 'bar', data: workshopData.value.map(i => i.quantity),
|
|
|
|
|
itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] }, barWidth: '40%',
|
|
|
|
|
}],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 机床状态饼图
|
|
|
|
|
if (statusPieRef.value) {
|
|
|
|
|
const d = statusDist.value
|
|
|
|
|
statusPie = echarts.init(statusPieRef.value)
|
|
|
|
|
statusPie.setOption({
|
|
|
|
|
tooltip: { trigger: 'item', formatter: '{b}: {c}台 ({d}%)' },
|
|
|
|
|
legend: { bottom: 0, itemWidth: 12, itemHeight: 12, textStyle: { fontSize: 12 } },
|
|
|
|
|
series: [{
|
|
|
|
|
type: 'pie', radius: ['40%', '65%'], center: ['50%', '45%'],
|
|
|
|
|
label: { show: true, formatter: '{b}\n{c}台', fontSize: 12 },
|
|
|
|
|
data: [
|
|
|
|
|
{ value: d.online, name: '在线', itemStyle: { color: '#67C23A' } },
|
|
|
|
|
{ value: d.offline, name: '离线', itemStyle: { color: '#909399' } },
|
|
|
|
|
{ value: d.disabled, name: '停用', itemStyle: { color: '#F56C6C' } },
|
|
|
|
|
],
|
|
|
|
|
}],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function disposeCharts() {
|
|
|
|
|
trendChart?.dispose()
|
|
|
|
|
workshopChart?.dispose()
|
|
|
|
|
statusPie?.dispose()
|
|
|
|
|
trendChart = null
|
|
|
|
|
workshopChart = null
|
|
|
|
|
statusPie = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadData() {
|
|
|
|
|
const [summaryRes, collectorRes, machineRankRes, workerRankRes]: [ApiResponse<DashboardSummary>, ApiResponse<CollectorStatus>, ApiResponse<{ items: MachineRankRow[] }>, ApiResponse<{ items: WorkerRankRow[] }> ] = await Promise.all([
|
|
|
|
|
request.get('/admin/dashboard/summary'),
|
|
|
|
|
request.get('/admin/collector/status'),
|
|
|
|
|
request.get('/admin/dashboard/machine-rank'),
|
|
|
|
|
request.get('/admin/dashboard/worker-rank'),
|
|
|
|
|
])
|
|
|
|
|
summary.value = summaryRes.data || summary.value
|
|
|
|
|
collectorStatus.value = collectorRes.data || collectorStatus.value
|
|
|
|
|
machineRank.value = machineRankRes.data?.items || []
|
|
|
|
|
workerRank.value = workerRankRes.data?.items || []
|
|
|
|
|
try {
|
|
|
|
|
const [summaryRes, collectorRes, machineRankRes, workerRankRes, trendRes, workshopRes, statusRes, alertsRes]: [
|
|
|
|
|
ApiResponse<DashboardSummary>, ApiResponse<CollectorStatus>, ApiResponse<{ items: MachineRankRow[] }>,
|
|
|
|
|
ApiResponse<{ items: WorkerRankRow }>, ApiResponse<{ items: DashboardTrendItem[] }>,
|
|
|
|
|
ApiResponse<{ items: WorkshopProduction }>, ApiResponse<MachineStatusDistribution>,
|
|
|
|
|
ApiResponse<{ items: RecentAlert[] }>
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
request.get('/admin/dashboard/summary'),
|
|
|
|
|
request.get('/admin/collector/status'),
|
|
|
|
|
request.get('/admin/dashboard/machine-rank'),
|
|
|
|
|
request.get('/admin/dashboard/worker-rank'),
|
|
|
|
|
request.get('/admin/dashboard/trend'),
|
|
|
|
|
request.get('/admin/dashboard/workshop-production'),
|
|
|
|
|
request.get('/admin/dashboard/machine-status-distribution'),
|
|
|
|
|
request.get('/admin/dashboard/recent-alerts'),
|
|
|
|
|
])
|
|
|
|
|
summary.value = summaryRes.data || summary.value
|
|
|
|
|
collectorStatus.value = collectorRes.data || collectorStatus.value
|
|
|
|
|
machineRank.value = machineRankRes.data?.items || []
|
|
|
|
|
workerRank.value = workerRankRes.data?.items || []
|
|
|
|
|
trendData.value = trendRes.data?.items || []
|
|
|
|
|
workshopData.value = workshopRes.data?.items || []
|
|
|
|
|
statusDist.value = statusRes.data || statusDist.value
|
|
|
|
|
recentAlerts.value = alertsRes.data?.items || []
|
|
|
|
|
|
|
|
|
|
disposeCharts()
|
|
|
|
|
await nextTick()
|
|
|
|
|
initCharts()
|
|
|
|
|
} catch {
|
|
|
|
|
// 保持容错
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始加载数据并开启自动刷新定时器
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadData()
|
|
|
|
|
// 每30秒自动刷新仪表盘数据
|
|
|
|
|
refreshTimer = window.setInterval(loadData, 30000) as unknown as number
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (typeof refreshTimer === 'number') {
|
|
|
|
|
clearInterval(refreshTimer)
|
|
|
|
|
}
|
|
|
|
|
if (typeof refreshTimer === 'number') clearInterval(refreshTimer)
|
|
|
|
|
disposeCharts()
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.dashboard-page {
|
|
|
|
|
.stat-row {
|
|
|
|
|
.el-card {
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
.el-card { height: 100%; }
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #303133;
|
|
|
|
|
.stat-card {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
|
|
|
|
|
.stat-unit {
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
color: #909399;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-sub {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
|
|
|
|
.stat-unit {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: normal;
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-sub {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.alert-value {
|
|
|
|
|
color: #e6a23c;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.alert-value {
|
|
|
|
|
color: #e6a23c;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
.collector-actions {
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-row,
|
|
|
|
|
.info-row,
|
|
|
|
|
.rank-row {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
@ -251,6 +426,7 @@ onUnmounted(() => {
|
|
|
|
|
color: #909399;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.machine-link {
|
|
|
|
|
color: var(--el-color-primary);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|