feat(system): 新增系统操作日志功能模块
- 添加系统操作日志路由及页面组件
- 实现操作日志查询、分页和导出功能
- 新增操作日志通用弹窗组件
- 添加对应的API服务接口
- 优化页面样式布局
| New file |
| | |
| | | <script setup lang="ts"> |
| | | import type { DataTableColumns, FormInst, SelectOption } from 'naive-ui' |
| | | import dayjs from 'dayjs' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | |
| | | interface OperationLogRow { |
| | | operatorName?: string |
| | | account?: string |
| | | operationType?: number | string |
| | | operationContent?: string |
| | | operationIp?: string |
| | | operationTime?: string |
| | | createTime?: string |
| | | } |
| | | |
| | | interface QueryPayload { |
| | | operatorName?: string |
| | | account?: string |
| | | operationType?: number |
| | | operationContent?: string |
| | | operationIp?: string |
| | | startTime?: string |
| | | endTime?: string |
| | | pageNum: number |
| | | pageSize: number |
| | | } |
| | | |
| | | interface FilterModel { |
| | | operatorName: string |
| | | account: string |
| | | operationType: number | null |
| | | operationContent: string |
| | | operationIp: string |
| | | operationTime: [number, number] | null |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<{ |
| | | show: boolean |
| | | loading?: boolean |
| | | data: OperationLogRow[] |
| | | total: number |
| | | pageNum: number |
| | | pageSize: number |
| | | title?: string |
| | | showExport?: boolean |
| | | operationTypeOptions?: SelectOption[] |
| | | }>(), { |
| | | loading: false, |
| | | title: '操作日志', |
| | | showExport: true, |
| | | operationTypeOptions: () => [ |
| | | { label: '新增', value: 1 }, |
| | | { label: '编辑', value: 2 }, |
| | | { label: '删除', value: 3 }, |
| | | { label: '导入', value: 4 }, |
| | | { label: '导出', value: 5 }, |
| | | { label: '批量冻结', value: 6 }, |
| | | ], |
| | | }) |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:show': [show: boolean] |
| | | 'search': [payload: QueryPayload] |
| | | 'reset': [payload: QueryPayload] |
| | | 'page-change': [payload: QueryPayload] |
| | | 'export': [payload: QueryPayload] |
| | | }>() |
| | | |
| | | const formRef = ref<FormInst | null>(null) |
| | | const isOperationTypeOpen = ref(false) |
| | | |
| | | const filterModel = ref<FilterModel>({ |
| | | operatorName: '', |
| | | account: '', |
| | | operationType: null, |
| | | operationContent: '', |
| | | operationIp: '', |
| | | operationTime: null, |
| | | }) |
| | | |
| | | const currentPage = ref(props.pageNum) |
| | | const currentPageSize = ref(props.pageSize) |
| | | |
| | | watch(() => props.pageNum, (val) => { |
| | | currentPage.value = val |
| | | }) |
| | | |
| | | watch(() => props.pageSize, (val) => { |
| | | currentPageSize.value = val |
| | | }) |
| | | |
| | | 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, |
| | | }, |
| | | { |
| | | title: '账号', |
| | | key: 'account', |
| | | width: 160, |
| | | ellipsis: true, |
| | | }, |
| | | { |
| | | title: '操作类型', |
| | | key: 'operationType', |
| | | width: 120, |
| | | render: row => operationTypeLabelMap.value[String(row.operationType)] || row.operationType || '-', |
| | | }, |
| | | { |
| | | title: '操作内容', |
| | | key: 'operationContent', |
| | | width: 320, |
| | | ellipsis: 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 = { pageNum, pageSize } |
| | | // 统一清洗查询条件,避免把空字符串交给父页面 |
| | | if (filterModel.value.operatorName.trim()) |
| | | payload.operatorName = filterModel.value.operatorName.trim() |
| | | if (filterModel.value.account.trim()) |
| | | payload.account = filterModel.value.account.trim() |
| | | if (filterModel.value.operationType !== null) |
| | | payload.operationType = filterModel.value.operationType |
| | | if (filterModel.value.operationContent.trim()) |
| | | payload.operationContent = filterModel.value.operationContent.trim() |
| | | if (filterModel.value.operationIp.trim()) |
| | | payload.operationIp = filterModel.value.operationIp.trim() |
| | | 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 handleSearch() { |
| | | currentPage.value = 1 |
| | | emit('search', buildPayload(1, currentPageSize.value)) |
| | | } |
| | | |
| | | function handleReset() { |
| | | filterModel.value = { |
| | | operatorName: '', |
| | | account: '', |
| | | operationType: null, |
| | | operationContent: '', |
| | | operationIp: '', |
| | | operationTime: null, |
| | | } |
| | | currentPage.value = 1 |
| | | emit('reset', buildPayload(1, currentPageSize.value)) |
| | | } |
| | | |
| | | function handlePageChange(page: number, pageSize: number) { |
| | | currentPage.value = page |
| | | currentPageSize.value = pageSize |
| | | emit('page-change', buildPayload(page, pageSize)) |
| | | } |
| | | |
| | | function handleExport() { |
| | | emit('export', buildPayload()) |
| | | } |
| | | |
| | | function handleUpdateShow(show: boolean) { |
| | | emit('update:show', show) |
| | | } |
| | | |
| | | function handleOperationTypeShow(show: boolean) { |
| | | isOperationTypeOpen.value = show |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <n-modal |
| | | :show="show" |
| | | preset="card" |
| | | :title="title" |
| | | class="operation-log-modal" |
| | | :style="{ width: '1200px' }" |
| | | :bordered="false" |
| | | size="huge" |
| | | closable |
| | | @update:show="handleUpdateShow" |
| | | > |
| | | <div class="roboTaxi w-full h-full"> |
| | | <div class="roboTaxi-form-card mb-12px"> |
| | | <NForm ref="formRef" :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="account"> |
| | | <NInput v-model:value="filterModel.account" 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="operationContent"> |
| | | <NInput v-model:value="filterModel.operationContent" placeholder="请输入" /> |
| | | </NFormItem> |
| | | </NGi> |
| | | <NGi> |
| | | <NFormItem label="操作IP" path="operationIp"> |
| | | <NInput v-model:value="filterModel.operationIp" placeholder="请输入" /> |
| | | </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 @click="handleReset"> |
| | | 重置 |
| | | </NButton> |
| | | <NButton type="primary" @click="handleSearch"> |
| | | 查询 |
| | | </NButton> |
| | | </template> |
| | | </SearchPanel> |
| | | </NForm> |
| | | </div> |
| | | |
| | | <NCard class="robotaxi-table-card" content-style="display: flex; flex-direction: column; min-height: 420px;"> |
| | | <template #header> |
| | | <n-flex justify="flex-end" align="center"> |
| | | <NButton v-if="showExport" @click="handleExport"> |
| | | 导出 |
| | | </NButton> |
| | | </n-flex> |
| | | </template> |
| | | <div class="flex-1 flex flex-col h-full"> |
| | | <NDataTable |
| | | class="flex-1" |
| | | :loading="loading" |
| | | :bordered="false" |
| | | :data="data" |
| | | :columns="columns" |
| | | :scroll-x="1080" |
| | | :header-height="18" |
| | | :row-height="18" |
| | | flex-height |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | 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> |
| | | <Pagination |
| | | :count="total" |
| | | :page="currentPage" |
| | | :page-size="currentPageSize" |
| | | class="robotaxi-pagination mt-12px" |
| | | @change="handlePageChange" |
| | | /> |
| | | </div> |
| | | </NCard> |
| | | </div> |
| | | </n-modal> |
| | | </template> |
| | |
| | | pid: 9, |
| | | }, |
| | | { |
| | | name: 'systemOperationLog', |
| | | path: '/systemOperationLog', |
| | | title: '系统操作日志', |
| | | requiresAuth: true, |
| | | icon: 'icon-park-outline:log', |
| | | menuType: 'page', |
| | | componentPath: '/system/operationLog/index.vue', |
| | | id: 12, |
| | | keepAlive: true, |
| | | pid: null, |
| | | fullScreen: false, |
| | | }, |
| | | { |
| | | name: 'testPlanAdd', |
| | | path: '/testDemo/testPlanList/add', |
| | | title: '新增测试计划', |
| New file |
| | |
| | | import { request } from '@/service/http' |
| | | |
| | | /** |
| | | * 获取系统操作日志分页列表 |
| | | * @param data 查询参数 |
| | | */ |
| | | export function fetchSystemOperationLogPageList(data: any) { |
| | | return request.Post<Service.ResponseResult<any>>('/operation/api/v1/systemOperationLog/pageList', data) |
| | | } |
| | | |
| | | /** |
| | | * 导出系统操作日志 |
| | | * @param data 查询参数 |
| | | */ |
| | | export function exportSystemOperationLog(data: any) { |
| | | const method = request.Post('/operation/api/v1/systemOperationLog/exportExcel', data, { responseType: 'blob' }) |
| | | method.meta = { ...method.meta, isBlob: true } |
| | | return method |
| | | } |
| | |
| | | |
| | | .roboTaxi { |
| | | height: calc(100vh - 76px - 24px); |
| | | // height: calc(100vh - 35px - 40px); |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | |
| | | padding-top: 0; |
| | | } |
| | | |
| | | .n-card-content { |
| | | padding: 12px; |
| | | padding-top: 0; |
| | | } |
| | | |
| | | .n-base-selection-input__content { |
| | | font-size: 13px; |
| | | } |
| New file |
| | |
| | | <script setup lang="tsx"> |
| | | import type { DataTableColumns, FormInst } from 'naive-ui' |
| | | import { NButton, NCard, NDataTable, NForm, NFormItem, NGi, NInput, NSelect, NTooltip } from 'naive-ui' |
| | | import dayjs from 'dayjs' |
| | | import SearchPanel from '@/components/custom/SearchPanel.vue' |
| | | import Pagination from '@/components/common/Pagination.vue' |
| | | import { useBoolean } from '@/hooks' |
| | | import { downloadFile } from '@/utils' |
| | | |
| | | interface QueryModel { |
| | | functionModule: string |
| | | operatorName: string |
| | | account: string |
| | | operationType: number | null |
| | | operationContent: string |
| | | operationIp: string |
| | | operationTime: [number, number] | null |
| | | pageNum: number |
| | | pageSize: number |
| | | } |
| | | |
| | | interface OperationLogItem { |
| | | functionModule?: string |
| | | operatorName?: string |
| | | account?: string |
| | | operationType?: number | string |
| | | operationContent?: string |
| | | operationIp?: string |
| | | operationTime?: string |
| | | createTime?: string |
| | | } |
| | | |
| | | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false) |
| | | const message = window.$message |
| | | |
| | | const initialModel: QueryModel = { |
| | | functionModule: '', |
| | | operatorName: '', |
| | | account: '', |
| | | operationType: null, |
| | | operationContent: '', |
| | | operationIp: '', |
| | | operationTime: null, |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | } |
| | | const model = ref<QueryModel>({ ...initialModel }) |
| | | const formRef = ref<FormInst | null>(null) |
| | | |
| | | const isOperationTypeOpen = ref(false) |
| | | const operationTypeOptions = [ |
| | | { label: '新增', value: 1 }, |
| | | { label: '编辑', value: 2 }, |
| | | { label: '删除', value: 3 }, |
| | | { label: '导入', value: 4 }, |
| | | { label: '导出', value: 5 }, |
| | | { label: '批量冻结', value: 6 }, |
| | | ] |
| | | |
| | | const operationTypeLabelMap: Record<number, string> = { |
| | | 1: '新增', |
| | | 2: '编辑', |
| | | 3: '删除', |
| | | 4: '导入', |
| | | 5: '导出', |
| | | 6: '批量冻结', |
| | | } |
| | | |
| | | function createMockData(total = 120): OperationLogItem[] { |
| | | const moduleList = ['白名单', '运营', '交易', '资产', '活动', '系统配置'] |
| | | const operatorList = ['杨倩芳', '曹敏倩', '罗凯艺', '丁汉辉', '刘丽丽', '方一强'] |
| | | const accountList = ['h33@china.com', 'five@eastday.com', 'bubj@cmju56.com', 's2724133@net.com.cn', 'mbajbigs@t2n.com', '1396827528'] |
| | | const contentList = [ |
| | | '新增账号“1198726442”', |
| | | '编辑账号“1198726442”', |
| | | '删除了 3 条无效数据', |
| | | '导入 126 条用户数据', |
| | | '导出了筛选后的报表', |
| | | '批量冻结 8 个账号', |
| | | ] |
| | | const ipList = ['198.235.14.9', '172.16.12.8', '10.10.22.33', '221.12.33.44', '36.110.5.7', '47.102.88.100'] |
| | | const typeList = [1, 2, 3, 4, 5, 6] |
| | | |
| | | return Array.from({ length: total }, (_, index) => { |
| | | const seed = index % 6 |
| | | return { |
| | | functionModule: moduleList[seed], |
| | | operatorName: operatorList[seed], |
| | | account: accountList[seed], |
| | | operationType: typeList[seed], |
| | | operationContent: contentList[seed], |
| | | operationIp: ipList[seed], |
| | | operationTime: dayjs().subtract(index * 13, 'minute').format('YYYY-MM-DD HH:mm:ss'), |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const mockSourceData = ref<OperationLogItem[]>(createMockData()) |
| | | |
| | | const filterItems = computed(() => [ |
| | | { |
| | | label: '功能模块', |
| | | path: 'functionModule', |
| | | render: () => <NInput v-model:value={model.value.functionModule} placeholder="请输入" />, |
| | | }, |
| | | { |
| | | label: '操作人', |
| | | path: 'operatorName', |
| | | render: () => <NInput v-model:value={model.value.operatorName} placeholder="请输入" />, |
| | | }, |
| | | { |
| | | label: '账号', |
| | | path: 'account', |
| | | render: () => <NInput v-model:value={model.value.account} placeholder="请输入" />, |
| | | }, |
| | | { |
| | | label: '操作类型', |
| | | path: 'operationType', |
| | | render: () => ( |
| | | <NSelect |
| | | v-model:value={model.value.operationType} |
| | | options={operationTypeOptions} |
| | | clearable |
| | | placeholder="请选择" |
| | | onUpdate:show={(value: boolean) => isOperationTypeOpen.value = value} |
| | | > |
| | | {{ |
| | | arrow: () => ( |
| | | <img |
| | | src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png" |
| | | class={`arrow-icon ${isOperationTypeOpen.value ? 'arrow-icon--open' : ''}`} |
| | | style={{ width: '16px', height: '16px' }} |
| | | alt="arrow" |
| | | /> |
| | | ), |
| | | empty: () => ( |
| | | <div class="flex-center"> |
| | | <img src="/src/assets/element-img/no-data.png" style={{ width: '50%' }} /> |
| | | </div> |
| | | ), |
| | | }} |
| | | </NSelect> |
| | | ), |
| | | }, |
| | | { |
| | | label: '操作内容', |
| | | path: 'operationContent', |
| | | render: () => <NInput v-model:value={model.value.operationContent} placeholder="请输入" />, |
| | | }, |
| | | { |
| | | label: '操作IP', |
| | | path: 'operationIp', |
| | | render: () => <NInput v-model:value={model.value.operationIp} placeholder="请输入" />, |
| | | }, |
| | | { |
| | | label: '操作时间', |
| | | path: 'operationTime', |
| | | render: () => ( |
| | | <n-date-picker |
| | | v-model:value={model.value.operationTime} |
| | | type="daterange" |
| | | clearable |
| | | format="yyyy/MM/dd" |
| | | start-placeholder="开始时间" |
| | | end-placeholder="结束时间" |
| | | separator="-" |
| | | close-on-select={true} |
| | | actions={[]} |
| | | /> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | | const listData = ref<OperationLogItem[]>([]) |
| | | const count = ref(0) |
| | | |
| | | const columns = computed<DataTableColumns<OperationLogItem>>(() => [ |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 60, |
| | | resizable: true, |
| | | render: (_, index) => <span>{(model.value.pageNum - 1) * model.value.pageSize + index + 1}</span>, |
| | | }, |
| | | { |
| | | title: '功能模块', |
| | | key: 'functionModule', |
| | | width: 160, |
| | | ellipsis: { |
| | | tooltip: true, |
| | | }, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '操作人', |
| | | key: 'operatorName', |
| | | width: 100, |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '账号', |
| | | key: 'account', |
| | | width: 150, |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '操作类型', |
| | | key: 'operationType', |
| | | width: 120, |
| | | resizable: true, |
| | | render: (row) => { |
| | | const value = row.operationType |
| | | return <span>{typeof value === 'number' ? (operationTypeLabelMap[value] || value) : (value || '-')}</span> |
| | | }, |
| | | }, |
| | | { |
| | | title: '操作内容', |
| | | key: 'operationContent', |
| | | width: 200, |
| | | ellipsis: true, |
| | | resizable: true, |
| | | render: row => ( |
| | | <NTooltip trigger="hover" placement="top" max-width="560px"> |
| | | {{ |
| | | trigger: () => <span class="cursor-pointer">{row.operationContent || '-'}</span>, |
| | | default: () => row.operationContent || '-', |
| | | }} |
| | | </NTooltip> |
| | | ), |
| | | }, |
| | | { |
| | | title: '操作IP', |
| | | key: 'operationIp', |
| | | width: 140, |
| | | ellipsis: true, |
| | | resizable: true, |
| | | }, |
| | | { |
| | | title: '操作时间', |
| | | key: 'operationTime', |
| | | width: 120, |
| | | ellipsis: true, |
| | | render: (row) => { |
| | | const timeText = row.operationTime || row.createTime |
| | | return <span>{timeText ? dayjs(timeText).format('YYYY/MM/DD HH:mm') : '-'}</span> |
| | | }, |
| | | }, |
| | | ]) |
| | | |
| | | function buildQueryParams() { |
| | | const params: Record<string, any> = { |
| | | pageNum: model.value.pageNum, |
| | | pageSize: model.value.pageSize, |
| | | } |
| | | if (model.value.functionModule.trim()) |
| | | params.functionModule = model.value.functionModule.trim() |
| | | if (model.value.operatorName.trim()) |
| | | params.operatorName = model.value.operatorName.trim() |
| | | if (model.value.account.trim()) |
| | | params.account = model.value.account.trim() |
| | | if (model.value.operationType !== null) |
| | | params.operationType = model.value.operationType |
| | | if (model.value.operationContent.trim()) |
| | | params.operationContent = model.value.operationContent.trim() |
| | | if (model.value.operationIp.trim()) |
| | | params.operationIp = model.value.operationIp.trim() |
| | | if (model.value.operationTime) { |
| | | params.startTime = dayjs(model.value.operationTime[0]).startOf('day').format('YYYY-MM-DD HH:mm:ss') |
| | | params.endTime = dayjs(model.value.operationTime[1]).endOf('day').format('YYYY-MM-DD HH:mm:ss') |
| | | } |
| | | return params |
| | | } |
| | | |
| | | function getFilteredData() { |
| | | const params = buildQueryParams() |
| | | return mockSourceData.value.filter((item) => { |
| | | if (params.functionModule && !String(item.functionModule || '').includes(params.functionModule)) |
| | | return false |
| | | if (params.operatorName && !String(item.operatorName || '').includes(params.operatorName)) |
| | | return false |
| | | if (params.account && !String(item.account || '').includes(params.account)) |
| | | return false |
| | | if (params.operationType !== undefined && Number(item.operationType) !== Number(params.operationType)) |
| | | return false |
| | | if (params.operationContent && !String(item.operationContent || '').includes(params.operationContent)) |
| | | return false |
| | | if (params.operationIp && !String(item.operationIp || '').includes(params.operationIp)) |
| | | return false |
| | | if (params.startTime && params.endTime) { |
| | | const currentTime = dayjs(item.operationTime || item.createTime).valueOf() |
| | | const startTime = dayjs(params.startTime).valueOf() |
| | | const endTime = dayjs(params.endTime).valueOf() |
| | | if (currentTime < startTime || currentTime > endTime) |
| | | return false |
| | | } |
| | | return true |
| | | }) |
| | | } |
| | | |
| | | async function getList() { |
| | | startLoading() |
| | | try { |
| | | const filteredData = getFilteredData() |
| | | count.value = filteredData.length |
| | | const start = (model.value.pageNum - 1) * model.value.pageSize |
| | | const end = start + model.value.pageSize |
| | | listData.value = filteredData.slice(start, end) |
| | | await Promise.resolve() |
| | | } |
| | | finally { |
| | | endLoading() |
| | | } |
| | | } |
| | | |
| | | function handleSearch() { |
| | | model.value.pageNum = 1 |
| | | getList() |
| | | } |
| | | |
| | | function handleResetSearch() { |
| | | model.value = { ...initialModel } |
| | | handleSearch() |
| | | } |
| | | |
| | | function changePage(page: number, size: number) { |
| | | model.value.pageNum = page |
| | | model.value.pageSize = size |
| | | getList() |
| | | } |
| | | |
| | | async function handleExport() { |
| | | const exportList = getFilteredData() |
| | | const headers = ['序号', '功能模块', '操作人', '账号', '操作类型', '操作内容', '操作IP', '操作时间'] |
| | | const lines = exportList.map((item, index) => { |
| | | const values = [ |
| | | String(index + 1), |
| | | item.functionModule || '-', |
| | | item.operatorName || '-', |
| | | item.account || '-', |
| | | operationTypeLabelMap[Number(item.operationType)] || String(item.operationType || '-'), |
| | | item.operationContent || '-', |
| | | item.operationIp || '-', |
| | | item.operationTime || item.createTime || '-', |
| | | ] |
| | | return values.map(value => `"${String(value).replace(/"/g, '""')}"`).join(',') |
| | | }) |
| | | const csvText = `\uFEFF${headers.join(',')}\n${lines.join('\n')}` |
| | | const blob = new Blob([csvText], { type: 'text/csv;charset=utf-8;' }) |
| | | downloadFile(blob, `系统操作日志_${dayjs().format('YYYYMMDDHHmmss')}.csv`) |
| | | message?.success('导出成功') |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="roboTaxi w-full h-full"> |
| | | <div class="roboTaxi-form-card mb-12px"> |
| | | <NForm ref="formRef" :model="model" label-placement="left" inline :show-feedback="false"> |
| | | <SearchPanel> |
| | | <NGi v-for="item in filterItems" :key="item.path" class="n-grid-item"> |
| | | <NFormItem :label="item.label" :path="item.path"> |
| | | <component :is="item.render" /> |
| | | </NFormItem> |
| | | </NGi> |
| | | <template #action> |
| | | <NButton @click="handleResetSearch"> |
| | | {{ $t('common.reset') }} |
| | | </NButton> |
| | | <NButton type="primary" @click="handleSearch"> |
| | | {{ $t('common.search') }} |
| | | </NButton> |
| | | </template> |
| | | </SearchPanel> |
| | | </NForm> |
| | | </div> |
| | | |
| | | <NCard class="flex-1 robotaxi-table-card"> |
| | | <template #header> |
| | | <n-flex justify="flex-end" align="center"> |
| | | <NButton @click="handleExport"> |
| | | 导出 |
| | | </NButton> |
| | | </n-flex> |
| | | </template> |
| | | <div class="flex-1 flex flex-col h-full"> |
| | | <NDataTable |
| | | class="flex-1" |
| | | :columns="columns" |
| | | :data="listData" |
| | | :loading="loading" |
| | | flex-height |
| | | :bordered="false" |
| | | :scroll-x="1280" |
| | | :header-height="18" |
| | | :row-height="18" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img |
| | | src="/src/assets/element-img/no-data.png" |
| | | 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> |
| | | <Pagination |
| | | :count="count" |
| | | :page="model.pageNum" |
| | | :page-size="model.pageSize" |
| | | class="robotaxi-pagination mt-12px" |
| | | @change="changePage" |
| | | /> |
| | | </div> |
| | | </NCard> |
| | | </div> |
| | | </template> |