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 {
|
||||
setDescProps(descProps: Partial<DescriptionProps>): void;
|
||||
setDescProps(descProps: Partial<DescriptionProps>, delay?: boolean): void;
|
||||
}
|
||||
|
||||
export type Register = (descInstance: DescInstance) => void;
|
||||
|
@ -27,8 +27,16 @@ export function useDescription(
|
||||
}
|
||||
|
||||
const methods: DescInstance = {
|
||||
setDescProps: (descProps: Partial<DescriptionProps>): void => {
|
||||
setDescProps: (
|
||||
descProps: Partial<DescriptionProps>,
|
||||
delay = false,
|
||||
): void => {
|
||||
if (!delay) {
|
||||
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">
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page>
|
||||
<Table :columns="columns" :data-source="loginDataList" size="middle">
|
||||
<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>
|
||||
|
@ -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",
|
||||
"iconify",
|
||||
"intlify",
|
||||
"ipaddr",
|
||||
"lockb",
|
||||
"logininfor",
|
||||
"lucide",
|
||||
"mkdist",
|
||||
"mockjs",
|
||||
|
Loading…
Reference in New Issue
Block a user