Bläddra i källkod

feat: 添加SDK详情页功能及图表组件

添加ECharts图表库支持
新增状态文本组件StatusText
实现转发规则、连接列表和使用分析页面
优化SDK详情页布局和交互
添加多个SVG图标资源
更新主题样式和组件全局注册
piks 4 dagar sedan
förälder
incheckning
c4f9947b89

+ 3 - 0
components.d.ts

@@ -19,6 +19,7 @@ declare module 'vue' {
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -36,6 +37,7 @@ declare module 'vue' {
     ElPopover: typeof import('element-plus/es')['ElPopover']
     ElProgress: typeof import('element-plus/es')['ElProgress']
     ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
@@ -48,6 +50,7 @@ declare module 'vue' {
     Pagination: typeof import('./src/components/Pagination/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    StatusText: typeof import('./src/components/StatusText/index.vue')['default']
     SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
     TabFilter: typeof import('./src/components/TabFilter/index.vue')['default']
     TableCard: typeof import('./src/components/TableCard/index.vue')['default']

+ 2 - 0
package.json

@@ -12,11 +12,13 @@
     "@element-plus/icons-vue": "^2.3.2",
     "@vueuse/core": "^14.2.1",
     "axios": "^1.13.6",
+    "echarts": "^6.0.0",
     "element-plus": "^2.13.5",
     "file-saver": "^2.0.5",
     "normalize.css": "^8.0.1",
     "pinia": "^3.0.4",
     "vue": "^3.5.30",
+    "vue-echarts": "^8.0.1",
     "vue-router": "^5.0.3"
   },
   "devDependencies": {

+ 37 - 0
pnpm-lock.yaml

@@ -17,6 +17,9 @@ importers:
       axios:
         specifier: ^1.13.6
         version: 1.13.6
+      echarts:
+        specifier: ^6.0.0
+        version: 6.0.0
       element-plus:
         specifier: ^2.13.5
         version: 2.13.5(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
@@ -32,6 +35,9 @@ importers:
       vue:
         specifier: ^3.5.30
         version: 3.5.30(typescript@5.9.3)
+      vue-echarts:
+        specifier: ^8.0.1
+        version: 8.0.1(echarts@6.0.0)(vue@3.5.30(typescript@5.9.3))
       vue-router:
         specifier: ^5.0.3
         version: 5.0.3(@vue/compiler-sfc@3.5.30)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))(vue@3.5.30(typescript@5.9.3))
@@ -784,6 +790,9 @@ packages:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
 
+  echarts@6.0.0:
+    resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
+
   element-plus@2.13.5:
     resolution: {integrity: sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==}
     peerDependencies:
@@ -1935,6 +1944,9 @@ packages:
     resolution: {integrity: sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==}
     engines: {node: '>= 0.4'}
 
+  tslib@2.3.0:
+    resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
   tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
@@ -2097,6 +2109,12 @@ packages:
   vscode-uri@3.1.0:
     resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
 
+  vue-echarts@8.0.1:
+    resolution: {integrity: sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==}
+    peerDependencies:
+      echarts: ^6.0.0
+      vue: ^3.3.0
+
   vue-router@5.0.3:
     resolution: {integrity: sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==}
     peerDependencies:
@@ -2150,6 +2168,9 @@ packages:
     engines: {node: '>= 14.6'}
     hasBin: true
 
+  zrender@6.0.0:
+    resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
+
 snapshots:
 
   '@babel/generator@7.29.1':
@@ -2867,6 +2888,11 @@ snapshots:
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  echarts@6.0.0:
+    dependencies:
+      tslib: 2.3.0
+      zrender: 6.0.0
+
   element-plus@2.13.5(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)):
     dependencies:
       '@ctrl/tinycolor': 4.2.0
@@ -4131,6 +4157,8 @@ snapshots:
       typedarray.prototype.slice: 1.0.5
       which-typed-array: 1.1.20
 
+  tslib@2.3.0: {}
+
   tslib@2.8.1: {}
 
   typed-array-buffer@1.0.3:
@@ -4313,6 +4341,11 @@ snapshots:
 
   vscode-uri@3.1.0: {}
 
+  vue-echarts@8.0.1(echarts@6.0.0)(vue@3.5.30(typescript@5.9.3)):
+    dependencies:
+      echarts: 6.0.0
+      vue: 3.5.30(typescript@5.9.3)
+
   vue-router@5.0.3(@vue/compiler-sfc@3.5.30)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))(vue@3.5.30(typescript@5.9.3)):
     dependencies:
       '@babel/generator': 7.29.1
@@ -4397,3 +4430,7 @@ snapshots:
       has-tostringtag: 1.0.2
 
   yaml@2.8.2: {}
+
+  zrender@6.0.0:
+    dependencies:
+      tslib: 2.3.0

+ 5 - 0
src/assets/svg/Harm.svg

@@ -0,0 +1,5 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.25 3.47087L9.00323 1.5L15.75 3.47087V7.51264C15.75 11.7608 13.0313 15.5323 9.00097 16.8752C4.96954 15.5323 2.25 11.76 2.25 7.51076V3.47087Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
+<path d="M11.063 6.90308L6.82031 11.1457" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.82031 6.9032L11.063 11.1458" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 7 - 0
src/assets/svg/Link-right.svg

@@ -0,0 +1,7 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 4.5V1.5H9V4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 13.5V16.5H3V13.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M16.5 9H9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 12.75V5.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 6L16.5 9L13.5 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 4 - 0
src/assets/svg/Reduce-one.svg

@@ -0,0 +1,4 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9 16.5C13.1421 16.5 16.5 13.1421 16.5 9C16.5 4.85786 13.1421 1.5 9 1.5C4.85786 1.5 1.5 4.85786 1.5 9C1.5 13.1421 4.85786 16.5 9 16.5Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
+<path d="M6 9H12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 2 - 2
src/assets/svg/protect.svg

@@ -1,4 +1,4 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 3.08519L8.00287 1.33331L14 3.08519V6.67788C14 10.454 11.5834 13.8064 8.00087 15.0001C4.41737 13.8065 2 10.4533 2 6.67621V3.08519Z" stroke="white" stroke-width="1.2" stroke-linejoin="round"/>
-<path d="M5 7.66667L7.33333 10L11.3333 6" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 3.08519L8.00287 1.33331L14 3.08519V6.67788C14 10.454 11.5834 13.8064 8.00087 15.0001C4.41737 13.8065 2 10.4533 2 6.67621V3.08519Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
+<path d="M5 7.66667L7.33333 10L11.3333 6" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

+ 51 - 0
src/components/StatusText/index.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="status-indicator">
+    <span class="dot"></span>
+    <span class="label">
+      <!-- 支持通过 prop 传入文本,也支持通过插槽 (slot) 传入更复杂的内容 -->
+      <slot>{{ text }}</slot>
+    </span>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 定义 Props 类型
+interface Props {
+  color?: string; // 圆点颜色
+  text?: string;  // 显示的文本
+}
+
+// 设置默认值
+withDefaults(defineProps<Props>(), {
+  color: '#35FFDD', // 默认使用图片中的青/荧光蓝色
+  text: '启用'
+});
+</script>
+
+<style scoped>
+.status-indicator {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  /* 圆点和文字之间的间距 */
+  font-size: 14px;
+  color: var(--admin-text-primary);
+  /* 适配你之前暗黑主题的文字颜色 */
+}
+
+.dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  /* 使用 Vue3 的 v-bind 特性直接绑定 prop 中的 color 到 CSS 变量 */
+  background-color: v-bind(color);
+  display: inline-block;
+
+  /* 可选:如果你想要一点点发光的效果,可以取消下面这行的注释 */
+  box-shadow: 0 0 4px v-bind(color);
+}
+
+.label {
+  line-height: 1;
+}
+</style>

+ 2 - 2
src/layout/components/AppMain.vue

@@ -7,9 +7,9 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn'
 <template>
   <main class="app-main">
     <ElConfigProvider :locale="zhCn">
-      <router-view v-slot="{ Component }">
+      <router-view v-slot="{ Component, route }">
         <transition name="fade" mode="out-in">
-          <component :is="Component" />
+          <component :is="Component" :key="route.fullPath" />
         </transition>
       </router-view>
     </ElConfigProvider>

+ 3 - 0
src/main.ts

@@ -10,6 +10,7 @@ import '@/styles/theme.scss'
 import '@/styles/element.scss'
 import Breadcrumb from '@/components/Breadcrumb/index.vue'
 import Pagination from '@/components/Pagination/index.vue'
+import StatusText from '@/components/StatusText/index.vue'
 
 // 注册 SVG 图标组件
 import SvgIcon from '@/components/SvgIcon/index.vue'
@@ -31,6 +32,8 @@ app.component('SvgIcon', SvgIcon)
 app.component('Breadcrumb', Breadcrumb)
 // 注册分页组件
 app.component('Pagination', Pagination)
+// 注册状态文本组件
+app.component('StatusText', StatusText)
 
 app.use(pinia).use(router)
 app.mount('#app')

+ 31 - 0
src/styles/theme.scss

@@ -18,6 +18,22 @@
   // ---- 暗色(按下/激活:与黑色混合 20%) ----
   --el-color-primary-dark-2: #665fcc;
 
+  // ---- Success 色 ----
+  --el-color-success: #35FFDD;
+  --el-color-success-light-3: #2bb9a1; /* 混合 30% 暗色 */
+  --el-color-success-light-5: #258a79; /* 混合 50% 暗色 (Plain按钮的默认边框色) */
+  --el-color-success-light-7: #1e5b50; /* 混合 70% 暗色 */
+  --el-color-success-light-8: #1b433c; /* 混合 80% 暗色 */
+  --el-color-success-light-9: #172c28; /* 混合 90% 暗色 (Plain按钮的默认背景色) */
+  --el-color-success-light-1: #32e8c9;
+  --el-color-success-light-2: #2ed0b5;
+  --el-color-success-light-4: #28a18d;
+  --el-color-success-light-6: #217264;
+  /* ---- Success 按压色(与纯白 #ffffff 混合产生) ---- */
+  /* 主要用于按钮按下 Active 时的状态反馈 */
+  --el-color-success-dark-2: #5dffe4;
+
+
   // 自定义渐变按钮变量
   --btn-primary-gradient: linear-gradient(91deg, #5f53ff -40.99%, #8d53ff 88.34%);
   --btn-primary-gradient-hover: linear-gradient(95deg, #8d22ff -9.99%, #ae00ff 91.44%);
@@ -54,6 +70,21 @@ html.dark {
   --el-color-primary-dark-2: #9992ff;
   --el-color-primary-dark: rgba(255, 255, 255, 0.08);
 
+  // ---- Success 色(暗色模式) ----
+  --el-color-success: #35FFDD;
+  --el-color-success-light-3: #2bb9a1; /* 混合 30% 暗色 */
+  --el-color-success-light-5: #258a79; /* 混合 50% 暗色 (Plain按钮的默认边框色) */
+  --el-color-success-light-7: #1e5b50; /* 混合 70% 暗色 */
+  --el-color-success-light-8: #1b433c; /* 混合 80% 暗色 */
+  --el-color-success-light-9: #172c28; /* 混合 90% 暗色 (Plain按钮的默认背景色) */
+  --el-color-success-light-1: #32e8c9;
+  --el-color-success-light-2: #2ed0b5;
+  --el-color-success-light-4: #28a18d;
+  --el-color-success-light-6: #217264;
+  /* ---- Success 按压色(与纯白 #ffffff 混合产生) ---- */
+  /* 主要用于按钮按下 Active 时的状态反馈 */
+  --el-color-success-dark-2: #5dffe4;
+
   // 暗色下的渐变按钮
   --btn-primary-gradient: linear-gradient(135deg, #5a6fe0 0%, #6a3f9a 100%);
   --btn-primary-gradient-hover: linear-gradient(135deg, #6a3f9a 0%, #5a6fe0 100%);

+ 111 - 0
src/views/sdk/components/ConnectionList.vue

@@ -0,0 +1,111 @@
+<template>
+  <TableCard :data="tableData" :columns="columns" v-model:page-no="queryParams.pageNo"
+    v-model:page-size="queryParams.pageSize" @change="handleChange" :total="total"
+    @selection-change="handleSelectionChange">
+    <template #toolbar-left>
+      <el-button plain type="warning" size="large" @click="dialogForm.visible = true">
+        <template #icon>
+          <SvgIcon iconClass="Harm" />
+        </template>
+        一键加入风险名单
+      </el-button>
+      <el-button plain type="danger" size="large">
+        <template #icon>
+          <SvgIcon iconClass="Reduce-one" />
+        </template>
+        一键加入黑名单
+      </el-button>
+      <el-button plain type="success" size="large">
+        <template #icon>
+          <SvgIcon iconClass="Link-right" />
+        </template>
+        一键解除封禁名单</el-button>
+      <el-button type="primary" :icon="Refresh" size="large" class="is-solid">刷新</el-button>
+    </template>
+    <template #toolbar-right>
+      <el-button :icon="Upload" type="primary" class="is-solid" size="large">导出</el-button>
+    </template>
+
+    <template #column-enableStatus="{ row }">
+      <StatusText :text="row.enableStatus" />
+    </template>
+
+    <template #column-sourceIpMethod="{ row }">
+      <el-tag :type="row.sourceIpMethod === '不获取' ? 'success' : 'danger'">
+        {{ row.sourceIpMethod }}
+      </el-tag>
+    </template>
+
+
+    <template #column-operation="{ row }">
+      <el-button type="primary" link>编辑</el-button>
+      <el-button type="danger" link>删除</el-button>
+    </template>
+  </TableCard>
+</template>
+<script setup lang="ts">
+import { CirclePlus, Delete, Refresh, Search, Upload } from '@element-plus/icons-vue'
+
+const dialogForm = reactive({
+  visible: false,
+})
+
+const state = reactive({
+  queryParams: {
+    pageNo: 1,
+    pageSize: 10,
+    searchKeyword: '',
+  },
+  total: 20,
+  tableData: [
+    {
+      id: 1,
+      instanceId: '123456',
+      protocolType: 'TCP',
+      sourceIp: '192.168.1.100',
+      sourcePort: 8080,
+      targetIp: '192.168.1.200',
+      targetPort: 8080,
+      enableStatus: '启用',
+      sourceIpMethod: '不获取',
+      priority: 1,
+      createTime: '2023-08-01 10:00:00',
+      updateTime: '2023-08-01 10:00:00'
+    }
+  ],
+  selectedList: [] as any[]
+})
+
+
+const columns = [
+  { type: 'selection', width: 55 },
+  { label: '设备ID', prop: 'id' },
+  { label: 'SDK版本', prop: 'instanceId' },
+  { label: '在线状态', prop: 'protocolType' },
+  { label: '调度状态', prop: 'sourceIp' },
+  { label: '请求次数', prop: 'sourcePort' },
+  { label: '设备IP', prop: 'targetIp' },
+  { label: '设备唯一ID', prop: 'enableStatus' },
+  { label: '客户备注', prop: 'sourceIpMethod' },
+  { label: '客户端地区', prop: 'priority', },
+  { label: '操作系统', prop: 'createTime' },
+  { label: '当前客户端版本号', prop: 'updateTime' },
+  { label: '上传流量汇总', prop: 'updateTime' },
+  { label: '下载流量汇总', prop: 'updateTime' },
+  { label: '创建时间', prop: 'updateTime' },
+  { label: '更新时间', prop: 'updateTime' },
+  { label: '操作', prop: 'operation', minWidth: 120, isAction: true }
+]
+function handleSelectionChange(selection: any[]) {
+  state.selectedList = selection
+}
+
+const handleChange = (val: any) => {
+  state.queryParams.pageNo = val.pageNo
+  state.queryParams.pageSize = val.pageSize
+}
+
+const { queryParams, total, tableData } = toRefs(state)
+
+</script>
+<style lang="scss" scoped></style>

+ 96 - 0
src/views/sdk/components/ForwardRules.vue

@@ -0,0 +1,96 @@
+<template>
+  <TableCard :data="tableData" :columns="columns" v-model:page-no="queryParams.pageNo"
+    v-model:page-size="queryParams.pageSize" @change="handleChange" :total="total"
+    @selection-change="handleSelectionChange">
+    <template #toolbar-left>
+      <el-button type="primary" :icon="CirclePlus" size="large" class="is-solid"
+        @click="dialogForm.visible = true">添加</el-button>
+      <el-button type="primary" disabled :icon="Delete" size="large" class="is-solid">删除</el-button>
+      <el-button type="primary" :icon="Refresh" size="large" class="is-solid">刷新</el-button>
+    </template>
+    <template #toolbar-right>
+      <el-input :prefix-icon="Search" v-model="queryParams.searchKeyword" size="large" placeholder="请输入关键词搜索"
+        style="width: 300px" clearable />
+      <el-button :icon="Search" type="primary" class="is-solid" size="large">搜索</el-button>
+      <el-button :icon="Upload" type="primary" class="is-solid" size="large">导出</el-button>
+    </template>
+
+    <template #column-enableStatus="{ row }">
+      <StatusText :text="row.enableStatus" />
+    </template>
+
+    <template #column-sourceIpMethod="{ row }">
+      <el-tag :type="row.sourceIpMethod === '不获取' ? 'success' : 'danger'">
+        {{ row.sourceIpMethod }}
+      </el-tag>
+    </template>
+
+
+    <template #column-operation="{ row }">
+      <el-button type="primary" link>编辑</el-button>
+      <el-button type="danger" link>删除</el-button>
+    </template>
+  </TableCard>
+</template>
+<script setup lang="ts">
+import { CirclePlus, Delete, Refresh, Search, Upload } from '@element-plus/icons-vue'
+
+const dialogForm = reactive({
+  visible: false,
+})
+
+const state = reactive({
+  queryParams: {
+    pageNo: 1,
+    pageSize: 10,
+    searchKeyword: '',
+  },
+  total: 20,
+  tableData: [
+    {
+      id: 1,
+      instanceId: '123456',
+      protocolType: 'TCP',
+      sourceIp: '192.168.1.100',
+      sourcePort: 8080,
+      targetIp: '192.168.1.200',
+      targetPort: 8080,
+      enableStatus: '启用',
+      sourceIpMethod: '不获取',
+      priority: 1,
+      createTime: '2023-08-01 10:00:00',
+      updateTime: '2023-08-01 10:00:00'
+    }
+  ],
+  selectedList: [] as any[]
+})
+
+
+const columns = [
+  { type: 'selection', width: 55 },
+  { label: '序号', prop: 'id' },
+  { label: '所属实例ID', prop: 'instanceId' },
+  { label: '协议类型', prop: 'protocolType' },
+  { label: '源IP', prop: 'sourceIp' },
+  { label: '源端口', prop: 'sourcePort' },
+  { label: '目标IP', prop: 'targetIp' },
+  { label: '规则启用状态', prop: 'enableStatus' },
+  { label: '源IP获取方式', prop: 'sourceIpMethod' },
+  { label: '优先级(数值越小优先级越高)', prop: 'priority', minWidth: 160 },
+  { label: '创建时间', prop: 'createTime' },
+  { label: '更新时间', prop: 'updateTime' },
+  { label: '操作', prop: 'operation', minWidth: 120, isAction: true }
+]
+function handleSelectionChange(selection: any[]) {
+  state.selectedList = selection
+}
+
+const handleChange = (val: any) => {
+  state.queryParams.pageNo = val.pageNo
+  state.queryParams.pageSize = val.pageSize
+}
+
+const { queryParams, total, tableData } = toRefs(state)
+
+</script>
+<style lang="scss" scoped></style>

+ 1 - 2
src/views/sdk/components/SDKDetail.vue

@@ -87,7 +87,7 @@
               <span class="value">{{ sdkData.package }}</span>
             </div>
             <div class="grid-item form-item">
-              <el-form-item label="回环IP:">
+              <el-form-item label="回环IP:" label-width="70px">
                 <el-input v-model="sdkForm.loopbackIp" @blur="handleSdkFormChange" />
               </el-form-item>
             </div>
@@ -304,7 +304,6 @@ const saveBackupNodeRules = () => {
 <style lang="scss" scoped>
 .sdk-detail-container {
   color: var(--admin-text-primary);
-  padding-top: 20px;
   background-color: var(--admin-bg);
 }
 

+ 264 - 0
src/views/sdk/components/UsageAnalysis.vue

@@ -0,0 +1,264 @@
+<template>
+	<div class="dashboard-container">
+		<!-- 顶部导航切换 -->
+		<div class="top-nav">
+			<div class="nav-item" :class="{ active: activeTab === 'bandwidth' }" @click="activeTab = 'bandwidth'">
+				<el-icon>
+					<Monitor />
+				</el-icon>
+				<span>带宽图表</span>
+			</div>
+			<div class="nav-item" :class="{ active: activeTab === 'users' }" @click="activeTab = 'users'">
+				<el-icon>
+					<User />
+				</el-icon>
+				<span>用户概况统计</span>
+			</div>
+		</div>
+
+		<!-- 过滤条件区域 -->
+		<div class="filter-bar">
+			<el-radio-group v-model="timeRange" class="dark-radio-group">
+				<el-radio-button label="3h">3小时</el-radio-button>
+				<el-radio-button label="12h">12小时</el-radio-button>
+				<el-radio-button label="1d">近1天</el-radio-button>
+				<el-radio-button label="7d">近7天</el-radio-button>
+			</el-radio-group>
+
+			<el-date-picker v-model="dateRange" type="datetimerange" range-separator="—" start-placeholder="开始日期"
+				end-placeholder="结束日期" class="dark-date-picker" />
+		</div>
+
+		<!-- 图表展示区域 -->
+		<div class="charts-wrapper">
+			<!-- 带宽图表视图 -->
+			<template v-if="activeTab === 'bandwidth'">
+				<div class="chart-card">
+					<div class="chart-title">收发带宽统计</div>
+					<v-chart class="chart-instance" :option="bandwidthInOutOption" autoresize />
+				</div>
+				<div class="chart-card">
+					<div class="chart-title">连接数统计</div>
+					<v-chart class="chart-instance" :option="connectionsOption" autoresize />
+				</div>
+			</template>
+
+			<!-- 用户概况统计视图 -->
+			<template v-if="activeTab === 'users'">
+				<div class="chart-card">
+					<div class="chart-title">新增用户</div>
+					<v-chart class="chart-instance" :option="newUsersOption" autoresize />
+				</div>
+				<div class="chart-card">
+					<div class="chart-title">活跃用户</div>
+					<v-chart class="chart-instance" :option="activeUsersOption" autoresize />
+				</div>
+			</template>
+		</div>
+	</div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { Monitor, User } from '@element-plus/icons-vue'
+import { use } from 'echarts/core'
+import { CanvasRenderer } from 'echarts/renderers'
+import { LineChart } from 'echarts/charts'
+import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
+import VChart from 'vue-echarts'
+import * as echarts from 'echarts/core'
+
+// 注册 ECharts 必须的组件
+use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent])
+
+// 状态控制
+const activeTab = ref('bandwidth')
+const timeRange = ref('3h')
+const dateRange = ref([new Date(2022, 9, 25, 14, 19, 39), new Date(2022, 9, 25, 14, 19, 39)])
+
+// --- Mock 数据生成工具 ---
+const generateTimeData = (count = 10) => {
+	let base = +new Date(2026, 2, 18, 11, 25, 2)
+	return Array.from({ length: count }, (_, i) => {
+		let d = new Date(base + i * 360000); // 间隔递增
+		return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
+	})
+}
+const generateRandomData = (count = 10, min = 0, max = 10) => {
+	return Array.from({ length: count }, () => (Math.random() * (max - min) + min).toFixed(3))
+}
+
+const timeAxisData = generateTimeData(15)
+
+// --- 图表通用配置 ---
+const commonAxisLine = { lineStyle: { color: '#555' } }
+const commonSplitLine = { show: true, lineStyle: { color: '#333', type: 'dashed' } }
+const commonLabel = { color: '#aaa' }
+
+// 1. 收发带宽统计图表 Option
+const bandwidthInOutOption = computed(() => ({
+	tooltip: { trigger: 'axis', backgroundColor: 'rgba(50,50,70,0.8)', textStyle: { color: '#fff' } },
+	legend: { data: ['实时上传流量', '实时下载流量'], textStyle: { color: '#ccc' }, top: 0, left: 100 },
+	grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+	xAxis: { type: 'category', boundaryGap: false, data: timeAxisData, axisLine: commonAxisLine, axisLabel: commonLabel },
+	yAxis: { type: 'value', axisLine: commonAxisLine, splitLine: commonSplitLine, axisLabel: { ...commonLabel, formatter: '{value}bps' } },
+	series: [
+		{
+			name: '实时上传流量', type: 'line', smooth: true,
+			lineStyle: { color: '#9b59b6' }, itemStyle: { color: '#9b59b6' },
+			areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(155,89,182,0.3)' }, { offset: 1, color: 'rgba(155,89,182,0)' }]) },
+			data: generateRandomData(15, 2, 8)
+		},
+		{
+			name: '实时下载流量', type: 'line', smooth: true,
+			lineStyle: { color: '#3498db' }, itemStyle: { color: '#3498db' },
+			areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(52,152,219,0.3)' }, { offset: 1, color: 'rgba(52,152,219,0)' }]) },
+			data: generateRandomData(15, 2, 8)
+		}
+	]
+}))
+
+// 2. 连接数统计图表 Option
+const connectionsOption = computed(() => ({
+	tooltip: { trigger: 'axis', backgroundColor: 'rgba(50,50,70,0.8)', textStyle: { color: '#fff' } },
+	legend: { data: ['连接数量'], textStyle: { color: '#ccc' }, top: 0, left: 100 },
+	grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+	xAxis: { type: 'category', boundaryGap: false, data: timeAxisData, axisLine: commonAxisLine, axisLabel: commonLabel },
+	yAxis: { type: 'value', axisLine: commonAxisLine, splitLine: commonSplitLine, axisLabel: commonLabel },
+	series: [
+		{
+			name: '连接数量', type: 'line', smooth: true,
+			lineStyle: { color: '#f39c12' }, itemStyle: { color: '#f39c12' },
+			areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(243,156,18,0.3)' }, { offset: 1, color: 'rgba(243,156,18,0)' }]) },
+			data: generateRandomData(15, 0, 1)
+		}
+	]
+}))
+
+// 3. 新增用户 Option
+const newUsersOption = computed(() => ({
+	tooltip: { trigger: 'axis', backgroundColor: 'rgba(50,50,70,0.8)', textStyle: { color: '#fff' } },
+	legend: { data: ['Android', 'IOS', 'Windows', 'Linux', 'Unknown'], textStyle: { color: '#ccc' }, top: 0, left: 100 },
+	grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+	xAxis: { type: 'category', boundaryGap: false, data: timeAxisData, axisLine: commonAxisLine, axisLabel: commonLabel },
+	yAxis: { type: 'value', axisLine: commonAxisLine, splitLine: commonSplitLine, axisLabel: commonLabel },
+	series: [
+		{ name: 'Android', type: 'line', smooth: true, data: generateRandomData(15, 0, 1) },
+		{ name: 'IOS', type: 'line', smooth: true, data: generateRandomData(15, 0, 1) },
+		{ name: 'Windows', type: 'line', smooth: true, data: generateRandomData(15, 0, 1) },
+		{ name: 'Linux', type: 'line', smooth: true, data: generateRandomData(15, 0, 1) },
+		{ name: 'Unknown', type: 'line', smooth: true, data: generateRandomData(15, 0, 1) }
+	]
+}))
+
+// 4. 活跃用户 Option
+const activeUsersOption = computed(() => ({
+	tooltip: { trigger: 'axis', backgroundColor: 'rgba(50,50,70,0.8)', textStyle: { color: '#fff' } },
+	legend: { data: ['活跃用户'], textStyle: { color: '#ccc' }, top: 0, left: 100 },
+	grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+	xAxis: { type: 'category', boundaryGap: false, data: timeAxisData, axisLine: commonAxisLine, axisLabel: commonLabel },
+	yAxis: { type: 'value', axisLine: commonAxisLine, splitLine: commonSplitLine, axisLabel: commonLabel },
+	series: [
+		{
+			name: '活跃用户', type: 'line', smooth: true,
+			lineStyle: { color: '#2980b9' }, itemStyle: { color: '#2980b9' },
+			areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(41,128,185,0.3)' }, { offset: 1, color: 'rgba(41,128,185,0)' }]) },
+			data: generateRandomData(15, 0, 1)
+		}
+	]
+}))
+</script>
+
+<style scoped>
+/* 全局暗黑背景 */
+.dashboard-container {
+	padding: 20px;
+	color: #fff;
+}
+
+/* 顶部导航 Tabs */
+.top-nav {
+	display: flex;
+	gap: 10px;
+	margin-bottom: 20px;
+}
+
+.nav-item {
+	display: flex;
+	align-items: center;
+	gap: 6px;
+	padding: 10px 20px;
+	background-color: #2a2a32;
+	border-radius: 4px;
+	cursor: pointer;
+	color: #a0a0a0;
+	transition: all 0.3s;
+}
+
+.nav-item.active {
+	background-color: #6366f1;
+	/* 类似截图中的紫色主色调 */
+	color: #fff;
+}
+
+/* 过滤栏 */
+.filter-bar {
+	background-color: #22222a;
+	padding: 15px;
+	border-radius: 6px;
+	display: flex;
+	align-items: center;
+	gap: 20px;
+	margin-bottom: 20px;
+}
+
+/* 强制修改 Element Plus 组件以适应深色背景 */
+:deep(.el-radio-button__inner) {
+	background-color: #2a2a32;
+	border-color: #333;
+	color: #a0a0a0;
+}
+
+:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+	background-color: #4a4a5a;
+	border-color: #555;
+	color: #fff;
+	box-shadow: none;
+}
+
+:deep(.el-input__wrapper) {
+	background-color: #2a2a32;
+	box-shadow: 0 0 0 1px #333 inset;
+}
+
+:deep(.el-range-input) {
+	color: #ccc;
+}
+
+/* 图表区域 */
+.charts-wrapper {
+	display: flex;
+	flex-direction: column;
+	gap: 20px;
+}
+
+.chart-card {
+	background-color: #22222a;
+	border-radius: 6px;
+	padding: 20px;
+}
+
+.chart-title {
+	font-size: 14px;
+	color: #fff;
+	margin-bottom: 10px;
+	position: absolute;
+	/* 让标题不影响 Echarts 的 Legend 布局 */
+	z-index: 10;
+}
+
+.chart-instance {
+	height: 350px;
+	width: 100%;
+}
+</style>

+ 35 - 26
src/views/sdk/detail.vue

@@ -1,19 +1,40 @@
 <template>
-  <Breadcrumb />
-  <TabFilter v-model="activeTab" :options="options" @change="handleTabChange">
-    <template #actions>
-      <el-button :icon="ArrowLeft" type="primary" class="is-gradient">返回</el-button>
-    </template>
-  </TabFilter>
-  <component :is="currentComponent" />
+  <div>
+    <Breadcrumb />
+    <TabFilter v-model="activeTab" :options="options" style="margin-bottom: 20px;" @change="handleTabChange">
+      <template #actions>
+        <el-button :icon="ArrowLeft" type="primary" class="is-gradient" @click="goBack">返回</el-button>
+      </template>
+    </TabFilter>
+    <component :is="currentComponent" />
+  </div>
 </template>
+
 <script setup lang="ts">
 import { ArrowLeft } from '@element-plus/icons-vue'
-import { ref, computed } from 'vue'
+import { computed, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import ForwardRules from './components/ForwardRules.vue'
 import SDKDetail from './components/SDKDetail.vue'
+import ConnectionList from './components/ConnectionList.vue'
+import UsageAnalysis from './components/UsageAnalysis.vue'
+
+const componentMap = {
+  sdkDetail: SDKDetail,
+  forwardRules: ForwardRules,
+  connectionList: ConnectionList,
+  usageAnalysis: UsageAnalysis,
+  multiOpenRestriction: SDKDetail,
+  riskIpSegments: SDKDetail,
+  blockedIpSegments: SDKDetail,
+  generateSdk: SDKDetail,
+}
+
 type TabKey = keyof typeof componentMap
 
+const router = useRouter()
 const activeTab = ref<TabKey>('sdkDetail')
+
 const options: Array<{ label: string; value: TabKey }> = [
   { label: 'SDK详情', value: 'sdkDetail' },
   { label: '转发规则', value: 'forwardRules' },
@@ -25,25 +46,13 @@ const options: Array<{ label: string; value: TabKey }> = [
   { label: '生成SDK', value: 'generateSdk' },
 ]
 
-// 组件映射对象
-const componentMap = {
-  sdkDetail: SDKDetail,
-  forwardRules: SDKDetail, // 临时映射到SDKDetail,后续可替换为实际组件
-  connectionList: SDKDetail, // 临时映射到SDKDetail,后续可替换为实际组件
-  usageAnalysis: SDKDetail, // 临时映射到SDKDetail,后续可替换为实际组件
-  multiOpenRestriction: SDKDetail, // 临时映射到SDKDetail,后续可替换为实际组件
-  riskIpSegments: SDKDetail, // 临时映射到SDKDetail,后续可替换为实际组件
-  blockedIpSegments: SDKDetail, // 临时映射到SDKDetail,后续可替换为实际组件
-  generateSdk: SDKDetail, // 临时映射到SDKDetail,后续可替换为实际组件
-}
+const currentComponent = computed(() => componentMap[activeTab.value] ?? SDKDetail)
 
-// 计算当前要显示的组件
-const currentComponent = computed(() => {
-  return componentMap[activeTab.value] || SDKDetail
-})
+function handleTabChange(_value: TabKey) { }
 
-function handleTabChange(value: TabKey) {
+function goBack() {
+  router.back()
 }
-
 </script>
-<style lang="scss" scoped></style>
+
+<style lang="scss" scoped></style>

+ 9 - 5
src/views/sdk/index.vue

@@ -2,8 +2,9 @@
   <div>
     <Breadcrumb />
     <TabFilter v-model="activeTab" :options="options" @change="handleTabChange" />
-    <TableCard :data="tableData" :columns="columns" v-model:page-no="pageNo" v-model:page-size="pageSize"
-      @change="handleChange" :total="total" @selection-change="handleSelectionChange">
+    <TableCard :data="tableData" :columns="columns" v-model:page-no="queryParams.pageNo"
+      v-model:page-size="queryParams.pageSize" @change="handleChange" :total="total"
+      @selection-change="handleSelectionChange">
       <template #toolbar-left>
         <el-button type="primary" :icon="CirclePlus" size="large" class="is-solid"
           @click="dialogForm.visible = true">添加实例</el-button>
@@ -58,8 +59,11 @@ const selectedRows = ref<any[]>([])
 const router = useRouter()
 
 const state = reactive({
-  pageNo: 1,
-  pageSize: 10,
+  queryParams: {
+    pageNo: 1,
+    pageSize: 10,
+    searchKeyword: '',
+  },
   total: 20
 })
 
@@ -67,7 +71,7 @@ const dialogForm = reactive({
   visible: false
 })
 
-const { pageNo, pageSize, total } = toRefs(state)
+const { queryParams, total } = toRefs(state)
 
 const options = [
   { label: '全部', value: 'all' },