Explorar o código

style:运营管理-设备统计页面美化

lihao16 hai 2 meses
pai
achega
01b0814fc9

+ 484 - 201
smsb-plus-ui/src/views/smsb/dashboard/device.vue

@@ -1,103 +1,103 @@
 <template>
-  <div class="play-dashboard-root">
-    <div class="play-dashboard-page">
-      <el-container>
-        <el-header>
-          <el-card shadow="hover" style="margin-top: 10px">
-            <el-row justify="end" align="middle">
-              <el-col :span="19" style="text-align: right">
-                <el-radio-group v-model="timeRadio" size="small" @change="handleDateRangeChange">
-                  <!--              <el-radio-button label="今日" value="today" />-->
-                  <el-radio-button label="近7天" value="week"/>
-                  <el-radio-button label="近30天" value="month"/>
-                  <el-radio-button label="自定义" value="diy"/>
-                </el-radio-group>
-              </el-col>
-              <el-col :span="5" style="text-align: right">
-                <el-date-picker v-model="dateRange" type="daterange" @change="handleDateRangeChange" range-separator="-"
-                                start-placeholder="开始日期"
-                                :disabled="diyFlag" :clearable="false" end-placeholder="结束日期"
-                                style="margin-left: 10px; margin-right: 30px"/>
-              </el-col>
-            </el-row>
+  <el-container class="play-info-container" style="height: 90vh">
+    <el-header style="height: auto; padding: 20px;">
+      <el-card shadow="hover" class="header-card">
+        <el-row justify="end" align="middle">
+          <el-col :span="19" style="text-align: right">
+            <el-radio-group v-model="timeRadio" size="small" @change="handleDateRangeChange"
+                            class="time-radio-group">
+              <el-radio-button label="近7天" value="week"/>
+              <el-radio-button label="近30天" value="month"/>
+              <el-radio-button label="自定义" value="diy"/>
+            </el-radio-group>
+          </el-col>
+          <el-col :span="5" style="text-align: right">
+            <el-date-picker v-model="dateRange" type="daterange" @change="handleDateRangeChange" range-separator="-"
+                            start-placeholder="开始日期"
+                            :disabled="diyFlag" :clearable="false" end-placeholder="结束日期"
+                            style="margin-left: 10px; margin-right: 30px"/>
+          </el-col>
+        </el-row>
+      </el-card>
+    </el-header>
+
+    <el-main style="padding: 0 20px;">
+      <el-row :gutter="20">
+        <el-col :span="6" v-for="(item, index) in stats" :key="index">
+          <el-card shadow="hover" class="stat-card">
+            <h3 class="stat-title">{{ item.label }}</h3>
+            <p :class="['stat-number', item.class]">{{ item.value }}</p>
+            <div class="stat-card-footer"></div>
           </el-card>
-        </el-header>
+        </el-col>
+      </el-row>
 
-        <el-main style="margin-top: 20px">
+      <el-row :gutter="20" style="margin-top: 20px;">
+        <el-col :span="18">
           <el-row :gutter="20">
-            <el-col :span="6" v-for="(item, index) in stats" :key="index">
-              <el-card shadow="hover" class="stat-card">
-                <h3>{{ item.label }}</h3>
-                <p :class="['number', item.class]">{{ item.value }}</p>
+            <el-col :span="8">
+              <el-card shadow="hover" class="chart-card">
+                <h3 class="chart-title">类型占比</h3>
+                <div ref="typePie" class="chart-placeholder"></div>
+              </el-card>
+            </el-col>
+            <el-col :span="8">
+              <el-card shadow="hover" class="chart-card">
+                <h3 class="chart-title">告警级别</h3>
+                <div ref="alarmLevel" class="chart-placeholder"></div>
+              </el-card>
+            </el-col>
+            <el-col :span="8">
+              <el-card shadow="hover" class="chart-card">
+                <h3 class="chart-title">告警问题统计</h3>
+                <div ref="alarmCount" class="chart-placeholder"></div>
               </el-card>
             </el-col>
           </el-row>
-          <el-row :gutter="20" style="margin-top: 20px">
-            <el-col :span="18">
-              <el-row :gutter="20">
-                <el-col :span="8">
-                  <el-card shadow="hover">
-                    <h3>类型占比</h3>
-                    <div ref="typePie" class="chart-placeholder"></div>
-                  </el-card>
-                </el-col>
-                <el-col :span="8">
-                  <el-card shadow="hover">
-                    <h3>告警级别</h3>
-                    <div ref="alarmLevel" class="chart-placeholder"></div>
-                  </el-card>
-                </el-col>
-                <el-col :span="8">
-                  <el-card shadow="hover">
-                    <h3>告警问题统计</h3>
-                    <div ref="alarmCount" class="chart-placeholder"></div>
-                  </el-card>
-                </el-col>
-              </el-row>
-              <el-row :gutter="20" style="margin-top: 20px">
-                <el-col :span="12">
-                  <el-card shadow="hover">
-                    <h3>在线时长排行</h3>
-                    <div ref="onlineTime" class="chart-placeholder"></div>
-                  </el-card>
-                </el-col>
-                <el-col :span="12">
-                  <el-card shadow="hover">
-                    <h3>告警数量排行</h3>
-                    <div ref="alarmNum" class="chart-placeholder"></div>
-                  </el-card>
-                </el-col>
-              </el-row>
+
+          <el-row :gutter="20" style="margin-top: 20px;">
+            <el-col :span="12">
+              <el-card shadow="hover" class="chart-card">
+                <h3 class="chart-title">在线时长排行</h3>
+                <div ref="onlineTime" class="chart-placeholder"></div>
+              </el-card>
             </el-col>
-            <el-col :span="6">
-              <el-card shadow="hover">
-                <h3>告警清单</h3>
-                <el-table v-loading="loading" :data="alarmList" row-key="id">
-                  <el-table-column label="设备名称" align="left" prop="deviceName" width="100" :show-overflow-tooltip="true" />
-                  <el-table-column label="告警等级" align="center" prop="errorLevel" width="80">
-                    <template #default="scope">
-                      <dict-tag :options="smsb_device_error_level" :value="scope.row.errorLevel" />
-                    </template>
-                  </el-table-column>
-                  <el-table-column label="告警类型" align="center" prop="errorType" width="80">
-                    <template #default="scope">
-                      <dict-tag :options="smsb_device_error_type" :value="scope.row.errorType"/>
-                    </template>
-                  </el-table-column>
-                  <el-table-column label="创建时间" align="left" prop="createTime" width="160"/>
-                </el-table>
+            <el-col :span="12">
+              <el-card shadow="hover" class="chart-card">
+                <h3 class="chart-title">告警数量排行</h3>
+                <div ref="alarmNum" class="chart-placeholder"></div>
               </el-card>
             </el-col>
           </el-row>
-        </el-main>
-      </el-container>
-    </div>
-  </div>
+        </el-col>
+
+        <el-col :span="6">
+          <el-card shadow="hover" class="alarm-list-card">
+            <h3 class="chart-title">告警清单</h3>
+            <el-table v-loading="loading" :data="alarmList" row-key="id" class="alarm-table">
+              <el-table-column label="设备名称" align="left" prop="deviceName" width="100"
+                               :show-overflow-tooltip="true"/>
+              <el-table-column label="告警等级" align="center" prop="errorLevel" width="80">
+                <template #default="scope">
+                  <dict-tag :options="smsb_device_error_level" :value="scope.row.errorLevel"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="告警类型" align="center" prop="errorType" width="80">
+                <template #default="scope">
+                  <dict-tag :options="smsb_device_error_type" :value="scope.row.errorType"/>
+                </template>
+              </el-table-column>
+              <el-table-column label="创建时间" align="left" prop="createTime" width="160"/>
+            </el-table>
+          </el-card>
+        </el-col>
+      </el-row>
+    </el-main>
+  </el-container>
 </template>
 
 <script setup lang="ts">
-// console.log('[device.vue] setup called');
-import {reactive} from 'vue';
+import {ComponentInternalInstance, getCurrentInstance, onMounted, reactive, ref, toRefs} from 'vue';
 import {deviceStatistics} from '@/api/smsb/device/device';
 import * as echarts from 'echarts';
 import {DeviceErrorRecordQuery, DeviceErrorRecordVO} from '@/api/smsb/device/errorRecord_type';
@@ -109,6 +109,17 @@ import {
   onlineTimeTop
 } from '@/api/smsb/device/errorRecord';
 
+// 处理坐标轴过长,保留5位
+const formatAxisValue = (value: number) => {
+  if (value.toString().length > 5) {
+    if (value >= 10000) {
+      return (value / 10000).toFixed(1) + '万';
+    }
+    return value.toString().slice(0, 5) + '...';
+  }
+  return value;
+};
+
 const alarmList = ref<DeviceErrorRecordVO[]>([]);
 const {proxy} = getCurrentInstance() as ComponentInternalInstance;
 const {
@@ -143,14 +154,11 @@ const stats = reactive([
 ]);
 
 const handleDateRangeChange = () => {
-  // console.log('[device.vue] handleDateRangeChange called');
   const rangeType = timeRadio.value;
   const today = new Date();
   const startDate = new Date();
   const endDate = new Date();
   switch (rangeType) {
-    case 'today':
-      break;
     case 'week':
       startDate.setDate(today.getDate() - 7);
       dateRange.value = [formatDate(startDate), formatDate(endDate)];
@@ -161,15 +169,15 @@ const handleDateRangeChange = () => {
       dateRange.value = [formatDate(startDate), formatDate(endDate)];
       diyFlag.value = true;
       break;
-    case "diy" :
+    case "diy":
       diyFlag.value = false;
-      dateRange.value = [formatDate(new Date(dateRange.value[0])), formatDate(new Date(dateRange.value[1]))];
+      if (dateRange.value[0] && dateRange.value[1]) {
+        dateRange.value = [formatDate(new Date(dateRange.value[0])), formatDate(new Date(dateRange.value[1]))];
+      }
       break;
     default:
       break;
-    // throw new Error('Invalid range type');
   }
-  console.log(dateRange.value);
   alarmCountData();
   getAlarmLevel();
   getAlarmNumTop();
@@ -182,33 +190,31 @@ const formatDate = (date: Date) => {
   const day = String(date.getDate()).padStart(2, '0');
   return `${year}-${month}-${day}`;
 };
+
 const getOnlineTimeTop = async () => {
-  // console.log('[device.vue] getOnlineTimeTop called');
   const params = {
     startTime: dateRange.value[0],
     endTime: dateRange.value[1]
   };
   const res = await onlineTimeTop(params);
-  if (!onlineTime.value) {
-    // console.log('[onlineTime] ref is null when initializing echarts');
-  } else {
-    // console.log('[onlineTime] ref:', onlineTime.value);
-    echarts.dispose(onlineTime.value);
-    // console.log('[onlineTime] echarts instance disposed before init');
-  }
+
+  echarts.dispose(onlineTime.value);
   const onlineTimeTopInstance = echarts.init(onlineTime.value, 'macaroons');
-  // console.log('[onlineTime] echarts instance created:', onlineTimeTopInstance);
+  // y文本截取函数
+  const truncateText = (text: string, maxLength: number = 15): string => {
+    if (!text) return '';
+    return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
+  };
+  // 格式化x轴数据
+  const formattedData = res.data.onlineTimeList.map(formatAxisValue);
+
   onlineTimeTopInstance.setOption({
-    title: {
-      text: ''
-    },
+    title: {text: ''},
     tooltip: {
       trigger: 'axis',
-      axisPointer: {
-        type: 'shadow'
-      }
+      axisPointer: {type: 'shadow'},
+      formatter: '{b}: {c}'
     },
-    legend: {},
     grid: {
       left: '3%',
       right: '4%',
@@ -217,48 +223,71 @@ const getOnlineTimeTop = async () => {
     },
     xAxis: {
       type: 'value',
-      boundaryGap: [0, 0.01]
+      boundaryGap: [0, 0.01],
+      axisLabel: {
+        formatter: function (value: number) {
+          return formatAxisValue(value);
+        }
+      }
     },
     yAxis: {
       type: 'category',
-      data: res.data.deviceNameList
+      data: res.data.deviceNameList,
+      axisLabel: {
+        formatter: (value) => truncateText(value, 8) // 限制y轴标签最多显示5个字符
+      }
     },
     series: [
       {
-        name: '',
+        name: '在线时长',
         type: 'bar',
-        data: res.data.onlineTimeList
+        data: formattedData,
+        itemStyle: {
+          borderRadius: [0, 4, 4, 0],
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            {offset: 0, color: '#409eff'},
+            {offset: 1, color: '#69b1ff'}
+          ])
+        },
+        emphasis: {
+          itemStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+              {offset: 0, color: '#3a8ee6'},
+              {offset: 1, color: '#5d9cec'}
+            ])
+          }
+        },
+        animationDuration: 1000
       }
     ]
   });
+
+  // 响应窗口大小变化
+  window.addEventListener('resize', () => {
+    onlineTimeTopInstance.resize();
+  });
 };
+
 const getAlarmNumTop = async () => {
-  // console.log('[device.vue] getAlarmNumTop called');
   const params = {
     startTime: dateRange.value[0],
     endTime: dateRange.value[1]
   };
   const res = await alarmNumTop(params);
-  if (!alarmNum.value) {
-    // console.log('[alarmNum] ref is null when initializing echarts');
-  } else {
-    // console.log('[alarmNum] ref:', alarmNum.value);
-    echarts.dispose(alarmNum.value);
-    // console.log('[alarmNum] echarts instance disposed before init');
-  }
+  // y文本截取函数
+  const truncateText = (text: string, maxLength: number = 15): string => {
+    if (!text) return '';
+    return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
+  };
+  echarts.dispose(alarmNum.value);
   const alarmNumTopInstance = echarts.init(alarmNum.value, 'macaroons');
-  // console.log('[alarmNum] echarts instance created:', alarmNumTopInstance);
+
   alarmNumTopInstance.setOption({
-    title: {
-      text: ''
-    },
+    title: {text: ''},
     tooltip: {
       trigger: 'axis',
-      axisPointer: {
-        type: 'shadow'
-      }
+      axisPointer: {type: 'shadow'}
     },
-    legend: {},
     grid: {
       left: '3%',
       right: '4%',
@@ -267,87 +296,144 @@ const getAlarmNumTop = async () => {
     },
     xAxis: {
       type: 'value',
-      boundaryGap: [0, 0.01]
+      boundaryGap: [0, 0.01],
+      axisLabel: {
+        formatter: function (value: number) {
+          return formatAxisValue(value);
+        }
+      }
     },
     yAxis: {
       type: 'category',
-      data: res.data.deviceNameList
+      data: res.data.deviceNameList,
+      axisLabel: {
+        formatter: (value) => truncateText(value, 8) // 限制y轴标签最多显示5个字符
+      }
     },
     series: [
       {
-        name: '',
+        name: '告警数量',
         type: 'bar',
-        data: res.data.alarmNumList
+        data: res.data.alarmNumList,
+        itemStyle: {
+          borderRadius: [0, 4, 4, 0],
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            {offset: 0, color: '#f56c6c'},
+            {offset: 1, color: '#f88a8a'}
+          ])
+        },
+        emphasis: {
+          itemStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+              {offset: 0, color: '#e34e4e'},
+              {offset: 1, color: '#ea6f6f'}
+            ])
+          }
+        },
+        animationDuration: 1000
       }
     ]
   });
+
+  window.addEventListener('resize', () => {
+    alarmNumTopInstance.resize();
+  });
 };
+
 const alarmCountData = async () => {
-  // console.log('[device.vue] alarmCountData called');
   const params = {
     startTime: dateRange.value[0],
     endTime: dateRange.value[1]
   };
   const res = await alarmCountByType(params);
-  if (!alarmCount.value) {
-    // console.log('[alarmCount] ref is null when initializing echarts');
-  } else {
-    // console.log('[alarmCount] ref:', alarmCount.value);
-    echarts.dispose(alarmCount.value);
-    // console.log('[alarmCount] echarts instance disposed before init');
-  }
+
+  echarts.dispose(alarmCount.value);
   const alarmTypeInstance = echarts.init(alarmCount.value, 'macaroons');
-  // console.log('[alarmCount] echarts instance created:', alarmTypeInstance);
+
   alarmTypeInstance.setOption({
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {type: 'shadow'}
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true
+    },
     xAxis: {
       type: 'category',
-      data: res.data.alarmType
+      data: res.data.alarmType,
+      axisLabel: {
+        rotate: 30,
+        interval: 0
+      }
     },
     yAxis: {
-      type: 'value'
+      type: 'value',
+      axisLabel: {
+        formatter: function (value: number) {
+          return formatAxisValue(value);
+        }
+      }
     },
     series: [
       {
         data: res.data.alarmCount,
-        type: 'bar'
+        type: 'bar',
+        barWidth: '60%',
+        itemStyle: {
+          borderRadius: 4,
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            {offset: 0, color: '#67c23a'},
+            {offset: 1, color: '#85ce61'}
+          ])
+        },
+        emphasis: {
+          itemStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              {offset: 0, color: '#52c41a'},
+              {offset: 1, color: '#73d13d'}
+            ])
+          }
+        },
+        animationDuration: 1000
       }
     ]
   });
+
+  window.addEventListener('resize', () => {
+    alarmTypeInstance.resize();
+  });
 };
+
 const getAlarmLevel = async () => {
-  // console.log('[device.vue] getAlarmLevel called');
   const params = {
     startTime: dateRange.value[0],
     endTime: dateRange.value[1]
   };
   const res = await alarmCountByLevel(params);
 
-  if (!alarmLevel.value) {
-    // console.log('[alarmLevel] ref is null when initializing echarts');
-  } else {
-    // console.log('[alarmLevel] ref:', alarmLevel.value);
-    echarts.dispose(alarmLevel.value);
-    // console.log('[alarmLevel] echarts instance disposed before init');
-  }
+  echarts.dispose(alarmLevel.value);
   const alarmLevelInstance = echarts.init(alarmLevel.value, 'macaroons');
-  // console.log('[alarmLevel] echarts instance created:', alarmLevelInstance);
+
   alarmLevelInstance.setOption({
-    title: {
-      text: ''
-    },
     tooltip: {
       trigger: 'axis'
     },
     legend: {
-      data: ['普通', '紧急']
+      data: ['普通', '紧急'],
+      top: 0
     },
     grid: {
       left: '3%',
       right: '4%',
       bottom: '3%',
+      top: '15%',
       containLabel: true
     },
     toolbox: {
+      show: false,
       feature: {
         saveAsImage: {}
       }
@@ -355,29 +441,66 @@ const getAlarmLevel = async () => {
     xAxis: {
       type: 'category',
       boundaryGap: false,
-      data: res.data.alarmDateList
+      data: res.data.alarmDateList,
+      axisLabel: {
+        rotate: 30,
+        interval: 0
+      }
     },
     yAxis: {
-      type: 'value'
+      type: 'value',
+      axisLabel: {
+        formatter: function (value: number) {
+          return formatAxisValue(value);
+        }
+      }
     },
     series: [
       {
         name: '普通',
         type: 'line',
         stack: 'Total',
-        data: res.data.normalAlamList
+        data: res.data.normalAlamList,
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 6,
+        lineStyle: {width: 2},
+        itemStyle: {color: '#409eff'},
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            {offset: 0, color: 'rgba(64, 158, 255, 0.3)'},
+            {offset: 1, color: 'rgba(64, 158, 255, 0)'}
+          ])
+        },
+        animationDuration: 1000
       },
       {
         name: '紧急',
         type: 'line',
         stack: 'Total',
-        data: res.data.dangerAlamList
+        data: res.data.dangerAlamList,
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 6,
+        lineStyle: {width: 2},
+        itemStyle: {color: '#f56c6c'},
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            {offset: 0, color: 'rgba(245, 108, 108, 0.3)'},
+            {offset: 1, color: 'rgba(245, 108, 108, 0)'}
+          ])
+        },
+        animationDuration: 1000
       }
     ]
   });
+
+  window.addEventListener('resize', () => {
+    alarmLevelInstance.resize();
+  });
 };
+
 const getDeviceStatistics = async () => {
-  // console.log('[device.vue] getDeviceStatistics called');
   const res = await deviceStatistics();
   stats.forEach((item) => {
     switch (item.label) {
@@ -395,59 +518,71 @@ const getDeviceStatistics = async () => {
         break;
     }
   });
-  if (!typePie.value) {
-    // console.log('[typePie] ref is null when initializing echarts');
-  } else {
-    // console.log('[typePie] ref:', typePie.value);
-    echarts.dispose(typePie.value);
-    // console.log('[typePie] echarts instance disposed before init');
-  }
+
+  echarts.dispose(typePie.value);
   const typePieInstance = echarts.init(typePie.value, 'macaroons');
-  // console.log('[typePie] echarts instance created:', typePieInstance);
+
   typePieInstance.setOption({
-    title: {
-      text: '',
-      subtext: '',
-      left: 'center'
-    },
     tooltip: {
-      trigger: 'item'
+      trigger: 'item',
+      formatter: '{a} <br/>{b}: {c} ({d}%)'
     },
     legend: {
       orient: 'vertical',
-      left: 'left'
+      left: 'left',
+      top: 'center'
     },
     series: [
       {
-        name: '类型占比',
+        name: '设备状态',
         type: 'pie',
-        radius: '65%',
-        data: [
-          { value: (res.data.onlineNum / res.data.totalNum) * 100, name: '在线' },
-          { value: (res.data.offlineNum / res.data.totalNum) * 100, name: '离线' },
-          { value: (res.data.initNum / res.data.totalNum) * 100, name: '待接入' }
-        ],
+        radius: ['40%', '70%'],
+        center: ['60%', '50%'],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 6,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: {
+          show: false,
+          position: 'center'
+        },
         emphasis: {
-          itemStyle: {
-            shadowBlur: 10,
-            shadowOffsetX: 0,
-            shadowColor: 'rgba(0, 0, 0, 0.5)'
+          label: {
+            show: true,
+            fontSize: '16',
+            fontWeight: 'bold'
           }
-        }
+        },
+        labelLine: {
+          show: false
+        },
+        data: [
+          {value: res.data.onlineNum, name: '在线', itemStyle: {color: '#67c23a'}},
+          {value: res.data.offlineNum, name: '离线', itemStyle: {color: '#f56c6c'}},
+          {value: res.data.initNum, name: '待接入', itemStyle: {color: '#e6a23c'}}
+        ],
+        animationDuration: 1000,
+        animationEasingUpdate: 'quinticInOut'
       }
     ]
   });
+
+  window.addEventListener('resize', () => {
+    typePieInstance.resize();
+  });
 };
+
 const getAlarmList = async () => {
-  // console.log('[device.vue] getAlarmList called');
   loading.value = true;
   const res = await listDeviceErrorRecord(dialogQueryParams.value);
   alarmList.value = res.rows;
   total.value = res.total;
   loading.value = false;
 };
+
 onMounted(() => {
-  // console.log('[device.vue] onMounted called');
   handleDateRangeChange();
   getDeviceStatistics();
   getAlarmList();
@@ -455,35 +590,183 @@ onMounted(() => {
 </script>
 
 <style scoped>
+
+.header-card {
+  margin-top: 20px;
+  background-color: #fff;
+  border: 1px solid #e6f7ff;
+}
+
+.time-radio-group {
+  display: inline-flex;
+  background-color: #fff;
+  border-radius: 4px;
+  padding: 2px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
 .stat-card {
   text-align: center;
+  transition: all 0.3s ease;
+  border-radius: 8px;
+  overflow: hidden;
+  background: #fff;
+}
+
+.stat-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
 }
 
-.number {
-  font-size: 24px;
+.stat-title {
+  color: #666;
+  font-size: 16px;
+  margin-bottom: 12px;
   font-weight: bold;
+  padding-top: 10px;
+}
+
+.stat-number {
+  font-size: 28px;
+  font-weight: bold;
+  margin: 0;
+  padding: 10px 0;
+  transition: all 0.3s ease;
+}
+
+.stat-card-footer {
+  height: 4px;
+  width: 100%;
 }
 
 .success {
-  color: green;
+  color: #67c23a;
+}
+
+.stat-card:has(.success) .stat-card-footer {
+  background-color: #67c23a;
 }
 
 .danger {
-  color: red;
+  color: #f56c6c;
+}
+
+.stat-card:has(.danger) .stat-card-footer {
+  background-color: #f56c6c;
 }
 
 .warning {
-  color: orange;
+  color: #e6a23c;
+}
+
+.stat-card:has(.warning) .stat-card-footer {
+  background-color: #e6a23c;
+}
+
+.chart-card {
+  border-radius: 8px;
+  transition: all 0.3s ease;
+  overflow: hidden;
+}
+
+.play-info-container {
+  background-color: #f0f2f5;
+  height: 100%;
+}
+
+.chart-card:hover {
+  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+}
+
+.chart-title {
+  color: #333;
+  font-size: 16px;
+  margin: 0 0 15px 0;
+  padding: 15px 15px 0;
+  font-weight: 500;
+  display: flex;
+  align-items: center;
+}
+
+.chart-title::before {
+  content: '';
+  display: inline-block;
+  width: 4px;
+  height: 16px;
+  background-color: #409eff;
+  margin-right: 8px;
+  border-radius: 2px;
 }
 
 .chart-placeholder {
   height: 250px;
-  /*background: #f5f5f5;*/
   border-radius: 8px;
+  width: 100%;
+}
+
+.alarm-list-card {
+  border-radius: 8px;
+  transition: all 0.3s ease;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.alarm-list-card:hover {
+  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+}
+
+.alarm-table {
+  border-radius: 8px;
+  flex: 1;
+}
+
+.el-table__row {
+  transition: background-color 0.2s ease;
+}
+
+.el-table__row:hover {
+  background-color: #f5f7fa !important;
+}
+
+.el-radio-button__inner {
+  transition: all 0.2s ease;
+}
+
+.el-radio-button.is-active .el-radio-button__inner {
+  background-color: #409eff;
+  border-color: #409eff;
+}
+
+/* 动画效果 */
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.stat-card, .chart-card, .alarm-list-card {
+  animation: fadeIn 0.5s ease forwards;
+}
+
+.stat-card:nth-child(1) {
+  animation-delay: 0.1s;
+}
+
+.stat-card:nth-child(2) {
+  animation-delay: 0.2s;
+}
+
+.stat-card:nth-child(3) {
+  animation-delay: 0.3s;
 }
 
-.play-dashboard-root {
-  height: 100vh;
-  overflow-y: auto;
+.stat-card:nth-child(4) {
+  animation-delay: 0.4s;
 }
 </style>

+ 32 - 25
smsb-plus-ui/src/views/smsb/dashboard/play.vue

@@ -1,27 +1,29 @@
 <template>
   <div class="play-dashboard-root">
     <div class="play-dashboard-page">
-      <el-container class="play-dashboard-container">
-        <el-main class="scrollable-main main-content">
-          <div class="play-dashboard-header">
-            <el-card shadow="never" class="header-card">
-              <el-row justify="end" align="middle">
-                <el-col :span="19" class="text-right" style="margin-top: 10px">
-                  <el-radio-group v-model="timeRadio" size="small" @change="handleDateRangeChange">
-                    <el-radio-button label="近7天" value="week" />
-                    <el-radio-button label="近30天" value="month" />
-                    <el-radio-button label="自定义" value="diy" />
-                  </el-radio-group>
-                </el-col>
-                <el-col :span="5" class="text-right" style="margin-top: 10px">
-                  <el-date-picker v-model="dateRange" :disabled="diyFlag" :clearable="false"
-                                  @change="handleDateRangeChange" type="daterange" range-separator="-" start-placeholder="开始日期"
-                                  end-placeholder="结束日期" class="date-picker" />
-                </el-col>
-              </el-row>
-            </el-card>
-          </div>
-
+      <el-container class="play-dashboard-container" style="height: 100vh">
+        <el-header style="height: auto; padding: 20px;">
+          <el-card shadow="hover" class="header-card">
+            <el-row justify="end" align="middle">
+              <el-col :span="19" style="text-align: right">
+                <el-radio-group v-model="timeRadio" size="small" @change="handleDateRangeChange"
+                                class="time-radio-group">
+                  <el-radio-button label="近7天" value="week"/>
+                  <el-radio-button label="近30天" value="month"/>
+                  <el-radio-button label="自定义" value="diy"/>
+                </el-radio-group>
+              </el-col>
+              <el-col :span="5" style="text-align: right">
+                <el-date-picker v-model="dateRange" type="daterange" @change="handleDateRangeChange"
+                                range-separator="-"
+                                start-placeholder="开始日期"
+                                :disabled="diyFlag" :clearable="false" end-placeholder="结束日期"
+                                style="margin-left: 10px; margin-right: 30px"/>
+              </el-col>
+            </el-row>
+          </el-card>
+        </el-header>
+        <el-main style="padding: 0 20px;">
           <el-row :gutter="20">
             <el-col :span="12">
               <el-card shadow="never" class="stat-card-large">
@@ -225,7 +227,6 @@ body,
 .header-card {
   border-radius: 8px;
   border: none;
-  margin-bottom: 20px;
 }
 
 .text-right {
@@ -382,9 +383,15 @@ body,
 
 <script setup lang="ts">
 import * as echarts from 'echarts';
-import { onUnmounted, ref, onMounted } from 'vue';
-import { diskUse, fileStatistics, fileStatisticsByTag, numLine, statisticsByTypeAndTag } from '@/api/smsb/source/minioData';
-import { playTopStatistics, sumOnlineTimeLine } from '@/api/smsb/source/play_record';
+import {onMounted, onUnmounted, ref} from 'vue';
+import {
+  diskUse,
+  fileStatistics,
+  fileStatisticsByTag,
+  numLine,
+  statisticsByTypeAndTag
+} from '@/api/smsb/source/minioData';
+import {playTopStatistics, sumOnlineTimeLine} from '@/api/smsb/source/play_record';
 
 // ECharts instances
 let onlineTimeLineInstance: echarts.ECharts | null = null;