refactor: add operation log

This commit is contained in:
wxg0103 2025-06-20 15:44:22 +08:00
parent 4c41a40438
commit 21bee7daeb
7 changed files with 593 additions and 13 deletions

View File

@ -0,0 +1,46 @@
import { Result } from '@/request/Result'
import { get, exportExcelPost } from '@/request/index'
import type { pageRequest } from '@/api/type/common'
import { type Ref } from 'vue'
const prefix = '/operate_log'
/**
*
* @param
* page {
"current_page": "string",
"page_size": "string",
}
* @query
param: any
*/
const getOperateLog: (
page: pageRequest,
param: any,
loading?: Ref<boolean>
) => Promise<Result<any>> = (page, param, loading) => {
return get(`${prefix}/${page.current_page}/${page.page_size}`, param, loading)
}
const getMenuList: () => Promise<Result<any>> = () => {
return get(`${prefix}/menu_operation_option/`, undefined, undefined)
}
const exportOperateLog: (
param: any,
loading?: Ref<boolean>
) => void = (param, loading) => {
exportExcelPost(
'log.xlsx',
`${prefix}/export/`,
param,
undefined,
loading
)
}
export default {
getOperateLog,
getMenuList,
exportOperateLog
}

View File

@ -16,4 +16,5 @@ export default {
common,
chat,
components,
}

View File

@ -17,7 +17,7 @@ import chatLog from './chat-log'
import chatUser from './chat-user'
// import notFound from './404'
// import operateLog from './operate-log'
import operateLog from './operate-log'
export default {
login,
model,
@ -38,5 +38,5 @@ export default {
chatUser,
// notFound,
// operateLog
operateLog
}

View File

@ -0,0 +1,31 @@
export default {
title: '操作日志',
table: {
menu: {
label: '操作菜单'
},
operate: {
label: '操作',
detail: '操作详情'
},
user: {
label: '操作用户'
},
status: {
label: '状态',
success: '成功',
fail: '失败',
all: '全部'
},
ip_address: {
label: 'IP地址'
},
opt: {
label: 'API详情'
},
operateTime: {
label: '操作时间'
}
},
close: '关闭'
}

View File

@ -1,9 +1,10 @@
import { PermissionConst, EditionConst, RoleConst } from '@/utils/permission/data'
import { ComplexPermission } from '@/utils/permission/type'
import {PermissionConst, EditionConst, RoleConst} from '@/utils/permission/data'
import {ComplexPermission} from '@/utils/permission/type'
const systemRouter = {
path: '/system',
name: 'system',
meta: { title: 'views.system.title' },
meta: {title: 'views.system.title'},
hidden: true,
component: () => import('@/layout/layout-template/SystemMainLayout.vue'),
children: [
@ -17,7 +18,7 @@ const systemRouter = {
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission: [RoleConst.ADMIN,EditionConst.IS_EE],
permission: [RoleConst.ADMIN, EditionConst.IS_EE],
},
component: () => import('@/views/user-manage/index.vue'),
},
@ -80,7 +81,7 @@ const systemRouter = {
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission:[new ComplexPermission([RoleConst.ADMIN,RoleConst.WORKSPACE_MANAGE.getWorkspaceRole],[PermissionConst.ROLE_READ],[EditionConst.IS_EE],'OR'),],
permission: [new ComplexPermission([RoleConst.ADMIN, RoleConst.WORKSPACE_MANAGE.getWorkspaceRole], [PermissionConst.ROLE_READ], [EditionConst.IS_EE], 'OR'),],
},
component: () => import('@/views/role/index.vue'),
},
@ -94,7 +95,7 @@ const systemRouter = {
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission:[new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole,RoleConst.ADMIN],[PermissionConst.WORKSPACE_WORKSPACE_READ],[EditionConst.IS_EE],'OR'),],
permission: [new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole, RoleConst.ADMIN], [PermissionConst.WORKSPACE_WORKSPACE_READ], [EditionConst.IS_EE], 'OR'),],
},
component: () => import('@/views/workspace/index.vue'),
},
@ -156,7 +157,7 @@ const systemRouter = {
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission:[new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole,RoleConst.ADMIN],[PermissionConst.WORKSPACE_USER_GROUP_READ],[EditionConst.IS_EE,EditionConst.IS_PE],'OR'),],
permission: [new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole, RoleConst.ADMIN], [PermissionConst.WORKSPACE_USER_GROUP_READ], [EditionConst.IS_EE, EditionConst.IS_PE], 'OR'),],
},
children: [
{
@ -167,7 +168,7 @@ const systemRouter = {
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission:[new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole,RoleConst.ADMIN],[PermissionConst.WORKSPACE_CHAT_USER_READ],[EditionConst.IS_EE,EditionConst.IS_PE],'OR'),],
permission: [new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole, RoleConst.ADMIN], [PermissionConst.WORKSPACE_CHAT_USER_READ], [EditionConst.IS_EE, EditionConst.IS_PE], 'OR'),],
},
component: () => import('@/views/system-chat-user/user-manage/index.vue'),
},
@ -179,7 +180,7 @@ const systemRouter = {
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission:[new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole,RoleConst.ADMIN],[PermissionConst.WORKSPACE_USER_GROUP_READ],[EditionConst.IS_EE,EditionConst.IS_PE],'OR'),],
permission: [new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole, RoleConst.ADMIN], [PermissionConst.WORKSPACE_USER_GROUP_READ], [EditionConst.IS_EE, EditionConst.IS_PE], 'OR'),],
},
component: () => import('@/views/system-chat-user/group/index.vue'),
},
@ -191,8 +192,8 @@ const systemRouter = {
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission:[new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole,RoleConst.ADMIN],
[PermissionConst.CHAT_USER_AUTH_READ],[EditionConst.IS_EE,EditionConst.IS_PE],'OR'),],
permission: [new ComplexPermission([RoleConst.WORKSPACE_MANAGE.getWorkspaceRole, RoleConst.ADMIN],
[PermissionConst.CHAT_USER_AUTH_READ], [EditionConst.IS_EE, EditionConst.IS_PE], 'OR'),],
},
component: () => import('@/views/system-chat-user/authentication/index.vue'),
},
@ -248,6 +249,20 @@ const systemRouter = {
},
],
},
{
path: '/operate',
name: 'operate',
meta: {
icon: 'app-document',
iconActive: 'app-document-active',
title: 'views.operateLog.title',
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
permission: [EditionConst.IS_PE, EditionConst.IS_EE],
},
component: () => import('@/views/operate-log/index.vue')
}
],
}

View File

@ -0,0 +1,36 @@
<template>
<el-dialog
:title="$t('views.operateLog.table.opt.label')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="border border-r-4 mb-16" style="white-space: pre-wrap; height: 400px">
<el-scrollbar>
<div class="p-16">
{{ details }}
</div>
</el-scrollbar>
</div>
<template #footer>
<span class="dialog-footer mt-16">
<el-button @click.prevent="dialogVisible = false">
{{ $t('views.operateLog.close') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref<boolean>(false)
const details = ref<string>()
const open = (data: any) => {
details.value = JSON.stringify(data.details, null, 4)
dialogVisible.value = true
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,451 @@
<template>
<div class="operate-log p-16-24">
<h2 class="mb-16">{{ $t('views.operateLog.title') }}</h2>
<el-card style="--el-card-padding: 0">
<div class="p-24">
<div class="flex-between">
<div>
<el-select
v-model="history_day"
class="mr-12"
@change="changeDayHandle"
style="width: 180px"
>
<el-option
v-for="item in dayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-date-picker
v-if="history_day === 'other'"
v-model="daterangeValue"
type="daterange"
:start-placeholder="$t('views.applicationOverview.monitor.startDatePlaceholder')"
:end-placeholder="$t('views.applicationOverview.monitor.endDatePlaceholder')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="changeDayRangeHandle"
/>
</div>
<div style="display: flex">
<div class="flex-between complex-search">
<el-select
v-model="filter_type"
class="complex-search__left"
@change="changeFilterHandle"
style="width: 120px"
>
<el-option
v-for="item in filterOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select
v-if="filter_type === 'status'"
v-model="filter_status"
@change="changeStatusHandle"
style="width: 220px"
clearable
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-else
v-model="searchValue"
@change="getList"
:placeholder="$t('common.search')"
prefix-icon="Search"
style="width: 220px"
clearable
/>
</div>
<el-button @click="exportLog" style="margin-left: 10px">{{
$t('common.export')
}}
</el-button>
</div>
</div>
<app-table
class="mt-16"
:data="tableData"
:pagination-config="paginationConfig"
@sizeChange="handleSizeChange"
@changePage="getList"
v-loading="loading"
>
<el-table-column prop="menu" :label="$t('views.operateLog.table.menu.label')" width="160">
<template #header>
<div>
<span>{{ $t('views.operateLog.table.menu.label') }}</span>
<el-popover :width="200" trigger="click" :visible="popoverVisible">
<template #reference>
<el-button
style="margin-top: -2px"
:type="operateTypeArr && operateTypeArr.length > 0 ? 'primary' : ''"
link
@click="popoverVisible = !popoverVisible"
>
<el-icon>
<Filter/>
</el-icon>
</el-button>
</template>
<div class="filter">
<div class="form-item mb-16">
<div @click.stop>
<el-scrollbar height="300" style="margin: 0 0 0 10px">
<el-checkbox-group
v-model="operateTypeArr"
style="display: flex; flex-direction: column"
>
<el-checkbox
v-for="item in operateOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-checkbox-group>
</el-scrollbar>
</div>
</div>
</div>
<div class="text-right">
<el-button size="small" @click="filterChange('clear')">{{
$t('common.clear')
}}
</el-button>
<el-button type="primary" @click="filterChange" size="small">{{
$t('common.confirm')
}}
</el-button>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column prop="operate" :label="$t('views.operateLog.table.operate.detail')">
<template #default="{ row }">
<el-tooltip
:content="
row.operate + (row.operation_object?.name ? `${row.operation_object.name}` : '')
"
effect="dark"
placement="top"
>
<span class="text-ellipsis">
{{
row.operate +
(row.operation_object?.name ? `${row.operation_object.name}` : '')
}}
</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
width="120"
prop="user.username"
:label="$t('views.operateLog.table.user.label')"
/>
<el-table-column v-if="user.isEE()"
width="200"
prop="workspace_name"
:label="$t('views.workspace.title') ">
<template #header>
<div>
<span>{{ $t('views.workspace.title') }}</span>
<el-popover :width="200" trigger="click" :visible="workspaceVisible">
<template #reference>
<el-button
style="margin-top: -2px"
:type="workspaceArr && workspaceArr.length > 0 ? 'primary' : ''"
link
@click="workspaceVisible = !workspaceVisible"
>
<el-icon>
<Filter/>
</el-icon>
</el-button>
</template>
<div class="filter">
<div class="form-item mb-16">
<div @click.stop>
<el-scrollbar height="300" style="margin: 0 0 0 10px">
<el-checkbox-group
v-model="workspaceArr"
style="display: flex; flex-direction: column"
>
<el-checkbox
v-for="item in workspaceOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-checkbox-group>
</el-scrollbar>
</div>
</div>
</div>
<div class="text-right">
<el-button size="small" @click="filterWorkspaceChange('clear')">{{
$t('common.clear')
}}
</el-button>
<el-button type="primary" @click="filterWorkspaceChange" size="small">{{
$t('common.confirm')
}}
</el-button>
</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column
prop="status"
:label="$t('views.operateLog.table.status.label')"
width="100"
>
<template #default="{ row }">
<span v-if="row.status === 200">{{ $t('views.operateLog.table.status.success') }}</span>
<span v-else style="color: red">{{ $t('views.operateLog.table.status.fail') }}</span>
</template>
</el-table-column>
<el-table-column
prop="ip_address"
:label="$t('views.operateLog.table.ip_address.label')"
width="160"
></el-table-column>
<el-table-column :label="$t('views.operateLog.table.operateTime.label')" width="180">
<template #default="{ row }">
{{ datetimeFormat(row.create_time) }}
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" width="110" align="left" fixed="right">
<template #default="{ row }">
<span class="mr-4">
<el-button type="primary" text @click.stop="showDetails(row)" class="text-button">
{{ $t('views.operateLog.table.opt.label') }}
</el-button>
</span>
</template>
</el-table-column>
</app-table>
</div>
<DetailDialog ref="DetailDialogRef"/>
</el-card>
</div>
</template>
<script setup lang="ts">
import {ref, onMounted, reactive} from 'vue'
import operateLog from '@/api/system/operate-log'
import DetailDialog from './component/DetailDialog.vue'
import {t} from '@/locales'
import {beforeDay, datetimeFormat, nowDate} from '@/utils/time'
import useStore from "@/stores";
import WorkspaceApi from "@/api/workspace/workspace.ts";
const {user} = useStore()
const popoverVisible = ref(false)
const operateTypeArr = ref<any[]>([])
const workspaceVisible = ref(false)
const workspaceArr = ref<any[]>([])
const DetailDialogRef = ref()
const loading = ref(false)
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
const searchValue = ref('')
const tableData = ref<any[]>([])
const history_day = ref<number | string>(7)
const filter_type = ref<string>('user')
const filter_status = ref<string>('')
const daterange = ref({
start_time: '',
end_time: ''
})
const daterangeValue = ref('')
const dayOptions = [
{
value: 7,
// @ts-ignore
label: t('views.applicationOverview.monitor.pastDayOptions.past7Days') // 使 t
},
{
value: 30,
label: t('views.applicationOverview.monitor.pastDayOptions.past30Days')
},
{
value: 90,
label: t('views.applicationOverview.monitor.pastDayOptions.past90Days')
},
{
value: 183,
label: t('views.applicationOverview.monitor.pastDayOptions.past183Days')
},
{
value: 'other',
label: t('views.applicationOverview.monitor.pastDayOptions.other')
}
]
const filterOptions = [
{
value: 'user',
label: t('views.operateLog.table.user.label')
},
{
value: 'status',
label: t('views.operateLog.table.status.label')
},
{
value: 'ip_address',
label: t('views.operateLog.table.ip_address.label')
}
]
const statusOptions = [
{
value: '200',
label: t('views.operateLog.table.status.success')
},
{
value: '500',
label: t('views.operateLog.table.status.fail')
}
]
const operateOptions = ref<any[]>([])
const workspaceOptions = ref<any[]>([])
function filterChange(val: string) {
if (val === 'clear') {
operateTypeArr.value = []
}
getList()
popoverVisible.value = false
}
function filterWorkspaceChange(val: string) {
if (val === 'clear') {
workspaceArr.value = []
}
getList()
workspaceVisible.value = false
}
function changeStatusHandle(val: string) {
getList()
}
function changeFilterHandle(val: string) {
filter_type.value = val
if (searchValue.value) {
getList()
}
}
function changeDayHandle(val: number | string) {
if (val !== 'other') {
daterange.value.start_time = beforeDay(val)
daterange.value.end_time = ''
getList()
}
}
function changeDayRangeHandle(val: string) {
daterange.value.start_time = val[0]
daterange.value.end_time = val[1]
getList()
}
function showDetails(row: any) {
DetailDialogRef.value.open(row)
}
function handleSizeChange() {
paginationConfig.current_page = 1
getList()
}
function getRequestParams() {
let obj: any = {
start_time: daterange.value.start_time,
end_time: daterange.value.end_time
}
if (searchValue.value && filter_type.value !== 'status') {
obj[filter_type.value] = searchValue.value
}
if (filter_type.value === 'status') {
obj['status'] = filter_status.value
}
if (operateTypeArr.value.length > 0) {
obj['menu'] = JSON.stringify(operateTypeArr.value)
}
if (workspaceArr.value.length > 0) {
obj['workspace_ids'] = JSON.stringify(workspaceArr.value)
}
return obj
}
function getList() {
return operateLog.getOperateLog(paginationConfig, getRequestParams(), loading).then((res) => {
tableData.value = res.data.records
paginationConfig.total = res.data.total
})
}
function getMenuList() {
return operateLog.getMenuList().then((res) => {
let arr: any[] = res.data
arr
.filter((item, index, self) => index === self.findIndex((i) => i['menu'] === item['menu']))
.forEach((ele) => {
operateOptions.value.push({label: ele.menu_label, value: ele.menu})
})
})
}
const exportLog = () => {
operateLog.exportOperateLog(getRequestParams(), loading)
}
async function getWorkspaceList() {
if (user.isEE()) {
const res = await WorkspaceApi.getSystemWorkspaceList(loading)
workspaceOptions.value = res.data.map((item: any) => ({
label: item.name,
value: item.id
}))
}
}
onMounted(() => {
getMenuList()
getWorkspaceList()
changeDayHandle(history_day.value)
})
</script>
<style lang="scss" scoped>
.text-button {
font-size: 14px;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: block;
}
</style>