|
|
@@ -1,21 +1,1283 @@
|
|
|
<template>
|
|
|
- <ModuleDashboardPage
|
|
|
- module-code="S1"
|
|
|
- title="S1 产销协同详情"
|
|
|
- left-title="产销协同执行"
|
|
|
- left-subtitle="合同/BOM/工艺/交期评审 — 周期与人效"
|
|
|
- right-title="产销协同结果"
|
|
|
- right-subtitle="评审满足率与流转周转"
|
|
|
- :extra-query-init="{ customer: '' }"
|
|
|
- >
|
|
|
- <template #extra-fields="{ query }">
|
|
|
+ <div class="s1-dashboard">
|
|
|
+ <!-- 顶部标题栏 -->
|
|
|
+ <div class="top-bar">
|
|
|
+ <div class="top-left">
|
|
|
+ <div class="page-title">
|
|
|
+ <el-icon :size="28"><Document /></el-icon>
|
|
|
+ <div>
|
|
|
+ <h1>产销协同指标看板</h1>
|
|
|
+ <div class="version-info">
|
|
|
+ <span>系统版本 V3.0.4</span>
|
|
|
+ <span class="status-dot"></span>
|
|
|
+ <span>状态:运行正常</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="top-center">
|
|
|
+ <div class="search-box">
|
|
|
+ <el-input
|
|
|
+ v-model="searchText"
|
|
|
+ placeholder="搜索指标名称、部门 (如:工艺)..."
|
|
|
+ prefix-icon="Search"
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="top-right">
|
|
|
+ <div class="datetime">
|
|
|
+ <div class="date">{{ currentDate }}</div>
|
|
|
+ <div class="time">{{ currentTime }}</div>
|
|
|
+ </div>
|
|
|
+ <el-button type="primary" size="small">
|
|
|
+ <el-icon><Download /></el-icon>
|
|
|
+ 导出实时快报
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small">系统设置</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 模式切换栏 -->
|
|
|
+ <div class="mode-bar">
|
|
|
+ <div class="mode-left">
|
|
|
+ <el-button size="small" :type="activeMode === 'overview' ? 'primary' : ''">概览模式</el-button>
|
|
|
+ <el-button size="small" :type="activeMode === 'manufacturing' ? 'primary' : ''">制造侧监控</el-button>
|
|
|
+ <el-button size="small" :type="activeMode === 'sales' ? 'primary' : ''">销售侧分析</el-button>
|
|
|
+ <el-button size="small" :type="activeMode === 'supply' ? 'primary' : ''">供应链链动</el-button>
|
|
|
+ </div>
|
|
|
+ <div class="mode-right">
|
|
|
+ <span class="auto-refresh">自动轮播:</span>
|
|
|
+ <el-switch v-model="autoRefresh" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 筛选栏 -->
|
|
|
+ <div class="filter-bar">
|
|
|
+ <div class="filter-left">
|
|
|
+ <span class="filter-label">部门视图</span>
|
|
|
+ <el-radio-group v-model="deptFilter" size="small">
|
|
|
+ <el-radio-button label="all">全部</el-radio-button>
|
|
|
+ <el-radio-button label="plan">主计划</el-radio-button>
|
|
|
+ <el-radio-button label="sales">销售</el-radio-button>
|
|
|
+ <el-radio-button label="design">设计</el-radio-button>
|
|
|
+ <el-radio-button label="tech">工艺</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </div>
|
|
|
+ <div class="filter-right">
|
|
|
+ <span class="filter-label">状态监控</span>
|
|
|
+ <el-radio-group v-model="statusFilter" size="small">
|
|
|
+ <el-radio-button label="all">全部</el-radio-button>
|
|
|
+ <el-radio-button label="optimal">最优</el-radio-button>
|
|
|
+ <el-radio-button label="standby">待机</el-radio-button>
|
|
|
+ <el-radio-button label="critical">严重</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ <div class="showing-info">
|
|
|
+ 展示:8 / 8 张卡片
|
|
|
+ <el-button link type="primary" size="small">
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 业务筛选:产销协同以订单/客户维度为主 -->
|
|
|
+ <DetailQueryBar
|
|
|
+ dark
|
|
|
+ band-title="基础查询:日期、产品、订单号、产线"
|
|
|
+ @query="onBizQuery"
|
|
|
+ @reset="onBizReset"
|
|
|
+ >
|
|
|
+ <SmartOpsBaseQueryFields v-model="bizQuery" compact />
|
|
|
<el-form-item label="客户">
|
|
|
- <el-input v-model="query.customer" placeholder="客户编码/名称" clearable style="width: 160px" />
|
|
|
+ <el-input v-model="bizQuery.customer" placeholder="客户名称/编码" clearable style="width: 160px" />
|
|
|
</el-form-item>
|
|
|
- </template>
|
|
|
- </ModuleDashboardPage>
|
|
|
+ </DetailQueryBar>
|
|
|
+
|
|
|
+ <!-- 主内容区 -->
|
|
|
+ <div class="main-content">
|
|
|
+ <!-- 第一行:L1 指标卡(由"运营指标建模"动态驱动) -->
|
|
|
+ <div class="row-1">
|
|
|
+ <!-- Slot 0 / Slot 1 / Slot 2:普通指标卡(动态 L1 前三位) -->
|
|
|
+ <div
|
|
|
+ v-for="(slot, idx) in l1Slots.slice(0, 3)"
|
|
|
+ :key="`l1-slot-${idx}`"
|
|
|
+ class="card metric-card"
|
|
|
+ >
|
|
|
+ <template v-if="slot">
|
|
|
+ <div class="card-header">
|
|
|
+ <el-tooltip v-if="slot.formulaText" placement="top" :show-after="200" :content="slot.formulaText">
|
|
|
+ <span class="card-title is-formula">{{ slot.displayName || slot.metricCode }}</span>
|
|
|
+ </el-tooltip>
|
|
|
+ <span v-else class="card-title">{{ slot.displayName || slot.metricCode }}</span>
|
|
|
+ <el-tag size="small" :type="kpiTagType(slot.achievementLevel)">{{ kpiTagText(slot.achievementLevel) }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="metric-value">
|
|
|
+ <span class="value large">{{ fmtKpiValue(slot.currentValue) }}</span>
|
|
|
+ <span v-if="slot.unit" class="unit">{{ slot.unit }}</span>
|
|
|
+ <span class="trend" :class="kpiTrendClass(slot.achievementLevel)">
|
|
|
+ {{ slot.gapLabel || (slot.targetValue != null ? `目标 ${fmtKpiValue(slot.targetValue)}${slot.unit || ''}` : '') }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div v-if="slot.targetValue != null" class="progress-section">
|
|
|
+ <div class="progress-label">
|
|
|
+ <span>目标达成</span>
|
|
|
+ <span>{{ fmtKpiValue(slot.targetValue) }}{{ slot.unit || '' }} 目标</span>
|
|
|
+ </div>
|
|
|
+ <div class="progress-bar">
|
|
|
+ <div
|
|
|
+ class="progress-fill"
|
|
|
+ :style="{
|
|
|
+ width: `${Math.max(0, Math.min(100, Number(slot.currentValue != null && slot.targetValue ? (Number(slot.currentValue) / Number(slot.targetValue)) * 100 : 0) || 0))}%`,
|
|
|
+ }"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="card-title is-placeholder">(未配置 L1)</span>
|
|
|
+ </div>
|
|
|
+ <div class="metric-value">
|
|
|
+ <span class="value large">—</span>
|
|
|
+ </div>
|
|
|
+ <div class="placeholder-hint">请到「运营指标建模」为 {{ MODULE_CODE }} 添加 L1</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Slot 3:评审队列负荷 仪表盘(保留原形态;值取自动态 L1 第 4 位达成率) -->
|
|
|
+ <div class="card gauge-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <el-tooltip v-if="l1Slots[3]?.formulaText" placement="top" :show-after="200" :content="l1Slots[3].formulaText">
|
|
|
+ <span class="card-title is-formula">{{ l1Slots[3]?.displayName || '评审队列负荷' }}</span>
|
|
|
+ </el-tooltip>
|
|
|
+ <span v-else class="card-title">{{ l1Slots[3]?.displayName || '评审队列负荷' }}</span>
|
|
|
+ </div>
|
|
|
+ <div id="gauge-pressure" class="gauge-chart"></div>
|
|
|
+ <div class="gauge-footer">
|
|
|
+ <div class="label">{{ l1Slots[3] ? '当前值' : '待评审订单数' }}</div>
|
|
|
+ <div class="value">
|
|
|
+ {{ l1Slots[3] ? fmtKpiValue(l1Slots[3].currentValue) : 128 }}
|
|
|
+ <span class="unit">{{ l1Slots[3]?.unit || '单' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第二行 -->
|
|
|
+ <div class="row-2">
|
|
|
+ <!-- 评审满足率与周期趋势 -->
|
|
|
+ <div class="card large-chart-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <div>
|
|
|
+ <span class="card-title">评审满足率与周期趋势(采购执行联动)</span>
|
|
|
+ <div class="subtitle">实时数据流</div>
|
|
|
+ </div>
|
|
|
+ <div class="legend">
|
|
|
+ <span class="legend-item">
|
|
|
+ <span class="dot" style="background: #60a5fa"></span>
|
|
|
+ 满足率
|
|
|
+ </span>
|
|
|
+ <span class="legend-item">
|
|
|
+ <span class="dot" style="background: #34d399"></span>
|
|
|
+ 均值线
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div id="chart-trend" class="main-chart"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 异常流 -->
|
|
|
+ <div class="card anomaly-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="card-title">
|
|
|
+ <span class="live-dot"></span>
|
|
|
+ 异常实时流
|
|
|
+ </span>
|
|
|
+ <span class="buffer-info">缓冲区:1024KB</span>
|
|
|
+ </div>
|
|
|
+ <div class="anomaly-list">
|
|
|
+ <div v-for="(item, idx) in anomalyItems" :key="`${item.time}-${idx}`" class="anomaly-item" :class="item.levelClass">
|
|
|
+ <div class="anomaly-header">
|
|
|
+ <span class="tag">{{ item.tag }}</span>
|
|
|
+ <span class="time">{{ item.time }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="anomaly-message">{{ item.message }}</div>
|
|
|
+ <div v-if="item.source" class="anomaly-source">{{ item.source }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="command-input">
|
|
|
+ <el-input
|
|
|
+ v-model="commandText"
|
|
|
+ placeholder="输入指令以响应..."
|
|
|
+ size="small"
|
|
|
+ >
|
|
|
+ <template #append>
|
|
|
+ <el-button type="primary">执行</el-button>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第三行 -->
|
|
|
+ <div class="row-3">
|
|
|
+ <!-- 异常状态分布 -->
|
|
|
+ <div class="card chart-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="card-title">异常状态分布(物料仓储)</span>
|
|
|
+ </div>
|
|
|
+ <div id="chart-donut" class="donut-chart"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 人效热度监控 -->
|
|
|
+ <div class="card chart-card">
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="card-title">人效热度监控(生产执行)</span>
|
|
|
+ </div>
|
|
|
+ <div id="chart-bar" class="bar-chart"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 底部版权信息 -->
|
|
|
+ <div class="footer">
|
|
|
+ <div class="footer-left">数据流已加密(AES-256-GCM)</div>
|
|
|
+ <div class="footer-right">版权所有 © 2026 核心智能运营 · 系统运行正常</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import ModuleDashboardPage from './components/ModuleDashboardPage.vue'
|
|
|
+import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { Document, Search, Download, Refresh } from '@element-plus/icons-vue'
|
|
|
+import DetailQueryBar from './components/DetailQueryBar.vue'
|
|
|
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
|
|
|
+import {
|
|
|
+ emptySmartOpsBaseQuery,
|
|
|
+ blobMatchesSmartOpsBase,
|
|
|
+ textMatches,
|
|
|
+ summarizeSmartOpsBaseQuery,
|
|
|
+ baseQueryToApiParams,
|
|
|
+} from './utils/smartOpsBaseQuery'
|
|
|
+import * as echarts from 'echarts'
|
|
|
+import { homeS1, loadHomeModuleMetrics } from './data/homeModulesSync'
|
|
|
+import { fetchModuleDetail, fetchHomeGrid, fetchDetailKpis } from '../api/kanbanData'
|
|
|
+import { AIDOP_LAYOUT_SAVED } from './utils/s4LayoutEvents'
|
|
|
+
|
|
|
+const MODULE_CODE = 'S1'
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+
|
|
|
+const searchText = ref('')
|
|
|
+const commandText = ref('')
|
|
|
+const currentTime = ref('')
|
|
|
+const currentDate = ref('')
|
|
|
+const autoRefresh = ref(true)
|
|
|
+const activeMode = ref('overview')
|
|
|
+const deptFilter = ref('all')
|
|
|
+const statusFilter = ref('all')
|
|
|
+const trendXAxis = ref(['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8'])
|
|
|
+const barXAxis = ref(['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'])
|
|
|
+const barSeries = ref([65, 78, 52, 92, 75, 68])
|
|
|
+
|
|
|
+const bizQuery = ref({
|
|
|
+ ...emptySmartOpsBaseQuery(),
|
|
|
+ customer: '',
|
|
|
+})
|
|
|
+
|
|
|
+const l1Items = ref([])
|
|
|
+const l2Items = ref([])
|
|
|
+
|
|
|
+const l1Slots = computed(() => {
|
|
|
+ const arr = Array.isArray(l1Items.value) ? l1Items.value : []
|
|
|
+ return [0, 1, 2, 3].map((i) => arr[i] || null)
|
|
|
+})
|
|
|
+
|
|
|
+function fmtKpiValue(v) {
|
|
|
+ if (v === null || v === undefined || v === '') return '—'
|
|
|
+ const n = Number(v)
|
|
|
+ if (!Number.isFinite(n)) return String(v)
|
|
|
+ if (Math.abs(n) >= 100) return n.toFixed(0)
|
|
|
+ if (Math.abs(n) >= 10) return n.toFixed(1)
|
|
|
+ return n.toFixed(2)
|
|
|
+}
|
|
|
+
|
|
|
+function kpiTagType(level) {
|
|
|
+ const lv = String(level || '').toLowerCase()
|
|
|
+ if (lv === 'red') return 'danger'
|
|
|
+ if (lv === 'yellow') return 'warning'
|
|
|
+ if (lv === 'green') return 'success'
|
|
|
+ return 'info'
|
|
|
+}
|
|
|
+
|
|
|
+function kpiTagText(level) {
|
|
|
+ const lv = String(level || '').toLowerCase()
|
|
|
+ if (lv === 'red') return '未达标'
|
|
|
+ if (lv === 'yellow') return '待改善'
|
|
|
+ if (lv === 'green') return '达标'
|
|
|
+ return '—'
|
|
|
+}
|
|
|
+
|
|
|
+function kpiTrendClass(level) {
|
|
|
+ const lv = String(level || '').toLowerCase()
|
|
|
+ if (lv === 'red') return 'down'
|
|
|
+ if (lv === 'green') return 'up'
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+function buildDonutFromList(list) {
|
|
|
+ const c = list.filter((x) => x.levelClass === 'critical').length
|
|
|
+ const w = list.filter((x) => x.levelClass === 'warning').length
|
|
|
+ const i = list.filter((x) => x.levelClass === 'info').length
|
|
|
+ const standby = Math.max(0, list.length - c - w - i)
|
|
|
+ return [
|
|
|
+ { value: c, name: '严重', itemStyle: { color: '#f87171' } },
|
|
|
+ { value: w, name: '警告', itemStyle: { color: '#fbbf24' } },
|
|
|
+ { value: i, name: '正常', itemStyle: { color: '#34d399' } },
|
|
|
+ { value: standby, name: '待机', itemStyle: { color: '#60a5fa' } },
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+const anomalyItemsAll = ref([])
|
|
|
+
|
|
|
+const anomalyItems = computed(() =>
|
|
|
+ anomalyItemsAll.value.filter((x) => {
|
|
|
+ const blob = `${x.message} ${x.tag} ${x.source}`
|
|
|
+ if (!blobMatchesSmartOpsBase(bizQuery.value, blob)) return false
|
|
|
+ if (!textMatches(bizQuery.value.customer, blob)) return false
|
|
|
+ return true
|
|
|
+ })
|
|
|
+)
|
|
|
+
|
|
|
+const donutSeries = ref(buildDonutFromList([]))
|
|
|
+
|
|
|
+watch(
|
|
|
+ anomalyItems,
|
|
|
+ (list) => {
|
|
|
+ donutSeries.value = buildDonutFromList(list)
|
|
|
+ nextTick(() => donutChart?.setOption({ series: [{ data: donutSeries.value }] }))
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+function bizQueryExtra() {
|
|
|
+ return { ...baseQueryToApiParams(bizQuery.value) }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadDynamicKpis() {
|
|
|
+ const extra = bizQueryExtra()
|
|
|
+ const [grid, detail] = await Promise.all([
|
|
|
+ fetchHomeGrid(MODULE_CODE, 1, extra),
|
|
|
+ fetchDetailKpis(MODULE_CODE, 1, undefined, extra),
|
|
|
+ ])
|
|
|
+ l1Items.value = Array.isArray(grid?.items) ? grid.items : []
|
|
|
+ l2Items.value = Array.isArray(detail) ? detail : []
|
|
|
+ applyDynamicChartData()
|
|
|
+}
|
|
|
+
|
|
|
+function applyDynamicChartData() {
|
|
|
+ const l2 = l2Items.value
|
|
|
+ if (l2.length > 0) {
|
|
|
+ const slice = l2.slice(0, 10)
|
|
|
+ barXAxis.value = slice.map((x, i) => x.displayName || x.metricCode || `N${i + 1}`)
|
|
|
+ barSeries.value = slice.map((x) => {
|
|
|
+ const n = Number(x.currentValue)
|
|
|
+ return Number.isFinite(n) ? n : 0
|
|
|
+ })
|
|
|
+ nextTick(() => barChart?.setOption({ xAxis: { data: barXAxis.value }, series: [{ data: barSeries.value }] }))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onBizQuery() {
|
|
|
+ loadDynamicKpis()
|
|
|
+ ElMessage.success(`已应用筛选(${summarizeSmartOpsBaseQuery(bizQuery.value)})`)
|
|
|
+}
|
|
|
+
|
|
|
+function onBizReset() {
|
|
|
+ bizQuery.value = { ...emptySmartOpsBaseQuery(), customer: '' }
|
|
|
+ loadDynamicKpis()
|
|
|
+ ElMessage.info('已重置业务筛选')
|
|
|
+}
|
|
|
+
|
|
|
+function onLayoutSaved(e) {
|
|
|
+ const mc = String(e?.detail?.moduleCode || '').toUpperCase()
|
|
|
+ if (!mc || mc === MODULE_CODE) loadDynamicKpis()
|
|
|
+}
|
|
|
+
|
|
|
+let gaugeChart = null
|
|
|
+let trendChart = null
|
|
|
+let donutChart = null
|
|
|
+let barChart = null
|
|
|
+
|
|
|
+const updateTime = () => {
|
|
|
+ const now = new Date()
|
|
|
+ currentTime.value = now.toLocaleString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit',
|
|
|
+ hour: '2-digit',
|
|
|
+ minute: '2-digit',
|
|
|
+ second: '2-digit',
|
|
|
+ hour12: false
|
|
|
+ })
|
|
|
+ currentDate.value = now.toLocaleDateString('zh-CN')
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化评审队列负荷仪表
|
|
|
+const initGaugeChart = () => {
|
|
|
+ const chartDom = document.getElementById('gauge-pressure')
|
|
|
+ if (!chartDom) return
|
|
|
+
|
|
|
+ gaugeChart = echarts.init(chartDom)
|
|
|
+ const option = {
|
|
|
+ series: [{
|
|
|
+ type: 'gauge',
|
|
|
+ startAngle: 180,
|
|
|
+ endAngle: 0,
|
|
|
+ min: 0,
|
|
|
+ max: 100,
|
|
|
+ radius: '90%',
|
|
|
+ center: ['50%', '60%'],
|
|
|
+ itemStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
|
|
+ { offset: 0, color: '#34d399' },
|
|
|
+ { offset: 0.5, color: '#fbbf24' },
|
|
|
+ { offset: 1, color: '#f87171' }
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ progress: {
|
|
|
+ show: true,
|
|
|
+ width: 12,
|
|
|
+ roundCap: true
|
|
|
+ },
|
|
|
+ pointer: { show: false },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ width: 12,
|
|
|
+ color: [[1, '#1e293b']]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisTick: { show: false },
|
|
|
+ splitLine: { show: false },
|
|
|
+ axisLabel: {
|
|
|
+ show: true,
|
|
|
+ color: '#64748b',
|
|
|
+ fontSize: 10,
|
|
|
+ distance: -15
|
|
|
+ },
|
|
|
+ title: { show: false },
|
|
|
+ detail: {
|
|
|
+ valueAnimation: true,
|
|
|
+ offsetCenter: [0, '10%'],
|
|
|
+ fontSize: 24,
|
|
|
+ fontWeight: 'bold',
|
|
|
+ color: '#e2e8f0',
|
|
|
+ formatter: '{value}%'
|
|
|
+ },
|
|
|
+ data: [{ value: computeGaugeValue() }]
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ gaugeChart.setOption(option)
|
|
|
+}
|
|
|
+
|
|
|
+function computeGaugeValue() {
|
|
|
+ const s = l1Slots.value[3]
|
|
|
+ if (s && s.currentValue != null && s.targetValue != null && Number(s.targetValue) !== 0) {
|
|
|
+ const pct = (Number(s.currentValue) / Number(s.targetValue)) * 100
|
|
|
+ if (Number.isFinite(pct)) return Math.max(0, Math.min(100, Math.round(pct)))
|
|
|
+ }
|
|
|
+ return homeS1.systemPressureGaugePct
|
|
|
+}
|
|
|
+
|
|
|
+watch(l1Slots, () => {
|
|
|
+ nextTick(() => gaugeChart?.setOption({ series: [{ data: [{ value: computeGaugeValue() }] }] }))
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
+// 初始化趋势图
|
|
|
+const initTrendChart = () => {
|
|
|
+ const chartDom = document.getElementById('chart-trend')
|
|
|
+ if (!chartDom) return
|
|
|
+
|
|
|
+ trendChart = echarts.init(chartDom)
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
|
|
+ borderColor: '#334155',
|
|
|
+ textStyle: { color: '#e2e8f0' },
|
|
|
+ axisPointer: { type: 'cross' }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '10%',
|
|
|
+ top: '15%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: trendXAxis.value,
|
|
|
+ axisLine: { lineStyle: { color: '#334155' } },
|
|
|
+ axisLabel: { color: '#64748b' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ min: 70,
|
|
|
+ max: 100,
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#1e293b',
|
|
|
+ type: 'solid'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLabel: { color: '#64748b' }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '满足率',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 6,
|
|
|
+ itemStyle: {
|
|
|
+ color: '#60a5fa',
|
|
|
+ borderColor: '#1e293b',
|
|
|
+ borderWidth: 2
|
|
|
+ },
|
|
|
+ lineStyle: {
|
|
|
+ color: '#60a5fa',
|
|
|
+ width: 3
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: 'rgba(96, 165, 250, 0.3)' },
|
|
|
+ { offset: 1, color: 'rgba(96, 165, 250, 0)' }
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ data: homeS1.trendSatisfaction
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '均值线',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'none',
|
|
|
+ lineStyle: {
|
|
|
+ color: '#34d399',
|
|
|
+ width: 2,
|
|
|
+ type: 'dashed'
|
|
|
+ },
|
|
|
+ data: Array(8).fill(homeS1.trendMeanLine)
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ trendChart.setOption(option)
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化环形图
|
|
|
+const initDonutChart = () => {
|
|
|
+ const chartDom = document.getElementById('chart-donut')
|
|
|
+ if (!chartDom) return
|
|
|
+
|
|
|
+ donutChart = echarts.init(chartDom)
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
|
|
+ borderColor: '#334155',
|
|
|
+ textStyle: { color: '#e2e8f0' }
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ name: '异常状态',
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['50%', '70%'],
|
|
|
+ center: ['50%', '50%'],
|
|
|
+ avoidLabelOverlap: false,
|
|
|
+ label: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ label: {
|
|
|
+ show: true,
|
|
|
+ fontSize: 14,
|
|
|
+ fontWeight: 'bold',
|
|
|
+ color: '#e2e8f0'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ labelLine: { show: false },
|
|
|
+ data: donutSeries.value
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ donutChart.setOption(option)
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化柱状图
|
|
|
+const initBarChart = () => {
|
|
|
+ const chartDom = document.getElementById('chart-bar')
|
|
|
+ if (!chartDom) return
|
|
|
+
|
|
|
+ barChart = echarts.init(chartDom)
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(15, 23, 42, 0.95)',
|
|
|
+ borderColor: '#334155',
|
|
|
+ textStyle: { color: '#e2e8f0' }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '15%',
|
|
|
+ top: '10%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: barXAxis.value,
|
|
|
+ axisLine: { lineStyle: { color: '#334155' } },
|
|
|
+ axisLabel: { color: '#64748b' }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ data: barSeries.value,
|
|
|
+ type: 'bar',
|
|
|
+ barWidth: '60%',
|
|
|
+ itemStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: '#60a5fa' },
|
|
|
+ { offset: 1, color: '#3b82f6' }
|
|
|
+ ]),
|
|
|
+ borderRadius: [4, 4, 0, 0]
|
|
|
+ },
|
|
|
+ showBackground: true,
|
|
|
+ backgroundStyle: {
|
|
|
+ color: 'rgba(51, 65, 85, 0.3)',
|
|
|
+ borderRadius: [4, 4, 0, 0]
|
|
|
+ }
|
|
|
+ }]
|
|
|
+ }
|
|
|
+ barChart.setOption(option)
|
|
|
+}
|
|
|
+
|
|
|
+const initCharts = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ initGaugeChart()
|
|
|
+ initTrendChart()
|
|
|
+ initDonutChart()
|
|
|
+ initBarChart()
|
|
|
+
|
|
|
+ window.addEventListener('resize', () => {
|
|
|
+ gaugeChart?.resize()
|
|
|
+ trendChart?.resize()
|
|
|
+ donutChart?.resize()
|
|
|
+ barChart?.resize()
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await loadHomeModuleMetrics()
|
|
|
+ const detail = await fetchModuleDetail(MODULE_CODE)
|
|
|
+ anomalyItemsAll.value = (detail.alerts ?? []).map((x) => ({
|
|
|
+ tag: `[${String(x.level ?? 'info').toUpperCase()}]`,
|
|
|
+ time: x.time ?? '--:--:--',
|
|
|
+ message: x.message ?? '异常告警',
|
|
|
+ source: `Source: Ai-DOP / Module: ${MODULE_CODE}`,
|
|
|
+ levelClass: ['critical', 'high'].includes(String(x.level)) ? 'critical' : String(x.level) === 'warning' ? 'warning' : 'info'
|
|
|
+ }))
|
|
|
+ await loadDynamicKpis()
|
|
|
+ updateTime()
|
|
|
+ setInterval(updateTime, 1000)
|
|
|
+ initCharts()
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
+ window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (typeof window !== 'undefined') {
|
|
|
+ window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
|
|
|
+ }
|
|
|
+})
|
|
|
</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.s1-dashboard {
|
|
|
+ box-sizing: border-box;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ max-height: 100%;
|
|
|
+ overflow-x: hidden;
|
|
|
+ overflow-y: auto;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
|
+ padding: 15px;
|
|
|
+ padding-bottom: 24px;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 顶部标题栏 */
|
|
|
+.top-bar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ background: rgba(15, 23, 42, 0.8);
|
|
|
+ padding: 15px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ border: 1px solid #1e293b;
|
|
|
+}
|
|
|
+
|
|
|
+.top-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ color: #60a5fa;
|
|
|
+}
|
|
|
+
|
|
|
+.page-title h1 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.version-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #64748b;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-dot {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #34d399;
|
|
|
+}
|
|
|
+
|
|
|
+.top-center {
|
|
|
+ flex: 1;
|
|
|
+ max-width: 500px;
|
|
|
+ margin: 0 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-box :deep(.el-input__wrapper) {
|
|
|
+ background: rgba(30, 41, 59, 0.6);
|
|
|
+ border: 1px solid #334155;
|
|
|
+}
|
|
|
+
|
|
|
+.search-box :deep(.el-input__inner) {
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.top-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.datetime {
|
|
|
+ text-align: right;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.datetime .date {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #93c5fd;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.datetime .time {
|
|
|
+ font-size: 18px;
|
|
|
+ color: #60a5fa;
|
|
|
+ font-weight: 700;
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+}
|
|
|
+
|
|
|
+/* 模式切换栏 */
|
|
|
+.mode-bar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ background: rgba(30, 41, 59, 0.6);
|
|
|
+ padding: 10px 15px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ border: 1px solid #334155;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-left {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 筛选栏 */
|
|
|
+.filter-bar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ background: rgba(30, 41, 59, 0.4);
|
|
|
+ padding: 12px 15px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+ border: 1px solid #1e293b;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-left, .filter-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.showing-info {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+ margin-left: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 主内容区 */
|
|
|
+.main-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.row-1 {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.row-2 {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 2fr 1fr;
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.row-3 {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ gap: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 卡片通用样式 */
|
|
|
+.card {
|
|
|
+ background: rgba(30, 41, 59, 0.6);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 15px;
|
|
|
+ border: 1px solid #334155;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-title {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.card-title.is-formula {
|
|
|
+ cursor: help;
|
|
|
+ border-bottom: 1px dashed rgba(148, 163, 184, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.card-title.is-placeholder {
|
|
|
+ color: #64748b;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.placeholder-hint {
|
|
|
+ margin-top: 10px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #64748b;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.subtitle {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #64748b;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 指标卡片 */
|
|
|
+.metric-card .metric-value {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-card .value {
|
|
|
+ font-size: 36px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-card .value.large {
|
|
|
+ font-size: 48px;
|
|
|
+}
|
|
|
+
|
|
|
+.metric-card .unit {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.trend {
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+.trend.up {
|
|
|
+ color: #34d399;
|
|
|
+}
|
|
|
+
|
|
|
+.trend.down {
|
|
|
+ color: #f87171;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-section {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-label {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #94a3b8;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-bar {
|
|
|
+ height: 6px;
|
|
|
+ background: #1e293b;
|
|
|
+ border-radius: 3px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.progress-fill {
|
|
|
+ height: 100%;
|
|
|
+ background: linear-gradient(90deg, #60a5fa, #3b82f6);
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.sub-metrics {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 10px;
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.sub-metric {
|
|
|
+ background: rgba(0, 0, 0, 0.2);
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.sub-metric .label {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #64748b;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.sub-metric .value {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 约束列表 */
|
|
|
+.constraint-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.constraint-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 8px;
|
|
|
+ background: rgba(0, 0, 0, 0.2);
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.constraint-item .dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #34d399;
|
|
|
+}
|
|
|
+
|
|
|
+.constraint-item .dot.warning {
|
|
|
+ background: #fbbf24;
|
|
|
+}
|
|
|
+
|
|
|
+.constraint-item .label {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.constraint-item .value {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.constraint-item .value.warning {
|
|
|
+ color: #fbbf24;
|
|
|
+}
|
|
|
+
|
|
|
+/* 仪表盘卡片 */
|
|
|
+.gauge-card {
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.gauge-chart {
|
|
|
+ width: 100%;
|
|
|
+ height: 150px;
|
|
|
+}
|
|
|
+
|
|
|
+.gauge-footer {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.gauge-footer .label {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #64748b;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.gauge-footer .value {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.gauge-footer .unit {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+/* 大图表卡片 */
|
|
|
+.large-chart-card .main-chart {
|
|
|
+ width: 100%;
|
|
|
+ height: 350px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend {
|
|
|
+ display: flex;
|
|
|
+ gap: 15px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item .dot {
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 异常流卡片 */
|
|
|
+.anomaly-card {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-card .card-header {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-card .card-title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.live-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #ef4444;
|
|
|
+ animation: pulse 2s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.5; }
|
|
|
+}
|
|
|
+
|
|
|
+.buffer-info {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-list {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+ overflow-y: auto;
|
|
|
+ max-height: 400px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-item {
|
|
|
+ padding: 10px;
|
|
|
+ background: rgba(0, 0, 0, 0.2);
|
|
|
+ border-radius: 4px;
|
|
|
+ border-left: 3px solid;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-item.critical {
|
|
|
+ border-color: #ef4444;
|
|
|
+ background: rgba(239, 68, 68, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-item.warning {
|
|
|
+ border-color: #f59e0b;
|
|
|
+ background: rgba(245, 158, 11, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-item.info {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ background: rgba(59, 130, 246, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-item.success {
|
|
|
+ border-color: #10b981;
|
|
|
+ background: rgba(16, 185, 129, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-item.stall {
|
|
|
+ border-color: #8b5cf6;
|
|
|
+ background: rgba(139, 92, 246, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-header .tag {
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 700;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-item.critical .tag { color: #ef4444; background: rgba(239, 68, 68, 0.2); }
|
|
|
+.anomaly-item.warning .tag { color: #f59e0b; background: rgba(245, 158, 11, 0.2); }
|
|
|
+.anomaly-item.info .tag { color: #3b82f6; background: rgba(59, 130, 246, 0.2); }
|
|
|
+.anomaly-item.success .tag { color: #10b981; background: rgba(16, 185, 129, 0.2); }
|
|
|
+.anomaly-item.stall .tag { color: #8b5cf6; background: rgba(139, 92, 246, 0.2); }
|
|
|
+
|
|
|
+.anomaly-header .time {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-message {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #e2e8f0;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-source {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #64748b;
|
|
|
+}
|
|
|
+
|
|
|
+.command-input {
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图表卡片 */
|
|
|
+.chart-card .donut-chart,
|
|
|
+.chart-card .bar-chart {
|
|
|
+ width: 100%;
|
|
|
+ height: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部版权信息 */
|
|
|
+.footer {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 20px;
|
|
|
+ padding-top: 15px;
|
|
|
+ border-top: 1px solid #1e293b;
|
|
|
+ font-size: 10px;
|
|
|
+ color: #475569;
|
|
|
+}
|
|
|
+
|
|
|
+/* Element Plus 覆盖 */
|
|
|
+:deep(.el-radio-button__inner) {
|
|
|
+ background: rgba(30, 41, 59, 0.6);
|
|
|
+ border: 1px solid #334155;
|
|
|
+ color: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
|
|
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
|
+ border-color: #3b82f6;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-button--primary) {
|
|
|
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-tag--success) {
|
|
|
+ background: rgba(16, 185, 129, 0.2);
|
|
|
+ border-color: rgba(16, 185, 129, 0.3);
|
|
|
+ color: #10b981;
|
|
|
+}
|
|
|
+</style>
|