OperationLogs.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <template>
  2. <div class="operation-logs admin-page">
  3. <!-- 筛选条件 -->
  4. <el-card class="filter-card">
  5. <el-form :inline="true" :model="filterForm" class="filter-form" size="small">
  6. <el-form-item label="用户名/ID">
  7. <el-input v-model="filterForm.userId" placeholder="请输入用户名或ID" clearable></el-input>
  8. </el-form-item>
  9. <el-form-item label="操作类型">
  10. <el-select v-model="filterForm.operationType" placeholder="请选择" clearable>
  11. <el-option label="登录" value="login"></el-option>
  12. <el-option label="登出" value="logout"></el-option>
  13. <el-option label="创建" value="create"></el-option>
  14. <el-option label="更新" value="update"></el-option>
  15. <el-option label="删除" value="delete"></el-option>
  16. <el-option label="查询" value="query"></el-option>
  17. </el-select>
  18. </el-form-item>
  19. <el-form-item label="时间范围">
  20. <el-date-picker
  21. v-model="dateRange"
  22. type="daterange"
  23. range-separator="至"
  24. start-placeholder="开始日期"
  25. end-placeholder="结束日期"
  26. value-format="yyyy-MM-dd">
  27. </el-date-picker>
  28. </el-form-item>
  29. <el-form-item>
  30. <el-button type="primary" @click="handleQuery" :loading="loading">查询</el-button>
  31. <el-button @click="handleReset">重置</el-button>
  32. </el-form-item>
  33. </el-form>
  34. </el-card>
  35. <!-- 日志列表 -->
  36. <el-card>
  37. <div class="adm-table-wrap">
  38. <el-table height="100%" :data="logs" v-loading="loading" stripe>
  39. <el-table-column prop="id" label="ID" width="80">
  40. <template slot="header">
  41. <span class="sort-header" @click.stop="onSortClick('id', $event)">ID<span v-if="sortIndex('id') !== null" class="sort-index" :class="sortClass('id') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('id') }}</span>{{ sortIcon('id') }}</span>
  42. </template>
  43. </el-table-column>
  44. <el-table-column prop="nickname" label="用户名" width="120">
  45. <template slot="header">
  46. <span class="sort-header" @click.stop="onSortClick('nickname', $event)">用户名<span v-if="sortIndex('nickname') !== null" class="sort-index" :class="sortClass('nickname') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('nickname') }}</span>{{ sortIcon('nickname') }}</span>
  47. </template>
  48. </el-table-column>
  49. <el-table-column prop="operationType" label="操作类型" width="100">
  50. <template slot-scope="scope">
  51. <el-tag :type="getOperationTypeTag(scope.row.operationType)" size="small">
  52. {{ scope.row.operationType }}
  53. </el-tag>
  54. </template>
  55. <template slot="header">
  56. <span class="sort-header" @click.stop="onSortClick('operationType', $event)">操作类型<span v-if="sortIndex('operationType') !== null" class="sort-index" :class="sortClass('operationType') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('operationType') }}</span>{{ sortIcon('operationType') }}</span>
  57. </template>
  58. </el-table-column>
  59. <el-table-column prop="description" label="操作描述" min-width="200">
  60. <template slot="header">
  61. <span class="sort-header" @click.stop="onSortClick('description', $event)">操作描述<span v-if="sortIndex('description') !== null" class="sort-index" :class="sortClass('description') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('description') }}</span>{{ sortIcon('description') }}</span>
  62. </template>
  63. </el-table-column>
  64. <el-table-column prop="requestPath" label="请求路径" min-width="180">
  65. <template slot="header">
  66. <span class="sort-header" @click.stop="onSortClick('requestPath', $event)">请求路径<span v-if="sortIndex('requestPath') !== null" class="sort-index" :class="sortClass('requestPath') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('requestPath') }}</span>{{ sortIcon('requestPath') }}</span>
  67. </template>
  68. </el-table-column>
  69. <el-table-column prop="requestMethod" label="方法" width="80">
  70. <template slot="header">
  71. <span class="sort-header" @click.stop="onSortClick('requestMethod', $event)">方法<span v-if="sortIndex('requestMethod') !== null" class="sort-index" :class="sortClass('requestMethod') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('requestMethod') }}</span>{{ sortIcon('requestMethod') }}</span>
  72. </template>
  73. </el-table-column>
  74. <el-table-column prop="responseStatus" label="状态" width="80">
  75. <template slot-scope="scope">
  76. <el-tag :type="scope.row.responseStatus === 200 ? 'success' : 'danger'" size="small">
  77. {{ scope.row.responseStatus }}
  78. </el-tag>
  79. </template>
  80. <template slot="header">
  81. <span class="sort-header" @click.stop="onSortClick('responseStatus', $event)">状态<span v-if="sortIndex('responseStatus') !== null" class="sort-index" :class="sortClass('responseStatus') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('responseStatus') }}</span>{{ sortIcon('responseStatus') }}</span>
  82. </template>
  83. </el-table-column>
  84. <el-table-column prop="clientIp" label="IP地址" width="130">
  85. <template slot="header">
  86. <span class="sort-header" @click.stop="onSortClick('clientIp', $event)">IP地址<span v-if="sortIndex('clientIp') !== null" class="sort-index" :class="sortClass('clientIp') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('clientIp') }}</span>{{ sortIcon('clientIp') }}</span>
  87. </template>
  88. </el-table-column>
  89. <el-table-column prop="createdAt" label="操作时间" width="160">
  90. <template slot-scope="scope">
  91. {{ formatDate(scope.row.createdAt) }}
  92. </template>
  93. <template slot="header">
  94. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">操作时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
  95. </template>
  96. </el-table-column>
  97. <el-table-column label="操作" width="80" fixed="right">
  98. <template slot-scope="{ row }">
  99. <el-button size="mini" @click="handleViewDetail(row)">详情</el-button>
  100. </template>
  101. </el-table-column>
  102. </el-table>
  103. </div>
  104. <!-- 分页 -->
  105. <el-pagination
  106. @current-change="handlePageChange"
  107. :current-page="pagination.page"
  108. :page-size="pagination.size"
  109. :total="pagination.total"
  110. layout="total, prev, pager, next"
  111. class="pagination">
  112. </el-pagination>
  113. </el-card>
  114. <!-- 详情对话框 -->
  115. <el-dialog title="操作日志详情" :visible.sync="detailVisible" width="600px">
  116. <el-descriptions :column="1" border v-if="currentLog">
  117. <el-descriptions-item label="ID">{{ currentLog.id }}</el-descriptions-item>
  118. <el-descriptions-item label="用户名">{{ currentLog.nickname || currentLog.userName || '-' }}</el-descriptions-item>
  119. <el-descriptions-item label="操作类型">{{ currentLog.operationType }}</el-descriptions-item>
  120. <el-descriptions-item label="操作描述">{{ currentLog.description }}</el-descriptions-item>
  121. <el-descriptions-item label="请求路径">{{ currentLog.requestPath }}</el-descriptions-item>
  122. <el-descriptions-item label="请求方法">{{ currentLog.requestMethod }}</el-descriptions-item>
  123. <el-descriptions-item label="请求参数">{{ currentLog.requestParams || '-' }}</el-descriptions-item>
  124. <el-descriptions-item label="响应状态">{{ currentLog.responseStatus }}</el-descriptions-item>
  125. <el-descriptions-item label="客户端IP">{{ currentLog.clientIp }}</el-descriptions-item>
  126. <el-descriptions-item label="User-Agent">{{ currentLog.userAgent || '-' }}</el-descriptions-item>
  127. <el-descriptions-item label="操作时间">{{ formatDate(currentLog.createdAt) }}</el-descriptions-item>
  128. </el-descriptions>
  129. </el-dialog>
  130. </div>
  131. </template>
  132. <script>
  133. import SortMixin from '@/mixins/SortMixin'
  134. import axios from '@/utils/request'
  135. import { getOperationLogs } from '@/api/operationLog'
  136. export default {
  137. name: 'OperationLogs',
  138. mixins: [SortMixin],
  139. computed: {
  140. tableHeight() {
  141. return window.innerHeight - 340
  142. }
  143. },
  144. data() {
  145. return {
  146. loading: false,
  147. logs: [],
  148. sortableColumns: { id: 'ID', nickname: '用户名', operationType: '操作类型', description: '操作描述', requestPath: '请求路径', requestMethod: '方法', responseStatus: '状态', clientIp: 'IP地址', createdAt: '操作时间' },
  149. filterForm: {
  150. userId: null,
  151. operationType: null
  152. },
  153. dateRange: null,
  154. pagination: {
  155. page: 1,
  156. size: 20,
  157. total: 0
  158. },
  159. detailVisible: false,
  160. currentLog: null
  161. }
  162. },
  163. mounted() {
  164. this.fetchLogs()
  165. },
  166. methods: {
  167. loadList() {
  168. this.fetchLogs()
  169. },
  170. async fetchLogs() {
  171. this.loading = true
  172. try {
  173. const params = {
  174. page: this.pagination.page,
  175. size: this.pagination.size,
  176. userId: this.filterForm.userId || undefined,
  177. operationType: this.filterForm.operationType || undefined,
  178. startDate: this.dateRange ? this.dateRange[0] : undefined,
  179. endDate: this.dateRange ? this.dateRange[1] : undefined
  180. }
  181. if (this.sortState.length > 0) params.sort = this.sortState
  182. const res = await getOperationLogs(params)
  183. if (res.code === 200) {
  184. this.logs = res.data.records || []
  185. this.pagination.total = res.data.total || 0
  186. }
  187. } catch (e) {
  188. this.$message.error('获取日志失败')
  189. } finally {
  190. this.loading = false
  191. }
  192. },
  193. handleQuery() {
  194. this.pagination.page = 1
  195. this.fetchLogs()
  196. },
  197. handleReset() {
  198. this.filterForm = { userId: null, operationType: null }
  199. this.dateRange = null
  200. this.pagination.page = 1
  201. this.fetchLogs()
  202. },
  203. handlePageChange(page) {
  204. this.pagination.page = page
  205. this.fetchLogs()
  206. },
  207. handleViewDetail(row) {
  208. this.currentLog = row
  209. this.detailVisible = true
  210. },
  211. getOperationTypeTag(type) {
  212. const tags = {
  213. login: 'success',
  214. logout: 'info',
  215. create: 'primary',
  216. update: 'warning',
  217. delete: 'danger',
  218. query: ''
  219. }
  220. return tags[type] || ''
  221. },
  222. formatDate(dateStr) {
  223. if (!dateStr) return '-'
  224. return new Date(dateStr).toLocaleString('zh-CN')
  225. }
  226. }
  227. }
  228. </script>
  229. <style scoped>
  230. .operation-logs {
  231. padding: 20px;
  232. }.filter-card {
  233. margin-bottom: 20px;
  234. }
  235. .filter-form {
  236. margin-bottom: 0;
  237. }
  238. .pagination {
  239. margin-top: 20px;
  240. text-align: right;
  241. }
  242. .sort-header {
  243. cursor: pointer;
  244. user-select: none;
  245. font-weight: 500;
  246. font-size: 12px;
  247. white-space: nowrap;
  248. }
  249. .sort-header:hover {
  250. color: #409eff;
  251. }
  252. </style>