Quellcode durchsuchen

feat(log): 添加 WAF 日志功能

- 新增 WAF 日志路由和页面组件
- 实现 WAF 日志列表和详情页面
- 添加 WAF 日志相关 API 接口
-优化日志列表页面样式和交互
fusu vor 5 Tagen
Ursprung
Commit
4d9ba41014

+ 13 - 13
api/v1/admin/wafLog.go

@@ -4,27 +4,27 @@ type WafLog struct {
 	Id         int    `json:"id" form:"id" gorm:"column:id;primary_key;AUTO_INCREMENT;not null"`
 	Uid        int    `json:"uid" form:"uid" gorm:"column:uid;default:0;not null"`
 	Name       string `json:"name" form:"name" gorm:"column:name"`
-	RequestIp  string `json:"request_ip" form:"request_ip" gorm:"column:request_ip"`
-	RuleId     int    `json:"rule_id" form:"rule_id" gorm:"column:rule_id;default:0"`
-	HostId     int    `json:"host_id" form:"host_id" gorm:"column:host_id;default:0"`
-	UserAgent  string `json:"user_agent" form:"user_agent" gorm:"column:user_agent"`
+	RequestIp  string `json:"requestIp" form:"requestIp" gorm:"column:request_ip"`
+	RuleId     int    `json:"ruleId" form:"ruleId" gorm:"column:rule_id;default:0"`
+	HostId     int    `json:"hostId" form:"hostId" gorm:"column:host_id;default:0"`
+	UserAgent  string `json:"userAgent" form:"userAgent" gorm:"column:user_agent"`
 	Api        string `json:"api" form:"api" gorm:"column:api"`
-	ApiName    string `json:"api_name" form:"api_name" gorm:"column:api_name"`
-	ApiType    string `json:"api_type" form:"api_type" gorm:"column:api_type"`
-	ExtraData  interface{} `json:"extra_data" form:"extra_data" gorm:"column:extra_data"`
+	ApiName    string `json:"apiName" form:"apiName" gorm:"column:api_name"`
+	ApiType    string `json:"apiType" form:"apiType" gorm:"column:api_type"`
+	ExtraData  interface{} `json:"extraData" form:"extraData" gorm:"column:extra_data"`
 }
 
 type SearchWafLogParams struct {
 	Id         int    `json:"id" form:"id" gorm:"column:id;primary_key;AUTO_INCREMENT;not null"`
 	Uid        int    `json:"uid" form:"uid" gorm:"column:uid;default:0;not null"`
 	Name       string `json:"name" form:"name" gorm:"column:name"`
-	RequestIp  string `json:"request_ip" form:"request_ip" gorm:"column:request_ip"`
-	RuleId     int    `json:"rule_id" form:"rule_id" gorm:"column:rule_id;default:0"`
-	HostId     int    `json:"host_id" form:"host_id" gorm:"column:host_id;default:0"`
-	UserAgent  string `json:"user_agent" form:"user_agent" gorm:"column:user_agent"`
+	RequestIp  string `json:"requestIp" form:"requestIp" gorm:"column:request_ip"`
+	RuleId     int    `json:"ruleId" form:"ruleId" gorm:"column:rule_id;default:0"`
+	HostId     int    `json:"hostId" form:"hostId" gorm:"column:host_id;default:0"`
+	UserAgent  string `json:"userAgent" form:"userAgent" gorm:"column:user_agent"`
 	Api        string `json:"api" form:"api" gorm:"column:api"`
-	ApiName    string `json:"api_name" form:"api_name" gorm:"column:api_name"`
-	ApiType    string `json:"api_type" form:"api_type" gorm:"column:api_type"`
+	ApiName    string `json:"apiName" form:"apiName" gorm:"column:api_name"`
+	ApiType    string `json:"apiType" form:"apiType" gorm:"column:api_type"`
 	Current  int	`form:"current" json:"current" default:"1"`
 	PageSize int	`form:"pageSize" json:"pageSize" default:"10"`
 	Column   string `form:"column" json:"column" default:"id"`

+ 64 - 0
claude.md

@@ -0,0 +1,64 @@
+# 项目分析总结:nunu-layout-advanced
+请用中文进行回答和注释
+
+本文档是对 `nunu-layout-advanced` 项目的全面分析和总结。
+
+## 1. 项目概述
+
+`nunu-layout-advanced` 是一个基于 Go 语言和 `nunu` 框架构建的后端项目。从其结构和文档来看,这是一个功能完备、考虑了工程化实践(如部署、测试、文档)的高级项目模板或实际应用。项目同时支持 API 服务和后台任务(Task)服务,并提供了非常便捷的 Docker 化部署方案。
+
+## 2. 技术栈
+
+项目采用了现代化的 Go 技术栈,涵盖了 Web 开发的各个方面:
+
+*   **Web 框架**: `Gin` (`github.com/gin-gonic/gin`) - 高性能的 HTTP Web 框架。
+*   **数据库 ORM**: `GORM` (`gorm.io/gorm`) - 功能强大的 Go ORM,支持 MySQL, PostgreSQL, SQLite。
+*   **NoSQL 数据库**: `MongoDB` (`go.mongodb.org/mongo-driver`, `github.com/qiniu/qmgo`) - 支持 MongoDB 数据库操作。
+*   **缓存**: `Redis` (`github.com/redis/go-redis/v9`) - 用于缓存和高速数据存储。
+*   **配置管理**: `Viper` (`github.com/spf13/viper`) - 支持多种格式的配置文件和环境变量。
+*   **日志**: `Zap` (`go.uber.org/zap`) - 高性能的结构化日志库。
+*   **认证与授权**:
+    *   `JWT` (`github.com/golang-jwt/jwt/v5`) - 用于生成和验证 Token。
+    *   `Casbin` (`github.com/casbin/casbin/v2`) - 强大的访问控制库,支持 ACL, RBAC, ABAC 等模型。
+*   **定时任务**: `gocron` (`github.com/go-co-op/gocron`) - 用于处理定时和周期性任务。
+*   **消息队列**: `RabbitMQ` (`github.com/rabbitmq/amqp091-go`) - 用于服务间的异步通信和解耦。
+*   **依赖注入**: `Wire` (`github.com/google/wire`) - Google 出品的编译期依赖注入工具。
+*   **API 文档**: `Swagger` (`github.com/swaggo/gin-swagger`) - 自动生成交互式 API 文档。
+*   **数据校验**: `validator/v10` (`github.com/go-playground/validator/v10`) - 用于结构体验证。
+
+## 3. 核心功能推断
+
+根据技术栈分析,项目具备以下核心能力:
+
+*   **RESTful API 服务**: 基于 Gin 提供高性能的 API 接口。
+*   **后台异步任务**: 通过 `gocron` 和 `RabbitMQ` 处理耗时或定时的后台任务。
+*   **多数据库支持**: 同时支持 SQL (MySQL, PostgreSQL) 和 NoSQL (MongoDB) 数据库,并通过 `dbresolver` 插件可能实现了读写分离。
+*   **完善的用户认证和权限管理**: 结合 JWT 和 Casbin,可以实现复杂的用户角色和权限控制。
+*   **配置热加载**: Viper 支持在不重启服务的情况下更新配置。
+*   **自动化测试**: 集成了 `testify`, `sqlmock` 等库,便于编写单元测试和集成测试。
+*   **高可用 ID 生成**: 使用 `sonyflake` 生成分布式唯一 ID。
+
+## 4. 部署与运维
+
+项目对部署非常友好,主要特点如下:
+
+*   **Docker 化部署**: 提供了 `Dockerfile` 和详细的 `docker build/run` 命令,是推荐的部署方式。
+*   **自动化部署脚本**: `deploy/deploy.sh` 脚本可以一键部署 API 和 Task 服务,并优化了构建缓存,提高了部署效率。
+*   **宝塔面板集成**: `README.md` 详细说明了如何在宝塔面板上进行 Docker 部署和配置反向代理。
+*   **环境分离**: 支持将配置文件、日志文件等通过 Docker Volume 挂载到宿主机,方便管理和持久化。
+
+## 5. 项目结构
+
+项目遵循了清晰的、可扩展的 Go 项目布局:
+
+*   `cmd/`: 程序入口,分离 `server` (API) 和 `task` 等不同应用。
+*   `internal/`: 项目核心业务逻辑,遵循 Go 的项目布局建议,外部无法直接导入。
+*   `pkg/`: 可被外部应用复用的公共库。
+*   `api/`: `protobuf` 或 `swagger` 定义文件。
+*   `config/`: 默认配置文件。
+*   `deploy/`: 存放 Dockerfile 和部署脚本。
+*   `web/`: 可能存放前端静态资源或模板。
+
+## 总结
+
+`nunu-layout-advanced` 是一个非常优秀的 Go 项目脚手架,集成了业界主流的最佳实践。它不仅提供了一个健壮的技术基础,还通过完善的文档和脚本极大地简化了开发和部署流程。无论是用于学习 Go 的工程化实践,还是作为新项目的起点,它都是一个绝佳的选择。

+ 9 - 9
internal/model/waflog.go

@@ -9,16 +9,16 @@ type WafLog struct {
 	Id         int    `json:"id" form:"id" gorm:"column:id;primary_key;AUTO_INCREMENT;not null"`
 	Uid        int    `json:"uid" form:"uid" gorm:"column:uid;default:0;not null"`
 	Name       string `json:"name" form:"name" gorm:"column:name"`
-	RequestIp  string `json:"request_ip" form:"request_ip" gorm:"column:request_ip"`
-	RuleId     int    `json:"rule_id" form:"rule_id" gorm:"column:rule_id;default:0"`
-	HostId     int    `json:"host_id" form:"host_id" gorm:"column:host_id;default:0"`
-	UserAgent  string `json:"user_agent" form:"user_agent" gorm:"column:user_agent"`
+	RequestIp  string `json:"requestIp" form:"requestIp" gorm:"column:request_ip"`
+	RuleId     int    `json:"ruleId" form:"ruleId" gorm:"column:rule_id;default:0"`
+	HostId     int    `json:"hostId" form:"hostId" gorm:"column:host_id;default:0"`
+	UserAgent  string `json:"userAgent" form:"userAgent" gorm:"column:user_agent"`
 	Api        string `json:"api" form:"api" gorm:"column:api"`
-	ApiName    string `json:"api_name" form:"api_name" gorm:"column:api_name"`
-	ApiType    string `json:"api_type" form:"api_type" gorm:"column:api_type"`
-	ExtraData  json.RawMessage `json:"extra_data" form:"extra_data" gorm:"column:extra_data"`
-	CreatedAt  time.Time `json:"created_at" form:"created_at" gorm:"column:created_at"`
-	UpdatedAt  time.Time `json:"updated_at" form:"updated_at" gorm:"column:updated_at"`
+	ApiName    string `json:"apiName" form:"apiName" gorm:"column:api_name"`
+	ApiType    string `json:"apiType" form:"apiType" gorm:"column:api_type"`
+	ExtraData  json.RawMessage `json:"extraData" form:"extraData" gorm:"column:extra_data"`
+	CreatedAt  time.Time `json:"createdAt" form:"createdAt" gorm:"column:created_at"`
+	UpdatedAt  time.Time `json:"updatedAt" form:"updatedAt" gorm:"column:updated_at"`
 }
 
 func (m *WafLog) TableName() string {

+ 2 - 2
internal/server/http.go

@@ -192,8 +192,8 @@ func NewHTTPServer(
 			strictAuthRouter.GET("/log/get", logHandler.GetLog)
 			strictAuthRouter.GET("/log/getList", logHandler.GetLogList)
 
-			strictAuthRouter.GET("/wafLog/get", wafLogHandler.GetWafLog)
-			strictAuthRouter.GET("/wafLog/getList", wafLogHandler.GetWafLogList)
+			strictAuthRouter.GET("admin/wafLog/get", wafLogHandler.GetWafLog)
+			strictAuthRouter.GET("admin/wafLog/getList", wafLogHandler.GetWafLogList)
 		}
 	}
 

+ 17 - 0
web/src/api/waf/waflog.js

@@ -0,0 +1,17 @@
+import request from '~/utils/request.js'
+
+export function getWafLogList(params) {
+  return request({
+    url: '/v1/admin/wafLog/getList',
+    method: 'get',
+    params
+  })
+}
+
+export function getWafLogInfo(id) {
+  return request({
+    url: '/v1/admin/wafLog/get',
+    method: 'get',
+    params: { id }
+  })
+}

+ 149 - 0
web/src/pages/log/components/waf-log-detail-modal.vue

@@ -0,0 +1,149 @@
+<template>
+  <a-drawer
+    v-model:visible="visible"
+    title="WAF日志详情"
+    placement="right"
+    :width="'60%'"
+    @close="handleCancel"
+  >
+    <a-spin :spinning="loading">
+      <a-descriptions bordered :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }">
+        <a-descriptions-item label="ID">{{ info.id }}</a-descriptions-item>
+        <a-descriptions-item label="用户ID">{{ info.uid }}</a-descriptions-item>
+        <a-descriptions-item label="名称">{{ info.name }}</a-descriptions-item>
+        <a-descriptions-item label="请求IP">{{ info.requestIp }}</a-descriptions-item>
+        <a-descriptions-item label="规则ID">{{ info.ruleId }}</a-descriptions-item>
+        <a-descriptions-item label="实例ID">{{ info.hostId }}</a-descriptions-item>
+        <a-descriptions-item label="API">{{ info.api }}</a-descriptions-item>
+        <a-descriptions-item label="API名称">{{ info.apiName }}</a-descriptions-item>
+        <a-descriptions-item label="API类型">{{ info.apiType }}</a-descriptions-item>
+        <a-descriptions-item label="创建时间">{{ info.createdAt }}</a-descriptions-item>
+        <a-descriptions-item label="更新时间">{{ info.updatedAt }}</a-descriptions-item>
+      </a-descriptions>
+      
+      <a-divider style="margin-top: 24px; margin-bottom: 24px" />
+      
+      <h3>详细信息</h3>
+      
+      <div class="detail-section">
+        <h4>User Agent</h4>
+        <div class="code-block">
+          <pre>{{ info.userAgent || '无数据' }}</pre>
+        </div>
+      </div>
+      
+      <div class="detail-section">
+        <h4>附加数据</h4>
+        <div class="code-block">
+          <pre>{{ info.extraData || '无数据' }}</pre>
+        </div>
+      </div>
+    </a-spin>
+  </a-drawer>
+</template>
+
+<script setup>
+import { ref, defineExpose } from 'vue';
+import { getWafLogInfo } from '~/api/waf/waflog.js';
+
+const visible = ref(false);
+const loading = ref(false);
+const info = ref({});
+
+const fetchInfo = async (id) => {
+  try {
+    loading.value = true;
+    console.log('获取WAF日志ID:', id);
+    
+    const response = await getWafLogInfo(id);
+    console.log('WAF日志详情响应:', response);
+    
+    // 处理不同的返回格式
+    if (response && response.code === 0 && response.data) {
+      info.value = response.data;
+    } else if (response && response.data) {
+      info.value = response.data;
+    } else {
+      info.value = response || {};
+    }
+    
+    // 处理特殊字段 - extraData可能是JSON格式
+    if (info.value.extraData && typeof info.value.extraData === 'string') {
+      try {
+        // 尝试解析JSON字符串为对象,便于美化显示
+        const parsedData = JSON.parse(info.value.extraData);
+        info.value.extraData = JSON.stringify(parsedData, null, 2);
+      } catch (e) {
+        // 不是JSON格式,保持原样
+      }
+    } else if (info.value.extraData && typeof info.value.extraData === 'object') {
+      // 如果已经是对象,格式化为美观的JSON字符串
+      info.value.extraData = JSON.stringify(info.value.extraData, null, 2);
+    }
+    
+  } catch (error) {
+    console.error('获取WAF日志详情出错:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const open = async (id) => {
+  visible.value = true;
+  info.value = {}; // 清空上一次的数据
+  await fetchInfo(id);
+};
+
+const handleCancel = () => {
+  visible.value = false;
+};
+
+defineExpose({
+  open,
+});
+</script>
+
+<style scoped>
+.detail-section {
+  margin-bottom: 24px;
+}
+
+.detail-section h4 {
+  font-size: 16px;
+  font-weight: 500;
+  margin-bottom: 12px;
+  color: rgba(0, 0, 0, 0.85);
+}
+
+.code-block {
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  padding: 0;
+}
+
+pre {
+  padding: 16px;
+  white-space: pre-wrap;       
+  word-wrap: break-word;
+  max-height: 300px;
+  overflow-y: auto;
+  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  margin: 0;
+}
+
+:deep(.ant-descriptions-item-label),
+:deep(.ant-descriptions-item-content) {
+  font-size: 14px;
+}
+
+:deep(.ant-descriptions-item-label) {
+  font-weight: bold;
+  min-width: 120px;
+}
+
+:deep(.ant-drawer-body) {
+  padding: 24px;
+}
+</style>

+ 2 - 2
web/src/pages/log/log-info.vue

@@ -1,5 +1,5 @@
 <template>
-  <page-header-wrapper>
+  <page-container>
     <a-card :bordered="false" :loading="loading">
       <a-descriptions title="日志详情" :column="{ xs: 1, sm: 2, md: 3 }">
         <a-descriptions-item label="ID">{{ info.Id }}</a-descriptions-item>
@@ -30,7 +30,7 @@
       <a-divider style="margin-bottom: 32px"/>
       <a-button type="primary" @click="handleBack">返回</a-button>
     </a-card>
-  </page-header-wrapper>
+  </page-container>
 </template>
 
 <script setup>

+ 3 - 0
web/src/pages/log/log.vue

@@ -72,6 +72,9 @@ const pagination = ref({
   pageSize: 10,
   total: 0,
   showTotal: (total) => `共 ${total} 条`,
+  showSizeChanger: true,
+  pageSizeOptions: ['10', '20', '50', '100'],
+  showQuickJumper: true,
 });
 
 const queryParam = ref({

+ 247 - 0
web/src/pages/log/waflog.vue

@@ -0,0 +1,247 @@
+<template>
+  <div>
+    <a-card>
+      <div class="table-page-search-wrapper">
+        <a-form layout="inline" @submit.prevent="handleSearch">
+          <a-row :gutter="48">
+            <a-col :md="6" :sm="24">
+              <a-form-item label="请求IP">
+                <a-input v-model:value="queryParam.requestIp" placeholder="请输入请求IP" />
+              </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="规则ID">
+                <a-input v-model:value="queryParam.ruleId" placeholder="请输入规则ID" />
+              </a-form-item>
+            </a-col>
+            <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="API">
+                <a-input v-model:value="queryParam.api" placeholder="请输入API路径" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="API名称">
+                <a-input v-model:value="queryParam.apiName" placeholder="请输入API名称" />
+              </a-form-item>
+            </a-col>
+            <a-col :md="6" :sm="24">
+              <a-form-item label="API类型">
+                <a-select v-model:value="queryParam.apiType" placeholder="请选择API类型" allow-clear>
+                  <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-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>
+              </span>
+            </a-col>
+          </a-row>
+        </a-form>
+      </div>
+
+      <a-table
+        :columns="columns"
+        :row-key="record => record.id"
+        :data-source="dataSource"
+        :loading="loading"
+        :pagination="pagination"
+        @change="handleTableChange"
+        :scroll="{ x: 'max-content' }"
+        bordered
+        size="middle"
+      >
+        <template #bodyCell="{ column, record }">
+          <template v-if="column.dataIndex === 'action'">
+            <a-button type="link" @click="goToInfo(record.id)">详情</a-button>
+          </template>
+        </template>
+      </a-table>
+    </a-card>
+    
+    <!-- WAF日志详情模态框 -->
+    <waf-log-detail-modal ref="detailModalRef" />
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { getWafLogList } from '~/api/waf/waflog.js';
+import WafLogDetailModal from './components/waf-log-detail-modal.vue';
+
+const loading = ref(false);
+const dataSource = ref([]);
+const pagination = ref({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+  showTotal: (total) => `共 ${total} 条`,
+  showSizeChanger: true,
+  pageSizeOptions: ['10', '20', '50', '100'],
+  showQuickJumper: true,
+});
+
+const queryParam = ref({
+  requestIp: '',
+  uid: '',
+  ruleId: '',
+  hostId: '',
+  api: '',
+  apiName: '',
+  apiType: '',
+  name: '',
+});
+
+const columns = [
+  { title: 'ID', dataIndex: 'id', fixed: 'left', width: 80 },
+  { title: '用户ID', dataIndex: 'uid', width: 100 },
+  { title: '名称', dataIndex: 'name', width: 150, ellipsis: true },
+  { title: '请求IP', dataIndex: 'requestIp', width: 140 },
+  { title: '实例ID', dataIndex: 'hostId', width: 100 },
+  { title: '规则ID', dataIndex: 'ruleId', width: 100 },
+  { title: 'User Agent', dataIndex: 'userAgent', width: 250, ellipsis: true },
+  { title: 'API', dataIndex: 'api', width: 180, ellipsis: true },
+  { title: 'API名称', dataIndex: 'apiName', width: 150, ellipsis: true },
+  { title: 'API类型', dataIndex: 'apiType', width: 100 },
+  { title: '创建时间', dataIndex: 'createdAt', width: 180 },
+  { title: '更新时间', dataIndex: 'updatedAt', width: 180 },
+  { title: '操作', dataIndex: 'action', fixed: 'right', width: 100 },
+];
+
+const fetchData = () => {
+  loading.value = true;
+  
+  // 构造请求参数,并处理特殊类型
+  const params = {
+    requestIp: queryParam.value.requestIp || '',
+    uid: queryParam.value.uid ? parseInt(queryParam.value.uid) : '',
+    ruleId: queryParam.value.ruleId ? parseInt(queryParam.value.ruleId) : '',
+    hostId: queryParam.value.hostId ? parseInt(queryParam.value.hostId) : '',
+    api: queryParam.value.api || '',
+    apiName: queryParam.value.apiName || '',
+    apiType: queryParam.value.apiType || '',
+    name: queryParam.value.name || '',
+    current: pagination.value.current,
+    pageSize: pagination.value.pageSize,
+    column: 'id',
+    order: 'desc'
+  };
+  
+  console.log('发送WAF日志查询参数:', params);
+  
+  getWafLogList(params).then(response => {
+    // 根据实际返回格式处理数据
+    if (response && response.code === 0 && response.data) {
+      const apiData = response.data;
+      if (apiData.records && Array.isArray(apiData.records)) {
+        dataSource.value = apiData.records;
+        pagination.value.total = apiData.total || 0;
+        pagination.value.current = apiData.page || 1;
+        pagination.value.pageSize = apiData.pageSize || 10;
+      } else {
+        dataSource.value = [];
+        pagination.value.total = 0;
+        console.warn('响应中无WAF日志数据记录');
+      }
+    } else {
+      dataSource.value = [];
+      pagination.value.total = 0;
+      console.error('无效的WAF日志响应格式或响应错误', response);
+    }
+    loading.value = false;
+  }).catch((error) => {
+    console.error('WAF日志请求失败:', error);
+    dataSource.value = [];
+    loading.value = false;
+  });
+};
+
+const handleTableChange = (pager) => {
+  pagination.value.current = pager.current;
+  pagination.value.pageSize = pager.pageSize;
+  fetchData();
+};
+
+const handleSearch = () => {
+  pagination.value.current = 1;
+  fetchData();
+};
+
+const resetSearch = () => {
+  queryParam.value.requestIp = '';
+  queryParam.value.uid = '';
+  queryParam.value.ruleId = '';
+  queryParam.value.hostId = '';
+  queryParam.value.api = '';
+  queryParam.value.apiName = '';
+  queryParam.value.apiType = '';
+  queryParam.value.name = '';
+  handleSearch();
+};
+
+const detailModalRef = ref(null);
+
+const goToInfo = (id) => {
+  // 打开模态框而不是跳转页面
+  detailModalRef.value?.open(id);
+};
+
+onMounted(() => {
+  fetchData();
+});
+</script>
+
+<style scoped>
+.table-page-search-wrapper .ant-form-inline .ant-form-item {
+  margin-bottom: 24px;
+}
+
+/* 表格样式优化 */
+:deep(.ant-table-body) {
+  overflow-x: auto !important;
+}
+
+:deep(.ant-table-fixed-header .ant-table-scroll .ant-table-header) {
+  margin-bottom: 0 !important;
+  padding-bottom: 0 !important;
+  overflow: hidden !important;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar) {
+  width: 8px;
+  height: 8px;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar-track) {
+  background: #f1f1f1;
+  border-radius: 4px;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar-thumb) {
+  background: #c1c1c1;
+  border-radius: 4px;
+}
+
+:deep(.ant-table-body::-webkit-scrollbar-thumb:hover) {
+  background: #a8a8a8;
+}
+</style>

+ 9 - 1
web/src/router/dynamic-routes.js

@@ -95,6 +95,14 @@ export default [
           title: '日志',
         },
       },
+      {
+        path: '/log/waflog',
+        name: 'LogWafLog',
+        component: () => import('~/pages/log/waflog.vue'),
+        meta: {
+          title: 'WAF日志',
+        },
+      },
       {
         path: '/log/log-info/:id',
         name: 'LogLogInfo',
@@ -102,7 +110,7 @@ export default [
         meta: {
           title: '日志详情',
           hidden: true, // 不在菜单中显示
-          activeMenu: '/log/og' // 高亮父菜单
+          activeMenu: '/log/log' // 高亮父菜单
         },
       },
     ],