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

<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>