feat: 添加登录日志
This commit is contained in:
30
src/api/monitor.ts
Normal file
30
src/api/monitor.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { http } from "@/utils/http";
|
||||
import type { UserLoginLogInfo } from "types/monitor";
|
||||
import { filterEmptyObject } from "./utils";
|
||||
|
||||
// --------------------------登录日志相关--------------------------------------
|
||||
/**
|
||||
* 用户获取登录日志
|
||||
*/
|
||||
|
||||
export const getUserLoginLogAPI = (params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}) => {
|
||||
return http.request<QueryListResult<UserLoginLogInfo>>(
|
||||
"get",
|
||||
"/api/log/login",
|
||||
{
|
||||
params: filterEmptyObject(params)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户强退
|
||||
* @param id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteUserOnlineAPI = (id: string) => {
|
||||
return http.request<null>("delete", `/api/log/logout/${id}`);
|
||||
};
|
||||
@@ -76,7 +76,7 @@ class Http {
|
||||
"/api/refreshToken",
|
||||
"/api/login"
|
||||
];
|
||||
return whiteList.some(url => config.url.endsWith(url))
|
||||
return whiteList.includes(config.url)
|
||||
? config
|
||||
: new Promise(resolve => {
|
||||
const data = getTokenInfo();
|
||||
|
||||
183
src/views/monitor/login/index.vue
Normal file
183
src/views/monitor/login/index.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useLogin } from "./utils/hook";
|
||||
import { getPickerShortcuts } from "../utils";
|
||||
import { PureTableBar } from "@/components/RePureTableBar";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import Plane from "@iconify-icons/ri/plane-line";
|
||||
import Refresh from "@iconify-icons/ep/refresh";
|
||||
|
||||
defineOptions({
|
||||
name: "MonitorLogin"
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const formRef = ref();
|
||||
const tableRef = ref();
|
||||
|
||||
const {
|
||||
form,
|
||||
loading,
|
||||
columns,
|
||||
dataList,
|
||||
pagination,
|
||||
selectedNum,
|
||||
onSearch,
|
||||
deleteUserHandle,
|
||||
resetForm,
|
||||
onbatchDel,
|
||||
onSelectionCancel,
|
||||
handleCurrentChange,
|
||||
handleSizeChange,
|
||||
handleSelectionChange
|
||||
} = useLogin(tableRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:inline="true"
|
||||
:model="form"
|
||||
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px] overflow-auto"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input
|
||||
v-model="form.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
class="!w-[150px]"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录状态" prop="status">
|
||||
<el-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="!w-[150px]"
|
||||
>
|
||||
<el-option label="成功" value="1" />
|
||||
<el-option label="失败" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录时间" prop="loginTime">
|
||||
<el-date-picker
|
||||
v-model="form.loginTime"
|
||||
:shortcuts="getPickerShortcuts()"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期时间"
|
||||
end-placeholder="结束日期时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="useRenderIcon('ri:search-line')"
|
||||
:loading="loading"
|
||||
@click="onSearch"
|
||||
>
|
||||
{{ t("buttons:Search") }}
|
||||
</el-button>
|
||||
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||
{{ t("buttons:Reset") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<PureTableBar title="登录日志" :columns="columns" @refresh="onSearch">
|
||||
<!-- <template #buttons>
|
||||
<el-popconfirm title="确定要删除所有日志数据吗?" @confirm="clearAll">
|
||||
<template #reference>
|
||||
<el-button type="danger" :icon="useRenderIcon(Delete)">
|
||||
清空日志
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template> -->
|
||||
<template v-slot="{ size, dynamicColumns }">
|
||||
<div
|
||||
v-if="selectedNum > 0"
|
||||
v-motion-fade
|
||||
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
|
||||
>
|
||||
<div class="flex-auto">
|
||||
<span
|
||||
style="font-size: var(--el-font-size-base)"
|
||||
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||
>
|
||||
已选 {{ selectedNum }} 项
|
||||
</span>
|
||||
<el-button type="primary" text @click="onSelectionCancel">
|
||||
{{ t("buttons:Deselect") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
|
||||
<template #reference>
|
||||
<el-button type="danger" text class="mr-1">
|
||||
{{ t("buttons:DeleteInBatches") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
<pure-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
align-whole="center"
|
||||
table-layout="auto"
|
||||
:loading="loading"
|
||||
:size="size"
|
||||
adaptive
|
||||
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||
:data="dataList"
|
||||
:columns="dynamicColumns"
|
||||
:pagination="{ ...pagination, size }"
|
||||
:header-cell-style="{
|
||||
background: 'var(--el-fill-color-light)',
|
||||
color: 'var(--el-text-color-primary)'
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
@page-size-change="handleSizeChange"
|
||||
@page-current-change="handleCurrentChange"
|
||||
>
|
||||
<template #operation="{ row }">
|
||||
<el-popconfirm
|
||||
:title="`是否强制下线${row.username}`"
|
||||
@confirm="deleteUserHandle(row.session_id)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
class="reset-margin"
|
||||
link
|
||||
type="primary"
|
||||
:disabled="!row.online"
|
||||
:size="size"
|
||||
:icon="useRenderIcon(Plane)"
|
||||
>
|
||||
{{ t("buttons:ForceToExit") }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</pure-table>
|
||||
</template>
|
||||
</PureTableBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-dropdown-menu__item i) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin: 24px 24px 0 !important;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
225
src/views/monitor/login/utils/hook.tsx
Normal file
225
src/views/monitor/login/utils/hook.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import dayjs from "dayjs";
|
||||
import { message } from "@/utils/message";
|
||||
import { getKeyList } from "@pureadmin/utils";
|
||||
import { deleteUserOnlineAPI, getUserLoginLogAPI } from "@/api/monitor";
|
||||
import { usePublicHooks } from "@/views/system/hooks";
|
||||
import type { PaginationProps } from "@pureadmin/table";
|
||||
import { type Ref, reactive, ref, onMounted } from "vue";
|
||||
import type { UserLoginLogInfo } from "types/monitor";
|
||||
|
||||
export const useLogin = (tableRef: Ref) => {
|
||||
const form = reactive({
|
||||
username: "",
|
||||
status: "",
|
||||
loginTime: ""
|
||||
});
|
||||
/**
|
||||
* 表格数据
|
||||
*/
|
||||
const dataList = ref<UserLoginLogInfo[]>([]);
|
||||
/**
|
||||
* 表格加载状态
|
||||
*/
|
||||
const loading = ref(true);
|
||||
/**
|
||||
* 已选数量
|
||||
*/
|
||||
const selectedNum = ref(0);
|
||||
const { tagStyle } = usePublicHooks();
|
||||
/**
|
||||
* 分页参数
|
||||
*/
|
||||
const pagination = reactive<PaginationProps>({
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
background: true
|
||||
});
|
||||
const columns: TableColumnList = [
|
||||
{
|
||||
label: "勾选列", // 如果需要表格多选,此处label必须设置
|
||||
type: "selection",
|
||||
fixed: "left",
|
||||
reserveSelection: true // 数据刷新后保留选项
|
||||
},
|
||||
{
|
||||
label: "会话ID",
|
||||
prop: "session_id",
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
label: "用户账号",
|
||||
prop: "username",
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
label: "用户昵称",
|
||||
prop: "user_nickname",
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
label: "所属部门",
|
||||
prop: "department_name",
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
label: "登录 IP",
|
||||
prop: "login_ip",
|
||||
minWidth: 140
|
||||
},
|
||||
{
|
||||
label: "登录地点",
|
||||
prop: "login_location",
|
||||
minWidth: 140
|
||||
},
|
||||
{
|
||||
label: "操作系统",
|
||||
prop: "os",
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
label: "浏览器类型",
|
||||
prop: "browser",
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
label: "登录状态",
|
||||
prop: "status",
|
||||
minWidth: 100,
|
||||
cellRenderer: ({ row, props }) => (
|
||||
<el-tag size={props.size} style={tagStyle.value(row.status)}>
|
||||
{row.status === 1 ? "成功" : "失败"}
|
||||
</el-tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: "登录时间",
|
||||
prop: "login_time",
|
||||
minWidth: 180,
|
||||
formatter: ({ login_time }) =>
|
||||
dayjs(login_time).format("YYYY-MM-DD HH:mm:ss")
|
||||
},
|
||||
{
|
||||
label: "操作",
|
||||
fixed: "right",
|
||||
slot: "operation"
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* 当前页变化
|
||||
* @param val
|
||||
*/
|
||||
const handleCurrentChange = async (val: number) => {
|
||||
const res = await getUserLoginLogAPI({
|
||||
page: val,
|
||||
pageSize: pagination.pageSize
|
||||
});
|
||||
if (res.success) {
|
||||
dataList.value = res.data.result;
|
||||
pagination.total = res.data.total;
|
||||
pagination.currentPage = res.data.page;
|
||||
}
|
||||
};
|
||||
|
||||
/**处理页码变化 */
|
||||
const handleSizeChange = async (val: number) => {
|
||||
const res = await getUserLoginLogAPI({
|
||||
page: pagination.currentPage,
|
||||
pageSize: val
|
||||
});
|
||||
if (res.success) {
|
||||
dataList.value = res.data.result;
|
||||
pagination.total = res.data.total;
|
||||
pagination.currentPage = res.data.page;
|
||||
}
|
||||
};
|
||||
|
||||
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||
const handleSelectionChange = val => {
|
||||
selectedNum.value = val.length;
|
||||
// 重置表格高度
|
||||
tableRef.value.setAdaptive();
|
||||
};
|
||||
|
||||
/** 取消选择 */
|
||||
const onSelectionCancel = () => {
|
||||
selectedNum.value = 0;
|
||||
// 用于多选表格,清空用户的选择
|
||||
tableRef.value.getTableRef().clearSelection();
|
||||
};
|
||||
|
||||
/** 批量删除 */
|
||||
const onbatchDel = () => {
|
||||
// 返回当前选中的行
|
||||
const curSelected = tableRef.value.getTableRef().getSelectionRows();
|
||||
// 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
|
||||
message(`已删除序号为 ${getKeyList(curSelected, "id")} 的数据`, {
|
||||
type: "success"
|
||||
});
|
||||
tableRef.value.getTableRef().clearSelection();
|
||||
onSearch();
|
||||
};
|
||||
|
||||
/** 清空日志 */
|
||||
const clearAll = async () => {
|
||||
// 根据实际业务,调用接口删除所有日志数据
|
||||
message("已删除所有日志数据", {
|
||||
type: "success"
|
||||
});
|
||||
await onSearch();
|
||||
};
|
||||
|
||||
const onSearch = async () => {
|
||||
loading.value = true;
|
||||
const res = await getUserLoginLogAPI({
|
||||
page: pagination.currentPage,
|
||||
pageSize: pagination.pageSize
|
||||
});
|
||||
if (res.success) {
|
||||
dataList.value = res.data.result;
|
||||
pagination.total = res.data.total;
|
||||
pagination.currentPage = res.data.page;
|
||||
}
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const resetForm = formEl => {
|
||||
if (!formEl) return;
|
||||
formEl.resetFields();
|
||||
onSearch();
|
||||
};
|
||||
const deleteUserHandle = async (id: string) => {
|
||||
const res = await deleteUserOnlineAPI(id);
|
||||
if (res.success) {
|
||||
message("账号强退成功!", {
|
||||
type: "success"
|
||||
});
|
||||
onSearch();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onSearch();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
loading,
|
||||
columns,
|
||||
dataList,
|
||||
pagination,
|
||||
selectedNum,
|
||||
onSearch,
|
||||
clearAll,
|
||||
resetForm,
|
||||
onbatchDel,
|
||||
deleteUserHandle,
|
||||
onSelectionCancel,
|
||||
handleCurrentChange,
|
||||
handleSizeChange,
|
||||
handleSelectionChange
|
||||
};
|
||||
};
|
||||
129
src/views/monitor/utils.ts
Normal file
129
src/views/monitor/utils.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/** 日期、时间选择器快捷选项,常搭配 [DatePicker](https://element-plus.org/zh-CN/component/date-picker.html) 和 [DateTimePicker](https://element-plus.org/zh-CN/component/datetime-picker.html) 的`shortcuts`属性使用 */
|
||||
export const getPickerShortcuts = (): Array<{
|
||||
text: string;
|
||||
value: Date | Function;
|
||||
}> => {
|
||||
return [
|
||||
{
|
||||
text: "今天",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayEnd = new Date();
|
||||
todayEnd.setHours(23, 59, 59, 999);
|
||||
return [today, todayEnd];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "昨天",
|
||||
value: () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
yesterday.setHours(0, 0, 0, 0);
|
||||
const yesterdayEnd = new Date();
|
||||
yesterdayEnd.setDate(yesterdayEnd.getDate() - 1);
|
||||
yesterdayEnd.setHours(23, 59, 59, 999);
|
||||
return [yesterday, yesterdayEnd];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "前天",
|
||||
value: () => {
|
||||
const beforeYesterday = new Date();
|
||||
beforeYesterday.setDate(beforeYesterday.getDate() - 2);
|
||||
beforeYesterday.setHours(0, 0, 0, 0);
|
||||
const beforeYesterdayEnd = new Date();
|
||||
beforeYesterdayEnd.setDate(beforeYesterdayEnd.getDate() - 2);
|
||||
beforeYesterdayEnd.setHours(23, 59, 59, 999);
|
||||
return [beforeYesterday, beforeYesterdayEnd];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "本周",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() - today.getDay() + (today.getDay() === 0 ? -6 : 1)
|
||||
);
|
||||
startOfWeek.setHours(0, 0, 0, 0);
|
||||
const endOfWeek = new Date(
|
||||
startOfWeek.getTime() +
|
||||
6 * 24 * 60 * 60 * 1000 +
|
||||
23 * 60 * 60 * 1000 +
|
||||
59 * 60 * 1000 +
|
||||
59 * 1000 +
|
||||
999
|
||||
);
|
||||
return [startOfWeek, endOfWeek];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "上周",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfLastWeek = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() - today.getDay() - 7 + (today.getDay() === 0 ? -6 : 1)
|
||||
);
|
||||
startOfLastWeek.setHours(0, 0, 0, 0);
|
||||
const endOfLastWeek = new Date(
|
||||
startOfLastWeek.getTime() +
|
||||
6 * 24 * 60 * 60 * 1000 +
|
||||
23 * 60 * 60 * 1000 +
|
||||
59 * 60 * 1000 +
|
||||
59 * 1000 +
|
||||
999
|
||||
);
|
||||
return [startOfLastWeek, endOfLastWeek];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "本月",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
startOfMonth.setHours(0, 0, 0, 0);
|
||||
const endOfMonth = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth() + 1,
|
||||
0
|
||||
);
|
||||
endOfMonth.setHours(23, 59, 59, 999);
|
||||
return [startOfMonth, endOfMonth];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "上个月",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfLastMonth = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth() - 1,
|
||||
1
|
||||
);
|
||||
startOfLastMonth.setHours(0, 0, 0, 0);
|
||||
const endOfLastMonth = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
0
|
||||
);
|
||||
endOfLastMonth.setHours(23, 59, 59, 999);
|
||||
return [startOfLastMonth, endOfLastMonth];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "本年",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfYear = new Date(today.getFullYear(), 0, 1);
|
||||
startOfYear.setHours(0, 0, 0, 0);
|
||||
const endOfYear = new Date(today.getFullYear(), 11, 31);
|
||||
endOfYear.setHours(23, 59, 59, 999);
|
||||
return [startOfYear, endOfYear];
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user