You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
6.4 KiB
Vue
262 lines
6.4 KiB
Vue
<template>
|
|
<div class="card">
|
|
<div class="panel-title">
|
|
<span class="title-dot"></span>
|
|
<span>设备概况</span>
|
|
</div>
|
|
<div class="panel-body overview-body">
|
|
<div v-for="item in overviewItems" :key="item.key" class="gauge-item">
|
|
<div class="gauge" :style="getGaugeStyle(item.percent, item.color)">
|
|
<div class="gauge-inner">
|
|
<div class="gauge-value">{{ item.value }}</div>
|
|
<div class="gauge-label">{{ item.label }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { colors } from '../utils'
|
|
import { DashboardApi } from '@/api/dashboard'
|
|
|
|
type OverviewItemKey = 'device' | 'running' | 'idle' | 'alarm' | 'utilization' | 'faultRate'
|
|
|
|
interface OverviewItem {
|
|
key: OverviewItemKey
|
|
label: string
|
|
value: string
|
|
percent: number
|
|
color: string
|
|
}
|
|
|
|
const route = useRoute()
|
|
const orgId = route.query.orgId
|
|
|
|
const overviewItems = ref<OverviewItem[]>([
|
|
{ key: 'device', label: '设备数量', value: '0', percent: 0, color: colors.cyan },
|
|
{ key: 'running', label: '运行数量', value: '0', percent: 0, color: colors.blue },
|
|
{ key: 'idle', label: '待机数量', value: '0', percent: 0, color: colors.warn },
|
|
{ key: 'alarm', label: '报警数量', value: '0', percent: 0, color: colors.danger },
|
|
{ key: 'utilization', label: '稼动率', value: '0%', percent: 0, color: colors.green },
|
|
{ key: 'faultRate', label: '故障率', value: '0%', percent: 0, color: colors.purple }
|
|
])
|
|
|
|
const getGaugeStyle = (percent: number, color: string) => {
|
|
const p = Math.max(0, Math.min(100, percent))
|
|
return {
|
|
background: `conic-gradient(${color} ${p * 3.6}deg, rgba(148,163,184,0.18) 0deg)`
|
|
}
|
|
}
|
|
|
|
const formatNumber = (value: number | string | undefined | null) => {
|
|
const n = Number(value ?? 0)
|
|
if (!Number.isFinite(n)) return '0'
|
|
return n.toLocaleString('zh-CN')
|
|
}
|
|
|
|
const calcPercent = (part: number, total: number) => {
|
|
if (!total || total <= 0 || !Number.isFinite(part)) return 0
|
|
const raw = (part / total) * 100
|
|
return Math.max(0, Math.min(100, Math.round(raw)))
|
|
}
|
|
|
|
const loadOverview = async () => {
|
|
try {
|
|
const res: any = await DashboardApi.getDeviceOperationalStatus({ orgId })
|
|
const data = (res && typeof res === 'object' ? (res.data ?? res) : {}) as any
|
|
|
|
const totalDevices = Number(data.totalDevices ?? 0)
|
|
const runningCount = Number(data.runningCount ?? 0)
|
|
const standbyCount = Number(data.standbyCount ?? 0)
|
|
const faultCount = Number(data.faultCount ?? 0)
|
|
const warningCount = Number(data.warningCount ?? 0)
|
|
|
|
const utilizationRateRaw = String(data.utilizationRate ?? '0')
|
|
const faultRateRaw = String(data.faultRate ?? '0')
|
|
|
|
const utilizationPercent = Number.parseFloat(utilizationRateRaw.replace('%', '')) || 0
|
|
const faultPercent = Number.parseFloat(faultRateRaw.replace('%', '')) || 0
|
|
|
|
const alarmCount = faultCount + warningCount
|
|
|
|
overviewItems.value = overviewItems.value.map((item) => {
|
|
if (item.key === 'device') {
|
|
return {
|
|
...item,
|
|
value: formatNumber(totalDevices),
|
|
percent: calcPercent(totalDevices, totalDevices || 1)
|
|
}
|
|
}
|
|
if (item.key === 'running') {
|
|
return {
|
|
...item,
|
|
value: formatNumber(runningCount),
|
|
percent: calcPercent(runningCount, totalDevices)
|
|
}
|
|
}
|
|
if (item.key === 'idle') {
|
|
return {
|
|
...item,
|
|
value: formatNumber(standbyCount),
|
|
percent: calcPercent(standbyCount, totalDevices)
|
|
}
|
|
}
|
|
if (item.key === 'alarm') {
|
|
return {
|
|
...item,
|
|
value: formatNumber(alarmCount),
|
|
percent: calcPercent(alarmCount, totalDevices)
|
|
}
|
|
}
|
|
if (item.key === 'utilization') {
|
|
const p = Math.max(0, Math.min(100, Math.round(utilizationPercent)))
|
|
return {
|
|
...item,
|
|
value: utilizationRateRaw.includes('%') ? utilizationRateRaw : `${utilizationPercent.toFixed(2)}%`,
|
|
percent: p
|
|
}
|
|
}
|
|
if (item.key === 'faultRate') {
|
|
const p = Math.max(0, Math.min(100, Math.round(faultPercent)))
|
|
return {
|
|
...item,
|
|
value: faultRateRaw.includes('%') ? faultRateRaw : `${faultPercent.toFixed(2)}%`,
|
|
percent: p
|
|
}
|
|
}
|
|
return item
|
|
})
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadOverview()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.card {
|
|
height: 100%;
|
|
background: linear-gradient(135deg, rgba(15,23,42,0.96), rgba(15,23,42,0.88));
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(30,64,175,0.85);
|
|
box-shadow:
|
|
0 18px 45px rgba(15,23,42,0.95),
|
|
0 0 0 1px rgba(15,23,42,1),
|
|
inset 0 0 0 1px rgba(56,189,248,0.05);
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
}
|
|
|
|
.card::before,
|
|
.card::after {
|
|
content: "";
|
|
position: absolute;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 2px;
|
|
border: 1px solid rgba(56,189,248,0.75);
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.card::before {
|
|
top: -1px;
|
|
left: -1px;
|
|
border-right: none;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.card::after {
|
|
right: -1px;
|
|
bottom: -1px;
|
|
border-left: none;
|
|
border-top: none;
|
|
}
|
|
|
|
.panel-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
|
|
font-size: 16px;
|
|
font-weight: 900;
|
|
color: #e5f0ff;
|
|
}
|
|
|
|
.title-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(56,189,248,0.95);
|
|
box-shadow: 0 0 12px rgba(56,189,248,0.45);
|
|
}
|
|
|
|
.panel-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.overview-body {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.gauge-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.gauge {
|
|
width: 110px;
|
|
height: 110px;
|
|
border-radius: 50%;
|
|
padding: 8px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.gauge-inner {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
background: rgba(2,6,23,0.92);
|
|
border: 1px solid rgba(148,163,184,0.25);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
text-align: center;
|
|
}
|
|
|
|
.gauge-value {
|
|
font-size: 18px;
|
|
font-weight: 900;
|
|
color: #e5f0ff;
|
|
}
|
|
|
|
.gauge-label {
|
|
font-size: 11px;
|
|
color: rgba(148,163,184,0.95);
|
|
}
|
|
|
|
@media (max-width: 1366px) {
|
|
.gauge {
|
|
width: 96px;
|
|
height: 96px;
|
|
}
|
|
}
|
|
</style>
|