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.

354 lines
7.7 KiB
Vue

<template>
<div class="card">
<div class="panel-title">
<div class="panel-title-left">
<span class="title-dot"></span>
<span>事件提醒</span>
</div>
<div class="panel-title-right">
<el-radio-group v-model="mode" size="small">
<el-radio-button label="device">设备</el-radio-button>
<el-radio-button label="mold">模具</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="panel-body body">
<div class="event-list">
<div v-for="item in eventItems" :key="item.key" class="event-row">
<div class="event-name">
<span class="event-bullet" :style="{ borderColor: item.color }"></span>
<span>{{ item.name }}</span>
</div>
<div class="event-count" :style="{ color: item.color }">{{ item.count }}</div>
</div>
</div>
<div class="event-chart">
<div class="chart-container">
<div ref="chartRef" class="chart"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import * as echarts from 'echarts'
import { DashboardApi, TaskStatisticsData } from '@/api/dashboard'
import { colors } from '../utils'
interface EventItem {
key: string
name: string
count: number
percent: number
color: string
}
const route = useRoute()
const orgId = route.query.orgId
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const mode = ref<'device' | 'mold'>('device')
const eventItems = ref<EventItem[]>([])
const rawStats = ref<TaskStatisticsData | null>(null)
let switchTimer: number | undefined
const render = () => {
if (!chart) return
const items = eventItems.value
const percentMap = Object.fromEntries(items.map((i) => [i.name, i.percent])) as Record<string, number>
const seriesData = items.map((i) => ({ value: i.count, name: i.name, itemStyle: { color: i.color } }))
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item' },
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
itemGap: 14,
textStyle: {
rich: {
percent: { fontSize: 18, fontWeight: 900, color: '#e5f0ff', lineHeight: 20 },
name: { fontSize: 12, fontWeight: 700, color: 'rgba(148,163,184,0.95)', lineHeight: 16 }
}
},
formatter: (name: string) => `{percent|${percentMap[name] ?? 0}%}\n{name|${name}}`
},
series: [
{
type: 'pie',
radius: ['48%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
label: { show: false },
labelLine: { show: false },
padAngle: 2,
itemStyle: { borderRadius: 8, borderWidth: 6, borderColor: 'rgba(2,6,23,0.9)' },
emphasis: { scale: false },
data: seriesData
}
]
})
}
const applyTaskStatistics = (data: TaskStatisticsData) => {
rawStats.value = data
const isDevice = mode.value === 'device'
const inspection = Number(isDevice ? data.deviceInspection : data.moldInspection) || 0
const maintenance = Number(isDevice ? data.deviceMaintenance : data.moldMaintenance) || 0
const repair = Number(isDevice ? data.deviceRepair : data.moldRepair) || 0
const total = inspection + maintenance + repair
const toPercent = (value: number) => (total > 0 ? Math.round((value / total) * 100) : 0)
eventItems.value = [
{
key: 'check',
name: '点检',
count: inspection,
percent: toPercent(inspection),
color: colors.cyan
},
{
key: 'maintain',
name: '保养',
count: maintenance,
percent: toPercent(maintenance),
color: colors.warn
},
{
key: 'repair',
name: '维修',
count: repair,
percent: toPercent(repair),
color: colors.danger
}
]
render()
}
const loadTaskStatistics = async () => {
try {
const res = await DashboardApi.getTaskStatistics({ orgId })
const payload = (res && typeof res === 'object' && 'data' in res ? (res as any).data : res) as
| TaskStatisticsData
| null
if (!payload) return
applyTaskStatistics(payload)
} catch (error) {
console.error(error)
}
}
watch(mode, () => {
if (!rawStats.value) return
applyTaskStatistics(rawStats.value)
})
const resize = () => {
chart?.resize()
}
onMounted(() => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
loadTaskStatistics()
switchTimer = window.setInterval(() => {
if (!rawStats.value) return
mode.value = mode.value === 'device' ? 'mold' : 'device'
}, 10000)
window.addEventListener('resize', resize)
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
if (switchTimer) {
clearInterval(switchTimer)
switchTimer = undefined
}
chart?.dispose()
})
</script>
<style scoped>
.card {
position: relative;
display: flex;
height: 100%;
min-height: 0;
overflow: hidden;
background: linear-gradient(135deg, rgb(15 23 42 / 96%), rgb(15 23 42 / 88%));
border: 1px solid rgb(30 64 175 / 85%);
border-radius: 8px;
box-shadow:
0 18px 45px rgb(15 23 42 / 95%),
0 0 0 1px rgb(15 23 42 / 100%),
inset 0 0 0 1px rgb(56 189 248 / 5%);
flex-direction: column;
}
.card::before,
.card::after {
position: absolute;
width: 12px;
height: 12px;
pointer-events: none;
border: 1px solid rgb(56 189 248 / 75%);
border-radius: 2px;
content: "";
opacity: 0.6;
}
.card::before {
top: -1px;
left: -1px;
border-right: none;
border-bottom: none;
}
.card::after {
right: -1px;
bottom: -1px;
border-top: none;
border-left: none;
}
.panel-title {
display: flex;
padding: 10px 12px;
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
border-bottom: 1px solid rgb(41 54 95 / 90%);
align-items: center;
justify-content: space-between;
gap: 10px;
}
.panel-title-left {
display: inline-flex;
align-items: center;
gap: 10px;
}
.panel-title-right {
display: inline-flex;
padding: 2px 4px;
background: radial-gradient(circle at 0 0, rgb(56 189 248 / 18%), transparent 70%);
border: 1px solid rgb(56 189 248 / 50%);
border-radius: 999px;
align-items: center;
gap: 6px;
}
.panel-title-right :deep(.el-radio-group) {
background: transparent;
}
.panel-title-right :deep(.el-radio-button__inner) {
padding: 4px 10px;
font-size: 12px;
color: rgb(148 163 184 / 95%);
background: transparent;
border: none;
box-shadow: none;
}
.panel-title-right :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
color: #e5f0ff;
background: rgb(15 23 42 / 90%);
border-radius: 999px;
box-shadow: 0 0 0 1px rgb(56 189 248 / 85%);
}
.title-dot {
width: 10px;
height: 10px;
border: 1px solid rgb(56 189 248 / 95%);
border-radius: 50%;
box-shadow: 0 0 12px rgb(56 189 248 / 45%);
}
.panel-body {
flex: 1;
min-height: 0;
padding: 10px 12px;
}
.body {
display: grid;
grid-template-columns: 0.7fr 1.3fr;
gap: 10px;
align-items: center;
}
.event-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.event-row {
display: flex;
padding: 8px 10px;
background: rgb(15 23 42 / 70%);
border: 1px solid rgb(30 64 175 / 70%);
border-radius: 6px;
align-items: center;
justify-content: space-between;
}
.event-name {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #e5f0ff;
}
.event-bullet {
width: 12px;
height: 12px;
border: 3px solid;
border-radius: 50%;
box-sizing: border-box;
}
.event-count {
font-size: 14px;
font-weight: 900;
}
.event-chart {
display: flex;
height: 100%;
min-height: 170px;
min-height: 0;
}
.chart-container {
display: flex;
width: 100%;
height: 100%;
min-height: 0;
align-items: center;
justify-content: center;
}
.chart {
width: 100%;
height: 100%;
}
@media (width <= 1366px) {
.body {
grid-template-columns: 0.8fr 1.2fr;
}
}
</style>