feat: 用户操作日志.
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
71
orion-ops-ui/src/views/user/operator-log/index.vue
Normal file
71
orion-ops-ui/src/views/user/operator-log/index.vue
Normal 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>
|
||||
24
orion-ops-ui/src/views/user/operator-log/types/const.ts
Normal file
24
orion-ops-ui/src/views/user/operator-log/types/const.ts
Normal 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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user