Просмотр исходного кода

feat(大屏 3.3): 新增大屏主页面 + API + 路由注册 (暗色主题/6卡片/5图表/预警走马灯)

yangjingjing 2 дней назад
Родитель
Сommit
cdc8a09029
3 измененных файлов с 907 добавлено и 0 удалено
  1. 96 0
      src/api/risk/bigScreen.ts
  2. 6 0
      src/router/index.ts
  3. 805 0
      src/views/risk/bigScreen/index.vue

+ 96 - 0
src/api/risk/bigScreen.ts

@@ -0,0 +1,96 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 3.3 大屏 - API
+ *
+ * 6 张业务卡片 + 2 饼图 + 1 折线图 + 1 柱状图 + 1 走马灯
+ */
+
+// ============ 类型定义 ============
+
+/** 6 张业务数据卡片 */
+export interface BigScreenCardVo {
+  // 1. 安全隐患
+  dangerTotal: number;
+  dangerHandled: number;
+  dangerPending: number;
+  // 2. 危险源
+  dangerSourceTotal: number;
+  dangerSourceMonthNew: number;
+  // 3. 作业上报
+  workReportTotal: number;
+  workReportOngoing: number;
+  // 4. 审计工作
+  personnelChangeTotal: number;
+  personnelLeave: number;
+  personnelTransfer: number;
+  // 5. 特种设备
+  specialEquipmentTotal: number;
+  specialEquipmentRunning: number;
+  specialEquipmentMaintaining: number;
+  // 6. 访厂审核
+  factoryVisitTotal: number;
+  factoryVisitPassed: number;
+  factoryVisitRejected: number;
+}
+
+/** 饼图名值对 */
+export interface NameValue {
+  name: string;
+  value: number;
+}
+
+/** 5 个图表数据 */
+export interface BigScreenChartVo {
+  // 饼图 1: 人员岗位占比
+  personnelDistribution: NameValue[];
+  // 饼图 2: 事故类型分布
+  accidentDistribution: NameValue[];
+  // 折线图: 巡检/抽检近 30 天
+  trendDates: string[];
+  qualityInspectionCount: number[];
+  riskInspectionCount: number[];
+  // 柱状图: 客诉月度
+  complaintMonths: string[];
+  complaintCounts: number[];
+}
+
+/** 预警走马灯单项 */
+export interface BigScreenWarningStreamVo {
+  id: number;
+  warningCode: string;
+  title: string;
+  warningType: string;
+  warningLevel: string;
+  levelColor: string; // red | yellow | blue
+  releaseTime: string;
+  storeName: string;
+}
+
+// ============ API ============
+
+/** ① 6 张业务数据卡片统计 */
+export const getBigScreenCards = (): AxiosPromise<BigScreenCardVo> => {
+  return request({
+    url: '/risk/bigScreen/cards',
+    method: 'get'
+  });
+};
+
+/** ② ③ ④ 图表数据 */
+export const getBigScreenCharts = (): AxiosPromise<BigScreenChartVo> => {
+  return request({
+    url: '/risk/bigScreen/charts',
+    method: 'get'
+  });
+};
+
+/** ⑤ 最新预警动态走马灯 */
+export const getBigScreenWarnings = (limit = 10): AxiosPromise<BigScreenWarningStreamVo[]> => {
+  return request({
+    url: '/risk/bigScreen/warnings',
+    method: 'get',
+    params: { limit }
+  });
+};

+ 6 - 0
src/router/index.ts

@@ -331,6 +331,12 @@ export const dynamicRoutes: RouteRecordRaw[] = [
         component: () => import('@/views/risk/factoryVisit/index.vue'),
         name: 'RiskFactoryVisit',
         meta: { title: '品控访厂', icon: 'guide', permissions: ['risk:factoryVisit:list'] }
+      },
+      {
+        path: 'bigScreen',
+        component: () => import('@/views/risk/bigScreen/index.vue'),
+        name: 'RiskBigScreen',
+        meta: { title: '风控大屏', icon: 'data-board', permissions: ['risk:bigScreen:view'] }
       }
     ]
   }

+ 805 - 0
src/views/risk/bigScreen/index.vue

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