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