From 7ad8af83cccaff98c3d30ec36fc70de17aa17bdc Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期六, 09 五月 2026 17:52:56 +0800
Subject: [PATCH] feat(system): 新增系统操作日志功能模块

---
 src/components/custom/OperationLogModal.vue |  330 +++++++++++++++++++++++
 src/views/system/operationLog/index.vue     |  422 ++++++++++++++++++++++++++++++
 src/service/api/systemOperationLog.ts       |   19 +
 src/styles/robotaxi.scss                    |    6 
 src/router/routes.static.ts                 |   13 
 5 files changed, 790 insertions(+), 0 deletions(-)

diff --git a/src/components/custom/OperationLogModal.vue b/src/components/custom/OperationLogModal.vue
new file mode 100644
index 0000000..8e23d2b
--- /dev/null
+++ b/src/components/custom/OperationLogModal.vue
@@ -0,0 +1,330 @@
+<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>
diff --git a/src/router/routes.static.ts b/src/router/routes.static.ts
index c5990a1..73bda99 100644
--- a/src/router/routes.static.ts
+++ b/src/router/routes.static.ts
@@ -124,6 +124,19 @@
     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: '新增测试计划',
diff --git a/src/service/api/systemOperationLog.ts b/src/service/api/systemOperationLog.ts
new file mode 100644
index 0000000..093cae2
--- /dev/null
+++ b/src/service/api/systemOperationLog.ts
@@ -0,0 +1,19 @@
+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
+}
diff --git a/src/styles/robotaxi.scss b/src/styles/robotaxi.scss
index 9adc536..4754c02 100644
--- a/src/styles/robotaxi.scss
+++ b/src/styles/robotaxi.scss
@@ -33,6 +33,7 @@
 
 .roboTaxi {
   height: calc(100vh - 76px - 24px);
+  // height: calc(100vh - 35px - 40px);
   display: flex;
   flex-direction: column;
 
@@ -80,6 +81,11 @@
     padding-top: 0;
   }
 
+  .n-card-content {
+    padding: 12px;
+    padding-top: 0;
+  }
+
   .n-base-selection-input__content {
     font-size: 13px;
   }
diff --git a/src/views/system/operationLog/index.vue b/src/views/system/operationLog/index.vue
new file mode 100644
index 0000000..bdca287
--- /dev/null
+++ b/src/views/system/operationLog/index.vue
@@ -0,0 +1,422 @@
+<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>

--
Gitblit v1.7.1