index.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <!-- API 请求日志 -->
  2. <template>
  3. <div class="page-container">
  4. <header class="page-head">
  5. <h1>接口日志</h1>
  6. <p class="page-desc">集中检索 API 请求轨迹,快速定位异常请求与响应。</p>
  7. </header>
  8. <section class="panel">
  9. <div class="panel-top">
  10. <h2 class="title-bar-heading">请求列表</h2>
  11. </div>
  12. <div class="filter-block">
  13. <el-form :model="searchForm" class="filter-form" label-position="top" @submit.prevent>
  14. <div class="filter-grid">
  15. <el-form-item label="方法">
  16. <el-select v-model="searchForm.httpMethod" placeholder="全部" clearable>
  17. <el-option label="全部" value="" />
  18. <el-option label="GET" value="GET" />
  19. <el-option label="POST" value="POST" />
  20. <el-option label="PUT" value="PUT" />
  21. <el-option label="PATCH" value="PATCH" />
  22. <el-option label="DELETE" value="DELETE" />
  23. </el-select>
  24. </el-form-item>
  25. <el-form-item label="状态码">
  26. <el-input v-model.number="searchForm.statusCode" placeholder="如 200" clearable @keyup.enter="handleSearch" />
  27. </el-form-item>
  28. <el-form-item label="路径包含">
  29. <el-input v-model="searchForm.pathPrefix" placeholder="/api/..." clearable @keyup.enter="handleSearch" />
  30. </el-form-item>
  31. <el-form-item label="关键词">
  32. <el-input v-model="searchForm.keyword" placeholder="路径/用户/内容" clearable @keyup.enter="handleSearch" />
  33. </el-form-item>
  34. </div>
  35. <div class="filter-actions-row">
  36. <span class="filter-hint">支持按请求方法、状态码和关键词组合筛选。</span>
  37. <div class="filter-actions-end">
  38. <el-button class="btn-ghost" @click="handleReset">重置</el-button>
  39. <el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
  40. </div>
  41. </div>
  42. </el-form>
  43. </div>
  44. <div class="list-toolbar">
  45. <div class="list-toolbar-row">
  46. <div class="list-toolbar-selected">共 <span class="sel-num">{{ pagination.total }}</span> 条请求</div>
  47. </div>
  48. </div>
  49. <div class="list-body list-wrap">
  50. <el-table class="list-table" :data="tableData" v-loading="loading" stripe>
  51. <el-table-column prop="httpMethod" label="方法" width="80" />
  52. <el-table-column prop="path" label="路径" min-width="200" show-overflow-tooltip />
  53. <el-table-column prop="statusCode" label="状态" width="80" />
  54. <el-table-column prop="durationMs" label="耗时(ms)" width="100" />
  55. <el-table-column prop="username" label="用户" min-width="100" show-overflow-tooltip />
  56. <el-table-column prop="role" label="角色" width="90">
  57. <template #default="{ row }">{{ roleText(row.role) }}</template>
  58. </el-table-column>
  59. <el-table-column prop="ipAddress" label="IP" width="130" show-overflow-tooltip />
  60. <el-table-column prop="createAt" label="时间" min-width="170" />
  61. <el-table-column label="操作" width="100" fixed="right">
  62. <template #default="{ row }">
  63. <el-button class="btn-op-item primary auto-icon-view" @click="openDetail(row)">查看</el-button>
  64. </template>
  65. </el-table-column>
  66. </el-table>
  67. <div class="pagination list-pager-bar">
  68. <el-pagination
  69. :current-page="pagination.page"
  70. :page-size="pagination.pageSize"
  71. :total="pagination.total"
  72. :page-sizes="[10, 20, 50]"
  73. layout="total, sizes, prev, pager, next"
  74. @update:page-size="
  75. (s) => {
  76. pagination.pageSize = s
  77. pagination.page = 1
  78. loadData()
  79. }
  80. "
  81. @update:current-page="
  82. (p) => {
  83. pagination.page = p
  84. loadData()
  85. }
  86. "
  87. />
  88. </div>
  89. </div>
  90. <el-dialog
  91. v-model="detailVisible"
  92. title="请求详情"
  93. width="920px"
  94. destroy-on-close
  95. class="order-module-dialog api-request-log-dialog"
  96. modal-class="order-module-dialog-mask"
  97. >
  98. <div class="dialog-scroll api-request-log-dialog-scroll">
  99. <el-descriptions v-if="detailRow" :column="1" border size="small">
  100. <el-descriptions-item label="方法">{{ detailRow.httpMethod }}</el-descriptions-item>
  101. <el-descriptions-item label="路径">{{ detailRow.path }}</el-descriptions-item>
  102. <el-descriptions-item label="Query">{{ detailRow.queryString || '-' }}</el-descriptions-item>
  103. <el-descriptions-item label="状态 / 耗时"
  104. >{{ detailRow.statusCode }} / {{ detailRow.durationMs }} ms</el-descriptions-item
  105. >
  106. <el-descriptions-item label="用户">{{ detailRow.username || '-' }} ({{ detailRow.userId || '-' }})</el-descriptions-item>
  107. <el-descriptions-item label="角色">{{ roleText(detailRow.role) }}</el-descriptions-item>
  108. <el-descriptions-item label="IP">{{ detailRow.ipAddress || '-' }}</el-descriptions-item>
  109. <el-descriptions-item label="UA">{{ detailRow.userAgent || '-' }}</el-descriptions-item>
  110. <el-descriptions-item label="请求体">
  111. <pre class="body-pre">{{ detailRow.requestBody || '-' }}</pre>
  112. </el-descriptions-item>
  113. <el-descriptions-item label="响应体">
  114. <pre class="body-pre">{{ detailRow.responseBody || '-' }}</pre>
  115. </el-descriptions-item>
  116. </el-descriptions>
  117. </div>
  118. <template #footer>
  119. <el-button class="btn-ghost" @click="detailVisible = false"><el-icon><Close /></el-icon>关闭</el-button>
  120. </template>
  121. </el-dialog>
  122. </section>
  123. </div>
  124. </template>
  125. <script setup lang="ts">
  126. import { ref, reactive, onMounted } from 'vue'
  127. import { Close } from '@element-plus/icons-vue'
  128. import { fetchApiRequestLogList } from '@/api/business'
  129. defineOptions({ name: 'ApiRequestLog' })
  130. const searchForm = reactive({
  131. httpMethod: '',
  132. statusCode: undefined as number | undefined,
  133. pathPrefix: '',
  134. keyword: ''
  135. })
  136. const tableData = ref<any[]>([])
  137. const loading = ref(false)
  138. const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
  139. const detailVisible = ref(false)
  140. const detailRow = ref<any>(null)
  141. const roleMap: Record<string, string> = { admin: '运营', supplier: '供应商', client: '客户', deliver: '配送员' }
  142. const roleText = (v: string) => (v ? roleMap[v] || v : '-')
  143. const loadData = async () => {
  144. loading.value = true
  145. try {
  146. const res: any = await fetchApiRequestLogList({
  147. page: pagination.page,
  148. pageSize: pagination.pageSize,
  149. httpMethod: searchForm.httpMethod || undefined,
  150. statusCode: searchForm.statusCode || undefined,
  151. pathPrefix: searchForm.pathPrefix || undefined,
  152. keyword: searchForm.keyword || undefined
  153. })
  154. tableData.value = res.list
  155. pagination.total = res.total
  156. } catch (e) {
  157. console.error(e)
  158. } finally {
  159. loading.value = false
  160. }
  161. }
  162. const handleSearch = () => {
  163. pagination.page = 1
  164. loadData()
  165. }
  166. const handleReset = () => {
  167. Object.assign(searchForm, { httpMethod: '', statusCode: undefined, pathPrefix: '', keyword: '' })
  168. pagination.page = 1
  169. loadData()
  170. }
  171. const openDetail = (row: any) => {
  172. detailRow.value = row
  173. detailVisible.value = true
  174. }
  175. onMounted(() => loadData())
  176. </script>
  177. <style lang="scss" scoped>
  178. .body-pre {
  179. margin: 0;
  180. white-space: pre-wrap;
  181. word-break: break-all;
  182. font-size: 12px;
  183. max-height: 280px;
  184. overflow: auto;
  185. }
  186. .api-request-log-dialog-scroll :deep(.el-descriptions__body) {
  187. background: #fff;
  188. }
  189. </style>