|
@@ -0,0 +1,805 @@
|
|
|
|
|
+<!--
|
|
|
|
|
+ 3.3 风控品控平台大屏
|
|
|
|
|
+ - ① 6 张业务数据卡片
|
|
|
|
|
+ - ② 2 个饼图(人员岗位 + 事故类型)
|
|
|
|
|
+ - ③ 1 个双折线面积图(巡检/抽检近 30 天)
|
|
|
|
|
+ - ④ 1 个渐变柱状图(客诉月度)
|
|
|
|
|
+ - ⑤ 1 个滚动走马灯(最新预警)
|
|
|
|
|
+-->
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="big-screen">
|
|
|
|
|
+ <!-- 顶部标题 -->
|
|
|
|
|
+ <div class="bs-header">
|
|
|
|
|
+ <div class="bs-header-left">
|
|
|
|
|
+ <div class="bs-time">{{ currentTime }}</div>
|
|
|
|
|
+ <div class="bs-date">{{ currentDate }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bs-header-title">
|
|
|
|
|
+ <div class="bs-title-zh">风控品控管理平台</div>
|
|
|
|
|
+ <div class="bs-title-en">RISK & QUALITY CONTROL PLATFORM</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bs-header-right">
|
|
|
|
|
+ <el-button class="bs-refresh" type="primary" @click="loadAll">
|
|
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
|
|
+ <span style="margin-left: 4px">刷新数据</span>
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 主体三列布局 -->
|
|
|
|
|
+ <div class="bs-body">
|
|
|
|
|
+ <!-- 左列:人员岗位 + 巡检/抽检 -->
|
|
|
|
|
+ <div class="bs-col bs-col-left">
|
|
|
|
|
+ <div class="bs-panel">
|
|
|
|
|
+ <div class="bs-panel-title">
|
|
|
|
|
+ <span class="bs-title-bar"></span>人员岗位占比
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="personnelPieRef" class="bs-chart bs-pie-chart"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bs-panel">
|
|
|
|
|
+ <div class="bs-panel-title">
|
|
|
|
|
+ <span class="bs-title-bar"></span>巡检/抽检 · 近30天趋势
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="trendLineRef" class="bs-chart bs-line-chart"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 中列:6 张卡片 -->
|
|
|
|
|
+ <div class="bs-col bs-col-center">
|
|
|
|
|
+ <div class="bs-cards">
|
|
|
|
|
+ <div class="bs-card bs-card-danger">
|
|
|
|
|
+ <div class="bs-card-title">安全隐患</div>
|
|
|
|
|
+ <div class="bs-card-value">{{ fmtNum(card.dangerTotal) }}</div>
|
|
|
|
|
+ <div class="bs-card-meta">
|
|
|
|
|
+ <span class="meta-success">已闭环 {{ fmtNum(card.dangerHandled) }}</span>
|
|
|
|
|
+ <span class="meta-danger">待处理 {{ fmtNum(card.dangerPending) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bs-card bs-card-source">
|
|
|
|
|
+ <div class="bs-card-title">危险源</div>
|
|
|
|
|
+ <div class="bs-card-value">{{ fmtNum(card.dangerSourceTotal) }}</div>
|
|
|
|
|
+ <div class="bs-card-meta">
|
|
|
|
|
+ <span class="meta-default">登记在册</span>
|
|
|
|
|
+ <span class="meta-danger">本月新增 {{ fmtNum(card.dangerSourceMonthNew) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bs-card bs-card-work">
|
|
|
|
|
+ <div class="bs-card-title">作业上报</div>
|
|
|
|
|
+ <div class="bs-card-value">{{ fmtNum(card.workReportTotal) }}</div>
|
|
|
|
|
+ <div class="bs-card-meta">
|
|
|
|
|
+ <span class="meta-default">历史累计</span>
|
|
|
|
|
+ <span class="meta-primary">进行中 {{ fmtNum(card.workReportOngoing) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bs-card bs-card-audit">
|
|
|
|
|
+ <div class="bs-card-title">审计工作 · 人员异动</div>
|
|
|
|
|
+ <div class="bs-card-value">{{ fmtNum(card.personnelChangeTotal) }}</div>
|
|
|
|
|
+ <div class="bs-card-meta">
|
|
|
|
|
+ <span class="meta-danger">离职 {{ fmtNum(card.personnelLeave) }}</span>
|
|
|
|
|
+ <span class="meta-primary">调任 {{ fmtNum(card.personnelTransfer) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bs-card bs-card-equip">
|
|
|
|
|
+ <div class="bs-card-title">特种设备</div>
|
|
|
|
|
+ <div class="bs-card-value">{{ fmtNum(card.specialEquipmentTotal) }}</div>
|
|
|
|
|
+ <div class="bs-card-meta">
|
|
|
|
|
+ <span class="meta-success">运行中 {{ fmtNum(card.specialEquipmentRunning) }}</span>
|
|
|
|
|
+ <span class="meta-warning">维护中 {{ fmtNum(card.specialEquipmentMaintaining) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="bs-card bs-card-visit">
|
|
|
|
|
+ <div class="bs-card-title">访厂审核</div>
|
|
|
|
|
+ <div class="bs-card-value">{{ fmtNum(card.factoryVisitTotal) }}</div>
|
|
|
|
|
+ <div class="bs-card-meta">
|
|
|
|
|
+ <span class="meta-success">通过 {{ fmtNum(card.factoryVisitPassed) }}</span>
|
|
|
|
|
+ <span class="meta-danger">不通过 {{ fmtNum(card.factoryVisitRejected) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右列:事故 + 客诉 -->
|
|
|
|
|
+ <div class="bs-col bs-col-right">
|
|
|
|
|
+ <div class="bs-panel">
|
|
|
|
|
+ <div class="bs-panel-title">
|
|
|
|
|
+ <span class="bs-title-bar"></span>事故类型分布
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="accidentPieRef" class="bs-chart bs-pie-chart"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bs-panel">
|
|
|
|
|
+ <div class="bs-panel-title">
|
|
|
|
|
+ <span class="bs-title-bar"></span>客诉统计 · 近12个月
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="complaintBarRef" class="bs-chart bs-bar-chart"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 底部:预警走马灯 -->
|
|
|
|
|
+ <div class="bs-footer">
|
|
|
|
|
+ <div class="bs-footer-title">
|
|
|
|
|
+ <span class="bs-title-bar"></span>最新预警事件
|
|
|
|
|
+ <span class="bs-footer-tag">实时刷新</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bs-stream">
|
|
|
|
|
+ <el-scrollbar height="100%">
|
|
|
|
|
+ <div class="bs-stream-list">
|
|
|
|
|
+ <div v-for="w in warningStream" :key="w.id" class="bs-stream-item">
|
|
|
|
|
+ <span class="bs-dot" :style="{ background: dotColor(w.levelColor) }"></span>
|
|
|
|
|
+ <span class="bs-stream-time">{{ w.releaseTime }}</span>
|
|
|
|
|
+ <span class="bs-stream-level" :style="{ color: dotColor(w.levelColor) }">
|
|
|
|
|
+ [{{ w.warningLevel || '一般' }}]
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="bs-stream-title">{{ w.title }}</span>
|
|
|
|
|
+ <span class="bs-stream-store">{{ w.storeName || '—' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!warningStream.length" class="bs-stream-empty">暂无预警</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-scrollbar>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
|
|
|
+import * as echarts from 'echarts';
|
|
|
|
|
+import { Refresh } from '@element-plus/icons-vue';
|
|
|
|
|
+import {
|
|
|
|
|
+ getBigScreenCards,
|
|
|
|
|
+ getBigScreenCharts,
|
|
|
|
|
+ getBigScreenWarnings,
|
|
|
|
|
+ type BigScreenCardVo,
|
|
|
|
|
+ type BigScreenChartVo,
|
|
|
|
|
+ type BigScreenWarningStreamVo
|
|
|
|
|
+} from '@/api/risk/bigScreen';
|
|
|
|
|
+
|
|
|
|
|
+// ===== 响应式数据 =====
|
|
|
|
|
+const card = ref<BigScreenCardVo>({
|
|
|
|
|
+ dangerTotal: 0,
|
|
|
|
|
+ dangerHandled: 0,
|
|
|
|
|
+ dangerPending: 0,
|
|
|
|
|
+ dangerSourceTotal: 0,
|
|
|
|
|
+ dangerSourceMonthNew: 0,
|
|
|
|
|
+ workReportTotal: 0,
|
|
|
|
|
+ workReportOngoing: 0,
|
|
|
|
|
+ personnelChangeTotal: 0,
|
|
|
|
|
+ personnelLeave: 0,
|
|
|
|
|
+ personnelTransfer: 0,
|
|
|
|
|
+ specialEquipmentTotal: 0,
|
|
|
|
|
+ specialEquipmentRunning: 0,
|
|
|
|
|
+ specialEquipmentMaintaining: 0,
|
|
|
|
|
+ factoryVisitTotal: 0,
|
|
|
|
|
+ factoryVisitPassed: 0,
|
|
|
|
|
+ factoryVisitRejected: 0
|
|
|
|
|
+});
|
|
|
|
|
+const warningStream = ref<BigScreenWarningStreamVo[]>([]);
|
|
|
|
|
+const currentTime = ref('');
|
|
|
|
|
+const currentDate = ref('');
|
|
|
|
|
+
|
|
|
|
|
+// 图表实例
|
|
|
|
|
+const personnelPieRef = ref<HTMLElement>();
|
|
|
|
|
+const accidentPieRef = ref<HTMLElement>();
|
|
|
|
|
+const trendLineRef = ref<HTMLElement>();
|
|
|
|
|
+const complaintBarRef = ref<HTMLElement>();
|
|
|
|
|
+
|
|
|
|
|
+let personnelPieChart: echarts.ECharts | null = null;
|
|
|
|
|
+let accidentPieChart: echarts.ECharts | null = null;
|
|
|
|
|
+let trendLineChart: echarts.ECharts | null = null;
|
|
|
|
|
+let complaintBarChart: echarts.ECharts | null = null;
|
|
|
|
|
+
|
|
|
|
|
+let timer: number | undefined;
|
|
|
|
|
+let streamTimer: number | undefined;
|
|
|
|
|
+let resizeHandler: (() => void) | undefined;
|
|
|
|
|
+
|
|
|
|
|
+// ===== 工具函数 =====
|
|
|
|
|
+const fmtNum = (n: number | undefined | null): string => {
|
|
|
|
|
+ if (n === undefined || n === null) return '0';
|
|
|
|
|
+ return n.toLocaleString('zh-CN');
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const dotColor = (c?: string): string => {
|
|
|
|
|
+ if (c === 'red') return '#ff4d4f';
|
|
|
|
|
+ if (c === 'yellow') return '#faad14';
|
|
|
|
|
+ return '#1890ff';
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// ===== 加载数据 =====
|
|
|
|
|
+async function loadCards() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getBigScreenCards();
|
|
|
|
|
+ card.value = (res as any).data ?? res;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('[BigScreen] 加载卡片数据失败', e);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadCharts() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getBigScreenCharts();
|
|
|
|
|
+ const data: BigScreenChartVo = (res as any).data ?? res;
|
|
|
|
|
+ renderPersonnelPie(data.personnelDistribution);
|
|
|
|
|
+ renderAccidentPie(data.accidentDistribution);
|
|
|
|
|
+ renderTrendLine(data.trendDates, data.qualityInspectionCount, data.riskInspectionCount);
|
|
|
|
|
+ renderComplaintBar(data.complaintMonths, data.complaintCounts);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('[BigScreen] 加载图表数据失败', e);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadStream() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getBigScreenWarnings(15);
|
|
|
|
|
+ const list: BigScreenWarningStreamVo[] = (res as any).data ?? res;
|
|
|
|
|
+ warningStream.value = Array.isArray(list) ? list : [];
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('[BigScreen] 加载预警走马灯失败', e);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function loadAll() {
|
|
|
|
|
+ loadCards();
|
|
|
|
|
+ loadCharts();
|
|
|
|
|
+ loadStream();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 时钟 =====
|
|
|
|
|
+function tickClock() {
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const pad = (n: number) => n.toString().padStart(2, '0');
|
|
|
|
|
+ currentTime.value = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
|
|
|
+ const weeks = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
|
|
|
|
+ currentDate.value = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${weeks[now.getDay()]}`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== ECharts =====
|
|
|
|
|
+const PIE_COLORS = ['#5b8ff9', '#5ad8a6', '#5d7092', '#f6bd16', '#e86452', '#6dc8ec', '#945fb9', '#ff9845'];
|
|
|
|
|
+
|
|
|
|
|
+function renderPersonnelPie(data: { name: string; value: number }[]) {
|
|
|
|
|
+ if (!personnelPieRef.value) return;
|
|
|
|
|
+ if (!personnelPieChart) {
|
|
|
|
|
+ personnelPieChart = echarts.init(personnelPieRef.value);
|
|
|
|
|
+ }
|
|
|
|
|
+ personnelPieChart.setOption({
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'item',
|
|
|
|
|
+ formatter: (params: any) => `${params.marker} ${params.name}<br/>${params.value} 人 (${params.percent}%)`,
|
|
|
|
|
+ backgroundColor: 'rgba(20,40,80,0.95)',
|
|
|
|
|
+ borderColor: '#3a8dff',
|
|
|
|
|
+ textStyle: { color: '#fff' }
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ type: 'scroll',
|
|
|
|
|
+ bottom: 0,
|
|
|
|
|
+ left: 'center',
|
|
|
|
|
+ textStyle: { color: '#aac8ff', fontSize: 12 },
|
|
|
|
|
+ itemWidth: 10,
|
|
|
|
|
+ itemHeight: 10
|
|
|
|
|
+ },
|
|
|
|
|
+ color: PIE_COLORS,
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'pie',
|
|
|
|
|
+ radius: ['45%', '70%'],
|
|
|
|
|
+ center: ['50%', '42%'],
|
|
|
|
|
+ avoidLabelOverlap: true,
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ borderColor: '#0a1a3a',
|
|
|
|
|
+ borderWidth: 2
|
|
|
|
|
+ },
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ color: '#aac8ff',
|
|
|
|
|
+ formatter: '{b}\n{d}%',
|
|
|
|
|
+ fontSize: 11
|
|
|
|
|
+ },
|
|
|
|
|
+ labelLine: { lineStyle: { color: '#3a8dff' } },
|
|
|
|
|
+ data: data.length ? data : [{ name: '暂无数据', value: 0 }]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderAccidentPie(data: { name: string; value: number }[]) {
|
|
|
|
|
+ if (!accidentPieRef.value) return;
|
|
|
|
|
+ if (!accidentPieChart) {
|
|
|
|
|
+ accidentPieChart = echarts.init(accidentPieRef.value);
|
|
|
|
|
+ }
|
|
|
|
|
+ accidentPieChart.setOption({
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'item',
|
|
|
|
|
+ backgroundColor: 'rgba(20,40,80,0.95)',
|
|
|
|
|
+ borderColor: '#3a8dff',
|
|
|
|
|
+ textStyle: { color: '#fff' }
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ type: 'scroll',
|
|
|
|
|
+ bottom: 0,
|
|
|
|
|
+ left: 'center',
|
|
|
|
|
+ textStyle: { color: '#aac8ff', fontSize: 12 }
|
|
|
|
|
+ },
|
|
|
|
|
+ color: ['#e86452', '#f6bd16', '#5b8ff9', '#5ad8a6', '#ff9845'],
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'pie',
|
|
|
|
|
+ radius: ['45%', '70%'],
|
|
|
|
|
+ center: ['50%', '42%'],
|
|
|
|
|
+ avoidLabelOverlap: true,
|
|
|
|
|
+ itemStyle: { borderColor: '#0a1a3a', borderWidth: 2 },
|
|
|
|
|
+ label: { color: '#aac8ff', formatter: '{b}\n{d}%', fontSize: 11 },
|
|
|
|
|
+ labelLine: { lineStyle: { color: '#3a8dff' } },
|
|
|
|
|
+ data: data.length ? data : [{ name: '暂无数据', value: 0 }]
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderTrendLine(dates: string[], quality: number[], risk: number[]) {
|
|
|
|
|
+ if (!trendLineRef.value) return;
|
|
|
|
|
+ if (!trendLineChart) {
|
|
|
|
|
+ trendLineChart = echarts.init(trendLineRef.value);
|
|
|
|
|
+ }
|
|
|
|
|
+ trendLineChart.setOption({
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ backgroundColor: 'rgba(20,40,80,0.95)',
|
|
|
|
|
+ borderColor: '#3a8dff',
|
|
|
|
|
+ textStyle: { color: '#fff' }
|
|
|
|
|
+ },
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ data: ['品控巡检', '风控抽检'],
|
|
|
|
|
+ top: 8,
|
|
|
|
|
+ textStyle: { color: '#aac8ff' }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: { left: 40, right: 30, top: 40, bottom: 40 },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ data: dates,
|
|
|
|
|
+ axisLine: { lineStyle: { color: '#3a8dff' } },
|
|
|
|
|
+ axisLabel: { color: '#aac8ff', fontSize: 10 }
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ axisLine: { lineStyle: { color: '#3a8dff' } },
|
|
|
|
|
+ axisLabel: { color: '#aac8ff' },
|
|
|
|
|
+ splitLine: { lineStyle: { color: 'rgba(58,141,255,0.15)' } }
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '品控巡检',
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ smooth: true,
|
|
|
|
|
+ symbol: 'circle',
|
|
|
|
|
+ symbolSize: 6,
|
|
|
|
|
+ lineStyle: { color: '#5ad8a6', width: 2 },
|
|
|
|
|
+ itemStyle: { color: '#5ad8a6' },
|
|
|
|
|
+ areaStyle: {
|
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
+ { offset: 0, color: 'rgba(90,216,166,0.5)' },
|
|
|
|
|
+ { offset: 1, color: 'rgba(90,216,166,0.05)' }
|
|
|
|
|
+ ])
|
|
|
|
|
+ },
|
|
|
|
|
+ data: quality
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: '风控抽检',
|
|
|
|
|
+ type: 'line',
|
|
|
|
|
+ smooth: true,
|
|
|
|
|
+ symbol: 'circle',
|
|
|
|
|
+ symbolSize: 6,
|
|
|
|
|
+ lineStyle: { color: '#5b8ff9', width: 2 },
|
|
|
|
|
+ itemStyle: { color: '#5b8ff9' },
|
|
|
|
|
+ areaStyle: {
|
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
+ { offset: 0, color: 'rgba(91,143,249,0.5)' },
|
|
|
|
|
+ { offset: 1, color: 'rgba(91,143,249,0.05)' }
|
|
|
|
|
+ ])
|
|
|
|
|
+ },
|
|
|
|
|
+ data: risk
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderComplaintBar(months: string[], counts: number[]) {
|
|
|
|
|
+ if (!complaintBarRef.value) return;
|
|
|
|
|
+ if (!complaintBarChart) {
|
|
|
|
|
+ complaintBarChart = echarts.init(complaintBarRef.value);
|
|
|
|
|
+ }
|
|
|
|
|
+ complaintBarChart.setOption({
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ trigger: 'axis',
|
|
|
|
|
+ axisPointer: { type: 'shadow' },
|
|
|
|
|
+ backgroundColor: 'rgba(20,40,80,0.95)',
|
|
|
|
|
+ borderColor: '#3a8dff',
|
|
|
|
|
+ textStyle: { color: '#fff' }
|
|
|
|
|
+ },
|
|
|
|
|
+ grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ type: 'category',
|
|
|
|
|
+ data: months,
|
|
|
|
|
+ axisLine: { lineStyle: { color: '#3a8dff' } },
|
|
|
|
|
+ axisLabel: { color: '#aac8ff', fontSize: 10, interval: 0 }
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ type: 'value',
|
|
|
|
|
+ axisLine: { lineStyle: { color: '#3a8dff' } },
|
|
|
|
|
+ axisLabel: { color: '#aac8ff' },
|
|
|
|
|
+ splitLine: { lineStyle: { color: 'rgba(58,141,255,0.15)' } }
|
|
|
|
|
+ },
|
|
|
|
|
+ series: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'bar',
|
|
|
|
|
+ barWidth: '50%',
|
|
|
|
|
+ data: counts,
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
+ { offset: 0, color: '#5b8ff9' },
|
|
|
|
|
+ { offset: 1, color: 'rgba(91,143,249,0.2)' }
|
|
|
|
|
+ ]),
|
|
|
|
|
+ borderRadius: [4, 4, 0, 0]
|
|
|
|
|
+ },
|
|
|
|
|
+ label: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ position: 'top',
|
|
|
|
|
+ color: '#aac8ff',
|
|
|
|
|
+ fontSize: 11
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 生命周期 =====
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadAll();
|
|
|
|
|
+ tickClock();
|
|
|
|
|
+ timer = window.setInterval(tickClock, 1000);
|
|
|
|
|
+ streamTimer = window.setInterval(loadStream, 30000);
|
|
|
|
|
+
|
|
|
|
|
+ resizeHandler = () => {
|
|
|
|
|
+ personnelPieChart?.resize();
|
|
|
|
|
+ accidentPieChart?.resize();
|
|
|
|
|
+ trendLineChart?.resize();
|
|
|
|
|
+ complaintBarChart?.resize();
|
|
|
|
|
+ };
|
|
|
|
|
+ window.addEventListener('resize', resizeHandler);
|
|
|
|
|
+
|
|
|
|
|
+ // 给图表初始化一点时间,以便容器有正确尺寸
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ personnelPieChart?.resize();
|
|
|
|
|
+ accidentPieChart?.resize();
|
|
|
|
|
+ trendLineChart?.resize();
|
|
|
|
|
+ complaintBarChart?.resize();
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ if (timer) {
|
|
|
|
|
+ clearInterval(timer);
|
|
|
|
|
+ timer = undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (streamTimer) {
|
|
|
|
|
+ clearInterval(streamTimer);
|
|
|
|
|
+ streamTimer = undefined;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resizeHandler) {
|
|
|
|
|
+ window.removeEventListener('resize', resizeHandler);
|
|
|
|
|
+ }
|
|
|
|
|
+ personnelPieChart?.dispose();
|
|
|
|
|
+ accidentPieChart?.dispose();
|
|
|
|
|
+ trendLineChart?.dispose();
|
|
|
|
|
+ complaintBarChart?.dispose();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.big-screen {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: radial-gradient(ellipse at center, #0a1a3a 0%, #050b1f 100%);
|
|
|
|
|
+ color: #e6f0ff;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ font-family: 'Microsoft YaHei', 'PingFang SC', Arial, sans-serif;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 顶部 =====
|
|
|
|
|
+.bs-header {
|
|
|
|
|
+ height: 70px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ border-bottom: 1px solid rgba(58, 141, 255, 0.3);
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ &-left,
|
|
|
|
|
+ &-right {
|
|
|
|
|
+ width: 320px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &-right {
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &-title {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-time {
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #5ad8ff;
|
|
|
|
|
+ text-shadow: 0 0 10px rgba(90, 216, 255, 0.6);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-date {
|
|
|
|
|
+ margin-left: 12px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-title-zh {
|
|
|
|
|
+ font-size: 30px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ background: linear-gradient(180deg, #ffffff, #5ad8ff);
|
|
|
|
|
+ -webkit-background-clip: text;
|
|
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
|
|
+ letter-spacing: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-title-en {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #5ad8ff;
|
|
|
|
|
+ letter-spacing: 2px;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-refresh {
|
|
|
|
|
+ background: linear-gradient(180deg, #1a4a8c, #0a2855) !important;
|
|
|
|
|
+ border: 1px solid #3a8dff !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 三列布局 =====
|
|
|
|
|
+.bs-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr 1.2fr 1fr;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-col {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-panel {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ background: linear-gradient(180deg, rgba(20, 40, 80, 0.5), rgba(10, 26, 58, 0.5));
|
|
|
|
|
+ border: 1px solid rgba(58, 141, 255, 0.25);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-panel-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+ padding-bottom: 8px;
|
|
|
|
|
+ border-bottom: 1px solid rgba(58, 141, 255, 0.2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-title-bar {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 14px;
|
|
|
|
|
+ background: linear-gradient(180deg, #5ad8ff, #3a8dff);
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-chart {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 卡片区 =====
|
|
|
|
|
+.bs-cards {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
|
|
+ grid-template-rows: repeat(3, 1fr);
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-card {
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ border: 1px solid rgba(58, 141, 255, 0.3);
|
|
|
|
|
+ background: linear-gradient(135deg, rgba(20, 40, 80, 0.7), rgba(10, 26, 58, 0.5));
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ height: 3px;
|
|
|
|
|
+ background: linear-gradient(90deg, transparent, #5ad8ff, transparent);
|
|
|
|
|
+ opacity: 0.6;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-card-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-card-value {
|
|
|
|
|
+ font-size: 32px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ text-shadow: 0 0 12px rgba(90, 216, 255, 0.4);
|
|
|
|
|
+ font-family: 'DIN Alternate', 'Microsoft YaHei', monospace;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-card-meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.meta-success {
|
|
|
|
|
+ color: #5ad8a6;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.meta-danger {
|
|
|
|
|
+ color: #ff4d4f;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.meta-warning {
|
|
|
|
|
+ color: #faad14;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.meta-primary {
|
|
|
|
|
+ color: #5ad8ff;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.meta-default {
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ===== 底部走马灯 =====
|
|
|
|
|
+.bs-footer {
|
|
|
|
|
+ height: 140px;
|
|
|
|
|
+ background: linear-gradient(180deg, rgba(20, 40, 80, 0.5), rgba(10, 26, 58, 0.5));
|
|
|
|
|
+ border: 1px solid rgba(58, 141, 255, 0.25);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ padding: 8px 16px;
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-footer-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+ padding-bottom: 6px;
|
|
|
|
|
+ border-bottom: 1px solid rgba(58, 141, 255, 0.2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-footer-tag {
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #5ad8a6;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ border: 1px solid #5ad8a6;
|
|
|
|
|
+ padding: 1px 8px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 6px 8px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #e6f0ff;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: rgba(10, 26, 58, 0.4);
|
|
|
|
|
+ transition: background 0.3s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(58, 141, 255, 0.15);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-dot {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ box-shadow: 0 0 8px currentColor;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream-time {
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+ font-family: 'DIN Alternate', monospace;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ min-width: 140px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream-level {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ min-width: 60px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream-title {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream-store {
|
|
|
|
|
+ color: #5ad8a6;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ min-width: 100px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.bs-stream-empty {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #aac8ff;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|