<script setup lang="ts">
|
import noDataImg from '@/assets/element-img/no-data.png'
|
import type { DataTableColumns, SelectOption } from 'naive-ui'
|
import dayjs from 'dayjs'
|
import { useRoute } from 'vue-router'
|
import Pagination from '@/components/common/Pagination.vue'
|
import SearchPanel from '@/components/custom/SearchPanel.vue'
|
import { exportSystemOperationLog, fetchOperationLogPageList } from '@/service/api/systemOperationLog'
|
import { downloadFile } from '@/utils'
|
|
interface OperationLogRow {
|
operatorName?: string
|
operatorAccount?: string
|
operationType?: number | string
|
operationContent?: string
|
operationIp?: string
|
operationTime?: string
|
createTime?: string
|
}
|
|
interface QueryPayload {
|
businessType?: number
|
operatorName?: string
|
operationName?: string
|
operationType?: number
|
startTime?: string
|
endTime?: string
|
pageNum: number
|
pageSize: number
|
}
|
|
type LogListRequest = (payload: QueryPayload) => Promise<any>
|
type LogExportRequest = (payload: Omit<QueryPayload, 'pageNum' | 'pageSize'>) => Promise<any>
|
|
interface FilterModel {
|
operatorName: string
|
operationType: number | null
|
operationTime: [number, number] | null
|
}
|
|
const props = withDefaults(defineProps<{
|
show: boolean
|
title?: string
|
showExport?: boolean
|
businessType?: number
|
requestList?: LogListRequest
|
requestExport?: LogExportRequest
|
exportFileName?: string
|
operationTypeOptions?: SelectOption[]
|
}>(), {
|
title: '操作日志',
|
showExport: true,
|
exportFileName: '操作日志',
|
operationTypeOptions: () => [
|
{ label: '新增', value: 1 },
|
{ label: '编辑', value: 2 },
|
{ label: '删除', value: 3 },
|
{ label: '导入', value: 4 },
|
{ label: '导出', value: 5 },
|
{ label: '冻结', value: 6 },
|
{ label: '解冻', value: 7 },
|
{ label: '启用', value: 8 },
|
{ label: '禁用', value: 9 },
|
{ label: '批量冻结', value: 10 },
|
{ label: '重置密码', value: 11 },
|
{ label: '审核', value: 12 },
|
{ label: '启用/禁用', value: 13 },
|
{ label: '发布', value: 14 },
|
{ label: '发放', value: 15 },
|
],
|
})
|
|
const emit = defineEmits<{
|
'update:show': [show: boolean]
|
}>()
|
|
const route = useRoute()
|
const isOperationTypeOpen = ref(false)
|
const loading = ref(false)
|
const listData = ref<OperationLogRow[]>([])
|
const total = ref(0)
|
const currentPage = ref(1)
|
const currentPageSize = ref(10)
|
|
const filterModel = ref<FilterModel>({
|
operatorName: '',
|
operationType: null,
|
operationTime: null,
|
})
|
|
const routeBusinessTypeMap: Array<{ path: string, type: number }> = [
|
{ path: '/testDemo/geofenceManagement', type: 11 },
|
{ path: '/testDemo/testPlanList', type: 12 },
|
{ path: '/system/accountPermission/accountManagement', type: 13 },
|
{ path: '/system/accountPermission/permissionGroup', type: 14 },
|
{ path: '/system/accountPermission/roleManagement', type: 15 },
|
{ path: '/system/appVersion/appVersionManage', type: 16 },
|
{ path: '/userContent/active/activityManagement', type: 18 },
|
{ path: '/userContent/active/coupon', type: 19 },
|
{ path: '/userContent/banner', type: 20 },
|
{ path: '/businessOperations/openCity', type: 21 },
|
{ path: '/businessOperations/geofenceManagement', type: 22 },
|
{ path: '/businessOperations/siteInfo', type: 23 },
|
{ path: '/businessOperations/cruise/parade-plan', type: 24 },
|
{ path: '/assetManagement/vehicleManagement/inventoryVehicle', type: 25 },
|
{ path: '/assetManagement/vehicleManagement/operatingVehicle', type: 26 },
|
{ path: '/assetManagement/fleetManagement', type: 27 },
|
{ path: '/assetManagement/vehicleModel', type: 28 },
|
{ path: '/transaction/pricingManagement', type: 29 },
|
{ path: '/transaction/navigationAndRoutePlanning', type: 30 },
|
{ path: '/transaction/directServiceManagement', type: 31 },
|
{ path: '/transaction/passwordFreeStrategy', type: 32 },
|
{ path: '/operations/personnel/personnelManagement', type: 33 },
|
{ path: '/operations/attendance/address', type: 34 },
|
{ path: '/operations/cruiseCheckInManagement/cruiseCheckIn', type: 34 },
|
{ path: '/operations/attendance/shift', type: 35 },
|
{ path: '/operations/taskOrderCenter', type: 37 },
|
{ path: '/operations/cruiseCheckInManagement/cruiseCheckInRule', type: 38 },
|
{ path: '/service/review/personnel', type: 39 },
|
{ path: '/service/review/policyApplication', type: 40 },
|
{ path: '/service/ticket/feedbackType', type: 41 },
|
{ path: '/service/ticket/workOrder', type: 42 },
|
{ path: '/transaction/order/orderManagement', type: 43 },
|
{ path: '/testDemo/testPointManagement/list', type: 44 },
|
{ path: '/businessOperations/pointManagement', type: 45 },
|
{ path: '/userContent/whitelist-management/invitation-code', type: 46 },
|
]
|
|
const resolvedBusinessType = computed<number | undefined>(() => {
|
if (props.businessType !== undefined && props.businessType !== null)
|
return props.businessType
|
const hit = routeBusinessTypeMap.find(item => route.path.includes(item.path))
|
return hit?.type
|
})
|
|
const isWhitelistOperationLog = computed(() => resolvedBusinessType.value === 44)
|
|
const operationTypeLabelMap = computed(() => {
|
return (props.operationTypeOptions || []).reduce<Record<string, string>>((result, item) => {
|
result[String(item.value)] = String(item.label ?? item.value)
|
return result
|
}, {})
|
})
|
|
const columns = computed<DataTableColumns<OperationLogRow>>(() => [
|
{
|
title: '序号',
|
key: 'index',
|
width: 80,
|
render: (_, index) => (currentPage.value - 1) * currentPageSize.value + index + 1,
|
},
|
{
|
title: '操作人',
|
key: 'operatorName',
|
width: 120,
|
ellipsis: true,
|
render: row => row.operatorName || '-',
|
},
|
{
|
title: '账号',
|
key: 'operatorAccount',
|
width: 160,
|
ellipsis: true,
|
render: row => row.operatorAccount || '-',
|
},
|
{
|
title: '操作类型',
|
key: 'operationType',
|
width: 120,
|
render: row => operationTypeLabelMap.value[String(row.operationType)] || row.operationType || '-',
|
},
|
{
|
title: '操作内容',
|
key: 'operationContent',
|
width: 320,
|
ellipsis: {
|
tooltip: true,
|
},
|
},
|
{
|
title: '操作IP',
|
key: 'operationIp',
|
width: 140,
|
ellipsis: true,
|
},
|
{
|
title: '操作时间',
|
key: 'operationTime',
|
width: 170,
|
ellipsis: true,
|
render: (row) => {
|
const timeText = row.operationTime || row.createTime
|
return timeText ? dayjs(timeText).format('YYYY/MM/DD HH:mm') : '-'
|
},
|
},
|
])
|
|
function buildPayload(pageNum = currentPage.value, pageSize = currentPageSize.value): QueryPayload {
|
const payload: QueryPayload = {
|
businessType: resolvedBusinessType.value,
|
pageNum,
|
pageSize,
|
}
|
if (filterModel.value.operatorName.trim()) {
|
const operatorName = filterModel.value.operatorName.trim()
|
if (isWhitelistOperationLog.value)
|
payload.operationName = operatorName
|
else
|
payload.operatorName = operatorName
|
}
|
if (filterModel.value.operationType !== null)
|
payload.operationType = filterModel.value.operationType
|
if (filterModel.value.operationTime) {
|
payload.startTime = dayjs(filterModel.value.operationTime[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss')
|
payload.endTime = dayjs(filterModel.value.operationTime[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss')
|
}
|
return payload
|
}
|
|
function normalizeRecord(row: any): OperationLogRow {
|
return {
|
operatorName: row.operatorName ?? row.operationName ?? '',
|
operatorAccount: row.operatorAccount ?? row.operationAccount ?? row.account ?? '',
|
operationType: row.operationType ?? row.operationType,
|
operationContent: row.operationContent ?? '',
|
operationIp: row.operationIp ?? '',
|
operationTime: row.operationTime ?? row.createTime ?? '',
|
createTime: row.createTime ?? '',
|
}
|
}
|
|
async function loadData(payload = buildPayload()) {
|
loading.value = true
|
try {
|
if (props.requestList) {
|
const customRes = await props.requestList(payload)
|
const records = customRes?.data?.records ?? customRes?.records ?? []
|
const totalCount = customRes?.data?.total ?? customRes?.total ?? 0
|
listData.value = records.map((item: any) => normalizeRecord(item))
|
total.value = totalCount
|
return
|
}
|
|
const res = await fetchOperationLogPageList(payload)
|
if (res.code === 200 && res.data) {
|
const records = res.data.records || []
|
listData.value = records.map((item: any) => normalizeRecord(item))
|
total.value = res.data.total || 0
|
return
|
}
|
listData.value = []
|
total.value = 0
|
}
|
finally {
|
loading.value = false
|
}
|
}
|
|
async function handleSearch() {
|
currentPage.value = 1
|
await loadData(buildPayload(1, currentPageSize.value))
|
}
|
|
async function handleReset() {
|
filterModel.value = {
|
operatorName: '',
|
operationType: null,
|
operationTime: null,
|
}
|
currentPage.value = 1
|
await loadData(buildPayload(1, currentPageSize.value))
|
}
|
|
async function handlePageChange(page: number, pageSize: number) {
|
currentPage.value = page
|
currentPageSize.value = pageSize
|
await loadData(buildPayload(page, pageSize))
|
}
|
|
function getExportPayload(payload: QueryPayload) {
|
const { pageNum, pageSize, ...rest } = payload
|
return rest
|
}
|
|
async function handleExport() {
|
const payload = getExportPayload(buildPayload())
|
const res = props.requestExport
|
? await props.requestExport(payload)
|
: await exportSystemOperationLog(payload)
|
if (res instanceof Blob) {
|
downloadFile(res, `${props.exportFileName}_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`)
|
window.$message?.success('导出成功')
|
return
|
}
|
window.$message?.error('导出失败')
|
}
|
|
function handleUpdateShow(show: boolean) {
|
emit('update:show', show)
|
}
|
|
function handleOperationTypeShow(show: boolean) {
|
isOperationTypeOpen.value = show
|
}
|
|
function handleClose() {
|
emit('update:show', false)
|
}
|
|
watch(() => props.show, (show) => {
|
if (show) {
|
handleReset()
|
}
|
})
|
</script>
|
|
<template>
|
<n-modal :mask-closable="false" :show="show" :auto-focus="false" @update:show="handleUpdateShow">
|
<n-card
|
class="operation-log-modal-card"
|
:bordered="false"
|
:segmented="{ content: true }"
|
content-style="padding: 16px 24px; flex: 1; display: flex; flex-direction: column; overflow: hidden;"
|
>
|
<template #header>
|
<span class="modal-title">{{ title }}</span>
|
</template>
|
<template #header-extra>
|
<div class="modal-close-trigger" @click="handleClose">
|
<span class="modal-close-icon modal-close-text">×</span>
|
</div>
|
</template>
|
|
<div class="flex flex-col h-full overflow-hidden">
|
<div class="mb-12px" style="flex-shrink: 0;">
|
<NForm :model="filterModel" label-placement="left" inline :show-feedback="false">
|
<SearchPanel>
|
<NGi>
|
<NFormItem label="操作人" path="operatorName">
|
<NInput v-model:value="filterModel.operatorName" placeholder="请输入" />
|
</NFormItem>
|
</NGi>
|
<NGi>
|
<NFormItem label="操作类型" path="operationType">
|
<NSelect
|
v-model:value="filterModel.operationType"
|
:options="operationTypeOptions"
|
clearable
|
placeholder="请选择"
|
@update:show="handleOperationTypeShow"
|
>
|
<template #arrow>
|
<img
|
src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
|
:class="`arrow-icon ${isOperationTypeOpen ? 'arrow-icon--open' : ''}`"
|
style="width: 16px; height: 16px;"
|
alt="arrow"
|
>
|
</template>
|
</NSelect>
|
</NFormItem>
|
</NGi>
|
<NGi>
|
<NFormItem label="操作时间" path="operationTime">
|
<NDatePicker
|
v-model:value="filterModel.operationTime"
|
type="daterange"
|
clearable
|
format="yyyy/MM/dd"
|
start-placeholder="开始时间"
|
end-placeholder="结束时间"
|
separator="-"
|
close-on-select
|
:actions="[]"
|
/>
|
</NFormItem>
|
</NGi>
|
<template #action>
|
<NButton class="w-84px" @click="handleReset">
|
重置
|
</NButton>
|
<NButton class="w-84px" type="primary" @click="handleSearch">
|
查询
|
</NButton>
|
</template>
|
</SearchPanel>
|
</NForm>
|
</div>
|
|
<div class="flex-1 overflow-hidden" style="min-height: 0;">
|
<n-flex justify="flex-end" align="center" class="mb-12px">
|
<NButton v-if="showExport" @click="handleExport">
|
导出
|
</NButton>
|
</n-flex>
|
<NDataTable
|
style="height: calc(100% - 44px);"
|
flex-height
|
:loading="loading"
|
:bordered="false"
|
:data="listData"
|
:columns="columns"
|
:scroll-x="1080"
|
:header-height="18"
|
:row-height="18"
|
>
|
<template #empty>
|
<div class="flex-center">
|
<img
|
:src="noDataImg"
|
style="width:123px;height:125px"
|
@error="(e: any) => { e.target.src = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/9bed493b-a6aa-4ef6-ac10-7f67f5c6adc5.png' }"
|
>
|
</div>
|
</template>
|
</NDataTable>
|
</div>
|
|
<Pagination
|
:count="total"
|
:page="currentPage"
|
:page-size="currentPageSize"
|
class="robotaxi-pagination mt-12px"
|
style="flex-shrink: 0;"
|
@change="handlePageChange"
|
/>
|
</div>
|
</n-card>
|
</n-modal>
|
</template>
|
|
<style scoped lang="scss">
|
.operation-log-modal-card {
|
width: 1280px;
|
height: 80vh;
|
max-height: 800px;
|
border-radius: 8px;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.modal-title {
|
font-size: 16px;
|
font-weight: 600;
|
}
|
|
.modal-close-icon {
|
font-size: 18px;
|
width: 10px;
|
height: 10px;
|
line-height: 10px;
|
color: rgba(23, 31, 38, 0.75);
|
}
|
|
.arrow-icon {
|
transition: transform 0.3s ease;
|
|
&--open {
|
transform: rotate(180deg);
|
}
|
}
|
</style>
|