Răsfoiți Sursa

feat(waf): 添加 WAF管理功能

- 新增 WAF 管理路由和实例管理页面
- 实现 WAF 实例列表展示、搜索、恢复和同步时间功能
- 添加相关 API 接口和请求方法
fusu 6 zile în urmă
părinte
comite
29fd121eea

+ 28 - 0
web/src/api/waf/wafmanage.js

@@ -0,0 +1,28 @@
+import request from '~/utils/request.js'
+
+// 获取WAF管理列表
+export function getWafManageList(params) {
+  return request({
+    url: '/v1/admin/wafManage/getList',
+    method: 'get',
+    params
+  })
+}
+
+// 恢复WAF实例
+export function recoverWaf(data) {
+  return request({
+    url: '/v1/admin/wafManage/recover',
+    method: 'get',
+    params: data
+  })
+}
+
+// 同步执行续费操作
+export function syncExecuteRenewalActions(data) {
+  return request({
+    url: '/v1/admin/wafManage/syncExecuteRenewalActions',
+    method: 'get',
+    params: data
+  })
+}

+ 426 - 0
web/src/pages/waf/instance-manage.vue

@@ -0,0 +1,426 @@
+<template>
+  <div>
+    <a-card>
+      <!-- 搜索表单 -->
+      <div class="table-page-search-wrapper">
+        <a-form layout="inline">
+          <a-row :gutter="48">
+            <a-col :md="6" :sm="24">
+              <a-form-item label="实例ID">
+                <a-input v-model:value="queryParam.hostId" placeholder="请输入实例ID" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="用户ID">
+                <a-input v-model:value="queryParam.uid" placeholder="请输入用户ID" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="用户名">
+                <a-input v-model:value="queryParam.username" placeholder="请输入用户名" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="实例名称">
+                <a-input v-model:value="queryParam.name" placeholder="请输入实例名称" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="24" :sm="24">
+              <span class="table-page-search-submitButtons">
+                <a-button type="primary" @click="handleSearch">查询</a-button>
+                <a-button style="margin-left: 8px" @click="resetSearch">重置</a-button>
+                <a-button 
+                  type="default" 
+                  style="margin-left: 8px" 
+                  @click="handleRecover"
+                  :disabled="selectedRowKeys.length === 0"
+                  :loading="recoverLoading"
+                >
+                  恢复实例
+                </a-button>
+                <a-button 
+                  type="default" 
+                  style="margin-left: 8px" 
+                  @click="handleSyncTime"
+                  :disabled="selectedRowKeys.length === 0"
+                  :loading="syncLoading"
+                >
+                  同步时间
+                </a-button>
+              </span>
+            </a-col>
+          </a-row>
+        </a-form>
+      </div>
+
+      <!-- 数据表格 -->
+      <a-table
+        :columns="columns"
+        :row-key="record => record.id"
+        :data-source="dataSource"
+        :loading="loading"
+        :pagination="pagination"
+        :row-selection="rowSelection"
+        @change="handleTableChange"
+        :scroll="{ x: 'max-content' }"
+        bordered
+        size="middle"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.dataIndex === 'expiredAt'">
+            {{ formatTimestamp(record.expiredAt) }}
+          </template>
+          <template v-if="column.dataIndex === 'nextDueDate'">
+            {{ formatTimestamp(record.nextDueDate) }}
+          </template>
+          <template v-if="column.dataIndex === 'action'">
+            <a-button type="link" size="small" @click="handleSingleRecover(record)">恢复</a-button>
+            <a-button type="link" size="small" @click="handleSingleSync(record)">同步</a-button>
+          </template>
+        </template>
+      </a-table>
+    </a-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { message, Modal } from 'ant-design-vue'
+import { 
+  getWafManageList, 
+  recoverWaf, 
+  syncExecuteRenewalActions 
+} from '~/api/waf/wafmanage.js'
+
+// 响应式数据
+const loading = ref(false)
+const recoverLoading = ref(false)
+const syncLoading = ref(false)
+const dataSource = ref([])
+const selectedRowKeys = ref([])
+
+// 查询参数
+const queryParam = reactive({
+  hostId: '',
+  uid: '',
+  username: '',
+  name: '',
+  current: 1,
+  pageSize: 10,
+  column: 'id',
+  order: 'desc'
+})
+
+// 分页配置
+const pagination = reactive({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showSizeChanger: true,
+  showQuickJumper: true,
+  showTotal: total => `共 ${total} 条记录`,
+  pageSizeOptions: ['10', '20', '50', '100']
+})
+
+// 表格列定义
+const columns = [
+  {
+    title: 'ID',
+    dataIndex: 'id',
+    width: 80,
+    sorter: true
+  },
+  {
+    title: '实例ID',
+    dataIndex: 'hostId',
+    width: 100,
+    sorter: true
+  },
+  {
+    title: '用户ID',
+    dataIndex: 'uid',
+    width: 100,
+    sorter: true
+  },
+  {
+    title: '用户名',
+    dataIndex: 'username',
+    width: 120
+  },
+  {
+    title: '实例名称',
+    dataIndex: 'name',
+    width: 150
+  },
+  {
+    title: '过期时间',
+    dataIndex: 'expiredAt',
+    width: 180,
+    sorter: true
+  },
+  {
+    title: '下次到期时间',
+    dataIndex: 'nextDueDate',
+    width: 180,
+    sorter: true
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    width: 120,
+    fixed: 'right'
+  }
+]
+
+// 行选择配置
+const rowSelection = reactive({
+  selectedRowKeys: selectedRowKeys,
+  onChange: (keys) => {
+    selectedRowKeys.value = keys
+  }
+})
+
+// 格式化时间戳
+const formatTimestamp = (timestamp) => {
+  if (!timestamp) return '-'
+  const date = new Date(timestamp * 1000)
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  })
+}
+
+// 获取数据
+const fetchData = async () => {
+  loading.value = true
+  try {
+    const params = {
+      ...queryParam,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    }
+    
+    const response = await getWafManageList(params)
+    if (response && response.code === 0) {
+      // 根据后端返回的PaginatedResponse结构调整
+      const apiData = response.data
+      if (apiData && apiData.records && Array.isArray(apiData.records)) {
+        dataSource.value = apiData.records
+        pagination.total = apiData.total || 0
+        pagination.current = apiData.page || 1
+        pagination.pageSize = apiData.pageSize || 10
+      } else {
+        dataSource.value = []
+        pagination.total = 0
+      }
+    } else {
+      message.error(response?.message || '获取数据失败')
+    }
+  } catch (error) {
+    console.error('获取WAF实例列表失败:', error)
+    message.error('获取数据失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 搜索
+const handleSearch = () => {
+  pagination.current = 1
+  fetchData()
+}
+
+// 重置搜索
+const resetSearch = () => {
+  Object.keys(queryParam).forEach(key => {
+    if (!['current', 'pageSize', 'column', 'order'].includes(key)) {
+      queryParam[key] = ''
+    }
+  })
+  pagination.current = 1
+  fetchData()
+}
+
+// 表格变化处理
+const handleTableChange = (pag, filters, sorter) => {
+  pagination.current = pag.current
+  pagination.pageSize = pag.pageSize
+  
+  if (sorter && sorter.field) {
+    queryParam.column = sorter.field
+    queryParam.order = sorter.order === 'ascend' ? 'asc' : 'desc'
+  }
+  
+  fetchData()
+}
+
+// 批量恢复实例
+const handleRecover = () => {
+  if (selectedRowKeys.value.length === 0) {
+    message.warning('请选择要恢复的实例')
+    return
+  }
+
+  Modal.confirm({
+    title: '确认恢复',
+    content: `确定要恢复选中的 ${selectedRowKeys.value.length} 个实例吗?`,
+    onOk: async () => {
+      recoverLoading.value = true
+      try {
+        const selectedRecords = dataSource.value.filter(item => 
+          selectedRowKeys.value.includes(item.id)
+        )
+        const uids = [...new Set(selectedRecords.map(item => item.uid))]
+        
+        if (uids.length > 1) {
+          message.warning('选中的实例必须属于同一用户')
+          return
+        }
+
+        const response = await recoverWaf({
+          hostIds: selectedRowKeys.value,
+          uid: uids[0]
+        })
+        
+        if (response && response.code === 0) {
+          message.success('恢复实例成功')
+          selectedRowKeys.value = []
+          fetchData()
+        } else {
+          message.error(response?.message || '恢复实例失败')
+        }
+      } catch (error) {
+        console.error('恢复实例失败:', error)
+        message.error('恢复实例失败')
+      } finally {
+        recoverLoading.value = false
+      }
+    }
+  })
+}
+
+// 批量同步时间
+const handleSyncTime = () => {
+  if (selectedRowKeys.value.length === 0) {
+    message.warning('请选择要同步的实例')
+    return
+  }
+
+  Modal.confirm({
+    title: '确认同步',
+    content: `确定要同步选中的 ${selectedRowKeys.value.length} 个实例的时间吗?`,
+    onOk: async () => {
+      syncLoading.value = true
+      try {
+        const selectedRecords = dataSource.value.filter(item => 
+          selectedRowKeys.value.includes(item.id)
+        )
+        const uids = [...new Set(selectedRecords.map(item => item.uid))]
+        
+        if (uids.length > 1) {
+          message.warning('选中的实例必须属于同一用户')
+          return
+        }
+
+        const response = await syncExecuteRenewalActions({
+          hostIds: selectedRowKeys.value,
+          uid: uids[0]
+        })
+        
+        if (response && response.code === 0) {
+          message.success('同步时间成功')
+          selectedRowKeys.value = []
+          fetchData()
+        } else {
+          message.error(response?.message || '同步时间失败')
+        }
+      } catch (error) {
+        console.error('同步时间失败:', error)
+        message.error('同步时间失败')
+      } finally {
+        syncLoading.value = false
+      }
+    }
+  })
+}
+
+// 单个实例恢复
+const handleSingleRecover = (record) => {
+  Modal.confirm({
+    title: '确认恢复',
+    content: `确定要恢复实例 "${record.name}" 吗?`,
+    onOk: async () => {
+      try {
+        const response = await recoverWaf({
+          hostIds: [record.id],
+          uid: record.uid
+        })
+        
+        if (response && response.code === 0) {
+          message.success('恢复实例成功')
+          fetchData()
+        } else {
+          message.error(response?.message || '恢复实例失败')
+        }
+      } catch (error) {
+        console.error('恢复实例失败:', error)
+        message.error('恢复实例失败')
+      }
+    }
+  })
+}
+
+// 单个实例同步
+const handleSingleSync = (record) => {
+  Modal.confirm({
+    title: '确认同步',
+    content: `确定要同步实例 "${record.name}" 的时间吗?`,
+    onOk: async () => {
+      try {
+        const response = await syncExecuteRenewalActions({
+          hostIds: [record.id],
+          uid: record.uid
+        })
+        
+        if (response && response.code === 0) {
+          message.success('同步时间成功')
+          fetchData()
+        } else {
+          message.error(response?.message || '同步时间失败')
+        }
+      } catch (error) {
+        console.error('同步时间失败:', error)
+        message.error('同步时间失败')
+      }
+    }
+  })
+}
+
+// 组件挂载时获取数据
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<style scoped>
+.table-page-search-wrapper {
+  margin-bottom: 16px;
+}
+
+.table-page-search-submitButtons {
+  float: right;
+}
+
+@media (max-width: 576px) {
+  .table-page-search-submitButtons {
+    float: none;
+    width: 100%;
+    text-align: center;
+    margin-top: 16px;
+  }
+}
+</style>

+ 20 - 0
web/src/router/dynamic-routes.js

@@ -77,6 +77,26 @@ export default [
       },
     ],
   },
+  {
+    path: '/waf',
+    redirect: '/waf/instance-manage',
+    name: 'WafManage',
+    meta: {
+      title: 'WAF管理',
+      icon: 'SafetyOutlined',
+    },
+    component: basicRouteMap.RouteView,
+    children: [
+      {
+        path: '/waf/instance-manage',
+        name: 'WafInstanceManage',
+        component: () => import('~/pages/waf/instance-manage.vue'),
+        meta: {
+          title: '实例管理',
+        },
+      },
+    ],
+  },
   {
     path: '/log',
     redirect: '/log/log',