442 lines
11 KiB
Vue
442 lines
11 KiB
Vue
<template>
|
|
<div class="container-box">
|
|
<div class="header-bar">
|
|
<h2>登录日志</h2>
|
|
<div class="header-actions">
|
|
<el-button @click="refresh">
|
|
<el-icon><Refresh /></el-icon>
|
|
刷新
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<el-divider></el-divider>
|
|
|
|
<!-- 搜索筛选 -->
|
|
<div class="filter-bar">
|
|
<el-form :inline="true" :model="filterForm" class="filter-form">
|
|
<el-form-item label="关键词">
|
|
<el-input
|
|
v-model="filterForm.keyword"
|
|
placeholder="搜索账号、姓名、租户、IP"
|
|
clearable
|
|
style="width: 200px"
|
|
@clear="handleSearch"
|
|
@keyup.enter="handleSearch"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item label="状态">
|
|
<el-select
|
|
v-model="filterForm.status"
|
|
placeholder="请选择状态"
|
|
clearable
|
|
style="width: 120px"
|
|
@change="handleSearch"
|
|
>
|
|
<el-option label="成功" value="1" />
|
|
<el-option label="失败" value="0" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="登录方式">
|
|
<el-select
|
|
v-model="filterForm.loginType"
|
|
placeholder="请选择登录方式"
|
|
clearable
|
|
style="width: 140px"
|
|
@change="handleSearch"
|
|
>
|
|
<el-option label="密码登录" value="password" />
|
|
<el-option label="短信验证码" value="sms" />
|
|
<el-option label="邮箱验证码" value="email" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="时间范围">
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="datetimerange"
|
|
range-separator="至"
|
|
start-placeholder="开始时间"
|
|
end-placeholder="结束时间"
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
style="width: 350px"
|
|
@change="handleDateRangeChange"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="handleSearch">
|
|
<el-icon><Search /></el-icon>
|
|
搜索
|
|
</el-button>
|
|
<el-button @click="handleReset">重置</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</div>
|
|
|
|
<!-- 登录日志列表 -->
|
|
<el-table
|
|
:data="logs"
|
|
style="width: 100%"
|
|
v-loading="loading"
|
|
@selection-change="handleSelectionChange"
|
|
>
|
|
<!--<el-table-column type="selection" width="55" align="center" />
|
|
<el-table-column prop="id" label="ID" width="60" align="center" />-->
|
|
<el-table-column
|
|
prop="user_name"
|
|
label="用户姓名"
|
|
width="110"
|
|
align="center"
|
|
/>
|
|
<el-table-column
|
|
prop="account"
|
|
label="登录账号"
|
|
width="130"
|
|
align="center"
|
|
/>
|
|
<el-table-column
|
|
prop="tenant_name"
|
|
label="租户名称"
|
|
width="130"
|
|
align="center"
|
|
/>
|
|
<el-table-column prop="login_type" label="登录方式" width="100" align="center">
|
|
<template #default="scope">
|
|
<el-tag type="primary" size="small">
|
|
{{ getLoginTypeLabel(scope.row.login_type) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="status" label="状态" width="80" align="center">
|
|
<template #default="scope">
|
|
<el-tag
|
|
:type="scope.row.status === 1 ? 'success' : 'danger'"
|
|
size="small"
|
|
>
|
|
{{ scope.row.status === 1 ? "成功" : "失败" }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column
|
|
prop="message"
|
|
label="结果信息"
|
|
min-width="140"
|
|
show-overflow-tooltip
|
|
/>
|
|
<el-table-column prop="ip" label="IP地址" width="130" align="center" />
|
|
<el-table-column prop="create_time" label="登录时间" width="180" align="center">
|
|
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="120" align="center" fixed="right">
|
|
<template #default="scope">
|
|
<el-button
|
|
size="small"
|
|
type="primary"
|
|
link
|
|
@click="handleViewDetail(scope.row)"
|
|
>
|
|
详情
|
|
</el-button>
|
|
<!-- <el-button
|
|
size="small"
|
|
type="danger"
|
|
link
|
|
@click="handleDelete(scope.row)"
|
|
>
|
|
删除
|
|
</el-button> -->
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 批量删除 -->
|
|
<!-- <div class="batch-bar" v-if="selectedLogs.length > 0">
|
|
<el-button type="danger" plain @click="handleBatchDelete">
|
|
<el-icon><Delete /></el-icon>
|
|
批量删除 ({{ selectedLogs.length }})
|
|
</el-button>
|
|
</div> -->
|
|
|
|
<!-- 分页 -->
|
|
<div class="pagination-bar">
|
|
<el-pagination
|
|
v-model:current-page="page"
|
|
v-model:page-size="pageSize"
|
|
:total="total"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
@size-change="handleSizeChange"
|
|
@current-change="handlePageChange"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 详情对话框 -->
|
|
<DetailDialog
|
|
ref="detailDialogRef"
|
|
v-model="detailDialogVisible"
|
|
:log-id="currentLogId"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, nextTick } from "vue";
|
|
import { ElMessage, ElMessageBox } from "element-plus";
|
|
import { Refresh, Delete, Search } from "@element-plus/icons-vue";
|
|
import {
|
|
getLoginLogs,
|
|
deleteLoginLog,
|
|
batchDeleteLoginLogs,
|
|
} from "@/api/loginLog";
|
|
import DetailDialog from "./components/detail.vue";
|
|
import { formatDateTime } from "@/utils/datetime";
|
|
|
|
interface LoginLog {
|
|
id: number;
|
|
tid: number | null;
|
|
user_id: number;
|
|
account: string;
|
|
user_name: string;
|
|
tenant_name: string;
|
|
login_type: string;
|
|
status: number;
|
|
message: string;
|
|
ip: string;
|
|
user_agent: string;
|
|
create_time: string;
|
|
}
|
|
|
|
const page = ref(1);
|
|
const pageSize = ref(20);
|
|
const total = ref(0);
|
|
const loading = ref(false);
|
|
const logs = ref<LoginLog[]>([]);
|
|
const selectedLogs = ref<LoginLog[]>([]);
|
|
|
|
// 筛选表单
|
|
const filterForm = ref({
|
|
keyword: "",
|
|
status: "",
|
|
loginType: "",
|
|
startTime: "",
|
|
endTime: "",
|
|
});
|
|
|
|
const dateRange = ref<[string, string] | null>(null);
|
|
|
|
// 详情对话框
|
|
const detailDialogVisible = ref(false);
|
|
const detailDialogRef = ref<any>(null);
|
|
const currentLogId = ref<number | undefined>(undefined);
|
|
|
|
// 登录方式标签
|
|
const getLoginTypeLabel = (type: string): string => {
|
|
const typeMap: Record<string, string> = {
|
|
password: "密码登录",
|
|
sms: "短信验证码",
|
|
email: "邮箱验证码",
|
|
};
|
|
return typeMap[type] || type;
|
|
};
|
|
|
|
// 获取登录日志列表
|
|
const fetchLogs = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const params: any = {
|
|
page: page.value,
|
|
pageSize: pageSize.value,
|
|
};
|
|
|
|
if (filterForm.value.keyword) {
|
|
params.keyword = filterForm.value.keyword;
|
|
}
|
|
if (filterForm.value.status !== "") {
|
|
params.status = filterForm.value.status;
|
|
}
|
|
if (filterForm.value.loginType) {
|
|
params.loginType = filterForm.value.loginType;
|
|
}
|
|
if (filterForm.value.startTime) {
|
|
params.startTime = filterForm.value.startTime;
|
|
}
|
|
if (filterForm.value.endTime) {
|
|
params.endTime = filterForm.value.endTime;
|
|
}
|
|
|
|
const res = await getLoginLogs(params);
|
|
if (res.code === 200) {
|
|
logs.value = res.data.list || [];
|
|
total.value = res.data.total || 0;
|
|
} else {
|
|
ElMessage.error(res.msg || "获取登录日志失败");
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || "获取登录日志失败");
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// 刷新
|
|
const refresh = () => {
|
|
fetchLogs();
|
|
};
|
|
|
|
// 搜索
|
|
const handleSearch = () => {
|
|
page.value = 1;
|
|
fetchLogs();
|
|
};
|
|
|
|
// 重置
|
|
const handleReset = () => {
|
|
filterForm.value = {
|
|
keyword: "",
|
|
status: "",
|
|
loginType: "",
|
|
startTime: "",
|
|
endTime: "",
|
|
};
|
|
dateRange.value = null;
|
|
page.value = 1;
|
|
fetchLogs();
|
|
};
|
|
|
|
// 时间范围变化
|
|
const handleDateRangeChange = (val: [string, string] | null) => {
|
|
if (val && val.length === 2) {
|
|
filterForm.value.startTime = val[0];
|
|
filterForm.value.endTime = val[1];
|
|
} else {
|
|
filterForm.value.startTime = "";
|
|
filterForm.value.endTime = "";
|
|
}
|
|
handleSearch();
|
|
};
|
|
|
|
// 分页改变
|
|
const handlePageChange = (val: number) => {
|
|
page.value = val;
|
|
fetchLogs();
|
|
};
|
|
|
|
// 每页数量改变
|
|
const handleSizeChange = (val: number) => {
|
|
pageSize.value = val;
|
|
page.value = 1;
|
|
fetchLogs();
|
|
};
|
|
|
|
// 选择改变
|
|
const handleSelectionChange = (selection: LoginLog[]) => {
|
|
selectedLogs.value = selection;
|
|
};
|
|
|
|
// 删除单条
|
|
const handleDelete = (log: LoginLog) => {
|
|
ElMessageBox.confirm("确定删除该条登录日志吗?", "提示", {
|
|
confirmButtonText: "确定",
|
|
cancelButtonText: "取消",
|
|
type: "warning",
|
|
}).then(async () => {
|
|
try {
|
|
const res = await deleteLoginLog(log.id);
|
|
if (res.code === 200) {
|
|
ElMessage.success("删除成功");
|
|
fetchLogs();
|
|
} else {
|
|
ElMessage.error(res.msg || "删除失败");
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || "删除失败");
|
|
}
|
|
});
|
|
};
|
|
|
|
// 批量删除
|
|
const handleBatchDelete = () => {
|
|
const ids = selectedLogs.value.map((item) => item.id);
|
|
ElMessageBox.confirm(
|
|
`确定删除选中的 ${ids.length} 条登录日志吗?`,
|
|
"提示",
|
|
{
|
|
confirmButtonText: "确定",
|
|
cancelButtonText: "取消",
|
|
type: "warning",
|
|
}
|
|
).then(async () => {
|
|
try {
|
|
const res = await batchDeleteLoginLogs(ids);
|
|
if (res.code === 200) {
|
|
ElMessage.success("批量删除成功");
|
|
fetchLogs();
|
|
} else {
|
|
ElMessage.error(res.msg || "批量删除失败");
|
|
}
|
|
} catch (error: any) {
|
|
ElMessage.error(error?.message || "批量删除失败");
|
|
}
|
|
});
|
|
};
|
|
|
|
// 查看详情
|
|
const handleViewDetail = (log: LoginLog) => {
|
|
currentLogId.value = log.id;
|
|
detailDialogVisible.value = true;
|
|
nextTick(() => {
|
|
if (detailDialogRef.value && detailDialogRef.value.open) {
|
|
detailDialogRef.value.open(log.id);
|
|
}
|
|
});
|
|
};
|
|
|
|
onMounted(() => {
|
|
fetchLogs();
|
|
});
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.container-box {
|
|
padding: 20px;
|
|
}
|
|
|
|
.header-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
|
|
h2 {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
}
|
|
|
|
.filter-bar {
|
|
margin-bottom: 20px;
|
|
padding: 20px;
|
|
border-radius: 4px;
|
|
|
|
.filter-form {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
.batch-bar {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.pagination-bar {
|
|
margin-top: 20px;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|