feat: 登录日志(demo)
This commit is contained in:
parent
3987c9e876
commit
45ed89c25f
60
apps/web-antd/src/api/monitor/logininfo/index.ts
Normal file
60
apps/web-antd/src/api/monitor/logininfo/index.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { LoginLog } from './model';
|
||||||
|
|
||||||
|
import type { IDS, PageQuery, PageResult } from '#/api/common';
|
||||||
|
|
||||||
|
import { commonExport } from '#/api/helper';
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
enum Api {
|
||||||
|
loginInfoClean = '/monitor/logininfor/clean',
|
||||||
|
loginInfoExport = '/monitor/logininfor/export',
|
||||||
|
loginInfoList = '/monitor/logininfor/list',
|
||||||
|
root = '/monitor/logininfor',
|
||||||
|
userUnlock = '/monitor/logininfor/unlock',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录日志列表
|
||||||
|
* @param params 查询参数
|
||||||
|
* @returns list[]
|
||||||
|
*/
|
||||||
|
export function loginInfoList(params?: PageQuery) {
|
||||||
|
return requestClient.get<PageResult<LoginLog>>(Api.loginInfoList, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出登录日志
|
||||||
|
* @param data 表单参数
|
||||||
|
* @returns excel
|
||||||
|
*/
|
||||||
|
export function loginInfoExport(data: any) {
|
||||||
|
return commonExport(Api.loginInfoExport, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除登录日志
|
||||||
|
* @param infoIds 登录日志id数组
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function loginInfoRemove(infoIds: IDS) {
|
||||||
|
return requestClient.deleteWithMsg<void>(`${Api.root}/${infoIds.join(',')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号解锁
|
||||||
|
* @param username 用户名(账号)
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function userUnlock(username: string) {
|
||||||
|
return requestClient.get<void>(`${Api.userUnlock}/${username}`, {
|
||||||
|
successMessageMode: 'message',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空全部登录日志
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function loginInfoClean() {
|
||||||
|
return requestClient.deleteWithMsg<void>(Api.loginInfoClean);
|
||||||
|
}
|
12
apps/web-antd/src/api/monitor/logininfo/model.d.ts
vendored
Normal file
12
apps/web-antd/src/api/monitor/logininfo/model.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface LoginLog {
|
||||||
|
infoId: string;
|
||||||
|
tenantId: string;
|
||||||
|
userName: string;
|
||||||
|
status: string;
|
||||||
|
ipaddr: string;
|
||||||
|
loginLocation: string;
|
||||||
|
browser: string;
|
||||||
|
os: string;
|
||||||
|
msg: string;
|
||||||
|
loginTime: string;
|
||||||
|
}
|
@ -36,7 +36,7 @@ export interface DescriptionProps extends DescriptionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DescInstance {
|
export interface DescInstance {
|
||||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
setDescProps(descProps: Partial<DescriptionProps>, delay?: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Register = (descInstance: DescInstance) => void;
|
export type Register = (descInstance: DescInstance) => void;
|
||||||
|
@ -27,8 +27,16 @@ export function useDescription(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const methods: DescInstance = {
|
const methods: DescInstance = {
|
||||||
setDescProps: (descProps: Partial<DescriptionProps>): void => {
|
setDescProps: (
|
||||||
|
descProps: Partial<DescriptionProps>,
|
||||||
|
delay = false,
|
||||||
|
): void => {
|
||||||
|
if (!delay) {
|
||||||
unref(desc)?.setDescProps(descProps);
|
unref(desc)?.setDescProps(descProps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 奇怪的问题 在modal中需要setTimeout才会生效
|
||||||
|
setTimeout(() => unref(desc)?.setDescProps(descProps));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
147
apps/web-antd/src/views/monitor/logininfor/data.tsx
Normal file
147
apps/web-antd/src/views/monitor/logininfor/data.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import type { ColumnsType } from 'ant-design-vue/es/table';
|
||||||
|
|
||||||
|
import type { DescItem } from '#/components/description';
|
||||||
|
|
||||||
|
import { DictEnum } from '@vben/constants';
|
||||||
|
|
||||||
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { renderBrowserIcon, renderDict, renderOsIcon } from '#/utils/render';
|
||||||
|
|
||||||
|
export const columns: ColumnsType = [
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
dataIndex: 'userName',
|
||||||
|
title: '用户账号',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
dataIndex: 'clientKey',
|
||||||
|
title: '登录平台',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
dataIndex: 'ipaddr',
|
||||||
|
title: 'IP地址',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
dataIndex: 'loginLocation',
|
||||||
|
title: 'IP地点',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
customRender({ value }) {
|
||||||
|
return renderBrowserIcon(value, true);
|
||||||
|
},
|
||||||
|
dataIndex: 'browser',
|
||||||
|
title: '浏览器',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
customRender({ value }) {
|
||||||
|
// Windows 10 or Windows Server 2016 太长了 分割一下 详情依旧能看到详细的
|
||||||
|
// 为什么不直接保存userAgent让前端解析 很奇怪
|
||||||
|
if (value) {
|
||||||
|
const split = value.split(' or ');
|
||||||
|
if (split.length === 2) {
|
||||||
|
value = split[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return renderOsIcon(value, true);
|
||||||
|
},
|
||||||
|
dataIndex: 'os',
|
||||||
|
title: '系统',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
customRender({ value }) {
|
||||||
|
return renderDict(value, DictEnum.SYS_COMMON_STATUS);
|
||||||
|
},
|
||||||
|
dataIndex: 'status',
|
||||||
|
title: '登录结果',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
customRender({ value }) {
|
||||||
|
return (
|
||||||
|
<Tooltip placement={'top'} title={value}>
|
||||||
|
{value}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
dataIndex: 'msg',
|
||||||
|
ellipsis: true,
|
||||||
|
title: '信息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
dataIndex: 'loginTime',
|
||||||
|
title: '日期',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
dataIndex: 'action',
|
||||||
|
title: '操作',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const modalSchema: () => DescItem[] = () => [
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
label: '登录状态',
|
||||||
|
labelMinWidth: 80,
|
||||||
|
render(value) {
|
||||||
|
return renderDict(value, DictEnum.SYS_COMMON_STATUS);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'clientKey',
|
||||||
|
label: '登录平台',
|
||||||
|
render(value) {
|
||||||
|
if (value) {
|
||||||
|
return value.toUpperCase();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'ipaddr',
|
||||||
|
label: '账号信息',
|
||||||
|
render(_, data) {
|
||||||
|
const { ipaddr, loginLocation, userName } = data;
|
||||||
|
return `账号: ${userName} / ${ipaddr} / ${loginLocation}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'loginTime',
|
||||||
|
label: '登录时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'msg',
|
||||||
|
label: '登录信息',
|
||||||
|
render(_, data: any) {
|
||||||
|
const { msg, status } = data;
|
||||||
|
return (
|
||||||
|
<span class={['font-bold', status === '0' ? '' : 'text-red-500']}>
|
||||||
|
{msg}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'os',
|
||||||
|
label: '登录设备',
|
||||||
|
render(value) {
|
||||||
|
return renderOsIcon(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'browser',
|
||||||
|
label: '浏览器',
|
||||||
|
render(value) {
|
||||||
|
return renderBrowserIcon(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
@ -1,9 +1,54 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import CommonSkeleton from '#/views/common';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { LoginLog } from '#/api/monitor/logininfo/model';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Space, Table } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { loginInfoList } from '#/api/monitor/logininfo';
|
||||||
|
|
||||||
|
import { columns } from './data';
|
||||||
|
import loginInfoModal from './login-info-modal.vue';
|
||||||
|
|
||||||
|
const [LoginInfoModal, modalApi] = useVbenModal({
|
||||||
|
connectedComponent: loginInfoModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginDataList = ref<LoginLog[]>([]);
|
||||||
|
async function reload() {
|
||||||
|
const resp = await loginInfoList({ pageNum: 1, pageSize: 20 });
|
||||||
|
loginDataList.value = resp.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(reload);
|
||||||
|
|
||||||
|
function handlePreview(record: Recordable<any>) {
|
||||||
|
modalApi.setData(record);
|
||||||
|
modalApi.open();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<Page>
|
||||||
<CommonSkeleton />
|
<Table :columns="columns" :data-source="loginDataList" size="middle">
|
||||||
</div>
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.dataIndex === 'action'">
|
||||||
|
<Space>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
@click="handlePreview(record)"
|
||||||
|
>
|
||||||
|
{{ $t('pages.common.info') }}
|
||||||
|
</a-button>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<LoginInfoModal />
|
||||||
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Description, useDescription } from '#/components/description';
|
||||||
|
|
||||||
|
import { modalSchema } from './data';
|
||||||
|
|
||||||
|
const [registerDescription, { setDescProps }] = useDescription({
|
||||||
|
column: 1,
|
||||||
|
schema: modalSchema(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [BasicModal, modalApi] = useVbenModal({
|
||||||
|
onOpenChange: (isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = modalApi.getData();
|
||||||
|
setDescProps({ data }, true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
:footer="false"
|
||||||
|
:fullscreen-button="false"
|
||||||
|
class="w-[550px]"
|
||||||
|
title="登录日志"
|
||||||
|
>
|
||||||
|
<Description @register="registerDescription" />
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
@ -20,7 +20,9 @@
|
|||||||
"Gitee",
|
"Gitee",
|
||||||
"iconify",
|
"iconify",
|
||||||
"intlify",
|
"intlify",
|
||||||
|
"ipaddr",
|
||||||
"lockb",
|
"lockb",
|
||||||
|
"logininfor",
|
||||||
"lucide",
|
"lucide",
|
||||||
"mkdist",
|
"mkdist",
|
||||||
"mockjs",
|
"mockjs",
|
||||||
|
Loading…
Reference in New Issue
Block a user