| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- <!-- API 请求日志 -->
- <template>
- <div class="page-container">
- <header class="page-head">
- <h1>接口日志</h1>
- <p class="page-desc">集中检索 API 请求轨迹,快速定位异常请求与响应。</p>
- </header>
- <section class="panel">
- <div class="panel-top">
- <h2 class="title-bar-heading">请求列表</h2>
- </div>
- <div class="filter-block">
- <el-form :model="searchForm" class="filter-form" label-position="top" @submit.prevent>
- <div class="filter-grid">
- <el-form-item label="方法">
- <el-select v-model="searchForm.httpMethod" placeholder="全部" clearable>
- <el-option label="全部" value="" />
- <el-option label="GET" value="GET" />
- <el-option label="POST" value="POST" />
- <el-option label="PUT" value="PUT" />
- <el-option label="PATCH" value="PATCH" />
- <el-option label="DELETE" value="DELETE" />
- </el-select>
- </el-form-item>
- <el-form-item label="状态码">
- <el-input v-model.number="searchForm.statusCode" placeholder="如 200" clearable @keyup.enter="handleSearch" />
- </el-form-item>
- <el-form-item label="路径包含">
- <el-input v-model="searchForm.pathPrefix" placeholder="/api/..." clearable @keyup.enter="handleSearch" />
- </el-form-item>
- <el-form-item label="关键词">
- <el-input v-model="searchForm.keyword" placeholder="路径/用户/内容" clearable @keyup.enter="handleSearch" />
- </el-form-item>
- </div>
- <div class="filter-actions-row">
- <span class="filter-hint">支持按请求方法、状态码和关键词组合筛选。</span>
- <div class="filter-actions-end">
- <el-button class="btn-ghost" @click="handleReset">重置</el-button>
- <el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
- </div>
- </div>
- </el-form>
- </div>
- <div class="list-toolbar">
- <div class="list-toolbar-row">
- <div class="list-toolbar-selected">共 <span class="sel-num">{{ pagination.total }}</span> 条请求</div>
- </div>
- </div>
- <div class="list-body list-wrap">
- <el-table class="list-table" :data="tableData" v-loading="loading" stripe>
- <el-table-column prop="httpMethod" label="方法" width="80" />
- <el-table-column prop="path" label="路径" min-width="200" show-overflow-tooltip />
- <el-table-column prop="statusCode" label="状态" width="80" />
- <el-table-column prop="durationMs" label="耗时(ms)" width="100" />
- <el-table-column prop="username" label="用户" min-width="100" show-overflow-tooltip />
- <el-table-column prop="role" label="角色" width="90">
- <template #default="{ row }">{{ roleText(row.role) }}</template>
- </el-table-column>
- <el-table-column prop="ipAddress" label="IP" width="130" show-overflow-tooltip />
- <el-table-column prop="createAt" label="时间" min-width="170" />
- <el-table-column label="操作" width="100" fixed="right">
- <template #default="{ row }">
- <el-button class="btn-op-item primary auto-icon-view" @click="openDetail(row)">查看</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="pagination list-pager-bar">
- <el-pagination
- :current-page="pagination.page"
- :page-size="pagination.pageSize"
- :total="pagination.total"
- :page-sizes="[10, 20, 50]"
- layout="total, sizes, prev, pager, next"
- @update:page-size="
- (s) => {
- pagination.pageSize = s
- pagination.page = 1
- loadData()
- }
- "
- @update:current-page="
- (p) => {
- pagination.page = p
- loadData()
- }
- "
- />
- </div>
- </div>
- <el-dialog
- v-model="detailVisible"
- title="请求详情"
- width="920px"
- destroy-on-close
- class="order-module-dialog api-request-log-dialog"
- modal-class="order-module-dialog-mask"
- >
- <div class="dialog-scroll api-request-log-dialog-scroll">
- <el-descriptions v-if="detailRow" :column="1" border size="small">
- <el-descriptions-item label="方法">{{ detailRow.httpMethod }}</el-descriptions-item>
- <el-descriptions-item label="路径">{{ detailRow.path }}</el-descriptions-item>
- <el-descriptions-item label="Query">{{ detailRow.queryString || '-' }}</el-descriptions-item>
- <el-descriptions-item label="状态 / 耗时"
- >{{ detailRow.statusCode }} / {{ detailRow.durationMs }} ms</el-descriptions-item
- >
- <el-descriptions-item label="用户">{{ detailRow.username || '-' }} ({{ detailRow.userId || '-' }})</el-descriptions-item>
- <el-descriptions-item label="角色">{{ roleText(detailRow.role) }}</el-descriptions-item>
- <el-descriptions-item label="IP">{{ detailRow.ipAddress || '-' }}</el-descriptions-item>
- <el-descriptions-item label="UA">{{ detailRow.userAgent || '-' }}</el-descriptions-item>
- <el-descriptions-item label="请求体">
- <pre class="body-pre">{{ detailRow.requestBody || '-' }}</pre>
- </el-descriptions-item>
- <el-descriptions-item label="响应体">
- <pre class="body-pre">{{ detailRow.responseBody || '-' }}</pre>
- </el-descriptions-item>
- </el-descriptions>
- </div>
- <template #footer>
- <el-button class="btn-ghost" @click="detailVisible = false"><el-icon><Close /></el-icon>关闭</el-button>
- </template>
- </el-dialog>
- </section>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted } from 'vue'
- import { Close } from '@element-plus/icons-vue'
- import { fetchApiRequestLogList } from '@/api/business'
- defineOptions({ name: 'ApiRequestLog' })
- const searchForm = reactive({
- httpMethod: '',
- statusCode: undefined as number | undefined,
- pathPrefix: '',
- keyword: ''
- })
- const tableData = ref<any[]>([])
- const loading = ref(false)
- const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
- const detailVisible = ref(false)
- const detailRow = ref<any>(null)
- const roleMap: Record<string, string> = { admin: '运营', supplier: '供应商', client: '客户', deliver: '配送员' }
- const roleText = (v: string) => (v ? roleMap[v] || v : '-')
- const loadData = async () => {
- loading.value = true
- try {
- const res: any = await fetchApiRequestLogList({
- page: pagination.page,
- pageSize: pagination.pageSize,
- httpMethod: searchForm.httpMethod || undefined,
- statusCode: searchForm.statusCode || undefined,
- pathPrefix: searchForm.pathPrefix || undefined,
- keyword: searchForm.keyword || undefined
- })
- tableData.value = res.list
- pagination.total = res.total
- } catch (e) {
- console.error(e)
- } finally {
- loading.value = false
- }
- }
- const handleSearch = () => {
- pagination.page = 1
- loadData()
- }
- const handleReset = () => {
- Object.assign(searchForm, { httpMethod: '', statusCode: undefined, pathPrefix: '', keyword: '' })
- pagination.page = 1
- loadData()
- }
- const openDetail = (row: any) => {
- detailRow.value = row
- detailVisible.value = true
- }
- onMounted(() => loadData())
- </script>
- <style lang="scss" scoped>
- .body-pre {
- margin: 0;
- white-space: pre-wrap;
- word-break: break-all;
- font-size: 12px;
- max-height: 280px;
- overflow: auto;
- }
- .api-request-log-dialog-scroll :deep(.el-descriptions__body) {
- background: #fff;
- }
- </style>
|