hejianhao
2026-03-23 ed131830215699b18b66d2df0d4fca18fcd3fc54
Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web
9个文件已修改
661 ■■■■■ 已修改文件
src/views/assetManagement/vehicleManagement/operatingVehicle/components/InspectionDetailModal.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/address/components/AddressFormModal.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/address/index.vue 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/shift/components/ShiftFormModal.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/shift/index.vue 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/statistics/detail.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/statistics/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/personnel/personnelManagement/components/PersonnelFormModal.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/components/InspectionDetailModal.vue
@@ -88,7 +88,7 @@
                  src="/src/assets/element-img/file-icon.png"
                />
                <div class="file-name">{{ file.name }}</div>
                <div class="relative mr-24px">
                <div class="relative ml-24px">
                  <img
                    class="file-look"
                    src="/src/assets/element-img/look.png"
@@ -149,7 +149,7 @@
                  src="/src/assets/element-img/file-icon.png"
                />
                <div class="file-name">{{ file.name }}</div>
                <div class="relative mr-24px">
                <div class="relative ml-24px">
                  <img
                    class="file-look"
                    src="/src/assets/element-img/look.png"
@@ -197,7 +197,7 @@
                  src="/src/assets/element-img/file-icon.png"
                />
                <div class="file-name">{{ file.name }}</div>
                <div class="relative mr-24px">
                <div class="relative ml-24px">
                  <img
                    class="file-look"
                    src="/src/assets/element-img/look.png"
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue
@@ -178,7 +178,7 @@
const maintenanceColumns = [
  { title: '序号', key: 'index', align: 'left' as const, width: 80, render: (row: any, index: number) => (maintenancePage.value.pageNum - 1) * maintenancePage.value.pageSize + index + 1 },
  { title: '维修原因', key: 'repairReason', align: 'left' as const, width: 180 },
  { title: '停运时间', key: 'repairTime', align: 'left' as const, width: 240, render: (row: any) => `${row.startTime ? dayjs(row.startTime).format("YYYY/MM/DD HH:mm:ss") : '-'} 至 ${row.endTime ? dayjs(row.endTime).format("YYYY/MM/DD HH:mm:ss") : '-'}` },
  { title: '停运时间', key: 'repairTime', align: 'left' as const, width: 240, render: (row: any) => `${row.startTime ? dayjs(row.startTime).format("YYYY/MM/DD") : '-'} 至 ${row.endTime ? dayjs(row.endTime).format("YYYY/MM/DD") : '-'}` },
  { title: '维修总费用', key: 'repairMoney', align: 'left' as const, width: 140, render: (row: any) => (row.repairMoney != null ? `¥${row.repairMoney}` : '-') },
  { title: '送修人', key: 'operatorStartName', align: 'left' as const, width: 120 },
  { title: '送修时间', key: 'sendTime', align: 'left' as const, width: 180 , render: (row: any) => row.sendTime ? dayjs(row.sendTime).format("YYYY/MM/DD HH:mm:ss") : '-' },
src/views/operations/attendance/address/components/AddressFormModal.vue
@@ -300,13 +300,13 @@
<template>
  <n-modal v-model:show="show" :mask-closable="false" preset="card" :title="mode === 'edit' ? '编辑打卡地址' : '新增打卡地址'"
    class="w-900px" :auto-focus="false" :segmented="{
    class="w-600px" :auto-focus="false" :segmented="{
      content: true,
      action: true,
    }">
    <n-form ref="formRef" :model="formModel" :rules="rules" label-placement="top" require-mark-placement="left"
      show-require-mark>
      <div class="grid grid-cols-2 gap-4">
      <div class="grid grid-cols-2 gap-8">
        <n-form-item label="打卡地址" path="locationAddress">
          <n-input v-model:value="formModel.locationAddress" placeholder="请输入" clearable maxlength="50" />
        </n-form-item>
@@ -314,13 +314,11 @@
        <n-form-item label="打卡范围" path="locationRange">
          <n-input-number v-model:value="formModel.locationRange" placeholder="请输入" clearable style="width: 100%"
            :min="0" :precision="0" :show-button="false">
            <template #suffix>
              <span>m以内</span>
            </template>
          </n-input-number>
          <span class="flex-shrink-0 ml-8px" style="color:rgba(23, 31, 38, 0.8)">m以内</span>
        </n-form-item>
      </div>
      <div class="grid grid-cols-2 gap-4">
      <div class="grid grid-cols-2 gap-8">
        <n-form-item label="经纬度" path="location">
          <n-input
            :value="formModel.locationLon && formModel.locationLat ? `${formModel.locationLon}, ${formModel.locationLat}` : ''"
@@ -343,7 +341,7 @@
    </n-form>
    <template #action>
      <n-space justify="center">
      <n-space justify="end">
        <n-button @click="handleCancel">取消</n-button>
        <n-button type="primary" @click="handleConfirm">保存</n-button>
      </n-space>
src/views/operations/attendance/address/index.vue
@@ -1,20 +1,25 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { useBoolean } from '@/hooks'
import { NButton, NSpace, NTag } from 'naive-ui'
import { getCheckLocationList, deleteCheckLocation, addCheckLocation, editCheckLocation, updateCheckLocationStatus } from '@/service/api/attendance'
import { NButton, NTag } from 'naive-ui'
import { addCheckLocation, deleteCheckLocation, editCheckLocation, getCheckLocationList, updateCheckLocationStatus } from '@/service/api/attendance'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import AddressFormModal from './components/AddressFormModal.vue'
import dayjs from 'dayjs'
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const { t } = useI18n()
const dialog = useDialog()
const pageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
const initialModel = {
  locationAddress: '',
}
const model = ref({ ...initialModel })
function handleResetSearch() {
  model.value = { ...initialModel }
  handleSearch()
@@ -28,102 +33,112 @@
const showFormModal = ref(false)
const formMode = ref<'add' | 'edit'>('add')
const pageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
const tableScrollX = computed(() => Math.min(1600, 70 + 180 + 360 + 120 + 200))
const columns = computed<DataTableColumns<any>>(() =>[
const columns = computed<DataTableColumns<any>>(() => [
  {
    title: '序号',
    key: 'serialNumber',
    width: 70,
    align: 'left',
    resizable: true,
  },
  {
    title: '创建时间',
    key: 'createTime',
    width: 180,
    align: 'left',
    resizable: true,
    render: (row) => {
      return row.createTime ? dayjs(row.createTime).format('YYYY-MM-DD HH:mm') : ''
    }
      return row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : ''
    },
  },
  {
    title: '打卡地',
    key: 'locationAddress',
    width: 360,
    align: 'left',
    resizable: true,
    minWidth: 200
  },
  {
    title: '状态',
    width: 300,
    width: 120,
    key: 'status',
    align: 'left',
    resizable: true,
   render(row) {
        return h(
          NTag,
          {
            type: row.status === 0  ? 'default' :  'primary',
            bordered: false
    render(row) {
      const isEnabled = row.status === 1
      return h(
        NTag,
        {
          bordered: false,
          round: true,
          style: {
            borderRadius: '4px',
            border: 'none',
            color: isEnabled ? '#0BB52D' : '#666666',
            backgroundColor: isEnabled ? '#DCF5DD' : 'rgba(23, 31, 38, 0.05)',
          },
          {
            default: () => row.status === 0 ? '禁用中' : '启用中'
          }
        )
      }
        },
        {
          default: () => isEnabled ? '启用中' : '禁用中',
        },
      )
    },
  },
  {
    title: '操作',
    key: 'actions',
    width: 150,
    width: 200,
    align: 'left',
    render: (row) => {
      const isEnabled = row.status === 1
      return (
        <NSpace>
          <NButton
            text
        <div class="flex items-center justify-start gap-2">
          <span
            class="cursor-pointer"
            v-permission="70212"
            type='primary'
            style={{ color: '#0068FF' }}
            onClick={() => handleEdit(row)}
          >
            编辑
          </NButton>
          <NButton
            text
          </span>
          <span
            class="cursor-pointer"
            v-permission="70214"
            type='primary'
            style={{ color: isEnabled ? '#0068FF' : '#0068FF' }}
            onClick={() => {
              dialog.create({
                title: '是否确认' + (row.status === 1 ? '禁用' : '启用') + '打卡地?',
                title: `是否确认${isEnabled ? '禁用' : '启用'}打卡地?`,
                content: '',
                positiveText: '确定',
                negativeText: '取消',
                draggable: true,
                onPositiveClick: () => {
                  updateCheckLocationStatus({ id: row.id, status: row.status === 1 ? 0 : 1 }).then(res => {
                  if (res.code === 200) {
                    window.$message.success('操作成功')
                    getList()
                  }
                })
                }
                  updateCheckLocationStatus({ id: row.id, status: isEnabled ? 0 : 1 }).then((res) => {
                    if (res.code === 200) {
                      window.$message.success('操作成功')
                      getList()
                    }
                  })
                },
              })
            }}
          >
            { row.status === 1 ? '禁用' : '启用' }
          </NButton>
          <NButton
            text
            {isEnabled ? '禁用' : '启用'}
          </span>
          <span
            class="cursor-pointer"
            v-permission="70213"
            type='error'
            style={{ color: '#0068FF' }}
            onClick={() => {
             dialog.create({
              title: '是否确认删除数据?',
              content: `确认删除该打卡地址?删除后不可恢复。`,
              positiveText: '确认',
              negativeText: '取消',
              draggable: true,
              onPositiveClick: () => {
              dialog.create({
                title: '是否确认删除数据?',
                content: '确认删除该打卡地址?删除后不可恢复。',
                positiveText: '确认',
                negativeText: '取消',
                draggable: true,
                onPositiveClick: () => {
                  deleteCheckLocation(row.id!).then((res) => {
                    if (res.code === 200) {
                      window.$message.success('删除成功')
@@ -131,13 +146,13 @@
                  }).finally(() => {
                    getList()
                  })
                }
                },
              })
            }}
          >
            删除
          </NButton>
        </NSpace>
          </span>
        </div>
      )
    },
  },
@@ -154,18 +169,18 @@
const count = ref(0)
const listData = ref<any[]>([])
const handleAdd = () => {
function handleAdd() {
  formMode.value = 'add'
  showFormModal.value = true
}
const handleEdit = (row: any) => {
function handleEdit(row: any) {
  formMode.value = 'edit'
  showFormModal.value = true
  modalRef.value.openEdit(row)
}
const handleConfirmForm = (data: any) => {
function handleConfirmForm(data: any) {
  if (data.id) {
    editCheckLocation(data).then((res) => {
      if (res.code === 200) {
@@ -174,7 +189,8 @@
    }).finally(() => {
      getList()
    })
  } else {
  }
  else {
    addCheckLocation(data).then((res) => {
      if (res.code === 200) {
        window.$message.success('新增成功')
@@ -185,7 +201,6 @@
  }
}
async function getList() {
  startLoading()
  const params = {
@@ -194,7 +209,7 @@
  }
  getCheckLocationList(params).then((res) => {
    listData.value = res.data.records || []
    listData.value.forEach((item: any, index: number)  => {
    listData.value.forEach((item: any, index: number) => {
      item.serialNumber = (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1
    })
    count.value = res.data.total || res.data.records?.length
@@ -215,44 +230,57 @@
</script>
<template>
  <n-flex class="roboTaxi">
    <NSpace vertical class="flex-1">
      <div class="roboTaxi-form-card">
        <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
          <SearchPanel>
            <n-gi v-for="item in filterItems" :key="item.path">
              <n-form-item :label="item.label" :path="item.path">
                <component :is="item.render" />
              </n-form-item>
            </n-gi>
            <template #action>
              <NButton strong @click="handleResetSearch">
                {{ $t('common.reset') }}
              </NButton>
              <NButton type="primary" @click="handleSearch">
                {{ $t('common.search') }}
              </NButton>
            </template>
          </SearchPanel>
        </n-form>
  <div class="roboTaxi">
    <div class="roboTaxi-form-card mb-12px">
      <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
        <SearchPanel>
          <n-gi v-for="item in filterItems" :key="item.path">
            <n-form-item :label="item.label" :path="item.path">
              <component :is="item.render" />
            </n-form-item>
          </n-gi>
          <template #action>
            <NButton strong @click="handleResetSearch">
              {{ $t('common.reset') }}
            </NButton>
            <NButton type="primary" @click="handleSearch">
              {{ $t('common.search') }}
            </NButton>
          </template>
        </SearchPanel>
      </n-form>
    </div>
    <n-card class="flex-1 robotaxi-table-card">
      <template #header>
        <n-flex v-permission="70211" justify="flex-end">
          <NButton type="primary" secondary @click="handleAdd">
            新增
          </NButton>
        </n-flex>
      </template>
      <div class="flex-1 flex flex-col h-full">
        <n-data-table
          class="flex-1"
          :columns="columns"
          :data="listData"
          :loading="loading"
          :bordered="false"
          flex-height
          :scroll-x="tableScrollX"
          :header-height="18"
          :row-height="18"
        />
        <Pagination
          :count="count"
          :page="pageParams.pageNum"
          :page-size="pageParams.pageSize"
          class="robotaxi-pagination mt-12px"
          @change="changePage"
        />
      </div>
      <n-card class="flex-1">
        <template #header>
          <n-flex justify="flex-end" v-permission="70211">
            <NButton type="primary" secondary @click="handleAdd">
              新增打卡地
            </NButton>
          </n-flex>
        </template>
        <NSpace vertical :size="12">
          <n-data-table :columns="columns" :data="listData" :loading="loading" :bordered="false" :header-height="18"
            :row-height="18" />
          <Pagination :count="count" :page="pageParams.pageNum" :page-size="pageParams.pageSize" @change="changePage" />
        </NSpace>
        <AddressFormModal v-model:show="showFormModal" ref="modalRef" :mode="formMode" @confirm="handleConfirmForm" />
      </n-card>
    </NSpace>
  </n-flex>
      <AddressFormModal ref="modalRef" v-model:show="showFormModal" :mode="formMode" @confirm="handleConfirmForm" />
    </n-card>
  </div>
</template>
src/views/operations/attendance/shift/components/ShiftFormModal.vue
@@ -163,7 +163,7 @@
    :mask-closable="false"
    preset="card"
    :title="mode === 'edit' ? '编辑班次' : '新增班次'"
    class="w-600px"
    class="w-400px"
    :segmented="{
      content: true,
      action: true,
@@ -176,6 +176,7 @@
      require-mark-placement="left"
      show-require-mark
    >
    <div class="flex gap-8px flex-col">
      <n-form-item label="班次" path="shiftName">
        <n-input v-model:value="formModel.shiftName" placeholder="请输入" clearable />
      </n-form-item>
@@ -220,12 +221,13 @@
          <n-time-picker v-model:formatted-value="formModel.restEndTime" format="HH:mm" value-format="HH:mm" placeholder="--:--" clearable style="width: 100%" />
        </n-form-item>
      </template>
      </div>
    </n-form>
    <template #action>
      <n-space justify="center">
      <n-space justify="end">
        <n-button @click="handleCancel">取消</n-button>
        <n-button type="primary" @click="handleConfirm">保存</n-button>
        <n-button type="primary" @click="handleConfirm">确定</n-button>
      </n-space>
    </template>
  </n-modal>
src/views/operations/attendance/shift/index.vue
@@ -1,21 +1,21 @@
<script setup lang="tsx">
import { ref, computed, h } from 'vue'
import { computed, h, ref } from 'vue'
import { useBoolean } from '@/hooks'
import type { DataTableColumns } from 'naive-ui'
import { getShiftList, addShift, editShift, deleteShift } from '@/service/api/attendance'
import { addShift, deleteShift, editShift, getShiftList } from '@/service/api/attendance'
import { NButton, NSpace } from 'naive-ui'
import ShiftFormModal from './components/ShiftFormModal.vue'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import dayjs from 'dayjs'
const dialog = useDialog()
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const shiftName = ref<string | null>(null)
const roleOptions = [
  { label: '网格员', value: '网格员' },
  { label: '运维管理', value: '运维管理' },
  { label: '角色1', value: '角色1' }
]
const initialModel = {
  shiftName: '',
}
const model = ref({ ...initialModel })
const formRef = ref()
const shiftList = ref<any[]>([])
const modalRef = ref()
@@ -28,26 +28,28 @@
const showFormModal = ref(false)
const formMode = ref<'add' | 'edit'>('add')
const handleSearch = () => {
  console.log('搜索', shiftName.value)
function handleResetSearch() {
  model.value = { ...initialModel }
  handleSearch()
}
const handleReset = () => {
  shiftName.value = null
function handleSearch() {
  pageParams.value.pageNum = 1
  getList()
}
const handleAdd = () => {
function handleAdd() {
  formMode.value = 'add'
  showFormModal.value = true
}
const handleEdit = (row: any) => {
function handleEdit(row: any) {
  formMode.value = 'edit'
  showFormModal.value = true
  modalRef.value.openEdit(row)
}
const handleConfirmForm = (data: any) => {
function handleConfirmForm(data: any) {
  if (data.id) {
    if (data.shiftColor !== '#D9F1EC') {
      if (data.shiftName === '早班') {
@@ -70,7 +72,8 @@
    }).finally(() => {
      getList()
    })
  } else {
  }
  else {
    data.shiftColor = '#D9F1EC'
    addShift(data).then((res) => {
      if (res.code === 200) {
@@ -82,12 +85,21 @@
  }
}
const tableScrollX = computed(() => Math.min(1600, 70 + 160 + 320 + 180 + 140))
const columns = computed<DataTableColumns<any>>(() => [
  { title: '序号', key: 'serialNumber', width: 158, resizable: true, },
  {
    title: '序号',
    key: 'serialNumber',
    width: 70,
    align: 'left',
    resizable: true,
  },
  {
    title: '状态',
    key: 'shiftName',
    width: 256,
    width: 160,
    align: 'left',
    resizable: true,
    render: (row) => {
      return h('span', {
@@ -97,84 +109,109 @@
          borderRadius: '4px',
          fontSize: '12px',
          backgroundColor: row.shiftColor,
          color: 'rgba(23, 31, 38, 0.8)'
        }
          color: 'rgba(23, 31, 38, 0.8)',
        },
      }, row.shiftName)
    }
    },
  },
  {
    title: '工作时间',
    key: 'workTime',
    minWidth: 200,
    width: 320,
    align: 'left',
    resizable: true,
    render: (row) => (
    render: row => (
      <span>
        {row.startTime}-{row.endTime}
        {row.isRest && <span style={{ color: '#0068FF', marginLeft: '4px' }}>({row.restStartTime}-{row.restEndTime}休息)</span>}
        <span>{`${row.startTime}-${row.endTime}`}</span>
        {row.isRest && <span style={{ color: '#0068FF', marginLeft: '4px' }}>{`(${row.restStartTime}-${row.restEndTime}休息)`}</span>}
      </span>
    )
    ),
  },
  { title: '添加时间', key: 'createTime', width: 290, resizable: true, },
  {
    title: '添加时间',
    key: 'createTime',
    width: 180,
    align: 'left',
    resizable: true,
    render: row => row.createTime ? dayjs(row.createTime).format('YYYY/MM/DD HH:mm') : '',
  },
  {
    title: '操作',
    key: 'actions',
    width: 134,
    width: 140,
    align: 'left',
    fixed: 'right',
    render: (row) => (
       <NSpace>
          <NButton
            text
            v-permission="70242"
            type='primary'
            onClick={() => handleEdit(row)}
          >
            编辑
          </NButton>
          <NButton
            text
            v-permission="70243"
            type='error'
            disabled={(row.shiftName === '早班' || row.shiftName === '中班' || row.shiftName === '晚班' || row.shiftName === '全天') && row.shiftColor !== '#D9F1EC'}
            onClick={() => {
             dialog.create({
    render: row => (
      <NSpace>
        <NButton
          text
          v-permission="70242"
          type="primary"
          onClick={() => handleEdit(row)}
        >
          编辑
        </NButton>
        <NButton
          text
          v-permission="70243"
          type="primary"
          disabled={(row.shiftName === '早班' || row.shiftName === '中班' || row.shiftName === '晚班' || row.shiftName === '全天') && row.shiftColor !== '#D9F1EC'}
          onClick={() => {
            dialog.create({
              title: '是否确认删除数据?',
              content: `确认删除该班次?删除后不可恢复。`,
              content: '确认删除该班次?删除后不可恢复。',
              positiveText: '确认',
              negativeText: '取消',
              draggable: true,
              onPositiveClick: () => {
                  deleteShift(row.id!).then((res) => {
                    if (res.code === 200) {
                      window.$message.success('删除成功')
                    }
                  }).finally(() => {
                    getList()
                  })
                }
              })
            }}
          >
            删除
          </NButton>
        </NSpace>
    )
  }
                deleteShift(row.id!).then((res) => {
                  if (res.code === 200) {
                    window.$message.success('删除成功')
                  }
                }).finally(() => {
                  getList()
                })
              },
            })
          }}
        >
          删除
        </NButton>
      </NSpace>
    ),
  },
])
const filterItems = computed(() => [
  {
    label: '班次名称',
    path: 'shiftName',
    render: () => <n-input v-model:value={model.value.shiftName} clearable />,
  },
])
async function getList() {
  startLoading()
  const params = {
    ...pageParams.value
  try {
    const params = {
      ...pageParams.value,
    }
    const res = await getShiftList(params)
    if (res.code === 200) {
      shiftList.value = res.data.records || []
      shiftList.value.forEach((item: any, index: number) => {
        item.serialNumber = (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1
      })
      total.value = res.data.total || res.data.records?.length
    }
    else {
      shiftList.value = []
      total.value = 0
    }
  }
  getShiftList(params).then((res) => {
    shiftList.value = res.data.records || []
    shiftList.value.forEach((item: any, index: number) => {
      item.serialNumber = (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1
    })
    total.value = res.data.total || res.data.records?.length
  }).finally(() => {
  finally {
    endLoading()
  })
  }
}
onMounted(() => {
@@ -190,38 +227,57 @@
<template>
  <div class="roboTaxi">
    <NSpace vertical class="flex-1">
      <!-- <n-card style="border-radius: 6px" content-style="padding: 12px">
        <div class="flex items-center justify-between">
          <div class="flex items-center gap-2">
            <div class="text-sm">角色</div>
            <n-select v-model:value="shiftName" :options="roleOptions" placeholder="请选择" clearable
              style="width: 269px" />
          </div>
          <div class="flex gap-2">
            <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button>
            <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button>
          </div>
        </div>
      </n-card> -->
      <n-card class="flex-1">
        <template #header>
          <n-flex justify="flex-end"  v-permission="70241">
            <NButton type="primary" secondary @click="handleAdd">
              新增班次
    <!-- <div class="roboTaxi-form-card mb-12px">
      <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
        <SearchPanel>
          <n-gi v-for="item in filterItems" :key="item.path">
            <n-form-item :label="item.label" :path="item.path">
              <component :is="item.render" />
            </n-form-item>
          </n-gi>
          <template #action>
            <NButton strong @click="handleResetSearch">
              {{ $t('common.reset') }}
            </NButton>
          </n-flex>
        </template>
        <NSpace vertical :size="12">
          <n-data-table :columns="columns" :data="shiftList" :loading="loading" :bordered="false" :header-height="18"
            :row-height="18" />
          <Pagination :count="total" :page="pageParams.pageNum" :page-size="pageParams.pageSize" @change="changePage" />
        </NSpace>
      </n-card>
    </NSpace>
            <NButton type="primary" @click="handleSearch">
              {{ $t('common.search') }}
            </NButton>
          </template>
        </SearchPanel>
      </n-form>
    </div> -->
    <ShiftFormModal v-model:show="showFormModal" ref="modalRef" :mode="formMode" @confirm="handleConfirmForm" />
    <n-card class="flex-1 robotaxi-table-card">
      <template #header>
        <n-flex v-permission="70241" justify="flex-end">
          <NButton type="primary" secondary @click="handleAdd">
            新增
          </NButton>
        </n-flex>
      </template>
      <div class="flex-1 flex flex-col h-full">
        <n-data-table
          class="flex-1"
          :columns="columns"
          :data="shiftList"
          :loading="loading"
          :bordered="false"
          flex-height
          :scroll-x="tableScrollX"
          :header-height="18"
          :row-height="18"
        />
        <Pagination
          :count="total"
          :page="pageParams.pageNum"
          :page-size="pageParams.pageSize"
          class="robotaxi-pagination mt-12px"
          @change="changePage"
        />
      </div>
      <ShiftFormModal ref="modalRef" v-model:show="showFormModal" :mode="formMode" @confirm="handleConfirmForm" />
    </n-card>
  </div>
</template>
src/views/operations/attendance/statistics/detail.vue
@@ -175,7 +175,7 @@
          </div>
          <div class="flex gap-2">
            <n-button
              style="width: 100px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF"
              style="width: 100px; height: 32px;"
              @click="handleExport">
              导出打卡明细
            </n-button>
src/views/operations/attendance/statistics/index.vue
@@ -1,9 +1,9 @@
<script setup lang="tsx">
import { ref, computed } from 'vue'
import { computed, ref } from 'vue'
import { useBoolean } from '@/hooks'
import { useRouter } from 'vue-router'
import type { DataTableColumns } from 'naive-ui'
import { getClockInStatisticsList, exportClockInStatistics } from '@/service/api/attendance'
import { exportClockInStatistics, getClockInStatisticsList } from '@/service/api/attendance'
import { fetchRoleList } from '@/service/api/fleet'
import { NButton } from 'naive-ui'
import { downloadFile } from '@/utils'
@@ -17,10 +17,11 @@
const searchModel = ref({
  name: null as string | null,
  roleId: null as string | null,
  statisticDate: null as [number, number] | null
  statisticDate: null as [number, number] | null,
})
const roleOptions = ref<any[]>([])
const isRoleSelectOpen = ref(false)
const statisticsList = ref<any[]>([])
@@ -30,11 +31,11 @@
  pageSize: 10,
})
const handleReset = () => {
function handleReset() {
  searchModel.value = {
    name: null,
    roleId: null,
    statisticDate: null
    statisticDate: null,
  }
  handleSearch()
}
@@ -43,7 +44,7 @@
  getList()
}
const handleExport = async () => {
async function handleExport() {
  const params = {
    ...pageParams.value,
    name: searchModel.value.name,
@@ -57,7 +58,7 @@
  }
}
const handleViewDetail = (row: any) => {
function handleViewDetail(row: any) {
  router.push(`/operations/attendance/statistics/detail/${row.appUserId}?shouldClockInDays=${row.shouldClockInDays}&clockInDays=${row.clockInDays}&notClockInDays=${row.notClockInDays}&lateEarly=${row.lateEarly}`)
}
@@ -65,41 +66,80 @@
  {
    label: '人员',
    path: 'name',
    render: () => <n-input v-model:value={searchModel.value.name} placeholder="请输入" clearable />
    render: () => <n-input v-model:value={searchModel.value.name} placeholder="请输入" clearable />,
  },
  {
    label: '角色',
    path: 'searchModel.value.roleId',
    render: () => <n-select v-model:value={searchModel.value.roleId} options={roleOptions.value} placeholder="请选择" clearable />
    path: 'roleId',
    render: () => (
      <n-select
        v-model:value={searchModel.value.roleId}
        options={roleOptions.value}
        placeholder="请选择"
        clearable
        onUpdate:show={(value: boolean) => { isRoleSelectOpen.value = value }}
      >
        {{
          arrow: () => (
            <img
              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
              class={`arrow-icon ${isRoleSelectOpen.value ? 'arrow-icon--open' : ''}`}
              style={{ width: '16px', height: '16px' }}
              alt="arrow"
            />
          ),
        }}
      </n-select>
    ),
  },
  {
    label: '打卡日期',
    path: 'statisticDate',
    render: () => <n-date-picker v-model:value={searchModel.value.statisticDate} type="daterange" clearable start-placeholder="开始时间" end-placeholder="结束时间" />
  }
    render: () => (
      <n-date-picker
        v-model:value={searchModel.value.statisticDate}
        type="daterange"
        clearable
        actions={[]}
        close-on-select={true}
        start-placeholder="开始时间"
        end-placeholder="结束时间"
        separator="-"
      />
    ),
  },
])
const columns = computed<DataTableColumns<any>>(() => [
  { title: '序号', key: 'serialNumber', width: 91, resizable: true, fixed: 'left' },
  { title: '人员', key: 'name', width: 143, resizable: true, fixed: 'left'  },
  { title: '角色', key: 'roleName', width: 134, resizable: true, },
  { title: '所属队伍', key: 'teamName', width: 142, resizable: true, },
  { title: '应打卡天数', key: 'shouldClockInDays', minWidth: 100, resizable: true, },
  { title: '正常', key: 'clockInDays', minWidth: 100, resizable: true, },
  { title: '缺卡', key: 'notClockInDays', minWidth: 100, resizable: true, },
  { title: '迟到', key: 'lateEarly', minWidth: 100, resizable: true, },
  { title: '序号', key: 'serialNumber', width: 80, align: 'left', resizable: true, fixed: 'left' },
  { title: '人员', key: 'name', width: 140, align: 'left', resizable: true, fixed: 'left' },
  { title: '角色', key: 'roleName', width: 140, align: 'left', resizable: true },
  { title: '所属队伍', key: 'teamName', width: 160, align: 'left', resizable: true },
  { title: '应打卡天数', key: 'shouldClockInDays', width: 120, align: 'left', resizable: true },
  { title: '正常', key: 'clockInDays', width: 120, align: 'left', resizable: true },
  { title: '缺卡', key: 'notClockInDays', width: 120, align: 'left', resizable: true },
  { title: '迟到', key: 'lateEarly', width: 120, align: 'left', resizable: true },
  {
    title: '操作',
    key: 'actions',
    width: 134,
    width: 110,
    align: 'left',
    fixed: 'right',
    render: (row) => (
    render: row => (
      <NButton v-permission="70222" text type="primary" onClick={() => handleViewDetail(row)}>
        打卡明细
      </NButton>
    )
  }
    ),
  },
])
const tableScrollX = computed(() => {
  const totalWidth = columns.value.reduce((sum, col) => {
    const width = Number((col as any).width ?? (col as any).minWidth ?? 0)
    return sum + (Number.isFinite(width) ? width : 0)
  }, 0)
  return Math.min(totalWidth, 1600)
})
async function getRoleList() {
  const res = await fetchRoleList()
@@ -144,7 +184,7 @@
<template>
  <div class="roboTaxi">
    <n-card style="border-radius: 6px" content-style="padding: 12px">
    <div class="roboTaxi-form-card mb-12px">
      <n-form :model="searchModel" label-placement="left" inline :show-feedback="false">
        <SearchPanel>
          <n-gi v-for="item in filterItems" :key="item.path">
@@ -153,26 +193,45 @@
            </n-form-item>
          </n-gi>
          <template #action>
            <n-button style="width: 84px; height: 32px" @click="handleReset">重置</n-button>
            <n-button type="primary" style="width: 84px; height: 32px" @click="handleSearch">查询</n-button>
            <NButton strong @click="handleReset">
              重置
            </NButton>
            <NButton type="primary" @click="handleSearch">
              查询
            </NButton>
          </template>
        </SearchPanel>
      </n-form>
    </n-card>
    </div>
    <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px">
       <template #header>
          <n-flex justify="flex-end" v-permission="70221">
            <NButton type="primary" secondary @click="handleExport">
              导出
            </NButton>
          </n-flex>
        </template>
      <NSpace vertical :size="12">
          <n-data-table :columns="columns" :data="statisticsList" :loading="loading" :bordered="false" :header-height="18"
            :row-height="18" />
          <Pagination :count="total" :page="pageParams.pageNum" :page-size="pageParams.pageSize" @change="changePage" />
        </NSpace>
    <n-card class="flex-1 robotaxi-table-card">
      <template #header>
        <n-flex v-permission="70221" justify="flex-end">
          <NButton type="primary" secondary @click="handleExport">
            导出
          </NButton>
        </n-flex>
      </template>
      <div class="flex-1 flex flex-col h-full">
        <n-data-table
          class="flex-1"
          :columns="columns"
          :data="statisticsList"
          :loading="loading"
          :bordered="false"
          flex-height
          :scroll-x="tableScrollX"
          :header-height="18"
          :row-height="18"
        />
        <Pagination
          :count="total"
          :page="pageParams.pageNum"
          :page-size="pageParams.pageSize"
          class="robotaxi-pagination mt-12px"
          @change="changePage"
        />
      </div>
    </n-card>
  </div>
</template>
src/views/operations/personnel/personnelManagement/components/PersonnelFormModal.vue
@@ -305,7 +305,7 @@
              :options="roleOptions"
              clearable
              multiple
              placeholder="请选择"
              placeholder="请选择(多选)"
              @update:show="handleRoleSelectShow"
            >
              <template #arrow>