Kaynağa Gözat

feat(log): 优化 WAF 日志导出功能

- 新增时间范围快捷选择按钮
- 新增全选和清空 API 名称功能
- 优化高级筛选条件布局
- 默认选中 TCP
fusu 3 gün önce
ebeveyn
işleme
9f00af506f

+ 4 - 4
internal/repository/admin/waflog.go

@@ -230,13 +230,13 @@ func (r *wafLogRepository) ExportWafLogWithPagination(ctx context.Context, req a
 	if req.StartTime != "" {
 		trimmedName := strings.TrimSpace(req.StartTime)
 		// 使用 LIKE 进行模糊匹配
-		query = query.Where("create_at > ?", trimmedName)
+		query = query.Where("created_at > ?", trimmedName)
 	}
 
 	if req.EndTime != "" {
 		trimmedName := strings.TrimSpace(req.EndTime)
 		// 使用 LIKE 进行模糊匹配
-		query = query.Where("create_at < ?", trimmedName)
+		query = query.Where("created_at < ?", trimmedName)
 	}
 
 	// 添加分页逻辑
@@ -304,12 +304,12 @@ func (r *wafLogRepository) GetWafLogExportCount(ctx context.Context, req adminAp
 
 	if req.StartTime != "" {
 		trimmedName := strings.TrimSpace(req.StartTime)
-		query = query.Where("create_at > ?", trimmedName)
+		query = query.Where("created_at > ?", trimmedName)
 	}
 
 	if req.EndTime != "" {
 		trimmedName := strings.TrimSpace(req.EndTime)
-		query = query.Where("create_at < ?", trimmedName)
+		query = query.Where("created_at < ?", trimmedName)
 	}
 
 	result := query.Count(&count)

+ 384 - 109
web/src/pages/log/waflog.vue

@@ -86,93 +86,25 @@
     <a-modal
       v-model:open="exportModalVisible"
       title="导出WAF日志"
-      :width="800"
+      :width="900"
       @ok="handleExport"
       @cancel="handleExportCancel"
       :confirm-loading="exportLoading"
+      class="export-modal"
     >
-      <a-form :model="exportForm" layout="vertical">
-        <a-row :gutter="16">
-          <a-col :span="12">
-            <a-form-item label="ID">
-              <a-input v-model:value="exportForm.id" placeholder="请输入ID" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="12">
-            <a-form-item label="用户ID">
-              <a-input v-model:value="exportForm.uid" placeholder="请输入用户ID" />
-            </a-form-item>
-          </a-col>
-        </a-row>
-        
-        <a-row :gutter="16">
-          <a-col :span="12">
-            <a-form-item label="名称">
-              <a-input v-model:value="exportForm.name" placeholder="请输入名称" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="12">
-            <a-form-item label="请求IP">
-              <a-input v-model:value="exportForm.requestIp" placeholder="请输入请求IP" />
-            </a-form-item>
-          </a-col>
-        </a-row>
-        
-        <a-row :gutter="16">
-          <a-col :span="12">
-            <a-form-item label="规则ID">
-              <a-input v-model:value="exportForm.ruleId" placeholder="请输入规则ID" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="12">
-            <a-form-item label="User Agent">
-              <a-input v-model:value="exportForm.userAgent" placeholder="请输入User Agent" />
-            </a-form-item>
-          </a-col>
-        </a-row>
-        
-        <a-row :gutter="16">
-          <a-col :span="24">
-            <a-form-item label="实例ID列表">
-              <a-select
-                v-model:value="exportForm.hostIds"
-                mode="tags"
-                placeholder="请输入实例ID,支持多个"
-                :token-separators="[',']"
-              />
-            </a-form-item>
-          </a-col>
-        </a-row>
-        
-        <a-row :gutter="16">
-          <a-col :span="12">
-            <a-form-item label="API路径">
-              <a-input v-model:value="exportForm.api" placeholder="请输入API路径" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="12">
-            <a-form-item label="API类型">
-              <a-select v-model:value="exportForm.apiTypes" mode="multiple" placeholder="请选择API类型">
-                <a-select-option value="get">get</a-select-option>
-                <a-select-option value="add">add</a-select-option>
-                <a-select-option value="delete">delete</a-select-option>
-                <a-select-option value="edit">edit</a-select-option>
-              </a-select>
-            </a-form-item>
-          </a-col>
-        </a-row>
-        
-        <a-row :gutter="16">
-          <a-col :span="24">
-            <a-form-item label="API名称">
-              <a-checkbox-group v-model:value="exportForm.apiNames" :options="apiNameOptions" />
-            </a-form-item>
-          </a-col>
-        </a-row>
-        
-        <a-row :gutter="16">
-          <a-col :span="12">
-            <a-form-item label="开始时间">
+      <div class="export-form-container">
+        <!-- 时间范围选择区域 -->
+        <div class="time-range-section">
+          <h4 class="section-title">时间范围</h4>
+          <div class="time-shortcuts">
+            <a-button size="small" @click="setTimeRange('today')">今天</a-button>
+            <a-button size="small" @click="setTimeRange('yesterday')">昨天</a-button>
+            <a-button size="small" @click="setTimeRange('last7days')">最近7天</a-button>
+            <a-button size="small" @click="setTimeRange('last30days')">最近30天</a-button>
+            <a-button size="small" type="text" @click="clearTimeRange">清空</a-button>
+          </div>
+          <a-row :gutter="16" style="margin-top: 12px;">
+            <a-col :span="12">
               <a-date-picker
                 v-model:value="exportForm.startTime"
                 show-time
@@ -180,10 +112,8 @@
                 placeholder="请选择开始时间"
                 style="width: 100%"
               />
-            </a-form-item>
-          </a-col>
-          <a-col :span="12">
-            <a-form-item label="结束时间">
+            </a-col>
+            <a-col :span="12">
               <a-date-picker
                 v-model:value="exportForm.endTime"
                 show-time
@@ -191,10 +121,110 @@
                 placeholder="请选择结束时间"
                 style="width: 100%"
               />
-            </a-form-item>
-          </a-col>
-        </a-row>
-      </a-form>
+            </a-col>
+          </a-row>
+        </div>
+
+        <!-- API名称选择区域 -->
+        <div class="api-names-section">
+          <div class="api-names-header">
+            <h4 class="section-title">API名称选择</h4>
+            <div class="api-names-actions">
+              <span class="selected-count">已选择: {{ exportForm.apiNames.length }} 个</span>
+              <a-button size="small" type="link" @click="selectAllApiNames">全选</a-button>
+              <a-button size="small" type="link" @click="clearAllApiNames">清空</a-button>
+            </div>
+          </div>
+          <div class="api-names-grid">
+            <a-checkbox-group v-model:value="exportForm.apiNames" class="custom-checkbox-group">
+              <div class="checkbox-grid">
+                <a-checkbox 
+                  v-for="option in apiNameOptions" 
+                  :key="option.value" 
+                  :value="option.value"
+                  class="checkbox-item"
+                >
+                  {{ option.label }}
+                </a-checkbox>
+              </div>
+            </a-checkbox-group>
+          </div>
+        </div>
+
+        <!-- 高级筛选条件 -->
+        <a-collapse v-model:activeKey="advancedFilterKey" ghost>
+          <a-collapse-panel key="advanced" header="高级筛选条件">
+            <a-form :model="exportForm" layout="vertical">
+              <a-row :gutter="16">
+                <a-col :span="8">
+                  <a-form-item label="ID">
+                    <a-input v-model:value="exportForm.id" placeholder="请输入ID" />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="8">
+                  <a-form-item label="用户ID">
+                    <a-input v-model:value="exportForm.uid" placeholder="请输入用户ID" />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="8">
+                  <a-form-item label="名称">
+                    <a-input v-model:value="exportForm.name" placeholder="请输入名称" />
+                  </a-form-item>
+                </a-col>
+              </a-row>
+              
+              <a-row :gutter="16">
+                <a-col :span="8">
+                  <a-form-item label="请求IP">
+                    <a-input v-model:value="exportForm.requestIp" placeholder="请输入请求IP" />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="8">
+                  <a-form-item label="规则ID">
+                    <a-input v-model:value="exportForm.ruleId" placeholder="请输入规则ID" />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="8">
+                  <a-form-item label="API路径">
+                    <a-input v-model:value="exportForm.api" placeholder="请输入API路径" />
+                  </a-form-item>
+                </a-col>
+              </a-row>
+              
+              <a-row :gutter="16">
+                <a-col :span="12">
+                  <a-form-item label="实例ID列表">
+                    <a-select
+                      v-model:value="exportForm.hostIds"
+                      mode="tags"
+                      placeholder="请输入实例ID,支持多个"
+                      :token-separators="[',']"
+                    />
+                  </a-form-item>
+                </a-col>
+                <a-col :span="12">
+                  <a-form-item label="API类型">
+                    <a-select v-model:value="exportForm.apiTypes" mode="multiple" placeholder="请选择API类型">
+                      <a-select-option value="get">get</a-select-option>
+                      <a-select-option value="add">add</a-select-option>
+                      <a-select-option value="delete">delete</a-select-option>
+                      <a-select-option value="edit">edit</a-select-option>
+                    </a-select>
+                  </a-form-item>
+                </a-col>
+              </a-row>
+              
+              <a-row :gutter="16">
+                <a-col :span="24">
+                  <a-form-item label="User Agent">
+                    <a-input v-model:value="exportForm.userAgent" placeholder="请输入User Agent" />
+                  </a-form-item>
+                </a-col>
+              </a-row>
+            </a-form>
+          </a-collapse-panel>
+        </a-collapse>
+      </div>
     </a-modal>
   </div>
 </template>
@@ -322,6 +352,7 @@ const detailModalRef = ref(null);
 const exportModalVisible = ref(false);
 const exportLoading = ref(false);
 const apiNameOptions = ref([]);
+const advancedFilterKey = ref([]);
 const exportForm = ref({
   id: '',
   uid: '',
@@ -337,6 +368,13 @@ const exportForm = ref({
   endTime: null
 });
 
+// 判断是否为TCP、UDP、Web相关的API
+const isTcpUdpWebRelated = (apiName) => {
+  const keywords = ['tcp', 'udp', 'web', '游戏盾', '转发', '网关', '限流', '防护', '规则', '策略'];
+  const lowerApiName = apiName.toLowerCase();
+  return keywords.some(keyword => lowerApiName.includes(keyword.toLowerCase()));
+};
+
 const goToInfo = (id) => {
   // 打开模态框而不是跳转页面
   detailModalRef.value?.open(id);
@@ -348,10 +386,10 @@ const showExportModal = async () => {
     // 获取API描述映射
     const response = await getApiDescriptions();
     if (response && response.code === 0 && response.data) {
-      // 将API描述映射转换为checkbox选项格式
+      // 将API描述映射转换为checkbox选项格式,只显示中文名称
       apiNameOptions.value = Object.entries(response.data).map(([key, value]) => ({
-        label: `${value} (${key})`,
-        value: key
+        label: value, // 只显示中文名称
+        value: value  // 传给后端的值也是中文名称
       }));
     }
     
@@ -371,6 +409,17 @@ const showExportModal = async () => {
       endTime: null
     };
     
+    // 默认选中TCP、UDP、Web相关的API
+    if (apiNameOptions.value.length > 0) {
+      const defaultSelected = apiNameOptions.value
+        .filter(option => isTcpUdpWebRelated(option.label))
+        .map(option => option.value);
+      exportForm.value.apiNames = defaultSelected;
+    }
+    
+    // 重置高级筛选折叠状态
+    advancedFilterKey.value = [];
+    
     exportModalVisible.value = true;
   } catch (error) {
     console.error('获取API描述失败:', error);
@@ -378,26 +427,77 @@ const showExportModal = async () => {
   }
 };
 
+// 时间范围快捷选择
+const setTimeRange = (range) => {
+  const now = dayjs();
+  
+  switch (range) {
+    case 'today':
+      exportForm.value.startTime = now.startOf('day');
+      exportForm.value.endTime = now.endOf('day');
+      break;
+    case 'yesterday':
+      const yesterday = now.subtract(1, 'day');
+      exportForm.value.startTime = yesterday.startOf('day');
+      exportForm.value.endTime = yesterday.endOf('day');
+      break;
+    case 'last7days':
+      exportForm.value.startTime = now.subtract(7, 'day').startOf('day');
+      exportForm.value.endTime = now.endOf('day');
+      break;
+    case 'last30days':
+      exportForm.value.startTime = now.subtract(30, 'day').startOf('day');
+      exportForm.value.endTime = now.endOf('day');
+      break;
+  }
+};
+
+// 清空时间范围
+const clearTimeRange = () => {
+  exportForm.value.startTime = null;
+  exportForm.value.endTime = null;
+};
+
+// 全选API名称
+const selectAllApiNames = () => {
+  exportForm.value.apiNames = apiNameOptions.value.map(option => option.value);
+};
+
+// 清空API名称选择
+const clearAllApiNames = () => {
+  exportForm.value.apiNames = [];
+};
+
 // 处理导出
 const handleExport = async () => {
   try {
     exportLoading.value = true;
     
-    // 构造导出参数
-    const params = {
-      id: exportForm.value.id ? parseInt(exportForm.value.id) : 0,
-      uid: exportForm.value.uid ? parseInt(exportForm.value.uid) : 0,
-      name: exportForm.value.name || '',
-      requestIp: exportForm.value.requestIp || '',
-      ruleId: exportForm.value.ruleId ? parseInt(exportForm.value.ruleId) : 0,
-      hostIds: exportForm.value.hostIds.map(id => parseInt(id)).filter(id => !isNaN(id)),
-      userAgent: exportForm.value.userAgent || '',
-      api: exportForm.value.api || '',
-      apiNames: exportForm.value.apiNames || [],
-      apiTypes: exportForm.value.apiTypes || [],
-      startTime: exportForm.value.startTime ? dayjs(exportForm.value.startTime).format('YYYY-MM-DD HH:mm:ss') : '',
-      endTime: exportForm.value.endTime ? dayjs(exportForm.value.endTime).format('YYYY-MM-DD HH:mm:ss') : ''
-    };
+    // 构造导出参数,只传递有值的参数
+    const params = {};
+    
+    if (exportForm.value.id) params.id = parseInt(exportForm.value.id);
+    if (exportForm.value.uid) params.uid = parseInt(exportForm.value.uid);
+    if (exportForm.value.name) params.name = exportForm.value.name;
+    if (exportForm.value.requestIp) params.requestIp = exportForm.value.requestIp;
+    if (exportForm.value.ruleId) params.ruleId = parseInt(exportForm.value.ruleId);
+    if (exportForm.value.hostIds && exportForm.value.hostIds.length > 0) {
+      params.hostIds = exportForm.value.hostIds.map(id => parseInt(id)).filter(id => !isNaN(id));
+    }
+    if (exportForm.value.userAgent) params.userAgent = exportForm.value.userAgent;
+    if (exportForm.value.api) params.api = exportForm.value.api;
+    if (exportForm.value.apiNames && exportForm.value.apiNames.length > 0) {
+      params.apiNames = exportForm.value.apiNames;
+    }
+    if (exportForm.value.apiTypes && exportForm.value.apiTypes.length > 0) {
+      params.apiTypes = exportForm.value.apiTypes;
+    }
+    if (exportForm.value.startTime) {
+      params.startTime = dayjs(exportForm.value.startTime).format('YYYY-MM-DD HH:mm:ss');
+    }
+    if (exportForm.value.endTime) {
+      params.endTime = dayjs(exportForm.value.endTime).format('YYYY-MM-DD HH:mm:ss');
+    }
     
     console.log('导出参数:', params);
     
@@ -471,4 +571,179 @@ onMounted(() => {
 :deep(.ant-table-body::-webkit-scrollbar-thumb:hover) {
   background: #a8a8a8;
 }
+
+/* 导出弹窗样式 */
+.export-form-container {
+  max-height: 70vh;
+  overflow-y: auto;
+  padding-right: 8px;
+}
+
+.export-form-container::-webkit-scrollbar {
+  width: 6px;
+}
+
+.export-form-container::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.export-form-container::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.export-form-container::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+.section-title {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: #262626;
+}
+
+/* 时间范围选择区域 */
+.time-range-section {
+  background: #fafafa;
+  border: 1px solid #f0f0f0;
+  border-radius: 8px;
+  padding: 16px;
+  margin-bottom: 20px;
+}
+
+.time-shortcuts {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+  margin-bottom: 8px;
+}
+
+.time-shortcuts .ant-btn {
+  border-radius: 4px;
+  transition: all 0.2s ease;
+}
+
+.time-shortcuts .ant-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* API名称选择区域 */
+.api-names-section {
+  background: #fafafa;
+  border: 1px solid #f0f0f0;
+  border-radius: 8px;
+  padding: 16px;
+  margin-bottom: 20px;
+}
+
+.api-names-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.api-names-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.selected-count {
+  font-size: 12px;
+  color: #666;
+  margin-right: 8px;
+}
+
+.api-names-grid {
+  max-height: 200px;
+  overflow-y: auto;
+  border: 1px solid #e8e8e8;
+  border-radius: 6px;
+  background: white;
+  padding: 12px;
+}
+
+.api-names-grid::-webkit-scrollbar {
+  width: 6px;
+}
+
+.api-names-grid::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.api-names-grid::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.api-names-grid::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+.custom-checkbox-group {
+  width: 100%;
+}
+
+.checkbox-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 8px 16px;
+}
+
+.checkbox-item {
+  display: flex;
+  align-items: center;
+  padding: 4px 0;
+  transition: background-color 0.2s ease;
+  border-radius: 4px;
+  padding-left: 8px;
+  margin-left: 0 !important;
+}
+
+.checkbox-item:hover {
+  background-color: #f5f5f5;
+}
+
+/* 高级筛选折叠面板样式 */
+:deep(.ant-collapse-ghost .ant-collapse-item) {
+  border: 1px solid #f0f0f0;
+  border-radius: 8px;
+  background: #fafafa;
+  margin-bottom: 0;
+}
+
+:deep(.ant-collapse-ghost .ant-collapse-header) {
+  padding: 12px 16px;
+  font-weight: 500;
+  color: #262626;
+}
+
+:deep(.ant-collapse-ghost .ant-collapse-content) {
+  background: white;
+  border-radius: 0 0 8px 8px;
+}
+
+:deep(.ant-collapse-ghost .ant-collapse-content-box) {
+  padding: 16px;
+}
+
+/* 导出弹窗整体样式 */
+:deep(.export-modal .ant-modal-body) {
+  padding: 20px 24px;
+}
+
+:deep(.export-modal .ant-modal-header) {
+  border-bottom: 1px solid #f0f0f0;
+  padding: 16px 24px;
+}
+
+:deep(.export-modal .ant-modal-title) {
+  font-size: 16px;
+  font-weight: 600;
+}
 </style>

+ 2 - 0
web/types/components.d.ts

@@ -21,6 +21,8 @@ declare module 'vue' {
     ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
     ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
     ACol: typeof import('ant-design-vue/es')['Col']
+    ACollapse: typeof import('ant-design-vue/es')['Collapse']
+    ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
     AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
     ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
     ADescriptions: typeof import('ant-design-vue/es')['Descriptions']