feat: 用户操作日志.

This commit is contained in:
lijiahang
2023-11-01 18:57:53 +08:00
parent cfcb5cb7a8
commit eafe69ebca
45 changed files with 1255 additions and 157 deletions

View File

@@ -100,7 +100,7 @@
<!-- 查看 -->
<a-button type="text"
size="mini"
@click="emits('openView', record)">
@click="openView(record)">
查看
</a-button>
<!-- 修改 -->
@@ -145,6 +145,7 @@
import { dictValueTypeKey } from '../types/const';
import useCopy from '@/hooks/copy';
import { useDictStore } from '@/store';
import { getDictValueList } from '@/api/system/dict-value';
const tableRenderData = ref<DictKeyQueryResponse[]>([]);
const emits = defineEmits(['openAdd', 'openUpdate', 'openView']);
@@ -191,6 +192,19 @@
addedCallback, updatedCallback
});
// 打开查看视图
const openView = async (record: DictKeyQueryResponse) => {
try {
setLoading(true);
// 查看
const { data } = await getDictValueList([record.keyName]);
emits('openView', data[record.keyName], `${record.keyName} - ${record.description}`);
} catch (e) {
} finally {
setLoading(false);
}
};
// 刷新缓存
const doRefreshCache = async () => {
try {

View File

@@ -1,73 +0,0 @@
<template>
<a-modal v-model:visible="visible"
title-align="start"
width="60%"
:body-style="{padding: '16px 8px'}"
:top="80"
:title="title"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:footer="false"
@close="handleClose">
<a-spin :loading="loading" style="width: 100%; height: calc(100vh - 240px)">
<editor v-model="value" readonly />
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'dict-key-view-modal'
};
</script>
<script lang="ts" setup>
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { getDictValueList } from '@/api/system/dict-value';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const title = ref<string>();
const value = ref<string>();
// 打开新增
const open = (e: any) => {
title.value = e.keyName;
value.value = undefined;
render(e.keyName);
setVisible(true);
};
// 渲染
const render = async (keyName: string) => {
try {
setLoading(true);
// 查看
const { data } = await getDictValueList([keyName]);
value.value = JSON.stringify(data[keyName], undefined, 4);
} catch (e) {
} finally {
setLoading(false);
}
};
defineExpose({ open });
// 关闭
const handleClose = () => {
setLoading(false);
setVisible(false);
};
</script>
<style lang="less" scoped>
:deep(.arco-modal-title) {
font-size: 14px;
}
</style>

View File

@@ -4,13 +4,13 @@
<dict-key-table ref="table"
@openAdd="() => modal.openAdd()"
@openUpdate="(e) => modal.openUpdate(e)"
@openView="(e) => view.open(e)" />
@openView="(v, t) => view.open(v, t)" />
<!-- 添加修改模态框 -->
<dict-key-form-modal ref="modal"
@added="modalAddCallback"
@updated="modalUpdateCallback" />
<!-- json 查看器模态框 -->
<dict-key-view-modal ref="view" />
<json-view-modal ref="view" />
</div>
</template>
@@ -24,7 +24,7 @@
import { ref, onBeforeMount } from 'vue';
import DictKeyTable from './components/dict-key-table.vue';
import DictKeyFormModal from './components/dict-key-form-modal.vue';
import DictKeyViewModal from './components/dict-key-view-modal.vue';
import JsonViewModal from '@/components/view/json/json-view-modal.vue';
import { useDictStore } from '@/store';
import { dictKeys } from './types/const';

View File

@@ -7,16 +7,17 @@
<!-- 图标 -->
<template #dot>
<div class="icon-container">
<icon-desktop />
<icon-mobile v-if="isMobile(item.userAgent)" />
<icon-desktop v-else />
</div>
</template>
<!-- 日志行 -->
<div class="log-line">
<!-- 地址行 -->
<span class="address-line">
<a-space class="address-line">
<span class="mr8">{{ item.address }}</span>
<span>{{ item.location }}</span>
</span>
</a-space>
<!-- 错误信息行 -->
<span class="error-line" v-if="item.result === ResultStatus.FAILED">
登录失败: {{ item.errorMessage }}
@@ -45,14 +46,13 @@
import type { LoginHistoryQueryResponse } from '@/api/user/operator-log';
import useLoading from '@/hooks/loading';
import { ref, onMounted } from 'vue';
import { useUserStore } from '@/store';
import { ResultStatus } from '../types/const';
import { getCurrentLoginHistory } from '@/api/user/mine';
import { dateFormat } from '@/utils';
import { isMobile } from '@/utils/is';
const list = ref<LoginHistoryQueryResponse[]>([]);
const userStore = useUserStore();
const { loading, setLoading } = useLoading();
// 查询操作日志
@@ -78,7 +78,7 @@
.extra-message {
margin-bottom: 38px;
margin-left: -20px;
margin-left: -24px;
display: block;
color: var(--color-text-3);
user-select: none;
@@ -112,8 +112,9 @@
.address-line {
color: var(--color-text-1);
font-size: 16px;
font-size: 15px;
font-weight: 600;
margin-bottom: 2px;
}
.time-line, .ua-line, .error-line {

View File

@@ -0,0 +1,234 @@
<template>
<!-- 搜索 -->
<a-card class="general-card table-search-card">
<a-query-header :model="formModel"
label-align="left"
@submit="fetchTableData"
@reset="fetchTableData"
@keyup.enter="() => fetchTableData()">
<!-- 角色名称 -->
<a-form-item field="name" label="角色名称" label-col-flex="50px">
<a-input v-model="formModel.name" placeholder="请输入角色名称" allow-clear />
</a-form-item>
<!-- 角色编码 -->
<a-form-item field="code" label="角色编码" label-col-flex="50px">
<a-input v-model="formModel.code" placeholder="请输入角色编码" allow-clear />
</a-form-item>
<!-- 角色状态 -->
<a-form-item field="status" label="角色状态" label-col-flex="50px">
<a-select v-model="formModel.status"
placeholder="请选择角色状态"
:options="toOptions(roleStatusKey)"
allow-clear />
</a-form-item>
</a-query-header>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧操作 -->
<div class="table-left-bar-handle">
<!-- 标题 -->
<div class="table-title">
角色列表
</div>
</div>
<!-- 右侧操作 -->
<div class="table-right-bar-handle">
<a-space>
<!-- 新增 -->
<a-button type="primary"
v-permission="['infra:system-role:create']"
@click="emits('openAdd')">
新增
<template #icon>
<icon-plus />
</template>
</a-button>
</a-space>
</div>
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
label-align="left"
:loading="loading"
:columns="columns"
:data="tableRenderData"
:pagination="pagination"
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size) => fetchTableData(1, size)"
:bordered="false">
<!-- 编码 -->
<template #code="{ record }">
<a-tag>{{ record.code }}</a-tag>
</template>
<!-- 状态 -->
<template #status="{ record }">
<span class="circle" :style="{
background: getDictValue(roleStatusKey, record.status, 'color')
}" />
{{ getDictValue(roleStatusKey, record.status) }}
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 修改状态 -->
<a-popconfirm :content="`确定要${toggleDictValue(roleStatusKey, record.status, 'label')}当前角色吗?`"
position="left"
type="warning"
@ok="toggleRoleStatus(record)">
<a-button v-permission="['infra:system-role:delete']"
:disabled="record.code === 'admin'"
:status="toggleDictValue(roleStatusKey, record.status, 'status')"
type="text"
size="mini">
{{ toggleDictValue(roleStatusKey, record.status, 'label') }}
</a-button>
</a-popconfirm>
<!-- 分配菜单 -->
<a-button v-permission="['infra:system-role:grant-menu']"
:disabled="record.code === 'admin'"
type="text"
size="mini"
@click="emits('openGrant', record)">
分配菜单
</a-button>
<!-- 修改 -->
<a-button v-permission="['infra:system-role:update']"
type="text"
size="mini"
@click="emits('openUpdate', record)">
修改
</a-button>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
position="left"
type="warning"
@ok="deleteRow(record)">
<a-button v-permission="['infra:system-role:delete']"
:disabled="record.code === 'admin'"
type="text"
size="mini"
status="danger">
删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</a-card>
</template>
<script lang="ts">
export default {
name: 'operator-log-list'
};
</script>
<script lang="ts" setup>
import type { RoleQueryRequest, RoleQueryResponse } from '@/api/user/role';
import { reactive, ref, onMounted } from 'vue';
import { deleteRole, getRolePage, updateRoleStatus } from '@/api/user/role';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../../role/types/table.columns';
import { roleStatusKey } from '../../role/types/const';
import { usePagination } from '@/types/table';
import { useDictStore } from '@/store';
const emits = defineEmits(['openAdd', 'openUpdate', 'openGrant']);
const tableRenderData = ref<RoleQueryResponse[]>([]);
const pagination = usePagination();
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue, toggleDictValue, toggleDict } = useDictStore();
const formModel = reactive<RoleQueryRequest>({
id: undefined,
name: undefined,
code: undefined,
status: undefined,
});
// 修改状态
const toggleRoleStatus = async (record: any) => {
try {
setLoading(true);
const toggleStatus = toggleDict(roleStatusKey, record.status);
// 调用修改接口
await updateRoleStatus({
id: record.id,
status: toggleStatus.value as number
});
Message.success(`${toggleStatus.label}成功`);
// 修改行状态
record.status = toggleStatus.value;
} catch (e) {
} finally {
setLoading(false);
}
};
// 删除当前行
const deleteRow = async ({ id }: {
id: number
}) => {
try {
setLoading(true);
// 调用删除接口
await deleteRole(id);
Message.success('删除成功');
// 重新加载数据
fetchTableData();
} catch (e) {
} finally {
setLoading(false);
}
};
// 添加后回调
const addedCallback = () => {
fetchTableData();
};
// 更新后回调
const updatedCallback = () => {
fetchTableData();
};
defineExpose({
addedCallback, updatedCallback
});
// 加载数据
const doFetchTableData = async (request: RoleQueryRequest) => {
try {
setLoading(true);
const { data } = await getRolePage(request);
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
onMounted(() => {
fetchTableData();
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,150 @@
<template>
<a-spin :loading="loading" class="main-container">
<span class="extra-message">所有登录设备的会话列表</span>
<a-timeline>
<template v-for="item in list"
:key="item.loginTime">
<a-timeline-item v-if="item.visible">
<!-- 图标 -->
<template #dot>
<div class="icon-container">
<icon-mobile v-if="isMobile(item.userAgent)" />
<icon-desktop v-else />
</div>
</template>
<!-- 会话行 -->
<div class="session-line">
<!-- 地址行 -->
<a-space class="address-line">
<span>{{ item.address }}</span>
<span>{{ item.location }}</span>
<a-tag v-if="item.current" color="arcoblue">当前会话</a-tag>
<a-button v-else
style="font-weight: 600;"
type="text"
size="mini"
status="danger"
@click="offline(item)">
下线
</a-button>
</a-space>
<!-- 时间行 -->
<span class="time-line">
{{ dateFormat(new Date(item.loginTime)) }}
</span>
<!-- ua -->
<span class="ua-line">
{{ item.userAgent }}
</span>
</div>
</a-timeline-item>
</template>
</a-timeline>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'user-session'
};
</script>
<script lang="ts" setup>
import type { UserSessionQueryResponse } from '@/api/user/user';
import useLoading from '@/hooks/loading';
import { ref, onMounted } from 'vue';
import { getCurrentUserSessionList, offlineCurrentUserSession } from '@/api/user/mine';
import { dateFormat } from '@/utils';
import { isMobile } from '@/utils/is';
import { Message } from '@arco-design/web-vue';
const list = ref<UserSessionQueryResponse[]>([]);
const { loading, setLoading } = useLoading();
// 下线
const offline = async (item: UserSessionQueryResponse) => {
try {
setLoading(true);
await offlineCurrentUserSession({
timestamp: item.loginTime
});
Message.success('操作成功');
item.visible = false;
} catch (e) {
} finally {
setLoading(false);
}
};
// 查询登录会话
onMounted(async () => {
try {
setLoading(true);
const { data } = await getCurrentUserSessionList();
data.forEach(s => s.visible = true);
list.value = data;
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
.main-container {
width: 100%;
min-height: 200px;
padding-left: 48px;
}
.extra-message {
margin-bottom: 38px;
margin-left: -24px;
display: block;
color: var(--color-text-3);
user-select: none;
}
.icon-container {
border-radius: 50%;
width: 56px;
height: 56px;
background: var(--color-fill-4);
font-size: 28px;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.arco-timeline-item-content-wrapper) {
position: relative;
margin-left: 44px;
margin-top: -22px;
}
:deep(.arco-timeline-item) {
padding-bottom: 36px;
}
.session-line {
display: flex;
flex-direction: column;
.address-line {
color: var(--color-text-1);
font-size: 15px;
font-weight: 600;
margin-bottom: 2px;
}
.time-line, .ua-line {
color: var(--color-text-3);
font-size: 14px;
margin-top: 2px;
}
}
</style>

View File

@@ -15,10 +15,11 @@
</a-tab-pane>
<!-- 登录设备 -->
<a-tab-pane key="3" title="登录设备">
<login-history />
<user-session />
</a-tab-pane>
<!-- 操作日志 -->
<a-tab-pane key="4" title="操作日志">
<operator-log-list />
</a-tab-pane>
</a-tabs>
</div>
@@ -33,6 +34,8 @@
<script lang="ts" setup>
import UserInfo from './components/user-info.vue';
import LoginHistory from './components/login-history.vue';
import UserSession from './components/user-session.vue';
import OperatorLogList from './components/operator-log-list.vue';
</script>
<style lang="less" scoped>

View File

@@ -0,0 +1,134 @@
<template>
<a-query-header :model="formModel"
label-align="left"
@submit="submit"
@reset="reset"
@keyup.enter="submit">
<!-- 操作用户 -->
<a-form-item v-if="visibleUser"
field="userId"
label="操作用户"
label-col-flex="50px">
<user-selector v-model="formModel.userId"
placeholder="请选择操作用户"
allow-clear />
</a-form-item>
<!-- 操作模块 -->
<a-form-item field="module" label="操作模块" label-col-flex="50px">
<a-select v-model="formModel.module"
:options="toOptions(operatorLogModuleKey)"
placeholder="请选择操作模块"
@change="selectedModule"
allow-clear />
</a-form-item>
<!-- 操作类型 -->
<a-form-item field="type" label="操作类型" label-col-flex="50px">
<a-select v-model="formModel.type"
:options="typeOptions"
placeholder="请选择操作类型"
allow-clear />
</a-form-item>
<!-- 风险等级 -->
<a-form-item field="riskLevel" label="风险等级" label-col-flex="50px">
<a-select v-model="formModel.riskLevel"
:options="toOptions(operatorRiskLevelKey)"
placeholder="请选择风险等级"
allow-clear />
</a-form-item>
<!-- 执行结果 -->
<a-form-item field="result" label="执行结果" label-col-flex="50px">
<a-select v-model="formModel.result"
:options="toOptions(operatorLogResultKey)"
placeholder="请选择执行结果"
allow-clear />
</a-form-item>
<!-- 执行时间 -->
<a-form-item field="startTime" label="执行时间" label-col-flex="50px">
<a-range-picker v-model="timeRange"
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
show-time
format="YYYY-MM-DD HH:mm:ss"
@ok="timeRangePicked" />
</a-form-item>
</a-query-header>
</template>
<script lang="ts">
export default {
name: 'operator-log-query-header'
};
</script>
<script lang="ts" setup>
import type { OperatorLogQueryRequest } from '@/api/user/operator-log';
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import { reactive, ref } from 'vue';
import useLoading from '@/hooks/loading';
import { useDictStore } from '@/store';
import UserSelector from '@/components/user/role/user-selector.vue';
import { operatorLogModuleKey, operatorLogTypeKey, operatorRiskLevelKey, operatorLogResultKey } from '../types/const';
const emits = defineEmits(['submit']);
const props = defineProps({
visibleUser: {
type: Boolean,
default: true
}
});
const { loading, setLoading } = useLoading();
const { $state: dictState, toOptions } = useDictStore();
const timeRange = ref<string[]>([]);
const typeOptions = ref<SelectOptionData[]>(toOptions(operatorLogTypeKey));
const formModel = reactive<OperatorLogQueryRequest>({
userId: undefined,
module: undefined,
type: undefined,
riskLevel: undefined,
result: undefined,
startTimeStart: undefined,
startTimeEnd: undefined,
});
// 选择时间
const timeRangePicked = (e: string[]) => {
formModel.startTimeStart = e[0];
formModel.startTimeEnd = e[1];
};
// 选择类型
const selectedModule = (module: string) => {
if (!module) {
// 不选择则重置 options
typeOptions.value = toOptions(operatorLogTypeKey);
return;
}
const moduleArr = module.split(':');
const modulePrefix = moduleArr[moduleArr.length - 1] + ':';
// 渲染 options
typeOptions.value = dictState[operatorLogTypeKey].filter(s => (s.value as string).startsWith(modulePrefix));
// 渲染输入框
if (formModel.type && !formModel.type.startsWith(modulePrefix)) {
formModel.type = undefined;
}
};
// 重置
const reset = () => {
timeRange.value = [];
formModel.startTimeStart = undefined;
formModel.startTimeEnd = undefined;
submit();
};
// 切换页码
const submit = () => {
emits('submit', { ...formModel });
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,139 @@
<template>
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
label-align="left"
:loading="loading"
:columns="tableColumns"
:data="tableRenderData"
:pagination="pagination"
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size) => fetchTableData(1, size)"
:bordered="false">
<!-- 操作模块 -->
<template #module="{ record }">
{{ getDictValue(operatorLogModuleKey, record.module) }}
</template>
<!-- 操作类型 -->
<template #type="{ record }">
{{ getDictValue(operatorLogTypeKey, record.type) }}
</template>
<!-- 风险等级 -->
<template #riskLevel="{ record }">
<a-tag :color="getDictValue(operatorRiskLevelKey, record.riskLevel, 'color')">
{{ getDictValue(operatorRiskLevelKey, record.riskLevel) }}
</a-tag>
</template>
<!-- 执行结果 -->
<template #result="{ record }">
<a-tag :color="getDictValue(operatorLogResultKey, record.result, 'color')">
{{ getDictValue(operatorLogResultKey, record.result) }}
</a-tag>
</template>
<!-- 操作日志 -->
<template #originLogInfo="{ record }">
<span v-html="replaceHtmlTag(record.logInfo)" />
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 详情 -->
<a-button type="text" size="mini" @click="viewDetail(record)">
详情
</a-button>
</div>
</template>
</a-table>
</template>
<script lang="ts">
export default {
name: 'operator-log-table'
};
</script>
<script lang="ts" setup>
import type { OperatorLogQueryRequest, OperatorLogQueryResponse } from '@/api/user/operator-log';
import { ref, onMounted } from 'vue';
import { operatorLogModuleKey, operatorLogTypeKey, operatorRiskLevelKey, operatorLogResultKey } from '../types/const';
import columns from '../types/table.columns';
import useLoading from '@/hooks/loading';
import { usePagination } from '@/types/table';
import { useDictStore } from '@/store';
import { getOperatorLogPage } from '@/api/user/operator-log';
import { replaceHtmlTag, clearHtmlTag, dateFormat } from '@/utils';
import { pick } from 'lodash';
const emits = defineEmits(['viewDetail']);
const props = defineProps({
visibleUser: {
type: Boolean,
default: true
}
});
const tableColumns = ref();
const tableRenderData = ref<OperatorLogQueryResponse[]>([]);
const pagination = usePagination();
const { loading, setLoading } = useLoading();
const { getDictValue } = useDictStore();
// 查看详情
const viewDetail = (record: OperatorLogQueryResponse) => {
try {
const detail = Object.assign({} as Record<string, any>,
pick(record, 'traceId', 'address', 'location',
'userAgent', 'errorMessage'));
detail.duration = `${record.duration} ms`;
detail.startTime = dateFormat(new Date(record.startTime));
detail.endTime = dateFormat(new Date(record.endTime));
detail.extra = JSON.parse(record?.extra);
detail.returnValue = JSON.parse(record?.returnValue);
emits('viewDetail', detail);
} catch (e) {
emits('viewDetail', record);
}
};
// 加载数据
const doFetchTableData = async (request: OperatorLogQueryRequest) => {
try {
setLoading(true);
const { data } = await getOperatorLogPage(request);
tableRenderData.value = data.rows.map(s => {
return { ...s, originLogInfo: clearHtmlTag(s.logInfo) };
});
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} catch (e) {
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = {}) => {
doFetchTableData({ page, limit, ...form });
};
onMounted(() => {
if (props.visibleUser) {
tableColumns.value = columns;
} else {
tableColumns.value = columns.filter(s => s.dataIndex !== 'username');
}
fetchTableData();
});
defineExpose({
fetchTableData
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,71 @@
<template>
<div class="layout-container" v-if="render">
<!-- 查询头 -->
<a-card class="general-card table-search-card">
<!-- 查询头组件 -->
<operator-log-query-header @submit="(e) => table.fetchTableData(undefined, undefined, e)" />
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧操作 -->
<div class="table-left-bar-handle">
<!-- 标题 -->
<div class="table-title">
操作日志
</div>
</div>
</template>
<!-- 表格组件 -->
<operator-log-table ref="table" @viewDetail="(e) => view.open(e)" />
</a-card>
<!-- json 查看器模态框 -->
<json-view-modal ref="view" />
</div>
</template>
<script lang="ts">
export default {
name: 'userOperatorLog'
};
</script>
<script lang="ts" setup>
import { ref, onBeforeMount, onUnmounted } from 'vue';
import { useCacheStore, useDictStore } from '@/store';
import { dictKeys } from './types/const';
import { getUserList } from '@/api/user/user';
import OperatorLogQueryHeader from './components/operator-log-query-header.vue';
import OperatorLogTable from './components/operator-log-table.vue';
import JsonViewModal from '@/components/view/json/json-view-modal.vue';
const cacheStore = useCacheStore();
const render = ref();
const table = ref();
const view = ref();
// 加载全部用户列表
const fetchUserList = async () => {
const { data } = await getUserList();
cacheStore.set('users', data);
};
onBeforeMount(async () => {
// 加载字典值
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
// 加载用户列表
await fetchUserList();
render.value = true;
});
// 卸载时清除 cache
onUnmounted(() => {
cacheStore.reset('users');
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,24 @@
// 结果状态
export const ResultStatus = {
// 失败
FAILED: 0,
// 成功
SUCCESS: 1,
};
// 操作日志模块 字典项
export const operatorLogModuleKey = 'operatorLogModule';
// 操作日志类型 字典项
export const operatorLogTypeKey = 'operatorLogType';
// 操作风险等级 字典项
export const operatorRiskLevelKey = 'operatorRiskLevel';
// 操作日志结果 字典项
export const operatorLogResultKey = 'operatorLogResult';
// 加载的字典值
export const dictKeys = [operatorLogModuleKey, operatorLogTypeKey, operatorRiskLevelKey, operatorLogResultKey];

View File

@@ -0,0 +1,69 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 70,
align: 'left',
fixed: 'left',
}, {
title: '操作用户',
dataIndex: 'username',
slotName: 'username',
width: 120,
ellipsis: true,
tooltip: true,
}, {
title: '操作模块',
dataIndex: 'module',
slotName: 'module',
width: 120,
ellipsis: true,
tooltip: true,
}, {
title: '操作类型',
dataIndex: 'type',
slotName: 'type',
width: 150,
ellipsis: true,
tooltip: true,
}, {
title: '风险等级',
dataIndex: 'riskLevel',
slotName: 'riskLevel',
width: 90,
align: 'center',
}, {
title: '执行结果',
dataIndex: 'result',
slotName: 'result',
width: 90,
align: 'center',
}, {
title: '操作日志',
dataIndex: 'originLogInfo',
slotName: 'originLogInfo',
ellipsis: true,
tooltip: true,
}, {
title: '创建时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
width: 180,
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
}, {
title: '操作',
slotName: 'handle',
width: 90,
align: 'center',
fixed: 'right',
},
] as TableColumnData[];
export default columns;