Ver código fonte

feat: 图表

reaper 20 horas atrás
pai
commit
f036c2130f

Diferenças do arquivo suprimidas por serem muito extensas
+ 6 - 0
app/assets/svg/home/an1.svg


Diferenças do arquivo suprimidas por serem muito extensas
+ 6 - 0
app/assets/svg/home/an2.svg


Diferenças do arquivo suprimidas por serem muito extensas
+ 6 - 0
app/assets/svg/home/an3.svg


+ 279 - 116
app/components/home/StatsSection.vue

@@ -1,158 +1,321 @@
 <template>
-  <div class="stats-container">
-    <div class="stats-bg-wrapper">
-      <img class="stats-bg" src="/images/home/home-bg.png" alt="首页背景" />
-      <section class="stats-section">
-        <NuxtImg width="154" class="stats-figure" src="/images/home/home-d.png" alt="游戏盾" />
-        <div class="stats-row">
-          <div class="stat-item">
-            <div class="stat-label">今日攻击次数</div>
-            <div class="stat-value">{{ formatNumber(stats.ccAttacks) }}</div>
-          </div>
-          <div class="stat-item">
-            <div class="stat-label">今日拦截次数</div>
-            <div class="stat-value">{{ formatNumber(stats.wafBlocks) }}</div>
-          </div>
+  <section class="stats-section">
+    <!-- 顶部数据卡片 -->
+    <div class="stats-cards">
+      <div :style="{ background: item.background }" class="card" v-for="(item, index) in statsList" :key="index">
+        <div class="card-icon">
+          <img width="76" :src="item.icon" :alt="item.label" />
         </div>
-        <div class="stat-item state-other">
-          <div class="stat-label">今日DDoS攻击峰值</div>
-          <div class="stat-value">{{ formatNumber(stats.ddosPeak) }}</div>
+        <div class="card-content">
+          <div :style="{ background: item.color }" class="card-bg"></div>
+          <div class="value">{{ formatNumber(item.value) }}</div>
+          <div class="label">{{ item.label }}</div>
         </div>
-      </section>
+      </div>
     </div>
-  </div>
+
+    <!-- 底部趋势图 -->
+    <div class="chart-section">
+      <h3 class="chart-title">24小时攻击趋势图</h3>
+      <div class="chart-container">
+        <client-only>
+          <v-chart class="chart" :option="chartOption" autoresize />
+        </client-only>
+      </div>
+    </div>
+  </section>
 </template>
 
 <script setup>
-import { onMounted, onUnmounted } from 'vue'
+import { computed, ref, onMounted } from 'vue'
+import { useStatsStore } from '~/stores/stats'
 import { storeToRefs } from 'pinia'
+import { use } from 'echarts/core'
+import { CanvasRenderer } from 'echarts/renderers'
+import { LineChart } from 'echarts/charts'
+import {
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  TitleComponent
+} from 'echarts/components'
+import VChart from 'vue-echarts'
+import * as echarts from 'echarts/core'
+
+// 注册 ECharts 组件
+use([
+  CanvasRenderer,
+  LineChart,
+  GridComponent,
+  TooltipComponent,
+  LegendComponent,
+  TitleComponent
+])
+
+// 图标资源 (使用 import 引入以确保 Vite 正确处理路径)
+import iconDDoS from '~/assets/svg/home/an1.svg'
+import iconCC from '~/assets/svg/home/an2.svg'
+import iconWAF from '~/assets/svg/home/an3.svg'
+
 const statsStore = useStatsStore()
 const { stats } = storeToRefs(statsStore)
 
-let intervalId = null
+// 格式化数字
+const formatNumber = (num) => {
+  return num?.toLocaleString() || '0'
+}
+
+// 统计数据列表
+const statsList = computed(() => [
+  { label: '今日 DDoS 攻击峰值', value: stats.value.ddosPeak, icon: iconDDoS, color: '#7D46FF', background: 'linear-gradient(181deg, rgba(130, 77, 255, 0.60) -50.14%, rgba(164, 125, 255, 0.00) 81.35%)' },
+  { label: '今日 CC 攻击次数', value: stats.value.ccAttacks, icon: iconCC, color: '#6971FF', background: 'linear-gradient(181deg, rgba(164, 169, 255, 0.60) -50.14%, rgba(56, 66, 255, 1.00) 81.35%)' },
+  { label: '今日 WAF 拦截次数', value: stats.value.wafBlocks, icon: iconWAF, color: '#9466FF', background: 'linear-gradient(181deg, rgba(130, 77, 255, 0.60) -50.14%, rgba(164, 125, 255, 0.00) 81.35%)' }
+]) 
 
+// 启动自动增长
 onMounted(() => {
-  intervalId = statsStore.startAutoIncrement()
+  statsStore.startAutoIncrement()
 })
 
-onUnmounted(() => {
-  if (intervalId) {
-    statsStore.stopAutoIncrement(intervalId)
-  }
-})
+// 生成 24小时 Mock 数据
+const generateData = () => {
+  const hours = ['00:00', '02:00', '04:00', '06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00']
+  const seriesData1 = [] // DDoS
+  const seriesData2 = [] // CC
+  const seriesData3 = [] // WAF
 
-const formatNumber = (num) => {
-  return num.toLocaleString()
+  for (let i = 0; i < hours.length; i++) {
+    // 使用正弦波+随机数模拟波浪效果
+    seriesData1.push(Math.floor(30000 + Math.sin(i / 2) * 10000 + Math.random() * 5000))
+    seriesData2.push(Math.floor(40000 + Math.sin(i / 2 + 1) * 15000 + Math.random() * 5000))
+    seriesData3.push(Math.floor(20000 + Math.sin(i / 2 + 2) * 8000 + Math.random() * 5000))
+  }
+  return { hours, seriesData1, seriesData2, seriesData3 }
 }
+
+const { hours, seriesData1, seriesData2, seriesData3 } = generateData()
+
+// ECharts 配置
+const chartOption = computed(() => ({
+  backgroundColor: 'transparent',
+  tooltip: {
+    trigger: 'axis',
+    backgroundColor: 'linear-gradient(181deg, rgba(101, 70, 255, 0.40) -42.03%, rgba(101, 70, 255, 0.10) 107.46%)',
+    borderColor: '#C6BAFF',
+    borderRadius: 24,
+    textStyle: {
+      color: '#fff'
+    },
+    axisPointer: {
+      type: 'line',
+      lineStyle: {
+        color: '#ffffff',
+        type: 'dashed',
+        opacity: 0.5
+      }
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: false,
+    data: hours,
+    axisLine: {
+      lineStyle: {
+        color: 'rgba(255, 255, 255, 0.2)'
+      }
+    },
+    axisLabel: {
+      color: 'rgba(255, 255, 255, 0.5)',
+      fontSize: 12
+    }
+  },
+  yAxis: {
+    type: 'value',
+    splitLine: {
+      lineStyle: {
+        color: 'rgba(255, 255, 255, 0.05)',
+        type: 'dashed'
+      }
+    },
+    axisLabel: {
+      color: 'rgba(255, 255, 255, 0.5)'
+    }
+  },
+  series: [
+    {
+      name: 'DDoS攻击',
+      type: 'line',
+      smooth: true,
+      showSymbol: false,
+      lineStyle: {
+        width: 3,
+        color: '#7D46FF'
+      },
+      itemStyle: {
+        color: '#7D46FF'
+      },
+      areaStyle: {
+        opacity: 0.3,
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: 'rgba(125, 70, 255, 0.5)' },
+          { offset: 1, color: 'rgba(3, 0, 20, 0)' }
+        ])
+      },
+      data: seriesData1
+    },
+    {
+      name: 'CC攻击',
+      type: 'line',
+      smooth: true,
+      showSymbol: false,
+      lineStyle: {
+        width: 3,
+        color: '#4B54FF'
+      },
+      itemStyle: {
+        color: '#4B54FF'
+      },
+      areaStyle: {
+        opacity: 0.3,
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: 'rgba(75, 84, 255, 0.5)' },
+          { offset: 1, color: 'rgba(3, 0, 20, 0)' }
+        ])
+      },
+      data: seriesData2
+    },
+    {
+      name: 'WAF拦截',
+      type: 'line',
+      smooth: true,
+      showSymbol: false,
+      lineStyle: {
+        width: 3,
+        color: '#AE8CFF'
+      },
+      itemStyle: {
+        color: '#AE8CFF'
+      },
+      areaStyle: {
+        opacity: 0.3,
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: 'rgba(174, 140, 255, 0.5)' },
+          { offset: 1, color: 'rgba(3, 0, 20, 0)' }
+        ])
+      },
+      data: seriesData3
+    }
+  ]
+}))
 </script>
 
 <style scoped lang="scss">
-.stats-container {
-  width: 100%;
-  display: flex;
-  justify-content: center;
-}
-
-.stats-bg-wrapper {
-  position: relative;
+.stats-section {
   width: 100%;
-  height: 824px;
-
-  .stats-bg {
-    display: flex;
-    justify-content: center;
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    width: 1400px;
-    height: auto;
-  }
+  max-width: 1200px;
+  margin: 60px auto;
+  padding: 0 20px;
+  color: #fff;
 }
 
-.stats-section {
-  width: 100%;
-  height: 100%;
-  position: absolute;
-  top: 0;
-  left: 0;
+.stats-cards {
   display: flex;
-  align-items: center;
-  justify-content: center;
-  overflow: hidden;
-
-  .stats-row {
-    position: absolute;
-    top: 40%;
-    left: 50%;
-    transform: translateX(-50%) translateY(-50%);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    z-index: 1;
-  }
+  justify-content: space-between;
+  max-width: 1200px;
+  box-sizing: border-box;
+  gap: 30px;
+  margin-bottom: 60px;
 
-  .stats-figure {
-    position: absolute;
-    top: 40%;
-    left: 50%;
-    transform: translateX(-50%) translateY(-50%);
-    object-fit: contain;
-    pointer-events: none;
-    z-index: 0;
-    will-change: transform;
-    animation: float 3s ease-in-out infinite;
-
-    :deep(img) {
-      width: 180px;
-      height: auto;
-    }
-  }
+  .card {
+    flex: 1;
+    position: relative;
+    width: 380px;
+    height: 180px;
+    background: rgba(255, 255, 255, 0.03);
+    border-radius: 20px;
+    transition: transform 0.3s ease;
+    backdrop-filter: blur(1px);
+    background: linear-gradient(181deg, rgba(130, 77, 255, 0.60) -50.14%, rgba(164, 125, 255, 0.00) 81.35%);
 
-  @keyframes float {
-    0% {
-      transform: translateX(-50%) translateY(-50%);
+    .card-bg {
+      position: absolute;
+      left: 50%;
+      top: 0;
+      transform: translate(-50%, -50%);
+      width: 152px;
+      height: 106px;
+      // background: #9466FF; // 已通过内联样式动态绑定
+      filter: blur(25px);
+      border-radius: 50%;
     }
 
-    50% {
-      transform: translateX(-50%) translateY(calc(-50% - 20px));
+    .card-content {
+      position: relative;
+      z-index: 1;
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      text-align: center;
+      overflow: hidden;
     }
 
-    100% {
-      transform: translateX(-50%) translateY(-50%);
+    .card-icon {
+      position: absolute;
+      left: 50%;
+      top: 0;
+      transform: translate(-50%, -50%);
+      border-radius: 100px;
+      border: 1px solid rgba(64, 64, 64, 0.50);
+      background: rgba(255, 255, 255, 0.10);
+      box-shadow: -20px 68px 20px 0 rgba(0, 0, 0, 0.00), -13px 43px 18px 0 rgba(0, 0, 0, 0.01), -7px 24px 15px 0 rgba(0, 0, 0, 0.04), -3px 11px 11px 0 rgba(0, 0, 0, 0.07), -1px 3px 6px 0 rgba(0, 0, 0, 0.08);
+      backdrop-filter: blur(7.5px);
+      z-index: 2;
     }
-  }
-
-  .state-other {
-    position: absolute;
-    top: 60px;
-  }
 
-  .stat-item {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: space-between;
-    margin: 0 300px;
-    z-index: 1;
-
-    .stat-label {
+    .value {
+      color: #FFF;
+      font-size: 60px;
+      font-weight: 700;
+      line-height: 60px;
+    }
 
+    .label {
+      margin-top: 20px;
+      color: #C6BAFF;
       font-size: 24px;
       font-weight: 400;
       line-height: 24px;
-      color: #ffffff;
-      text-align: center;
-      margin-bottom: 20px;
     }
+  }
+}
 
-    .stat-value {
+.chart-section {
+  text-align: center;
 
-      font-size: 56px;
-      font-weight: 700;
-      line-height: 56px;
-      color: #a182ff;
-      text-align: center;
+  .chart-title {
+    font-size: 30px;
+    font-weight: 400;
+    margin-bottom: 40px;
+    letter-spacing: 1px;
+    color: #FFF;
+  }
+
+  .chart-container {
+    width: 100%;
+    height: 400px;
+    position: relative;
+
+    .chart {
+      width: 100%;
+      height: 100%;
     }
   }
 }

+ 3 - 2
app/pages/web/index.vue

@@ -18,7 +18,6 @@
     <!-- 安全洞见标题 -->
 
     <!-- 数据展示区域 -->
-    <!-- <StatsSection /> -->
 
     <!-- 安全运营与方案模块 -->
     <PlansSection />
@@ -51,6 +50,8 @@
       <h2 class="insight-title">安全洞见&nbsp;&nbsp;&nbsp;全网感知</h2>
       <p class="insight-subtitle">实时攻防态势数据</p>
     </section>
+    
+    <StatsSection />
 
     <!-- 助力各行业客户成功 -->
     <section class="cain-section">
@@ -142,7 +143,7 @@ definePageMeta({
 })
 
 import { ref } from 'vue'
-// import StatsSection from '~/components/home/StatsSection.vue'
+import StatsSection from '~/components/home/StatsSection.vue'
 import ProductTabs from '~/components/home/ProductTabs.vue'
 import PlansSection from '~/components/PlansSection.vue'
 import ParticlesCanvas from '~/components/ParticlesCanvas.vue'

+ 2 - 0
package.json

@@ -15,8 +15,10 @@
     "@nuxt/image": "2.0.0",
     "@pinia/nuxt": "^0.5.5",
     "@vueuse/nuxt": "^14.1.0",
+    "echarts": "^6.0.0",
     "nuxt": "^4.2.2",
     "vue": "^3.5.26",
+    "vue-echarts": "^8.0.1",
     "vue-router": "^4.6.4"
   },
   "devDependencies": {

Diferenças do arquivo suprimidas por serem muito extensas
+ 26 - 547
pnpm-lock.yaml


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff