jiaxing.zhou

feat(log): 新增系统日志管理页面

- 实现日志列表展示与分页功能- 添加日志类型、用户名、操作模块等多条件搜索
- 支持查看日志详情信息
- 提供单条及批量删除日志功能
- 实现过期日志清理功能
- 添加日志导出功能入口
- 集成Element Plus组件库实现UI界面
- 使用Vue 3 Composition API编写组件逻辑
1 +<template>
2 + <div class="log-management">
3 + <!-- 页面标题 -->
4 + <div class="page-header">
5 + <h2>日志管理</h2>
6 + <p>系统操作日志查询与管理</p>
7 + </div>
8 +
9 + <!-- 搜索表单 -->
10 + <el-card class="search-card" shadow="never">
11 + <el-form :model="searchForm" :inline="true" class="search-form">
12 + <el-form-item label="日志类型">
13 + <el-select v-model="searchForm.logType" placeholder="请选择日志类型" clearable style="width: 200px">
14 + <el-option label="操作日志" value="0" />
15 + <el-option label="异常日志" value="1" />
16 + <el-option label="登录日志" value="2" />
17 + </el-select>
18 + </el-form-item>
19 +
20 + <el-form-item label="用户名">
21 + <el-input
22 + v-model="searchForm.username"
23 + placeholder="请输入用户名"
24 + clearable
25 + style="width: 200px"
26 + />
27 + </el-form-item>
28 +
29 + <el-form-item label="操作模块">
30 + <el-input
31 + v-model="searchForm.module"
32 + placeholder="请输入操作模块"
33 + clearable
34 + style="width: 200px"
35 + />
36 + </el-form-item>
37 +
38 + <el-form-item label="操作结果">
39 + <el-select v-model="searchForm.status" placeholder="请选择操作结果" clearable style="width: 200px">
40 + <el-option label="成功" :value="1" />
41 + <el-option label="失败" :value="0" />
42 + </el-select>
43 + </el-form-item>
44 +
45 + <el-form-item label="时间范围">
46 + <el-date-picker
47 + v-model="dateRange"
48 + type="datetimerange"
49 + range-separator="至"
50 + start-placeholder="开始时间"
51 + end-placeholder="结束时间"
52 + format="YYYY-MM-DD HH:mm:ss"
53 + value-format="YYYY-MM-DD HH:mm:ss"
54 + @change="handleDateRangeChange"
55 + style="width: 350px"
56 + />
57 + </el-form-item>
58 +
59 + <el-form-item>
60 + <el-button type="primary" @click="handleSearch" :loading="loading">
61 + <el-icon><Search /></el-icon>
62 + 搜索
63 + </el-button>
64 + <el-button @click="handleReset">
65 + <el-icon><Refresh /></el-icon>
66 + 重置
67 + </el-button>
68 + </el-form-item>
69 + </el-form>
70 + </el-card>
71 +
72 + <!-- 操作按钮 -->
73 + <el-card class="toolbar-card" shadow="never">
74 + <div class="toolbar">
75 + <div class="toolbar-left">
76 + <el-button
77 + type="danger"
78 + :disabled="selectedLogs.length === 0"
79 + @click="handleBatchDelete"
80 + >
81 + <el-icon><Delete /></el-icon>
82 + 批量删除
83 + </el-button>
84 + <el-button type="warning" @click="handleCleanExpired">
85 + <el-icon><Delete /></el-icon>
86 + 清理过期日志
87 + </el-button>
88 + <el-button type="info" @click="handleExport">
89 + <el-icon><Download /></el-icon>
90 + 导出日志
91 + </el-button>
92 + </div>
93 + <div class="toolbar-right">
94 + <el-button @click="handleRefresh" :loading="loading">
95 + <el-icon><Refresh /></el-icon>
96 + 刷新
97 + </el-button>
98 + </div>
99 + </div>
100 + </el-card>
101 +
102 + <!-- 日志列表 -->
103 + <el-card class="table-card" shadow="never">
104 +
105 + <el-table
106 + v-loading="loading"
107 + :data="logList"
108 + @selection-change="handleSelectionChange"
109 + stripe
110 + border
111 + style="width: 100%"
112 + >
113 + <el-table-column type="selection" width="50" align="center" />
114 +
115 + <el-table-column prop="logId" label="日志ID" width="80" align="center" />
116 +
117 + <el-table-column prop="logTypeText" label="日志类型" width="100" align="center">
118 + <template #default="{ row }">
119 + <el-tag :type="getLogTypeTagType(row.logType)" size="small">
120 + {{ row.logTypeText }}
121 + </el-tag>
122 + </template>
123 + </el-table-column>
124 +
125 + <el-table-column prop="username" label="用户名" width="100" align="center" />
126 +
127 + <el-table-column prop="module" label="操作模块" width="120" align="center" />
128 +
129 + <el-table-column prop="operation" label="操作内容" min-width="180" show-overflow-tooltip />
130 +
131 + <el-table-column prop="ip" label="IP地址" width="120" align="center" />
132 +
133 + <el-table-column prop="logTime" label="操作时间" width="160" align="center">
134 + <template #default="{ row }">
135 + {{ formatDate(row.logTime) }}
136 + </template>
137 + </el-table-column>
138 +
139 + <el-table-column prop="statusText" label="操作结果" width="100" align="center">
140 + <template #default="{ row }">
141 + <el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
142 + {{ row.statusText }}
143 + </el-tag>
144 + </template>
145 + </el-table-column>
146 +
147 + <el-table-column label="操作" width="140" fixed="right" align="center">
148 + <template #default="{ row }">
149 + <el-button
150 + type="primary"
151 + size="small"
152 + @click="handleViewDetail(row)"
153 + link
154 + >
155 + 详情
156 + </el-button>
157 + <el-button
158 + type="danger"
159 + size="small"
160 + @click="handleDelete(row)"
161 + link
162 + >
163 + 删除
164 + </el-button>
165 + </template>
166 + </el-table-column>
167 + </el-table>
168 +
169 + <!-- 分页 -->
170 + <div class="pagination-container">
171 + <el-pagination
172 + v-model:current-page="pagination.pageNum"
173 + v-model:page-size="pagination.pageSize"
174 + :page-sizes="[10, 20, 50, 100]"
175 + :total="pagination.total"
176 + layout="total, sizes, prev, pager, next, jumper"
177 + @size-change="handleSizeChange"
178 + @current-change="handleCurrentChange"
179 + />
180 + </div>
181 + </el-card>
182 +
183 + <!-- 日志详情对话框 -->
184 + <el-dialog
185 + v-model="detailDialogVisible"
186 + title="日志详情"
187 + width="800px"
188 + :close-on-click-modal="false"
189 + >
190 + <div v-if="logDetail" class="log-detail">
191 + <el-descriptions :column="2" border>
192 + <el-descriptions-item label="日志ID">
193 + {{ logDetail.logId }}
194 + </el-descriptions-item>
195 + <el-descriptions-item label="日志类型">
196 + <el-tag :type="getLogTypeTagType(logDetail.logType)">
197 + {{ logDetail.logTypeText }}
198 + </el-tag>
199 + </el-descriptions-item>
200 + <el-descriptions-item label="用户名">
201 + {{ logDetail.username }}
202 + </el-descriptions-item>
203 + <el-descriptions-item label="用户ID">
204 + {{ logDetail.userId }}
205 + </el-descriptions-item>
206 + <el-descriptions-item label="操作模块">
207 + {{ logDetail.module }}
208 + </el-descriptions-item>
209 + <el-descriptions-item label="操作内容">
210 + {{ logDetail.operation }}
211 + </el-descriptions-item>
212 + <el-descriptions-item label="IP地址">
213 + {{ logDetail.ip }}
214 + </el-descriptions-item>
215 + <el-descriptions-item label="操作时间">
216 + {{ formatDate(logDetail.logTime) }}
217 + </el-descriptions-item>
218 + <el-descriptions-item label="操作结果">
219 + <el-tag :type="logDetail.status === 1 ? 'success' : 'danger'">
220 + {{ logDetail.statusText }}
221 + </el-tag>
222 + </el-descriptions-item>
223 + <el-descriptions-item label="错误信息" v-if="logDetail.errorMsg">
224 + {{ logDetail.errorMsg }}
225 + </el-descriptions-item>
226 + </el-descriptions>
227 +
228 + <div v-if="logDetail.requestParam" class="request-param">
229 + <h4>请求参数</h4>
230 + <el-input
231 + v-model="logDetail.requestParam"
232 + type="textarea"
233 + :rows="6"
234 + readonly
235 + placeholder="无请求参数"
236 + />
237 + </div>
238 + </div>
239 +
240 + <template #footer>
241 + <el-button @click="detailDialogVisible = false">关闭</el-button>
242 + </template>
243 + </el-dialog>
244 +
245 + <!-- 清理过期日志对话框 -->
246 + <el-dialog
247 + v-model="cleanDialogVisible"
248 + title="清理过期日志"
249 + width="400px"
250 + :close-on-click-modal="false"
251 + >
252 + <el-form :model="cleanForm" label-width="100px">
253 + <el-form-item label="过期天数">
254 + <el-input-number
255 + v-model="cleanForm.expireDays"
256 + :min="1"
257 + :max="365"
258 + placeholder="请输入过期天数"
259 + style="width: 100%"
260 + />
261 + </el-form-item>
262 + <el-form-item>
263 + <el-text type="warning">
264 + 将删除指定天数之前的所有日志记录,此操作不可恢复!
265 + </el-text>
266 + </el-form-item>
267 + </el-form>
268 +
269 + <template #footer>
270 + <el-button @click="cleanDialogVisible = false">取消</el-button>
271 + <el-button type="danger" @click="handleConfirmClean" :loading="cleanLoading">
272 + 确认清理
273 + </el-button>
274 + </template>
275 + </el-dialog>
276 + </div>
277 +</template>
278 +
279 +<script setup lang="ts">
280 +import { ref, reactive, onMounted } from 'vue'
281 +import { ElMessage, ElMessageBox } from 'element-plus'
282 +import { Search, Refresh, Delete, Download } from '@element-plus/icons-vue'
283 +import { logApi, type Log, type LogDetail, type LogSearchParams } from '@/api/log'
284 +import { formatDate } from '@/utils/index'
285 +
286 +// 响应式数据
287 +const loading = ref(false)
288 +const logList = ref<Log[]>([])
289 +const selectedLogs = ref<Log[]>([])
290 +const detailDialogVisible = ref(false)
291 +const logDetail = ref<LogDetail | null>(null)
292 +const cleanDialogVisible = ref(false)
293 +const cleanLoading = ref(false)
294 +const dateRange = ref<[string, string] | null>(null)
295 +
296 +// 搜索表单
297 +const searchForm = reactive<LogSearchParams>({
298 + pageNum: 1,
299 + pageSize: 10,
300 + logType: '',
301 + username: '',
302 + module: '',
303 + status: undefined,
304 + startTime: '',
305 + endTime: ''
306 +})
307 +
308 +// 分页信息
309 +const pagination = reactive({
310 + pageNum: 1,
311 + pageSize: 10,
312 + total: 0
313 +})
314 +
315 +// 清理表单
316 +const cleanForm = reactive({
317 + expireDays: 30
318 +})
319 +
320 +// 获取日志类型标签类型
321 +const getLogTypeTagType = (logType: string) => {
322 + switch (logType) {
323 + case '0':
324 + return 'primary'
325 + case '1':
326 + return 'danger'
327 + case '2':
328 + return 'success'
329 + default:
330 + return 'info'
331 + }
332 +}
333 +
334 +
335 +// 导出日志
336 +const handleExport = () => {
337 + ElMessage.info('导出功能开发中...')
338 +}
339 +
340 +// 获取日志列表
341 +const getLogList = async () => {
342 + try {
343 + loading.value = true
344 + const params = {
345 + ...searchForm,
346 + pageNum: pagination.pageNum,
347 + pageSize: pagination.pageSize
348 + }
349 +
350 + const response = await logApi.getLogList(params)
351 + if (response.code === 200) {
352 + logList.value = response.data.records
353 + pagination.total = response.data.total
354 + } else {
355 + ElMessage.error(response.message || '获取日志列表失败')
356 + }
357 + } catch (error) {
358 + console.error('获取日志列表失败:', error)
359 + ElMessage.error('获取日志列表失败')
360 + } finally {
361 + loading.value = false
362 + }
363 +}
364 +
365 +// 搜索
366 +const handleSearch = () => {
367 + pagination.pageNum = 1
368 + getLogList()
369 +}
370 +
371 +// 重置
372 +const handleReset = () => {
373 + Object.assign(searchForm, {
374 + pageNum: 1,
375 + pageSize: 10,
376 + logType: '',
377 + username: '',
378 + module: '',
379 + status: undefined,
380 + startTime: '',
381 + endTime: ''
382 + })
383 + dateRange.value = null
384 + pagination.pageNum = 1
385 + getLogList()
386 +}
387 +
388 +// 刷新
389 +const handleRefresh = () => {
390 + getLogList()
391 +}
392 +
393 +// 时间范围变化
394 +const handleDateRangeChange = (value: [string, string] | null) => {
395 + if (value) {
396 + searchForm.startTime = value[0]
397 + searchForm.endTime = value[1]
398 + } else {
399 + searchForm.startTime = ''
400 + searchForm.endTime = ''
401 + }
402 +}
403 +
404 +// 分页大小变化
405 +const handleSizeChange = (size: number) => {
406 + pagination.pageSize = size
407 + pagination.pageNum = 1
408 + getLogList()
409 +}
410 +
411 +// 当前页变化
412 +const handleCurrentChange = (page: number) => {
413 + pagination.pageNum = page
414 + getLogList()
415 +}
416 +
417 +// 选择变化
418 +const handleSelectionChange = (selection: Log[]) => {
419 + selectedLogs.value = selection
420 +}
421 +
422 +// 查看详情
423 +const handleViewDetail = async (row: Log) => {
424 + try {
425 + const response = await logApi.getLogDetail(row.logId)
426 + if (response.code === 200) {
427 + logDetail.value = response.data
428 + detailDialogVisible.value = true
429 + } else {
430 + ElMessage.error(response.message || '获取日志详情失败')
431 + }
432 + } catch (error) {
433 + console.error('获取日志详情失败:', error)
434 + ElMessage.error('获取日志详情失败')
435 + }
436 +}
437 +
438 +// 删除日志
439 +const handleDelete = async (row: Log) => {
440 + try {
441 + await ElMessageBox.confirm(
442 + `确定要删除日志ID为 ${row.logId} 的记录吗?`,
443 + '确认删除',
444 + {
445 + confirmButtonText: '确定',
446 + cancelButtonText: '取消',
447 + type: 'warning'
448 + }
449 + )
450 +
451 + const response = await logApi.deleteLog(row.logId)
452 + if (response.code === 200) {
453 + ElMessage.success('删除成功')
454 + getLogList()
455 + } else {
456 + ElMessage.error(response.message || '删除失败')
457 + }
458 + } catch (error) {
459 + if (error !== 'cancel') {
460 + console.error('删除日志失败:', error)
461 + ElMessage.error('删除失败')
462 + }
463 + }
464 +}
465 +
466 +// 批量删除
467 +const handleBatchDelete = async () => {
468 + if (selectedLogs.value.length === 0) {
469 + ElMessage.warning('请选择要删除的日志')
470 + return
471 + }
472 +
473 + try {
474 + await ElMessageBox.confirm(
475 + `确定要删除选中的 ${selectedLogs.value.length} 条日志记录吗?`,
476 + '确认批量删除',
477 + {
478 + confirmButtonText: '确定',
479 + cancelButtonText: '取消',
480 + type: 'warning'
481 + }
482 + )
483 +
484 + const logIds = selectedLogs.value.map(log => log.logId)
485 + const response = await logApi.deleteLogs(logIds)
486 + if (response.code === 200) {
487 + ElMessage.success('批量删除成功')
488 + getLogList()
489 + } else {
490 + ElMessage.error(response.message || '批量删除失败')
491 + }
492 + } catch (error) {
493 + if (error !== 'cancel') {
494 + console.error('批量删除日志失败:', error)
495 + ElMessage.error('批量删除失败')
496 + }
497 + }
498 +}
499 +
500 +// 清理过期日志
501 +const handleCleanExpired = () => {
502 + cleanDialogVisible.value = true
503 +}
504 +
505 +// 确认清理
506 +const handleConfirmClean = async () => {
507 + if (!cleanForm.expireDays || cleanForm.expireDays <= 0) {
508 + ElMessage.warning('请输入有效的过期天数')
509 + return
510 + }
511 +
512 + try {
513 + await ElMessageBox.confirm(
514 + `确定要清理 ${cleanForm.expireDays} 天前的所有日志记录吗?此操作不可恢复!`,
515 + '确认清理',
516 + {
517 + confirmButtonText: '确定',
518 + cancelButtonText: '取消',
519 + type: 'warning'
520 + }
521 + )
522 +
523 + cleanLoading.value = true
524 + const response = await logApi.cleanExpiredLogs(cleanForm.expireDays)
525 + if (response.code === 200) {
526 + ElMessage.success(response.data)
527 + cleanDialogVisible.value = false
528 + getLogList()
529 + } else {
530 + ElMessage.error(response.message || '清理失败')
531 + }
532 + } catch (error) {
533 + if (error !== 'cancel') {
534 + console.error('清理过期日志失败:', error)
535 + ElMessage.error('清理失败')
536 + }
537 + } finally {
538 + cleanLoading.value = false
539 + }
540 +}
541 +
542 +// 组件挂载时获取数据
543 +onMounted(() => {
544 + getLogList()
545 +})
546 +</script>
547 +
548 +<style scoped>
549 +.log-management {
550 + padding: 20px;
551 +}
552 +
553 +.page-header {
554 + margin-bottom: 20px;
555 +}
556 +
557 +.page-header h2 {
558 + margin: 0 0 8px 0;
559 + color: #303133;
560 + font-size: 20px;
561 + font-weight: 600;
562 +}
563 +
564 +.page-header p {
565 + margin: 0;
566 + color: #909399;
567 + font-size: 14px;
568 +}
569 +
570 +.search-card {
571 + margin-bottom: 16px;
572 +}
573 +
574 +.search-form {
575 + margin-bottom: 0;
576 +}
577 +
578 +.toolbar-card {
579 + margin-bottom: 16px;
580 +}
581 +
582 +.toolbar {
583 + display: flex;
584 + justify-content: space-between;
585 + align-items: center;
586 +}
587 +
588 +.toolbar-left,
589 +.toolbar-right {
590 + display: flex;
591 + gap: 8px;
592 +}
593 +
594 +.table-card {
595 + margin-bottom: 16px;
596 +}
597 +
598 +.pagination-container {
599 + display: flex;
600 + justify-content: center;
601 + margin-top: 20px;
602 +}
603 +
604 +.log-detail {
605 + max-height: 600px;
606 + overflow-y: auto;
607 +}
608 +
609 +.request-param {
610 + margin-top: 20px;
611 +}
612 +
613 +.request-param h4 {
614 + margin: 0 0 10px 0;
615 + color: #303133;
616 + font-size: 14px;
617 + font-weight: 600;
618 +}
619 +
620 +/* 表格优化 */
621 +.el-table {
622 + font-size: 14px;
623 +}
624 +
625 +.el-table .cell {
626 + padding: 8px 12px;
627 + line-height: 1.5;
628 +}
629 +
630 +.el-table th.el-table__cell {
631 + background: #fafafa;
632 + color: #606266;
633 + font-weight: 600;
634 +}
635 +
636 +.el-table td.el-table__cell {
637 + padding: 12px 0;
638 +}
639 +
640 +.el-table .el-button + .el-button {
641 + margin-left: 8px;
642 +}
643 +</style>