|
|
@@ -2,24 +2,21 @@
|
|
|
import { reactive, onMounted, ref, nextTick } from "vue";
|
|
|
import { http } from "@/utils/http";
|
|
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
|
|
+import { ElMessage, ElMessageBox } from "element-plus";
|
|
|
|
|
|
defineOptions({ name: "AdminLog" });
|
|
|
|
|
|
const queryRef = ref();
|
|
|
-
|
|
|
-interface ColumnItem {
|
|
|
- label: string;
|
|
|
- prop: string;
|
|
|
- width?: number;
|
|
|
-}
|
|
|
+const detailVisible = ref(false);
|
|
|
+const currentLog = ref<any>(null);
|
|
|
|
|
|
const state = reactive({
|
|
|
formQuery: {
|
|
|
- operation: "",
|
|
|
username: "",
|
|
|
- startTime: "",
|
|
|
- endTime: ""
|
|
|
+ startDate: "",
|
|
|
+ endDate: ""
|
|
|
},
|
|
|
+ dateRange: [] as string[],
|
|
|
pageQuery: {
|
|
|
pageNum: 1,
|
|
|
pageSize: 10,
|
|
|
@@ -30,12 +27,14 @@ const state = reactive({
|
|
|
data: [] as Array<any>,
|
|
|
loading: false,
|
|
|
columns: [
|
|
|
- { label: "操作", prop: "operation", width: 200 },
|
|
|
- { label: "操作人", prop: "username", width: 150 },
|
|
|
- { label: "IP地址", prop: "ip", width: 150 },
|
|
|
- { label: "操作时间", prop: "createTime", width: 180 },
|
|
|
- { label: "详情", prop: "detail" }
|
|
|
- ] as ColumnItem[]
|
|
|
+ { label: "ID", prop: "id", width: 80 },
|
|
|
+ { label: "操作用户", prop: "username", width: 120 },
|
|
|
+ { label: "操作名称", prop: "operation", width: 200 },
|
|
|
+ { label: "请求方法", prop: "method", width: 150 },
|
|
|
+ { label: "IP地址", prop: "ip", width: 130 },
|
|
|
+ { label: "耗时(ms)", prop: "executeTime", width: 100 },
|
|
|
+ { label: "操作时间", prop: "createTime", width: 170 }
|
|
|
+ ]
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -52,8 +51,16 @@ const loadData = (refresh: boolean = false) => {
|
|
|
if (refresh) {
|
|
|
state.pageQuery.pageNum = 1;
|
|
|
}
|
|
|
+
|
|
|
+ if (state.dateRange && state.dateRange.length === 2) {
|
|
|
+ state.formQuery.startDate = state.dateRange[0];
|
|
|
+ state.formQuery.endDate = state.dateRange[1];
|
|
|
+ } else {
|
|
|
+ state.formQuery.startDate = "";
|
|
|
+ state.formQuery.endDate = "";
|
|
|
+ }
|
|
|
+
|
|
|
state.tableData.loading = true;
|
|
|
- // TODO: 后端日志接口待实现
|
|
|
http.request<any>("get", "/system-log/list", { params: { ...state.formQuery, ...state.pageQuery } })
|
|
|
.then((res: any) => {
|
|
|
const { list, total } = res || {};
|
|
|
@@ -84,24 +91,61 @@ const handleSearch = () => {
|
|
|
|
|
|
const handleReset = () => {
|
|
|
state.formQuery = {
|
|
|
- operation: "",
|
|
|
username: "",
|
|
|
- startTime: "",
|
|
|
- endTime: ""
|
|
|
+ startDate: "",
|
|
|
+ endDate: ""
|
|
|
};
|
|
|
+ state.dateRange = [];
|
|
|
loadData(true);
|
|
|
};
|
|
|
+
|
|
|
+const handleDetail = (row: any) => {
|
|
|
+ currentLog.value = row;
|
|
|
+ detailVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleClearLog = async () => {
|
|
|
+ await ElMessageBox.confirm("确定要清空所有操作日志吗?此操作不可恢复!", "警告", { type: "warning" });
|
|
|
+ try {
|
|
|
+ await http.request("post", "/system-log/clear");
|
|
|
+ ElMessage.success("清空成功");
|
|
|
+ loadData(true);
|
|
|
+ } catch (error) {
|
|
|
+ ElMessage.error("清空失败");
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const getExecuteTimeType = (time: number) => {
|
|
|
+ if (time > 1000) return "danger";
|
|
|
+ if (time > 500) return "warning";
|
|
|
+ return "success";
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="page-container">
|
|
|
<el-card shadow="hover">
|
|
|
<el-form ref="queryRef" :model="state.formQuery" inline class="search-form">
|
|
|
- <el-form-item label="操作">
|
|
|
- <el-input v-model="state.formQuery.operation" placeholder="请输入操作关键词" clearable @keyup.enter="handleSearch" />
|
|
|
+ <el-form-item label="操作用户">
|
|
|
+ <el-input
|
|
|
+ v-model="state.formQuery.username"
|
|
|
+ placeholder="请输入操作用户"
|
|
|
+ clearable
|
|
|
+ style="width: 150px"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="操作人">
|
|
|
- <el-input v-model="state.formQuery.username" placeholder="请输入操作人" clearable @keyup.enter="handleSearch" />
|
|
|
+ <el-form-item label="操作时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="state.dateRange"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ style="width: 240px"
|
|
|
+ @change="handleSearch"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
|
<el-button
|
|
|
@@ -117,8 +161,17 @@ const handleReset = () => {
|
|
|
>
|
|
|
重置
|
|
|
</el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ :icon="useRenderIcon('ri/delete-bin-line')"
|
|
|
+ @click="handleClearLog"
|
|
|
+ :disabled="state.tableData.data.length === 0"
|
|
|
+ >
|
|
|
+ 清空日志
|
|
|
+ </el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
+
|
|
|
<el-table
|
|
|
v-loading="state.tableData.loading"
|
|
|
:data="state.tableData.data"
|
|
|
@@ -136,8 +189,27 @@ const handleReset = () => {
|
|
|
:label="col.label"
|
|
|
:width="col.width"
|
|
|
show-overflow-tooltip
|
|
|
- />
|
|
|
+ >
|
|
|
+ <template #default="{ row }">
|
|
|
+ <template v-if="col.prop === 'executeTime'">
|
|
|
+ <el-tag :type="getExecuteTimeType(row.executeTime)" size="small">
|
|
|
+ {{ row.executeTime }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ {{ row[col.prop] }}
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button type="primary" link size="small" @click="handleDetail(row)">
|
|
|
+ 详情
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
</el-table>
|
|
|
+
|
|
|
<div class="pagination-container">
|
|
|
<el-pagination
|
|
|
v-model:current-page="state.pageQuery.pageNum"
|
|
|
@@ -150,6 +222,30 @@ const handleReset = () => {
|
|
|
/>
|
|
|
</div>
|
|
|
</el-card>
|
|
|
+
|
|
|
+ <!-- 详情对话框 -->
|
|
|
+ <el-dialog v-model="detailVisible" title="日志详情" width="600px">
|
|
|
+ <el-descriptions v-if="currentLog" :column="2" border>
|
|
|
+ <el-descriptions-item label="操作用户">{{ currentLog.username }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="IP地址">{{ currentLog.ip }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="操作名称" :span="2">{{ currentLog.operation }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="请求方法" :span="2">{{ currentLog.method }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="耗时">{{ currentLog.executeTime }}ms</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="操作时间">{{ currentLog.createTime }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+ <div v-if="currentLog && currentLog.requestParam" class="detail-section">
|
|
|
+ <div class="section-title">请求参数</div>
|
|
|
+ <el-input
|
|
|
+ type="textarea"
|
|
|
+ :rows="6"
|
|
|
+ :model-value="currentLog.requestParam"
|
|
|
+ readonly
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="detailVisible = false">关闭</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -167,4 +263,20 @@ const handleReset = () => {
|
|
|
justify-content: flex-end;
|
|
|
margin-top: 15px;
|
|
|
}
|
|
|
+
|
|
|
+.detail-section {
|
|
|
+ margin-top: 15px;
|
|
|
+
|
|
|
+ .section-title {
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-textarea__inner) {
|
|
|
+ font-family: monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ background: var(--el-fill-color-lighter);
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|