hejianhao
2026-03-06 e6109507dfcb44b927ad4009c2df397040e170c5
Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web
1个文件已添加
42个文件已修改
2502 ■■■■■ 已修改文件
src/service/api/vehicle.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/http/alova copy.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/auth.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/router/helper.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/router/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/crypto.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/fleetManagement/components/StaffSelectModal.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/fleetManagement/detail.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/fleetManagement/index.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/ruleConfig/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/inventoryVehicle/add.vue 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/inventoryVehicle/index.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/car-detail.txt 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/assetManagement/vehicleManagement/operatingVehicle/index.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/geofenceManagement/detail.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/geofenceManagement/index.scss 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/geofenceManagement/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/pointManagement/index.scss 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/pointManagement/index.vue 259 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/pointManagement/list.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/address/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/plan/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/shift/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/statistics/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/message/index.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/personnel/personnelManagement/components/PersonnelFormModal.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/personnel/personnelManagement/components/ResetPasswordModal.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/personnel/personnelManagement/detail.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/personnel/personnelManagement/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/components/detailModal.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/review/personnel/index.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/review/policyApplication/index.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/ticket/feedbackType/index.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/service/ticket/workOrder/index.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/accountPermission/permissionGroup/form.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/pricingManagement/components/TableModal.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/pricingManagement/detail.vue 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/pricingManagement/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/transactionStrategy/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelist/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/vehicle.ts
@@ -99,6 +99,16 @@
}
/**
 * 下载导入模板
 * @returns
 */
export function getVehicleImportTemplate() {
  const method = request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/vehicle/importTemplate', { responseType: 'blob' })
  method.meta = { ...method.meta, isBlob: true }
  return method
}
/**
 * 删除车辆
 * @param id 
 * @returns 
src/service/http/alova copy.ts
@@ -93,8 +93,8 @@
      const rsaPublicKey = method.meta?.rsaPublicKey || '' // 从meta中获取RSA公钥
      const secretKey = method.meta?.secretKey || '' // 从meta中获取签名密钥
      const sensitiveFields = method.meta?.sensitiveFields || [] // 从meta中获取敏感字段
      console.log('secretKey', secretKey)
      console.log('method.meta', method.meta, 'method.data', method.data)
      // console.log('secretKey', secretKey)
      // console.log('method.meta', method.meta, 'method.data', method.data)
      // 获取请求参数:GET请求使用params,其他请求使用data
      const requestData = method.type === 'GET' ? (method.config.params || {}) : (method.data || {})
@@ -172,7 +172,7 @@
        }
        // 使用包含时间戳的对象生成签名
        const sign = generateSign(paramsWithTimestamp, secretKey, timestamp)
        console.log('method', method)
        // console.log('method', method)
        // 根据请求类型设置参数
        // if (method.type === 'GET') {
src/store/auth.ts
@@ -38,14 +38,13 @@
      // 重置当前存储库
      this.$reset()
      // 重定向到登录页
      if (route.meta.requiresAuth) {
        router.push({
          name: 'login',
          query: {
            redirect: route.fullPath,
          },
        })
      }
      const redirect = route.meta.requiresAuth && route.name !== 'not-found' ? route.fullPath : undefined
      router.push({
        name: 'login',
        query: {
          redirect,
        },
      })
    },
    clearAuthStorage() {
      local.remove('accessToken')
src/store/router/helper.ts
@@ -21,7 +21,7 @@
export function createRoutes(routes: AppRoute.RowRoute[]) {
  const { hasPermission } = usePermission()
  console.log('Creating routes with input:', routes)
  // console.log('Creating routes with input:', routes)
  // Structure the meta field
@@ -29,31 +29,33 @@
  let resultRouter = standardizedRoutes(routes)
  console.log('After standardization:', resultRouter)
  // console.log('After standardization:', resultRouter)
  // Route permission filtering
  resultRouter = resultRouter.filter(i => {
    if (i.meta?.requiresAuth === false)
      return true
    if (i.id) {
      return hasPermission(i.id)
    }
    return hasPermission(i.meta.roles)
  })
  // 正式解开
  console.log('After permission filtering:', resultRouter)
  // console.log('After permission filtering:', resultRouter)
  // Generate routes, no need to import files for those with redirect
  const modules = import.meta.glob('@/views/**/*.vue')
  console.log('Available modules:', Object.keys(modules))
  // console.log('Available modules:', Object.keys(modules))
  resultRouter = resultRouter.map((item: AppRoute.Route) => {
    if (item.componentPath && !item.redirect) {
      const componentPath = `/src/views${item.componentPath}`
      console.log(`Looking for component at: ${componentPath}`)
      // console.log(`Looking for component at: ${componentPath}`)
      if (modules[componentPath]) {
        item.component = modules[componentPath]
        console.log(`Component found for: ${item.path}`)
        // console.log(`Component found for: ${item.path}`)
      } else {
        console.warn(`Component not found for: ${componentPath}`)
        // console.warn(`Component not found for: ${componentPath}`)
      }
    }
    return item
@@ -80,7 +82,7 @@
  // Insert the processed route into the root route
  appRootRoute.children = resultRouter as unknown as RouteRecordRaw[]
  console.log('Final app root route:', appRootRoute)
  // console.log('Final app root route:', appRootRoute)
  return appRootRoute
}
@@ -125,6 +127,8 @@
  // Route permission filtering
  resultRouter = resultRouter.filter(i => {
    if (i.meta?.requiresAuth === false)
      return true
    // 优先检查路由 ID 是否在权限列表中
    if (i.id) {
      return hasPermission(i.id)
src/store/router/index.ts
@@ -41,15 +41,20 @@
      if (import.meta.env.VITE_ROUTE_LOAD_MODE === 'dynamic') {
        try {
          // Get user's route
          const authStore = useAuthStore()
          const result = await fetchUserRoutes({
            id: 1,
            id: authStore.userInfo?.id || 1,
          })
          if (!result.isSuccess || !result.data) {
            throw new Error('Failed to fetch user routes')
          }
          return result.data
          const publicRoutes = staticRoutes.filter(route => route.requiresAuth === false)
          const userRoutes = result.data
          const mergedRoutes = [...publicRoutes.filter(p => !userRoutes.some(u => u.name === p.name)), ...userRoutes]
          return mergedRoutes
        }
        catch (error) {
          console.error('Failed to initialize route info:', error)
src/utils/crypto.ts
@@ -147,16 +147,16 @@
  const sortedParams = Object.keys(params)
    .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '')
    .sort()
  console.log('sortedParams', sortedParams)
  // console.log('sortedParams', sortedParams)
  const paramsList = sortedParams.map(key => `${key}=${String(params[key])}`)
  const paramsStr = [...paramsList, `timestamp=${String(timestamp)}`].join('&')
  console.log('paramsStr paramsStr', paramsStr)
  console.log('secretKey secretKey', secretKey)
  // console.log('paramsStr paramsStr', paramsStr)
  // console.log('secretKey secretKey', secretKey)
  const hmac = CryptoJS.HmacSHA256(paramsStr, secretKey)
  console.log('hmac hmac', hmac)
  console.log('CryptoJS.enc.Base64.stringify(hmac)', CryptoJS.enc.Base64.stringify(hmac))
  // console.log('hmac hmac', hmac)
  // console.log('CryptoJS.enc.Base64.stringify(hmac)', CryptoJS.enc.Base64.stringify(hmac))
  return CryptoJS.enc.Base64.stringify(hmac)
}
src/views/assetManagement/fleetManagement/components/StaffSelectModal.vue
@@ -1,8 +1,8 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { NAvatar, NButton, NSpace } from 'naive-ui'
import { NAvatar, NButton, NSpace, NSelect } from 'naive-ui'
import { useBoolean } from '@/hooks'
import { fetchOperationsUserList } from '@/service/api/fleet'
import { fetchOperationsUserList, fetchRoleList } from '@/service/api/fleet'
interface StaffItem {
  id: number
@@ -26,6 +26,9 @@
  phoneNumber: '',
  roleId: undefined as number | undefined,
})
// 角色下拉选项
const roleOptions = ref<{ label: string; value: number }[]>([])
// 选中的行
const checkedRowKeys = ref<number[]>([])
@@ -71,7 +74,7 @@
    resizable: true,
  },
  {
    title: '岗位',
    title: '角色',
    align: 'center',
    key: 'roleName',
    ellipsis: true,
@@ -109,6 +112,21 @@
    window.$message?.error('获取人员列表失败')
  } finally {
    endLoading()
  }
}
// 获取角色列表
async function getRoleList() {
  try {
    const res = await fetchRoleList()
    if (res.code === 200 && res.data) {
      roleOptions.value = res.data.map((item: any) => ({
        label: item.roleName,
        value: item.id
      }))
    }
  } catch (error) {
    console.error('获取角色列表失败', error)
  }
}
@@ -160,6 +178,7 @@
watch(visible, (val) => {
  if (val) {
    getStaffList()
    getRoleList()
  }
})
</script>
@@ -192,6 +211,10 @@
          <div class="mb-2 text-sm">联系电话</div>
          <n-input v-model:value="searchModel.phoneNumber" placeholder="请输入" clearable />
        </div>
        <div class="flex-1">
          <div class="mb-2 text-sm">角色</div>
          <n-select v-model:value="searchModel.roleId" :options="roleOptions" placeholder="请选择" clearable />
        </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>
src/views/assetManagement/fleetManagement/detail.vue
@@ -101,29 +101,29 @@
    ellipsis: true,
    resizable: true,
  },
   {
  {
    title: '车辆颜色',
    align: 'left',
    key: 'vehicleColor',
    ellipsis: true,
    resizable: true,
  },
  // {
  //   title: '采购来源',
  //   align: 'left',
  //   key: 'procurementSource',
  //   ellipsis: true,
  //   resizable: true,
  //   render: (row) => <span>{row.procurementSource || '-'}</span>,
  // },
  // {
  //   title: '运营区域',
  //   align: 'left',
  //   key: 'locationName',
  //   ellipsis: true,
  //   resizable: true,
  //   render: (row) => <span>{row.locationName || '-'}</span>,
  // },
  {
    title: '采购来源',
    align: 'left',
    key: 'procurementSource',
    ellipsis: true,
    resizable: true,
    render: row => <span>{row.procurementSource || '-'}</span>,
  },
  {
    title: '运营区域',
    align: 'left',
    key: 'electronicName',
    ellipsis: true,
    resizable: true,
    render: row => <span>{row.electronicName || '-'}</span>,
  },
  {
    title: '状态',
    align: 'left',
@@ -568,7 +568,7 @@
            </n-gi>
            <n-gi>
              <div class="info-item">
                <span class="label">所属区域:</span>
                <span class="label">打卡地址:</span>
                <span class="value">{{ fleetInfo.locationName || "-" }}</span>
              </div>
            </n-gi>
src/views/assetManagement/fleetManagement/index.vue
@@ -188,6 +188,7 @@
    fleetName: row.fleetName,
    operationsUserName: row.operationsUserName,
    phoneNumber: row.phoneNumber,
    electronicName: row.electronicName
  }
  fleetFormModalVisible.value = true
src/views/assetManagement/ruleConfig/index.vue
@@ -888,7 +888,7 @@
    <!-- Footer Actions (Fixed at Bottom via Flexbox Layout) -->
    <div class=" bg-white px-6 py-4 flex justify-end gap-3 border-t border-gray-100 flex-shrink-0 z-50">
      <n-button>取消</n-button>
      <n-button type="primary" @click="handleSave">保存</n-button>
      <n-button type="primary" @click="handleSave" v-permission="5031">保存</n-button>
    </div>
  </div>
</template>
src/views/assetManagement/vehicleManagement/inventoryVehicle/add.vue
@@ -9,7 +9,10 @@
const router = useRouter()
const formRef = ref<FormInst | null>(null)
const vehicleFormRef = ref<FormInst | null>(null)
const completeInfoFormRef = ref<FormInst | null>(null)
type UploadField = 'vehiclePhotos' | 'inspectionFiles' | 'insuranceFiles' | 'onlineCarLicenseFiles'
// 表单数据
const formModel = ref({
@@ -126,12 +129,6 @@
  // 完善信息验证
  inspectionExpireDate: [
    {
      required: true,
      type: 'number',
      message: '请选择年检到期时间',
      trigger: ['blur', 'change'],
    },
    {
      validator: (rule, value) => {
        if (value && value <= Date.now()) {
          return new Error('年检到期时间必须大于当前时间')
@@ -143,18 +140,12 @@
  ],
  insuranceCompany: [
    {
      required: true,
      message: '请输入保险公司',
      required: false,
      message: '',
      trigger: ['blur', 'input'],
    },
  ],
  insuranceExpireDate: [
    {
      required: true,
      type: 'number',
      message: '请选择保险到期时间',
      trigger: ['blur', 'change'],
    },
    {
      validator: (rule, value) => {
        if (value && value <= Date.now()) {
@@ -165,25 +156,58 @@
      trigger: ['blur', 'change'],
    },
  ],
  onlineCarLicenseExpireDate: [
    {
      validator: (rule, value) => {
        if (value && value <= Date.now()) {
          return new Error('网约车运营资质到期时间必须大于当前时间')
        }
        return true
      },
      trigger: ['blur', 'change'],
    },
  ],
  insuranceFiles: [
    {
      validator: (rule, value: UploadFileInfo[]) => {
        const hasInsuranceInfo = Boolean(
          formModel.value.insuranceCompany
          || formModel.value.insurancePhone
          || formModel.value.insuranceExpireDate,
        )
        if (hasInsuranceInfo && (!value || value.length === 0)) {
          return new Error('填写了保险信息后,必须上传保险资料')
        }
        return true
      },
      trigger: ['blur', 'change'],
    },
  ],
  onlineCarLicenseFiles: [
    {
      validator: (rule, value: UploadFileInfo[]) => {
        if (formModel.value.onlineCarLicenseExpireDate && (!value || value.length === 0)) {
          return new Error('设置了网约车运营到期时间后,必须上传网约车运营资质')
        }
        return true
      },
      trigger: ['blur', 'change'],
    },
  ],
  insurancePhone: [
    {
      required: true,
      message: '请输入保险电话',
      trigger: ['blur', 'input'],
    },
    {
      pattern: /^1[3-9]\d{9}$/,
      message: '请输入正确的手机号码',
      validator: (rule, value) => {
        if (!value)
          return true
        if (!/^1[3-9]\d{9}$/.test(String(value))) {
          return new Error('请输入正确的手机号码')
        }
        return true
      },
      trigger: ['blur', 'input'],
    },
  ],
  inspectionFiles: [
    {
      required: true,
      type: 'array',
      message: '请上传年检资料附件',
      trigger: ['blur', 'change'],
    },
    {
      validator: (rule, value: UploadFileInfo[]) => {
        // 如果设置了年检到期时间,必须上传年检资料
@@ -207,7 +231,7 @@
  return beforeUploadCheck(data, { allowedTypes: ['image/jpeg', 'image/png', 'image/jpg'] })
}
// 自定义上传请求
async function customRequest({ file, onFinish, onError, onProgress }: any, field: keyof typeof formModel.value) {
async function customRequest({ file, onFinish, onError, onProgress }: any, field: UploadField) {
  const res = await customUploadRequest({ file, onFinish, onError, onProgress })
  if (res) {
    // 不需要手动添加文件,v-model:file-list 会自动管理
@@ -220,14 +244,14 @@
}
// 删除文件
function handleRemove(data: { file: UploadFileInfo }, field: keyof typeof formModel.value) {
  const list = formModel.value[field] as UploadFileInfo[]
  formModel.value[field] = list.filter(item => item.id !== data.file.id) as any
function handleRemove(data: { file: UploadFileInfo }, field: UploadField) {
  const list = formModel.value[field]
  formModel.value[field] = list.filter(item => item.id !== data.file.id)
}
// 删除指定文件
function removeFile(index: number, field: keyof typeof formModel.value) {
  const list = formModel.value[field] as UploadFileInfo[]
function removeFile(index: number, field: UploadField) {
  const list = formModel.value[field]
  list.splice(index, 1)
}
@@ -254,65 +278,67 @@
// 处理保存
async function handleSave() {
  await formRef.value?.validate(async (errors) => {
    if (!errors) {
      try {
        saving.value = true
  try {
    await vehicleFormRef.value?.validate()
    await completeInfoFormRef.value?.validate()
  }
  catch {
    window.$message.error('请完善必填信息')
    return
  }
        const params: any = {
          vehicleCode: formModel.value.vehicleCode,
          vehicleVin: formModel.value.vin,
          plateNumber: formModel.value.plateNumber,
          vehicleColor: formModel.value.vehicleColor,
          vehicleModel: formModel.value.model,
          procurementSource: formModel.value.purchaseSource,
          remarks: formModel.value.remark,
          electronicId:formModel.value.operationArea,
          annualInspectionDate: formModel.value.inspectionExpireDate ? dayjs(formModel.value.inspectionExpireDate).format('YYYY-MM-DD HH:mm:ss') : null,
          insuranceCompany: formModel.value.insuranceCompany,
          insuranceEndTime: formModel.value.insuranceExpireDate ? dayjs(formModel.value.insuranceExpireDate).format('YYYY-MM-DD HH:mm:ss') : null,
          qualificationExpireTime: formModel.value.onlineCarLicenseExpireDate ? dayjs(formModel.value.onlineCarLicenseExpireDate).format('YYYY-MM-DD HH:mm:ss') : null,
          insurancePhone: formModel.value.insurancePhone,
        }
        // 处理附件
        const mapFiles = (files: UploadFileInfo[]) => {
          return JSON.stringify(files.map(f => ({
            id: f.id,
            name: f.name,
            url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || f.url,
          })))
        }
  try {
    saving.value = true
        if (formModel.value.vehiclePhotos.length > 0) {
          const photo = formModel.value.vehiclePhotos[0]
          params.vehiclePictures = JSON.stringify({
            id: photo.id,
            name: photo.name,
            url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || photo.url,
          })
        }
        params.inspectionAttachments = mapFiles(formModel.value.inspectionFiles)
        params.policyInformation = mapFiles(formModel.value.insuranceFiles)
        params.operationQualification = mapFiles(formModel.value.onlineCarLicenseFiles)
        const { code } = await addVehicle(params)
        if (code === 200) {
          window.$message.success('新增车辆成功')
          // 返回列表页
          router.go(-1)
        }
      }
      catch (error) {
        // 错误已经在 request 拦截器中处理
      }
      finally {
        saving.value = false
      }
    const params: any = {
      vehicleCode: formModel.value.vehicleCode,
      vehicleVin: formModel.value.vin,
      plateNumber: formModel.value.plateNumber,
      vehicleColor: formModel.value.vehicleColor,
      vehicleModel: formModel.value.model,
      procurementSource: formModel.value.purchaseSource,
      remarks: formModel.value.remark,
      electronicId: formModel.value.operationArea,
      annualInspectionDate: formModel.value.inspectionExpireDate ? dayjs(formModel.value.inspectionExpireDate).format('YYYY-MM-DD HH:mm:ss') : null,
      insuranceCompany: formModel.value.insuranceCompany,
      insuranceEndTime: formModel.value.insuranceExpireDate ? dayjs(formModel.value.insuranceExpireDate).format('YYYY-MM-DD HH:mm:ss') : null,
      qualificationExpireTime: formModel.value.onlineCarLicenseExpireDate ? dayjs(formModel.value.onlineCarLicenseExpireDate).format('YYYY-MM-DD HH:mm:ss') : null,
      insurancePhone: formModel.value.insurancePhone,
    }
    else {
      window.$message.error('请完善必填信息')
    // 处理附件
    const mapFiles = (files: UploadFileInfo[]) => {
      return JSON.stringify(files.map(f => ({
        id: f.id,
        name: f.name,
        url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || f.url,
      })))
    }
  })
    if (formModel.value.vehiclePhotos.length > 0) {
      const photo = formModel.value.vehiclePhotos[0]
      params.vehiclePictures = JSON.stringify({
        id: photo.id,
        name: photo.name,
        url: 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' || photo.url,
      })
    }
    params.inspectionAttachments = mapFiles(formModel.value.inspectionFiles)
    params.policyInformation = mapFiles(formModel.value.insuranceFiles)
    params.operationQualification = mapFiles(formModel.value.onlineCarLicenseFiles)
    const { code } = await addVehicle(params)
    if (code === 200) {
      window.$message.success('新增车辆成功')
      // 返回列表页
      router.go(-1)
    }
  }
  catch (error) {
    // 错误已经在 request 拦截器中处理
  }
  finally {
    saving.value = false
  }
}
// 处理返回
@@ -323,14 +349,15 @@
// 监听年检/保险到期时间变化,触发附件验证
watch([
  () => formModel.value.inspectionExpireDate,
  () => formModel.value.insuranceCompany,
  () => formModel.value.insuranceExpireDate,
  () => formModel.value.insurancePhone,
  () => formModel.value.onlineCarLicenseExpireDate,
], () => {
  formRef.value?.validate(
    (errors) => {
      // 验证完成
    },
  completeInfoFormRef.value?.validate(
    () => {},
    (rule) => {
      return rule?.key === 'inspectionFiles'
      return ['inspectionFiles', 'insuranceFiles', 'onlineCarLicenseFiles'].includes(String(rule?.key))
    },
  )
})
@@ -347,7 +374,7 @@
          </div>
        </div>
        <n-form
          ref="formRef" :model="formModel" :rules="formRules" label-placement="left" :show-feedback="false"
          ref="vehicleFormRef" :model="formModel" :rules="formRules" label-placement="left"
          in-line require-mark-placement="left"
        >
          <div class="vehicle-info-layout">
@@ -383,7 +410,7 @@
            <!-- 右侧:表单 -->
            <div class="form-section">
              <n-grid :cols="3" :x-gap="24" :y-gap="16">
              <n-grid :cols="3" :x-gap="24" :y-gap="8">
                <!-- 车辆VIN码 -->
                <n-gi>
                  <n-form-item label="车辆VIN码" path="vin">
@@ -456,7 +483,7 @@
          </div>
        </div>
        <n-form
          ref="formRef" :model="formModel" :rules="formRules" label-placement="left" :show-feedback="false"
          ref="completeInfoFormRef" :model="formModel" :rules="formRules" label-placement="left"
          in-line require-mark-placement="left"
        >
          <!-- 年检到期时间 - 单独一行 -->
@@ -740,14 +767,14 @@
/* 完善信息表单布局 */
.form-row {
  margin-bottom: 16px;
  margin-bottom: 8px;
}
.form-row-group {
  display: flex;
  justify-content: flex-start;
  gap: 24px;
  margin-bottom: 16px;
  margin-bottom:8px;
}
.form-item-inline {
src/views/assetManagement/vehicleManagement/inventoryVehicle/index.vue
@@ -5,11 +5,12 @@
import dayjs from 'dayjs'
import { useBoolean } from '@/hooks'
import { downloadFile } from '@/utils'
import { batchOperation, batchSetElectronic, deleteVehicle, exportVehicleExcel, getVehiclePageList, importVehicleExcel } from '@/service/api/vehicle'
import { batchOperation, batchSetElectronic, deleteVehicle, exportVehicleExcel, getVehicleImportTemplate, getVehiclePageList, importVehicleExcel } from '@/service/api/vehicle'
import { fetchElectronicFenceAreaList } from '@/service/api/point'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import CompleteVehicleInfoModal from './components/CompleteVehicleInfoModal.vue'
import EditVehicleModal from './components/EditVehicleModal.vue'
import ConfirmDialog from '@/components/ConfirmDialog/index.vue'
const router = useRouter()
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
@@ -34,7 +35,7 @@
const vehicleStatusOptions = [
  { label: '待完善信息', value: '1' },
  { label: '待运营', value: '2' },
  { label: '保险过期', value: '3' },
  { label: '保险/年检过期', value: '3' },
]
const filterItems = computed(() => [
@@ -79,7 +80,7 @@
const statusMap: Record<string, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = {
  1: { type: 'warning', text: '待完善信息' },
  2: { type: 'info', text: '待运营' },
  3: { type: 'error', text: '保险过期' },
  3: { type: 'error', text: '保险/年检过期' },
}
// 运营区域选项
@@ -126,6 +127,18 @@
const editVehicle = ref<any>(null)
const editMode = ref<'add' | 'edit'>('edit')
// 通用二次确认对话框
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
  title: '',
  description: '',
  cancelText: '取消',
  confirmText: '确定',
  onConfirm: async () => {},
  onCancel: () => {
    showConfirmDialog.value = false
  },
})
// 表单验证规则
const areaFormRules = {
  operationArea: {
@@ -358,12 +371,44 @@
    return
  }
  const idStr = selectedVehicles.value.map(item => item.id).join(',')
  const res = await batchOperation(idStr)
  if (res && (res as any).code === 200) {
    window.$message.success('批量运营成功')
    selectedVehicles.value = []
    checkedRowKeys.value = []
    getVehicleList()
  confirmDialogConfig.value = {
    title: '确认开始运营',
    description: `确认对选中的 ${selectedVehicles.value.length} 辆车辆执行开始运营吗?`,
    cancelText: '取消',
    confirmText: '确定',
    onConfirm: async () => {
      const res = await batchOperation(idStr)
      if (res && (res as any).code === 200) {
        window.$message.success('批量运营成功')
        selectedVehicles.value = []
        checkedRowKeys.value = []
        getVehicleList()
      } else {
        window.$message.error((res as any).message || '批量运营失败')
      }
      showConfirmDialog.value = false
    },
    onCancel: () => {
      showConfirmDialog.value = false
    },
  }
  showConfirmDialog.value = true
}
// 下载模板
async function handleDownloadTemplate() {
  try {
    const response = await getVehicleImportTemplate()
    if (response instanceof Blob) {
      downloadFile(response, `车辆导入模板_${new Date().getTime()}.xlsx`)
      window.$message.success('下载成功')
    }
    else {
      window.$message.error('下载失败:接口未返回正确的文件模板')
    }
  }
  catch (error) {
    console.error('下载模板失败:', error)
  }
}
@@ -431,11 +476,26 @@
    window.$message.warning('车辆信息不完整,无法开始运营')
    return
  }
  const res = await batchOperation(String(row.id))
  if (res && (res as any).code === 200) {
    window.$message.success(`开始运营: ${row.plateNumber}`)
    getVehicleList()
  confirmDialogConfig.value = {
    title: '确认开始运营',
    description: `确认对车辆 ${row.plateNumber || row.vehicleCode || row.id} 执行开始运营吗?`,
    cancelText: '取消',
    confirmText: '确定',
    onConfirm: async () => {
      const res = await batchOperation(String(row.id))
      if (res && (res as any).code === 200) {
        window.$message.success(`开始运营: ${row.plateNumber}`)
        getVehicleList()
      } else {
        window.$message.error((res as any).message || '开始运营失败')
      }
      showConfirmDialog.value = false
    },
    onCancel: () => {
      showConfirmDialog.value = false
    },
  }
  showConfirmDialog.value = true
}
function handleUpdateInfo(row: any) {
@@ -445,12 +505,11 @@
}
function handleDetail(row: any) {
  const payload = window.btoa(encodeURIComponent(JSON.stringify(row)))
  router.push({
    name: 'operatingVehicleDetail',
    query: {
      id: row.id,
      payload,
      type: 'inventory',
    },
  })
}
@@ -537,6 +596,9 @@
              </NButton>
            </NSpace>
            <NSpace>
              <NButton v-permission="50114" @click="handleDownloadTemplate">
                下载导入模板
              </NButton>
              <NButton v-permission="50114" @click="handleImport">
                导入
              </NButton>
@@ -567,6 +629,17 @@
      </n-card>
    </NSpace>
    <!-- 通用确认弹窗 -->
    <ConfirmDialog
      v-model:visible="showConfirmDialog"
      :title="confirmDialogConfig.title"
      :description="confirmDialogConfig.description"
      :cancel-text="confirmDialogConfig.cancelText"
      :confirm-text="confirmDialogConfig.confirmText"
      @confirm="confirmDialogConfig.onConfirm"
      @cancel="confirmDialogConfig.onCancel"
    />
    <!-- 批量设置运营区域弹窗 -->
    <n-modal
      v-model:show="showAreaModal"
src/views/assetManagement/vehicleManagement/operatingVehicle/car-detail.txt
New file
@@ -0,0 +1,104 @@
acceptOrTest    integer($int32)
当前模式 1接单 2测试
acceptOrTestStatus    integer($int32)
模式状态 1开始接单/开始测试 2停止接单/停止测试
annualInspectionDate    string($date-time)
年检到期时间
createBy    string
记录创建人,前端忽略
createTime    string($date-time)
记录创建时间,前端忽略
currentElectricity    integer($int32)
当前电量
deleted    boolean
是否删除 0未删除 1已删除
electronicId    integer($int64)
围栏id
electronicName    string
运营区域
fleetId    integer($int64)
车队id
id    integer($int64)
自增id
inspectionAttachments    string
年检资料附件
insuranceCompany    string
保险公司
insuranceEndTime    string($date-time)
保险到期时间
insurancePhone    string
保险电话
mileageRange    number($float)
续航公里
operatingStatus    integer($int32)
运营状态 1运营中 2维修中 3已收车 4车辆异常
operationQualification    string
运营资质
plateNumber    string
车牌号
policyInformation    string
保单资料
procurementSource    string
采购来源
qualificationExpireTime    string($date-time)
运营资质到期时间
remarks    string
备注
state    integer($int32)
车辆接单状态 1接单中 0空闲
status    integer($int32)
车辆状态 1=待完善 2=待运营 3=保险/年检过期
updateBy    string
记录修改人,前端忽略
updateTime    string($date-time)
记录修改时间,前端忽略
vehicleCode    string
车辆编码
vehicleColor    string
车辆颜色
vehicleModel    string
型号
vehiclePictures    string
车辆图片
vehicleType    integer($int32)
车辆类型 1=在库 2=运营
vehicleVin    string
车辆vin码
warehousingTime    string($date-time)
入库时间
xpuId    string
车辆xpuid
src/views/assetManagement/vehicleManagement/operatingVehicle/detail.vue
@@ -9,6 +9,7 @@
import TaskDetailModal from './components/TaskDetailModal.vue'
import Pagination from '@/components/common/Pagination.vue'
import dayjs from 'dayjs'
import { getVehicleById } from '@/service/api/vehicle'
import { deleteChargingRecordById, deleteTaskById, queryChargingByVehicleId, queryDetectionByVehicleId, queryPreparationByVehicleId, queryRepairByVehicleId, queryTaskByVehicleId, queryTaskRoutineByVehicleId } from '@/service/api/vehicleConvoy'
import { parseFileJson } from '@/utils/common'
@@ -16,7 +17,8 @@
const router = useRouter()
const { t } = useI18n()
const payload = computed(() => route.query.payload as string | undefined)
const detailId = computed(() => route.query.id as string | number | undefined)
const detailType = computed(() => route.query.type as string | undefined)
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
@@ -38,8 +40,8 @@
  vehicleModel: '',
  color: '',
  purchaseSource: '',
  operatingArea: '',
  vehicleStatus: 'operating',
  electronicName: '',
  status: 1,
  operatingStatus: 1,
  abnormalStatus: 'normal',
  inspectionExpireDate: '',
@@ -48,16 +50,14 @@
  remark: '',
  vehicleImage: '',
  needCompleteInfo: false,
  inspectionOrInsuranceExpired: false,
  isSecondHand: false,
})
// 车辆状态映射
const vehicleStatusMap: Record<string, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = {
  operating: { type: 'info', text: '运营' },
  inventory: { type: 'default', text: '在库' },
  recalled: { type: 'info', text: '已收车' },
  scrapped: { type: 'error', text: '已报废' },
const vehicleStatusMap: Record<number, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = {
  1: { type: 'error', text: '待完善信息' },
  2: { type: 'success', text: '待运营' },
  3: { type: 'warning', text: '保险/年检过期' },
}
const operatingStatusMap: Record<number, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = {
@@ -67,13 +67,21 @@
  4: { type: 'error', text: '车辆异常' },
}
// 车辆异常状态映射
const abnormalStatusMap: Record<string, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = {
  normal: { type: 'success', text: '正常' },
  repairing: { type: 'error', text: '维修中' },
  malfunction: { type: 'error', text: '故障' },
  accident: { type: 'error', text: '事故' },
}
const vehicleTag = computed(() => {
  if (detailType.value === 'operating') {
    return { text: '运营', cls: 'status--operating' }
  }
  const m = vehicleStatusMap[vehicleDetail.value.status]
  return { text: m?.text || '-', cls: `status--${m?.type || 'default'}` }
})
const operatingTag = computed(() => {
  if (detailType.value === 'inventory') {
    return { text: '在库', cls: 'status--inventory' }
  }
  const m = operatingStatusMap[vehicleDetail.value.operatingStatus]
  return { text: m?.text || '-', cls: `status--${m?.type || 'default'}` }
})
// 年检记录搜索条件
const inspectionSearchModel = ref({
@@ -359,33 +367,35 @@
async function loadVehicleDetail() {
  startLoading()
  try {
    if (payload.value) {
      const jsonStr = decodeURIComponent(window.atob(String(payload.value)))
      const row = JSON.parse(jsonStr)
      console.log('111111111111',row)
      const imgs = parseFileJson(row.vehiclePictures)
    if (!detailId.value) {
      window.$message.error('缺少车辆ID,无法加载详情')
      return
    }
    const { data } = await getVehicleById(detailId.value)
    if (data) {
      const imgs = parseFileJson(data.vehiclePictures)
      vehicleDetail.value = {
        id: row.id,
        id: data.id,
        vehicleImage: imgs.length > 0 ? imgs[0].url : '',
        vehicleCode: row.vehicleCode || '',
        plateNumber: row.plateNumber || '',
        vinCode: row.vehicleVin || '',
        vehicleModel: row.vehicleModel || '',
        color: row.vehicleColor || '',
        purchaseSource: row.procurementSource || '',
        operatingArea: row.electronicName || '',
        vehicleStatus: row.vehicleType === 2 ? 'operating' : 'inventory',
        operatingStatus: row.operatingStatus || 1,
        vehicleCode: data.vehicleCode || '',
        plateNumber: data.plateNumber || '',
        vinCode: data.vehicleVin || '',
        vehicleModel: data.vehicleModel || '',
        color: data.vehicleColor || '',
        purchaseSource: data.procurementSource || '',
        electronicId: data.electronicId,
        electronicName: data.electronicName || '',
        status: data.status ?? vehicleDetail.value.status,
        operatingStatus: data.operatingStatus || vehicleDetail.value.operatingStatus,
        abnormalStatus: 'normal',
        inspectionExpireDate: row.annualInspectionDate || '',
        insuranceExpireDate: row.insuranceEndTime || '',
        entryTime: row.warehousingTime || '',
        remark: row.remarks || '',
        inspectionExpireDate: data.annualInspectionDate || '',
        insuranceExpireDate: data.insuranceEndTime || '',
        entryTime: data.warehousingTime || '',
        remark: data.remarks || '',
        needCompleteInfo: false,
        inspectionOrInsuranceExpired: false,
        isSecondHand: false,
      }
      // 状态直接使用接口返回的 data.status 与 data.operatingStatus
    }
    if (vehicleDetail.value.id) {
      await Promise.all([
@@ -622,11 +632,6 @@
  router.back()
}
// 查看容器备/信息
function handleViewContainer() {
  window.$message.info('查看容器备/信息')
}
// 已收车
function handleRecall() {
  window.$dialog.warning({
@@ -642,7 +647,13 @@
// 查看运营区域
function handleViewArea() {
  window.$message.info('查看运营区域')
  router.push({
    path: '/businessOperations/geofenceManagement/detail',
    query: {
      type:'detail',
      id: vehicleDetail.value.electronicId,
    },
  })
}
// 搜索年检记录
@@ -1083,15 +1094,8 @@
                <div class="plate-number">
                  {{ vehicleDetail.plateNumber }}
                </div>
                <NTag v-if="vehicleDetail.needCompleteInfo" type="warning" size="small" class="tag-info">
                  待完善信息
                </NTag>
                <NTag v-if="vehicleDetail.inspectionOrInsuranceExpired" type="error" size="small" class="tag-info">
                  年检/保险已过期
                </NTag>
                <NTag :type="vehicleStatusMap[vehicleDetail.vehicleStatus]?.type" size="small" class="tag-info">
                  {{ vehicleStatusMap[vehicleDetail.vehicleStatus]?.text }}
                </NTag>
                <span class="status-chip" :class="vehicleTag.cls">{{ vehicleTag.text }}</span>
                <span class="status-chip" :class="operatingTag.cls">{{ operatingTag.text }}</span>
              </div>
              <div class="basic-info-row">
                <span class="basic-label">{{ vehicleDetail.color || '白色' }}</span>
@@ -1129,9 +1133,9 @@
            </div>
            <div class="detail-item">
              <span class="detail-label">运营区域:</span>
              <span class="detail-value">{{ vehicleDetail.operatingArea || '-' }}</span>
              <span class="detail-value area-ellipsis">{{ vehicleDetail.electronicName || '-' }}</span>
              <NButton
                v-if="vehicleDetail.operatingArea" text type="primary" size="small"
                v-if="vehicleDetail.electronicName" text type="primary" size="small"
                style="padding: 0; margin-left: 4px;" @click="handleViewArea"
              >
                查看
@@ -1588,6 +1592,10 @@
          .detail-value {
            color: rgba(23, 31, 38, 1);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            max-width: 300px;
          }
          .expire-warning {
@@ -1605,6 +1613,44 @@
    }
  }
  .status-chip {
    display: inline-flex;
    align-items: center;
    height: 20px;
    line-height: 22px;
    padding: 0 8px;
    border-radius: 4px;
    font-size: 12px;
    margin-left: 8px;
  }
  .status--default {
    background: rgba(230, 244, 255, 1);
    color:rgba(0, 104, 255, 1)
  }
  .status--info {
     background:rgba(220, 245, 221, 1);
    color: rgba(11, 181, 45, 1)
  }
  .status--warning {
    background: rgba(255, 236, 230, 1);
    color:rgba(224, 39, 22, 1)}
  .status--error {
     background: rgba(255, 248, 230, 1);
    color:rgba(255, 144, 0, 1)
  }
    .status--success {
    background: rgba(230, 244, 255, 1);
    color:rgba(0, 104, 255, 1)
  }
  .status--operating {
    background: rgba(230, 244, 255, 1);
    color:rgba(0, 104, 255, 1)
  }
  .status--inventory {
    background: rgba(230, 244, 255, 1);
    color:rgba(0, 104, 255, 1)
  }
  .search-form-card {
    border-radius: 4px;
    margin-bottom: 12px;
src/views/assetManagement/vehicleManagement/operatingVehicle/index.vue
@@ -1,6 +1,7 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NPopconfirm, NSpace, NTag } from 'naive-ui'
import ConfirmDialog from '@/components/ConfirmDialog/index.vue'
import { useRouter } from 'vue-router'
import { useBoolean } from '@/hooks'
import { downloadFile } from '@/utils'
@@ -28,6 +29,8 @@
function handleResetSearch() {
  model.value = { ...initialModel }
  pageParams.value.pageNum = 1
  getOperatingVehicleList()
}
const formRef = ref<FormInst | null>()
@@ -77,6 +80,16 @@
const count = ref(0)
const listData = ref<any[]>([])
const selectedRowKeys = ref<number[]>([])
const showConfirmDialog = ref(false)
const confirmDialogConfig = ref({
  title: '',
  description: '',
  cancelText: '取消',
  confirmText: '确定',
  onConfirm: async () => {},
  onCancel: () => { showConfirmDialog.value = false },
})
const statusMap: Record<number, { type: 'default' | 'success' | 'info' | 'warning' | 'error', text: string }> = {
  1: { type: 'success', text: '运营中' },
@@ -222,45 +235,50 @@
    window.$message.warning('请先选择要转为在库车辆的数据')
    return
  }
  window.$dialog.warning({
  confirmDialogConfig.value = {
    title: '批量转为在库车辆',
    content: `确认将选中的 ${selectedRowKeys.value.length} 辆车转为在库车辆吗?`,
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
    description: `确认将选中的 ${selectedRowKeys.value.length} 辆车转为在库车辆吗?`,
    cancelText: '取消',
    confirmText: '确定',
    onConfirm: async () => {
      const { code } = await batchStock({ idStr: selectedRowKeys.value.join(',') })
      if (code==200) {
        window.$message.success(`已成功将 ${selectedRowKeys.value.length} 辆车转为在库车辆`)
        selectedRowKeys.value = []
        getOperatingVehicleList()
      }
      showConfirmDialog.value = false
    },
  })
    onCancel: () => { showConfirmDialog.value = false },
  }
  showConfirmDialog.value = true
}
function handleConvertToInventory(row: any) {
  window.$dialog.warning({
  confirmDialogConfig.value = {
    title: '转为在库车辆',
    content: `确认将车辆 ${row.plateNumber} 转为在库车辆吗?`,
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
    description: `确认将车辆 ${row.plateNumber} 转为在库车辆吗?`,
    cancelText: '取消',
    confirmText: '确定',
    onConfirm: async () => {
      const { code } = await batchStock({ idStr: String(row.id) })
      if (code==200) {
        window.$message.success(`车辆 ${row.plateNumber} 已转为在库车辆`)
        getOperatingVehicleList()
      }
      showConfirmDialog.value = false
    },
  })
    onCancel: () => { showConfirmDialog.value = false },
  }
  showConfirmDialog.value = true
}
function handleViewDetail(row: any) {
  const payload = window.btoa(encodeURIComponent(JSON.stringify(row)))
  router.push({
    name: 'operatingVehicleDetail',
    query: {
      id: row.id,
      payload,
      type: 'operating',
    },
  })
}
@@ -358,6 +376,15 @@
            :header-height="18"
            :row-height="18"
          />
          <ConfirmDialog
            v-model:visible="showConfirmDialog"
            :title="confirmDialogConfig.title"
            :description="confirmDialogConfig.description"
            :cancel-text="confirmDialogConfig.cancelText"
            :confirm-text="confirmDialogConfig.confirmText"
            @confirm="confirmDialogConfig.onConfirm"
            @cancel="confirmDialogConfig.onCancel"
          />
          <Pagination :count="count" @change="changePage" />
        </NSpace>
      </n-card>
src/views/businessOperations/geofenceManagement/detail.vue
@@ -255,6 +255,23 @@
      drawDisallowedAreaPolygon(id)
      return
    }
    // 检查禁行区域是否在运营区域内
    if (!fence) {
      message.warning('请先绘制运营区域')
      map.remove(e.obj)
      drawDisallowedAreaPolygon(id)
      return
    }
    const operationPath = fence.getPath()
    if (!isDisallowedAreaInOperationArea(e.obj.getPath(), operationPath)) {
      message.warning('禁行区域必须在运营区域内')
      map.remove(e.obj)
      drawDisallowedAreaPolygon(id)
      return
    }
    const disallowedArea = disallowedAreaList.value.find((sub: any) => sub.id === id)
    disallowedArea.fence = e.obj
    disallowedArea.area = area
@@ -305,34 +322,64 @@
      e.target.setPath(path);
    }
  });
  /** 拖拽/调整时校验面积 **/
  /** 拖拽/调整时校验面积和是否在运营区域内 **/
  fenceEditor.on('adjust', (e: any) => {
    const path = e.target.getPath()
    const area = calcPolygonArea(path)
    // 检查面积限制
    if (area > MAX_AREA) {
      message.warning(`禁行区域面积不能超过${MAX_AREA}平方公里`)
      e.target.setPath(disallowedArea.lastValidPath)
      closeFenceEditor()
      handleDisallowedAreaEdit(disallowedArea)
    } else {
      disallowedArea.lastValidPath = path
      disallowedArea.area = area
      return
    }
    // 检查是否在运营区域内
    if (fence) {
      const operationPath = fence.getPath()
      if (!isDisallowedAreaInOperationArea(path, operationPath)) {
        message.warning('禁行区域必须在运营区域内')
        e.target.setPath(disallowedArea.lastValidPath)
        closeFenceEditor()
        handleDisallowedAreaEdit(disallowedArea)
        return
      }
    }
    disallowedArea.lastValidPath = path
    disallowedArea.area = area
  })
  /** 编辑完成后最终校验(兜底) **/
  fenceEditor.on('end', (e: any) => {
    const path = e.target.getPath()
    const area = calcPolygonArea(path)
    // 检查面积限制
    if (area > MAX_AREA) {
      message.warning(`禁行区域面积不能超过${MAX_AREA}平方公里`)
      e.target.setPath(disallowedArea.lastValidPath)
      closeFenceEditor()
      handleDisallowedAreaEdit(disallowedArea)
    } else {
      disallowedArea.lastValidPath = path
      disallowedArea.area = area
      return
    }
    // 检查是否在运营区域内
    if (fence) {
      const operationPath = fence.getPath()
      if (!isDisallowedAreaInOperationArea(path, operationPath)) {
        message.warning('禁行区域必须在运营区域内')
        e.target.setPath(disallowedArea.lastValidPath)
        closeFenceEditor()
        handleDisallowedAreaEdit(disallowedArea)
        return
      }
    }
    disallowedArea.lastValidPath = path
    disallowedArea.area = area
  })
}
@@ -485,6 +532,15 @@
  )
}
// 判断禁行区域是否完全在运营区域内
function isDisallowedAreaInOperationArea(disallowedPath: any, operationPath: any): boolean {
  // 使用高德地图的几何工具来判断禁行区域是否完全在运营区域内
  return AMap.GeometryUtil.isRingInRing(
    disallowedPath,
    operationPath
  )
}
// ==================== 图层切换 ====================
const layerOptions = reactive({
  isActive: false,  // 默认为关闭状态
@@ -528,7 +584,13 @@
// 居中定位
const centerMap = () => {
  if (map) {
    map.setCenter(centerPoint);
    // 如果是详情或编辑模式
    if (modalType.value === 'detail' || modalType.value === 'edit') {
      map.setFitView([fence], false, [220, 220, 220, 220])
    } else {
      map.setCenter(centerPoint);
      map.setZoom(14);
    }
  }
}
// 放大
@@ -596,14 +658,65 @@
  effectTime: {
    type: 'number',
    required: true,
    message: '请选择生效时间',
    trigger: ['blur', 'change'],
    validator: (_rule: any, value: any) => {
      // 验证不能为空
      if (value == null) {
        return new Error('请选择生效时间')
      }
      // 如果是立即生效,则跳过时间比较验证
      if (value === IMMEDIATE_EFFECT) {
        return true
      }
      // 如果是具体时间值,则验证不能早于当前时间
      if (typeof value === 'number') {
        // 计算当前时间加一分钟作为最小允许时间
        const now = new Date();
        const minAllowedDateTime = new Date(
          now.getFullYear(),
          now.getMonth(),
          now.getDate(),
          now.getHours(),
          now.getMinutes() + 1, // 当前时间加一分钟
          0, // 秒数设为0
          0  // 毫秒设为0
        );
        if (value < minAllowedDateTime.getTime()) {
          return new Error('生效时间不能早于当前时间')
        }
      }
      return true
    },
  } as FormItemRule,
  failureTime: {
    type: 'number',
    required: true,
    message: '请选择失效时间',
    trigger: ['blur', 'change'],
    validator: (_rule: any, value: any) => {
      // 验证不能为空
      if (value == null) {
        return new Error('请选择失效时间')
      }
      // 如果是永不失效,则跳过时间比较验证
      if (value === NEVER_EXPIRE) {
        return true
      }
      // 如果生效时间是立即生效(IMMEDIATE_EFFECT),则失效时间需要大于当前时间
      if (formModel.value.effectTime === IMMEDIATE_EFFECT && typeof value === 'number') {
        const currentTime = Date.now()
        if (value <= currentTime) {
          return new Error('失效时间不能小于当前时间')
        }
        return true
      }
      // 如果生效时间和失效时间都是具体时间,则失效时间需要大于生效时间
      if (typeof formModel.value.effectTime === 'number' && typeof value === 'number') {
        if (value <= formModel.value.effectTime) {
          return new Error('失效时间必须大于生效时间')
        }
      }
      return true
    },
  } as FormItemRule,
}
@@ -778,9 +891,9 @@
  disallowedAreaList.value.forEach((sub: any) => {
    sub.isActive = false
  })
  const id = disallowedAreaList.value.length + 1
  const id = 'temp' + disallowedAreaList.value.length + 1
  disallowedAreaList.value.push({
    areaName: `禁行区${id}`,
    areaName: `禁行区${disallowedAreaList.value.length + 1}`,
    areaStatus: 1,
    isActive: true,
    edit: false,
@@ -822,6 +935,10 @@
/** 保存草稿/发布围栏 */
const handleAddFence = (type: number) => {
  fencePoints = fence.getPath().map((p: any) => [p.lng, p.lat])
  if (disallowedAreaList.value.some((item: any) => item.fence === null)) {
    message.error('请先绘制禁行区')
    return
  }
  if (isPolygonSelfIntersect(fencePoints)) {
    message.error('围栏存在自相交,请调整形状')
    return
@@ -851,7 +968,33 @@
    message.error(`${disallowedAreaIntersect.name} 与运营区相交,请调整形状`)
    return
  }
  // 禁行区不能在运营区外
  const disallowedAreaOutside = disallowedAreaList.value.find((item: any) => {
    const points = item.fence.getPath().map((p: any) => [p.lng, p.lat])
    return !isPolygonContained(points, fencePoints)
  })
  if (disallowedAreaOutside) {
    message.error(`禁行区 ${disallowedAreaOutside.areaName} 必须在运营区范围内`)
    return
  }
  formRef.value.validate().then(() => {
    // 验证运营策略时间段
    for (const item of groupList.value) {
      for (const sub of item.time) {
        if (sub.startTime && sub.endTime) {
          // 比较开始时间和结束时间
          if (sub.startTime >= sub.endTime) {
            message.error('运营策略时间段中,结束时间不能小于或等于开始时间')
            return
          }
        } else if (!sub.startTime || !sub.endTime) {
          message.error('运营策略时间段中,开始时间和结束时间不能为空')
          return
        }
      }
    }
    // 策略组
    const operationStrategy = groupList.value.map((item: any) => ({
      name: item.name,
@@ -867,6 +1010,7 @@
      areaType: 1,
      fencePoints: fencePoints.map((item: any) => (item[0] + '_' + item[1])).join(','),
      area: calcPolygonArea(fencePoints).toFixed(2),
      id: fenceDetail.value.electronicFenceAreas?.find((item: any) => item.areaType === 1)?.id ?? null,
    }]
    disallowedAreaList.value.forEach((item: any) => {
      electronicFenceAreas.push({
@@ -876,13 +1020,14 @@
        areaType: 2,
        fencePoints: item.fence.getPath().map((p: any) => (p.lng + '_' + p.lat)).join(','),
        area: item.area.toFixed(2),
        id: item.id.startsWith('temp') ? null : item.id,
      })
    })
    // 组装请求参数
    const params = {
      status: modalType.value !== 'add' && formModel.value.status !== 0 && formModel.value.status !== 1 ?  (formModel.value.areaStatus === 1 ? 2 : 3  ) : type,
      // status: type === 0 ? type : formModel.value.areaStatus === 1 ? 1 : 3 , // 0: 草稿 1: 发布 2: 启用 3: 停用
      cityCode: 510100,
      cityCode: 0,
      // cityCode: 440100,
      area: calcPolygonArea(fencePoints).toFixed(2),
      electronicName: formModel.value.electronicName,
@@ -997,6 +1142,7 @@
        infoWindow = new AMap.InfoWindow({
          isCustom: true,
          content: '<div class="map-info"><div class="map-info-name">' + item.areaName + '</div><img class="map-info-icon" src="/src/assets/geofence/down.png" alt="图标"></div>',
          autoMove: false
        });
        // 打开信息窗口
@@ -1302,7 +1448,7 @@
<template>
  <div class="w-full h-full geofence-detail-page" style="position: relative">
    <!-- 地图容器 -->
    <div ref="domRef" class="w-full h-full" />
    <div ref="domRef" class="w-full h-full" :style="drawerVisible ? 'width: calc(100% - 280px);' : ''" />
    <!-- 提示 -->
    <div class="tips" v-if="modalType === 'add'">
      {{ !isActiceDisallowedArea ? '请在地图上【非运营区域】绘制多边形区域,双击闭合' : '请在地图上【蓝色运营区域】绘制多边形区域,双击闭合' }}
@@ -1317,7 +1463,7 @@
        <img src="../../../assets/geofence/foldUp.png" class="icon" @click="toggleDrawer" v-else>
      </div>
      <div class="drawer-content">
        <div class="drawer-head" v-if="(modalType === 'edit' || modalType === 'detail') && showDrawerHead">
        <!-- <div class="drawer-head" v-if="(modalType === 'edit' || modalType === 'detail') && showDrawerHead">
          <div class="title" v-if="fenceDetail">{{ fenceDetail?.electronicName || '' }}
            <NTag style="font-weight: 500;"
              :type="fenceDetail?.status === 0 || fenceDetail?.status === 3 ? 'default' : fenceDetail?.status === 2 ? 'success' : 'warning'"
@@ -1331,11 +1477,10 @@
        <div class="drawer-head" v-if="modalType === 'add' && showDrawerHead">
          <div class="title">围栏详情</div>
          <img src="../../../assets/geofence/close2.png" class="w-24px h-24px ml-auto cursor-pointer" @click="onClose">
        </div>
        </div> -->
        <div class="drawer-main" ref="mainScrollRef">
          <!-- <div class="main-head" v-if="!showDrawerHead"> -->
          <div class="main-head">
            <div class="flex items-center gap-4px mb-4px" v-if="modalType === 'add'">
          <div class="main-head" :class="{ 'sticky-top': showDrawerHead }">
            <div class="flex items-center gap-4px mb-16px" v-if="modalType === 'add'">
              <div class="title">围栏详情</div>
              <n-switch v-model:value="formModel.areaStatus" :checked-value="1" :unchecked-value="2">
                <template #checked>
@@ -1349,7 +1494,7 @@
                @click="onClose">
            </div>
            <div class="flex items-center gap-4px mb-4px" v-else>
              <div class="title" v-if="fenceDetail">
              <div class="title items-center" v-if="fenceDetail">
                {{ fenceDetail.electronicName || '' }}
                <NTag style="font-weight: 500;"
                  :type="fenceDetail.status === 0 || fenceDetail.status === 3 ? 'default' : fenceDetail.status === 2 ? 'success' : 'warning'"
@@ -1360,26 +1505,28 @@
                </NTag>
                <n-switch v-model:value="formModel.areaStatus" :checked-value="1" :unchecked-value="2" :disabled="fenceDetail.status === 0 || fenceDetail.status === 1" class="text-13px" v-if="modalType !== 'detail'">
                  <template #checked>
                    启用
                  </template>
                  启用
                </template>
                  <template #unchecked>
                    停用
                  </template>
                  停用
                </template>
                </n-switch>
              </div>
            </div>
            <div class="head-des" v-if="modalType !== 'add' && fenceDetail">围栏ID: {{ fenceDetail.electronicNumber || '-' }}</div>
            <div class="head-des" v-if="modalType !== 'add' && fenceDetail">覆盖面积: {{ fenceDetail.area || '-' }}km²</div>
          </div>
          <div class="flex flex-col gap-4px px-12px pb-16px" v-if="modalType !== 'add' && fenceDetail">
            <div class="head-des">围栏ID: {{ fenceDetail.electronicNumber || '-' }}</div>
            <div class="head-des">覆盖面积: {{ fenceDetail.area || '-' }}km²</div>
          </div>
          <div class="line"></div>
          <div v-if="modalType === 'detail' && fenceDetail">
            <div class="gap-16px flex flex-col">
            <div class="gap-16px flex flex-col px-12px py-16px">
              <!-- 基本信息 -->
              <div class="box flex flex-col">
                <div class="box-title mb-8px">基本信息</div>
                <div class="box-item mb-8px">
                  <div class="label">区域类型</div>
                  <div class="value">{{regionTypeOptions.find(item => item.value === fenceDetail.areaType)?.label || ''
                  <div class="value">{{regionTypeOptions.find(item => item.value === fenceDetail.electronicFenceAreas?.[0].areaType)?.label || ''
                  }}</div>
                </div>
                <div class="box-item">
@@ -1464,7 +1611,7 @@
              </div>
            </div>
          </div>
          <n-form ref="formRef" :rules="rules" :model="formModel" require-mark-placement="left" v-else>
          <n-form class="px-12px py-16px" ref="formRef" :rules="rules" :model="formModel" require-mark-placement="left" v-else>
            <div class="gap-16px flex flex-col">
              <!-- 基本信息 -->
              <div class="box flex flex-col">
@@ -1696,9 +1843,37 @@
  left: 50%;
  transform: translateX(-50%);
}
.n-pro-layout--full-content {
  .geofence-detail-page {
    margin-top: -76px;
    height: 100vh;
  }
}
</style>
<style lang="scss" scoped>
@use './index.scss';
.main-head {
  // padding: 16px;
  padding: 16px 12px;
  padding-bottom: 0;
  background: white;
  z-index: 10;
  &.sticky-top {
    position: sticky;
    top: 0;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
}
.main-head.sticky-top {
  position: sticky;
  top: 0;
  z-index: 10;
  background: white;
  // padding: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tips {
  position: absolute;
src/views/businessOperations/geofenceManagement/index.scss
@@ -228,7 +228,7 @@
      background-color: #fff;
      display: flex;
      flex-direction: column;
      padding: 16px 12px;
      // padding: 16px 12px;
      // gap: 16px;
      scrollbar-width: none;
      /* Firefox */
@@ -261,8 +261,8 @@
      .line {
        border-top: 1px solid rgba(23, 31, 38, 0.15);
        width: 100%;
        margin: 16px 0;
        width: calc(100% - 24px);
        margin: 0 12px;
      }
      .box {
src/views/businessOperations/geofenceManagement/index.vue
@@ -44,14 +44,15 @@
const columns = computed<DataTableColumns<any>>(() => [
   {
    title: '围栏ID',
    key: 'id',
    key: 'electronicNumber',
    resizable: true,
    ellipsis: true
    minWidth: 120,
  },
  {
    title: '围栏名称',
    key: 'electronicName',
    resizable: true,
    minWidth: 150,
    ellipsis: true,
    render(row) {
      return (
@@ -74,6 +75,7 @@
    key: 'operatingAreaCount',
    resizable: true,
    ellipsis: true,
    minWidth: 180,
    render(row) {
      return (
          <div class='cursor-pointer text-ellipsis whitespace-nowrap w-full overflow-hidden'>
@@ -85,8 +87,8 @@
  {
    title: '状态',
    key: 'status',
    ellipsis: true,
    resizable: true,
    minWidth: 80,
    render(row) {
        return h(
          NTag,
@@ -104,7 +106,8 @@
    title: '更新人',
    key: 'updateBy',
    ellipsis: true,
    resizable: true
    resizable: true,
    minWidth: 100,
  },
  {
    title: '更新时间',
@@ -112,6 +115,7 @@
    ellipsis: true,
    resizable: true,
    sorter: true,
    minWidth: 200,
    renderSorterIcon({ order }) {
      let src = sortDefault
    if (order === 'ascend') src = sortAsc
@@ -126,7 +130,8 @@
    title: '创建人',
    key: 'createBy',
    ellipsis: true,
    resizable: true
    resizable: true,
    minWidth: 100,
  },
  {
    title: '创建时间',
@@ -134,6 +139,7 @@
    ellipsis: true,
    sorter: true,
    resizable: true,
    minWidth: 200,
    renderSorterIcon({ order }) {
      let src = sortDefault
    if (order === 'ascend') src = sortAsc
@@ -268,6 +274,7 @@
    label: '围栏状态',
    path: 'status',
    render: () => <n-select v-model:value={model.value.status} options={options} clearable />,
    // render: () => <n-select v-model:value={model.value.status} options={options} clearable multiple/>,
  },
  {
    label: '更新人',
src/views/businessOperations/pointManagement/index.scss
@@ -1114,6 +1114,25 @@
  flex-direction: column;
  background: #fff;
  position: relative;
  transition: width 0.3s ease;
  &.is-collapsed {
    width: 0;
    overflow: visible;
    border: none;
    background: transparent;
    // 隐藏除按钮外的所有内容
    > *:not(.collapse-btn) {
      display: none;
    }
    .collapse-btn {
      .collapse-icon {
        transform: rotate(180deg);
      }
    }
  }
}
.point-info {
src/views/businessOperations/pointManagement/index.vue
@@ -5,6 +5,12 @@
import { createPoint, deletePoint, enableDisablePoint, fetchElectronicFenceAreaList, fetchPointList, updatePoint } from '@/service/api/point'
import PointStatusSteps from '@/components/PointStatusSteps/custom.vue'
import ConfirmDialog from '@/components/ConfirmDialog/index.vue'
// 定义地图图标在线链接
const MARK_ICON_RED = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/36c926b7-1e6d-4310-86c2-a71b69dc49b7.png'
const MARK_ICON_GREEN = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/8f048a0f-3c36-4b6e-ae14-77d85bc919bf.png'
const MARK_ICON_BLUE = 'https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e7f49805-c8fd-42aa-9c92-af48d3ec8eec.png'
const router = useRouter()
const { VITE_MAP_KEY, VITE_MAP_SECURITY } = import.meta.env
@@ -43,6 +49,7 @@
  name: '',
  address: '',
  fullAddress: '',
  city: '', // 城市名称
  lon: '',
  lat: '',
})
@@ -96,6 +103,9 @@
// 点位详情面板状态:view-查看, edit-编辑, add-新增
const pointDetailMode = ref<'view' | 'edit' | 'add'>('view')
// 详情面板折叠状态
const isCollapsed = ref(false)
// 点位状态是否已被本地修改
const isStatusModified = ref(false)
@@ -163,8 +173,8 @@
function clearSearch() {
  searchKeyword.value = ''
  hasSearched.value = false
  pointList.value = []
  filterExpanded.value = false
  showStatusDropdown.value = false
  filterExpanded.value = true
  loadPointList()
}
@@ -211,19 +221,11 @@
})
const showLeftContent = computed(() => {
  return hasSearched.value && searchKeyword.value.trim().length > 0
  return true
})
watch([searchKeyword, hasSearched], ([kw, searched]) => {
  const hasKeyword = kw.trim().length > 0
  filterExpanded.value = hasKeyword && searched
  if (!hasKeyword) {
    showStatusDropdown.value = false
  }
})
watch(searchKeyword, (val) => {
  const hasKeyword = val.trim().length > 0
  filterExpanded.value = hasKeyword
  if (!hasKeyword) {
    showStatusDropdown.value = false
  }
@@ -253,6 +255,7 @@
    name: point.name || '',
    address: fullAddress,
    fullAddress,
    city: '', // 手动添加时可能无法获取城市,需要通过 reverseGeocode 或其他方式,这里先置空
    lon: Number(point.lon)?.toFixed ? Number(point.lon).toFixed(7) : `${point.lon}`,
    lat: Number(point.lat)?.toFixed ? Number(point.lat).toFixed(7) : `${point.lat}`,
  }
@@ -289,6 +292,7 @@
// 点击点位
function handlePointClick(point: any) {
  isCollapsed.value = false
  activePointId.value = point.id
  currentPoint.value = JSON.parse(JSON.stringify(point)) // 深拷贝,避免直接修改列表数据
  pointDetailMode.value = 'view'
@@ -589,13 +593,38 @@
  addressOptions.value = currentPoint.value.name ? [{ label: currentPoint.value.name, value: currentPoint.value.name }] : []
  createTempMarker(position)
  // 重新渲染点位,此时 renderPoints 会跳过当前选中的点,避免重叠
  renderPoints(allPoints.value)
  renderPoints(pointList.value)
}
// 取消编辑
function handleCancelEdit() {
  if (pointDetailMode.value === 'add') {
    handleCancelAddPoint()
    // 新增模式下的取消,需要二次确认
    // 检查是否已录入内容
    const hasContent = currentPoint.value && (
      currentPoint.value.name
      || currentPoint.value.remark
      || (uploadedImages.value && uploadedImages.value.length > 0)
    )
    if (hasContent) {
      confirmDialogConfig.value = {
        title: '当前点位还未保存,确定离开吗?',
        description: '删除点位,编辑的内容将丢失,确定离开吗?',
        cancelText: '丢弃离开',
        confirmText: '保存离开',
        onConfirm: () => {
          showConfirmDialog.value = false
        },
        onCancel: () => {
          handleCancelAddPoint()
        },
      }
      showConfirmDialog.value = true
    }
    else {
      handleCancelAddPoint()
    }
  }
  else {
    pointDetailMode.value = 'view'
@@ -607,7 +636,7 @@
    else {
      // 必须在 nextTick 中渲染,确保 pointDetailMode 已经生效
      setTimeout(() => {
        renderPoints(allPoints.value)
        renderPoints(pointList.value)
      }, 0)
    }
  }
@@ -683,7 +712,51 @@
  }
  // 5. 获取电子围栏区域ID
  const electronicAreaId = electronicFenceAreas.value[0]?.id?.toString() || ''
  // 需求:取点位所在的运营区域的 electronicId
  let electronicAreaId = ''
  if (currentPoint.value.lon && currentPoint.value.lat && typeof AMap !== 'undefined') {
    // 确保使用 AMap.GeometryUtil.isPointInRing 需要加载 GeometryUtil 插件
    // 或者手动实现点在多边形内判断(射线法),但 AMap 一般自带
    // 注意:JS API 2.0 默认包含 GeometryUtil 静态方法,但如果是 1.4 可能需要插件
    // 这里假设是 2.0+
    const point = new AMap.LngLat(Number(currentPoint.value.lon), Number(currentPoint.value.lat))
    // 遍历所有运营区域(areaType === 1)
    const targetArea = electronicFenceAreas.value.find((area) => {
      if (area.areaType !== 1 || !area.fencePoints)
        return false
      try {
        const path = area.fencePoints.split(',').map((p) => {
          const [lng, lat] = p.split('_')
          return new AMap.LngLat(Number(lng), Number(lat))
        })
        // 兼容性处理:如果 GeometryUtil 不可用,使用 Polygon 实例的 contains 方法
        if (AMap.GeometryUtil && AMap.GeometryUtil.isPointInRing) {
          return AMap.GeometryUtil.isPointInRing(point, path)
        } else {
           // 创建临时 polygon 用于判断
           const tempPolygon = new AMap.Polygon({ path })
           return tempPolygon.contains(point)
        }
      }
      catch (e) {
        console.error('判断点位区域失败:', e)
        return false
      }
    })
    if (targetArea) {
      electronicAreaId = targetArea.electronicId?.toString() || targetArea.id?.toString() || ''
    }
  }
  // 兜底:如果没有匹配到任何区域,或者计算失败,尝试使用默认值(虽然业务上应该报错提示)
  if (!electronicAreaId) {
    electronicAreaId = electronicFenceAreas.value.find(area => area.areaType === 1)?.electronicId || electronicFenceAreas.value[0]?.id?.toString() || ''
    // 可以考虑是否提示用户“点位不在任何运营区域内”
    // window.$message?.warning('当前点位不在任何已知的运营区域内,将使用默认区域')
  }
  const payload = {
    pointName: currentPoint.value.name,
@@ -1052,8 +1125,8 @@
          updateTime: point.updateTime,
          source: 'api',
        }))
        pointList.value = []
        renderPoints(allPoints.value)
        pointList.value = allPoints.value
        renderPoints(pointList.value)
      }
      return
    }
@@ -1097,7 +1170,7 @@
    }
    pointList.value = [...apiPoints, ...amapPoints]
    renderPoints(allPoints.value)
    renderPoints(pointList.value)
  }
  catch (error) {
    console.error('获取点位列表失败:', error)
@@ -1134,12 +1207,12 @@
  const greenStatus = ['可运营', '启用中']
  if (redStatus.includes(status)) {
    return '/src/assets/point/mark-red.png'
    return MARK_ICON_RED
  }
  else if (greenStatus.includes(status)) {
    return '/src/assets/point/mark-green.png'
    return MARK_ICON_GREEN
  }
  return '/src/assets/point/mark-red.png'
  return MARK_ICON_RED
}
function clampNumber(value: number, min: number, max: number) {
@@ -1234,7 +1307,7 @@
    if (extData.isNew) {
      const iconSize = getScaledSize(32, 42)
      const iconUrl = '/src/assets/point/mark-blue.png'
      const iconUrl = MARK_ICON_BLUE
      marker.setIcon(new AMap.Icon({
        size: iconSize,
        image: iconUrl,
@@ -1249,7 +1322,7 @@
    const isSelected = activePointId.value == point.id
      && !(pointDetailMode.value === 'edit' || pointDetailMode.value === 'add')
    const iconUrl = isSelected ? '/src/assets/point/mark-blue.png' : getPointIconUrl(point.status)
    const iconUrl = isSelected ? MARK_ICON_BLUE : getPointIconUrl(point.status)
    const iconSize = isSelected ? getScaledSize(31, 42) : getScaledSize(32, 32)
    marker.setIcon(new AMap.Icon({
      size: iconSize,
@@ -1282,7 +1355,7 @@
      return
    }
    const iconUrl = isSelected ? '/src/assets/point/mark-blue.png' : getPointIconUrl(point.status)
    const iconUrl = isSelected ? MARK_ICON_BLUE : getPointIconUrl(point.status)
    // 选中时尺寸为 40x40,未选中时为 32x32
    const iconSize = isSelected ? getScaledSize(31, 42) : getScaledSize(32, 32)
@@ -1310,6 +1383,8 @@
        return
      if (point.source === 'amap') {
        startAddFromAmap(point)
        // 触发一次反查以获取城市信息用于生成ID
        reverseGeocode([Number(point.lon), Number(point.lat)])
      }
      else {
        handlePointClick(point)
@@ -1389,6 +1464,7 @@
    AMap.plugin('AMap.AutoComplete', () => {
      const autoOptions = {
        city: '全国',
        pageSize: 50, // 设置返回结果数量为50
      }
      const autoComplete = new AMap.AutoComplete(autoOptions)
      autoComplete.search(keyword, (status: string, result: any) => {
@@ -1420,6 +1496,7 @@
    AMap.plugin('AMap.AutoComplete', () => {
      const autoOptions = {
        city: '全国',
        pageSize: 50, // 设置返回结果数量为50
      }
      const autoComplete = new AMap.AutoComplete(autoOptions)
      autoComplete.search(query, (status: string, result: any) => {
@@ -1520,7 +1597,8 @@
      const address = regeocode.formattedAddress
      const comp = regeocode.addressComponent
      // 提取更精准的简短地名:优先 建筑 > 兴趣点 > 社区 > 街道
      const defaultName = comp.building || comp.neighborhood || comp.township || comp.streetNumber || address.slice(0, 10)
      // const defaultName = comp.building || comp.neighborhood || comp.township || comp.streetNumber || address.slice(0, 10)
      const defaultName = address
      // 更新地址下拉选项
      // label 决定显示内容,value 决定绑定值
@@ -1537,6 +1615,7 @@
        tempMarkerForm.value.name = defaultName
        tempMarkerForm.value.address = defaultName
        tempMarkerForm.value.fullAddress = address
        tempMarkerForm.value.city = comp.city || comp.province // 优先使用城市名,直辖市可能在省份字段
        tempAddressInput.value = defaultName
      })
    }
@@ -1572,6 +1651,7 @@
    name: '',
    address: '正在查询地址...',
    fullAddress: '',
    city: '',
    lon: lnglat.getLng().toFixed(7),
    lat: lnglat.getLat().toFixed(7),
  }
@@ -1722,7 +1802,7 @@
  content.innerHTML = `
    <div class="temp-marker-wrapper" style="position: relative; width: 32px; height: 42px;">
      <img src="/src/assets/point/mark-blue.png" style="width: 32px; height: 42px; display: block;" />
      <img src="${MARK_ICON_BLUE}" style="width: 32px; height: 42px; display: block;" />
      <div class="marker-delete-btn" style="
        position: absolute;
        top: -4px;
@@ -1880,7 +1960,96 @@
  })
  // 重新渲染点位列表,以移除未保存的新增标记
  renderPoints(allPoints.value)
  renderPoints(pointList.value)
}
// 城市拼音映射表
const CITY_PINYIN_MAP: Record<string, string> = {
  北京: 'BJ',
  上海: 'SH',
  广州: 'GZ',
  深圳: 'SZ',
  杭州: 'HZ',
  成都: 'CD',
  武汉: 'WH',
  重庆: 'CQ',
  西安: 'XA',
  南京: 'NJ',
  苏州: 'SZ',
  天津: 'TJ',
  郑州: 'ZZ',
  长沙: 'CS',
  沈阳: 'SY',
  青岛: 'QD',
  宁波: 'NB',
  东莞: 'DG',
  无锡: 'WX',
  佛山: 'FS',
  合肥: 'HF',
  大连: 'DL',
  福州: 'FZ',
  厦门: 'XM',
  哈尔滨: 'HEB',
  济南: 'JN',
  温州: 'WZ',
  长春: 'CC',
  石家庄: 'SJZ',
  常州: 'CZ',
  泉州: 'QZ',
  南宁: 'NN',
  贵阳: 'GY',
  南昌: 'NC',
  南通: 'NT',
  金华: 'JH',
  徐州: 'XZ',
  太原: 'TY',
  嘉兴: 'JX',
  烟台: 'YT',
  惠州: 'HZ',
  保定: 'BD',
  台州: 'TZ',
  中山: 'ZS',
  绍兴: 'SX',
  乌鲁木齐: 'WLMQ',
  潍坊: 'WF',
  兰州: 'LZ',
}
function getCityCode(cityName: string) {
  if (!cityName) return 'XX'
  // 移除“市”后缀
  const name = cityName.replace(/市$/, '')
  // 查找映射表
  for (const key in CITY_PINYIN_MAP) {
    if (name.includes(key)) {
      return CITY_PINYIN_MAP[key]
    }
  }
  // 如果没找到,默认返回 XX
  return 'XX'
}
function generatePointId(cityName: string) {
  const cityCode = getCityCode(cityName)
  const typeCode = 'PUD'
  const prefix = `${cityCode}-${typeCode}-`
  // 查找当前最大的序列号
  let maxSeq = 0
  if (allPoints.value && allPoints.value.length > 0) {
    allPoints.value.forEach(p => {
      if (p.id && typeof p.id === 'string' && p.id.startsWith(prefix)) {
        const seqStr = p.id.slice(prefix.length)
        const seq = parseInt(seqStr, 10)
        if (!isNaN(seq) && seq > maxSeq) {
          maxSeq = seq
        }
      }
    })
  }
  const newSeq = maxSeq + 1
  return `${prefix}${newSeq.toString().padStart(6, '0')}`
}
function handleConfirmTempMarker() {
@@ -1906,7 +2075,8 @@
  }
  if (!tempMarkerForm.value.id) {
    tempMarkerForm.value.id = `P${Date.now()}`
    // 根据城市生成规则ID
    tempMarkerForm.value.id = generatePointId(tempMarkerForm.value.city)
  }
  // 将地图弹窗的数据回显到右侧表单对象中
@@ -1934,7 +2104,7 @@
  }
  // 使用 mark-blue.png,因为它还处于打点/新增状态,未保存到数据库
  const iconUrl = '/src/assets/point/mark-blue.png'
  const iconUrl = MARK_ICON_BLUE
  const iconSize = getScaledSize(32, 42)
  const icon = new AMap.Icon({
    size: iconSize,
@@ -2047,7 +2217,7 @@
      loadPointList() // 撤销修改,重新加载数据
    } else {
      setTimeout(() => {
        renderPoints(allPoints.value)
        renderPoints(pointList.value)
      }, 0)
    }
  } else {
@@ -2055,20 +2225,20 @@
    activePointId.value = null
    currentPoint.value = null
    pointDetailMode.value = 'view'
    renderPoints(allPoints.value)
    renderPoints(pointList.value)
  }
}
// 关闭点位详情
// 关闭/折叠点位详情
function handleCloseDetail() {
  if (pointDetailMode.value !== 'view') {
  // 如果是新增模式,保持原有逻辑(取消新增)
  if (pointDetailMode.value === 'add') {
    handleCancelEdit()
    return
  }
  else {
    activePointId.value = null
    currentPoint.value = null
    renderPoints(allPoints.value)
  }
  // 如果是查看或编辑模式,切换折叠状态
  isCollapsed.value = !isCollapsed.value
}
function toPointList() {
  router.push({ path: '/businessOperations/pointManagement' })
@@ -2233,14 +2403,15 @@
          <div class="flex a-center j-between">
            <div>
              <div class="flex a-center j-between">
                <div class="flex a-center">
                  <div class="point-id">
                <div class="flex a-center" style="align-items: flex-start;">
                  <div class="point-id" style="word-break: break-all; white-space: normal;">
                    {{ point.name }}
                  </div>
                  <div
                    v-if="point.source === 'api'"
                    class="point-status"
                    :class="getStatusClass(point.status)"
                    style="flex-shrink: 0;"
                  >
                    {{ point.status }}
                  </div>
@@ -2451,6 +2622,7 @@
      <div
        v-if="currentPoint && currentPoint.lon && currentPoint.lat"
        class="point-detail"
        :class="{ 'is-collapsed': isCollapsed }"
      >
        <!-- 关闭详情按钮 -->
        <div
@@ -2488,7 +2660,7 @@
                  class="form-input"
                  type="text"
                  placeholder="请输入点位名称"
                  disabled
                  :disabled="pointDetailMode === 'view'"
                />
              </div>
            </div>
@@ -2773,13 +2945,14 @@
        <div v-else class="point-detail-info">
          <!-- 点位名称 状态 -->
          <div>
            <div class="flex a-center">
              <div class="point-name">
            <div class="flex a-center" style="align-items: flex-start;">
              <div class="point-name" style="word-break: break-all; white-space: normal;">
                {{ currentPoint?.name }}
              </div>
              <div
                class="point-status"
                :class="getStatusClass(currentPoint?.status)"
                style="flex-shrink: 0;"
              >
                {{ currentPoint?.status }}
              </div>
src/views/businessOperations/pointManagement/list.vue
@@ -125,7 +125,9 @@
  {
    title: '状态',
    key: 'operationStatus',
    width: 100,
    width: 140,
    resizable: true,
    ellipsis: true,
    render(row) {
      const statusText = getPointStatusText(row.operationStatus, row.verificationStatus)
      const statusClass = getStatusClass(statusText)
src/views/operations/attendance/address/index.vue
@@ -84,6 +84,7 @@
        <NSpace>
          <NButton
            text
            v-permission="70212"
            type='primary'
            onClick={() => handleEdit(row)}
          >
@@ -91,6 +92,7 @@
          </NButton>
          <NButton
            text
            v-permission="70214"
            type='primary'
            onClick={() => {
              dialog.create({
@@ -112,7 +114,7 @@
          >
            { row.status === 1 ? '禁用' : '启用' }
          </NButton>
          <NPopconfirm onPositiveClick={() => delteteUser(row.id!)}>
          <NPopconfirm v-permission="70213" onPositiveClick={() => delteteUser(row.id!)}>
            {{
              default: () => '确认删除',
              trigger: () => <NButton text type="error">删除</NButton>,
@@ -220,7 +222,7 @@
      <n-card class="flex-1">
        <template #header>
          <n-flex justify="flex-end">
          <n-flex justify="flex-end" v-permission="70211">
            <NButton type="primary" secondary @click="handleAdd">
              新增打卡地
            </NButton>
src/views/operations/attendance/plan/index.vue
@@ -238,11 +238,11 @@
        <n-date-picker v-model:value="selectedMonth" placeholder="选择月份" type="month" style="width: 260px"
          @update:value="handleMonthChange" />
        <div class="flex gap-2">
          <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;"
          <n-button  v-permission="70252" style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;"
            @click="handleCopy">复制</n-button>
          <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;"
          <n-button  v-permission="70253" style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;"
            @click="handleDownload">下载表</n-button>
          <n-button type="primary" style="width: 84px; height: 32px" @click="handleEdit">编辑</n-button>
          <n-button  v-permission="70251" type="primary" style="width: 84px; height: 32px" @click="handleEdit">编辑</n-button>
        </div>
      </div>
src/views/operations/attendance/shift/index.vue
@@ -118,12 +118,13 @@
       <NSpace>
          <NButton
            text
            v-permission="70242"
            type='primary'
            onClick={() => handleEdit(row)}
          >
            编辑
          </NButton>
          <NPopconfirm onPositiveClick={() => handleDelete(row.id!)}>
          <NPopconfirm  v-permission="70243" onPositiveClick={() => handleDelete(row.id!)}>
            {{
              default: () => '确认删除',
              trigger: () => <NButton text type="error">删除</NButton>,
@@ -180,7 +181,7 @@
      <n-card class="flex-1">
        <template #header>
          <n-flex justify="flex-end">
          <n-flex justify="flex-end"  v-permission="70241">
            <NButton type="primary" secondary @click="handleAdd">
              新增班次
            </NButton>
src/views/operations/attendance/statistics/index.vue
@@ -89,7 +89,7 @@
    width: 134,
    fixed: 'right',
    render: (row) => (
      <NButton text type="primary" onClick={() => handleViewDetail(row)}>
      <NButton v-permission="70222" text type="primary" onClick={() => handleViewDetail(row)}>
        打卡明细
      </NButton>
    )
@@ -157,7 +157,7 @@
    <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px">
       <template #header>
          <n-flex justify="flex-end">
          <n-flex justify="flex-end" v-permission="70221">
            <NButton type="primary" secondary @click="handleExport">
              导出
            </NButton>
src/views/operations/message/index.vue
@@ -1,128 +1,163 @@
<script setup lang="ts">
import { ref, h } from 'vue'
import { useBoolean } from '@/hooks'
import type { DataTableColumns } from 'naive-ui'
import { NCheckbox } from 'naive-ui'
import { getNoticeList, updateNoticeConfig } from '@/service/api/attendance'
import { ref, h } from "vue";
import { useBoolean } from "@/hooks";
import type { DataTableColumns } from "naive-ui";
import { NCheckbox } from "naive-ui";
import { getNoticeList, updateNoticeConfig } from "@/service/api/attendance";
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const {
  bool: loading,
  setTrue: startLoading,
  setFalse: endLoading,
} = useBoolean(false);
const tableData = ref<any[]>([])
const tableData = ref<any[]>([]);
const columns: DataTableColumns<any> = [
  {
    title: '序号',
    key: 'serialNumber',
    title: "序号",
    key: "serialNumber",
    width: 80,
  },
  {
    title: '通知类型',
    key: 'notificationType',
    title: "通知类型",
    key: "notificationType",
    render: (row) => {
      switch (row.notificationType) {
        //通知类型 1=新增事故,2=事故即将超时提示,3=事故超时提示,4=排班信息变动,5=事故状态变更,6=新增任务,7=任务过期提示,8=版本更新提示,9=变更协议政策内容,10=变更协议内容并需用户授权,11=打卡成功通知
        case 1:
          return '新增事故'
          return "新增事故";
        case 2:
          return '事故即将超时提示'
          return "事故即将超时提示";
        case 3:
          return '事故超时提示'
          return "事故超时提示";
        case 4:
          return '排班信息变动'
          return "排班信息变动";
        case 5:
          return '事故状态变更'
          return "事故状态变更";
        case 6:
          return '新增任务'
          return "新增任务";
        case 7:
          return '任务过期提示'
          return "任务过期提示";
        case 8:
          return '版本更新提示'
          return "版本更新提示";
        case 9:
          return '变更协议政策内容'
          return "变更协议政策内容";
        case 10:
          return '变更协议内容并需用户授权'
          return "变更协议内容并需用户授权";
        case 11:
          return '打卡成功通知'
          return "打卡成功通知";
        default:
          return row.notificationType
          return row.notificationType;
      }
    }
    },
  },
  {
    title: '通知方式',
    key: 'method',
    title: "通知方式",
    key: "method",
    render: (row) => {
      return h('div', { class: 'flex items-center gap-4' }, [
        h(NCheckbox, {
          checked: row.isPush,
          onUpdateChecked: (value: boolean) => {
            row.isPush = value
          }
        }, { default: () => 'PUSH推送' }),
        h(NCheckbox, {
          checked: row.isMessage,
          onUpdateChecked: (value: boolean) => {
            row.isMessage = value
          }
        }, { default: () => '短信通知' })
      ])
    }
      return h("div", { class: "flex items-center gap-4" }, [
        h(
          NCheckbox,
          {
            checked: row.isPush,
            onUpdateChecked: (value: boolean) => {
              row.isPush = value;
            },
          },
          { default: () => "PUSH推送" }
        ),
        h(
          NCheckbox,
          {
            checked: row.isMessage,
            onUpdateChecked: (value: boolean) => {
              row.isMessage = value;
            },
          },
          { default: () => "短信通知" }
        ),
      ]);
    },
  },
  {
    title: '更新时间',
    key: 'updateTime',
  }
]
    title: "更新时间",
    key: "updateTime",
  },
];
const handleSave = () => {
  console.log('保存', tableData.value)
  startLoading()
  updateNoticeConfig({ notifications: tableData.value }).then((res) => {
    if (res.code === 200) {
      window.$message.success('保存成功')
    }
  }).finally(() => {
    getList()
    endLoading()
  })
}
  console.log("保存", tableData.value);
  startLoading();
  updateNoticeConfig({ notifications: tableData.value })
    .then((res) => {
      if (res.code === 200) {
        window.$message.success("保存成功");
      }
    })
    .finally(() => {
      getList();
      endLoading();
    });
};
async function getList() {
  startLoading()
  getNoticeList().then((res) => {
    tableData.value = res.data || []
    tableData.value.forEach((item: any, index: number) => {
     item.serialNumber = (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1
  startLoading();
  getNoticeList()
    .then((res) => {
      tableData.value = res.data || [];
      tableData.value.forEach((item: any, index: number) => {
        item.serialNumber =
          (pageParams.value.pageNum - 1) * pageParams.value.pageSize +
          index +
          1;
      });
    })
  }).finally(() => {
    endLoading()
  })
    .finally(() => {
      endLoading();
    });
}
onMounted(() => {
  getList()
})
  getList();
});
</script>
<template>
  <div class="h-full flex-col">
    <n-card style="border-radius: 6px; overflow: hidden;"
      content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;">
      <div style="flex: 1; overflow: auto;">
        <n-data-table :columns="columns" :data="tableData" :loading="loading" :bordered="false" :header-height="18"
          :row-height="18" />
    <n-card
      style="border-radius: 6px; overflow: hidden"
      content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;"
    >
      <div style="flex: 1; overflow: auto">
        <n-data-table
          :columns="columns"
          :data="tableData"
          :loading="loading"
          :bordered="false"
          :header-height="18"
          :row-height="18"
        />
      </div>
    </n-card>
    <div class="flex justify-end absolute bottom-0 left-0 right-0"
      style="background-color: white; padding: 16px 24px; border-radius: 6px;">
      <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button>
    <div
      class="flex justify-end absolute bottom-0 left-0 right-0"
      style="background-color: white; padding: 16px 24px; border-radius: 6px"
    >
      <n-button
        v-permission="7041"
        type="primary"
        style="width: 84px; height: 32px"
        @click="handleSave"
        >保存</n-button
      >
    </div>
  </div>
</template>
<style scoped>
:deep(.n-data-table-th) {
  background-color: #F4F6F8;
  background-color: #f4f6f8;
}
</style>
src/views/operations/personnel/personnelManagement/components/PersonnelFormModal.vue
@@ -10,6 +10,7 @@
  password: string
  roleIds: string[] | null
  remarks: string
  sex: number
}
const props = defineProps<{
@@ -36,6 +37,7 @@
  avatar: '',
  password: '',
  roleIds: null,
  sex: 1,
  remarks: ''
})
@@ -48,10 +50,39 @@
    { required: true, message: '请输入用户名称', trigger: 'blur' }
  ],
  uniqueNumber: [
    { required: true, message: '请输入编号', trigger: 'blur' }
    { required: true, message: '请输入编号', trigger: 'blur' },
    {
      pattern: /^[a-zA-Z0-9]{1,20}$/,
      message: '编号支持字母数字,最多20个字符',
      trigger: 'blur'
    }
  ],
  password: [
    { required: true, message: '请输入初始密码', trigger: 'blur' }
    { required: true, message: '请输入初始密码', trigger: 'blur' },
    {
      pattern: /^.{8,20}$/,
      message: '密码长度必须为8-20位字符',
      trigger: 'blur'
    },
    {
      validator: (rule: any, value: string) => {
        if (!value) return true;
        // 检查是否包含大小写字母、数字、特殊符号
        const hasLowercase = /[a-z]/.test(value);
        const hasUppercase = /[A-Z]/.test(value);
        const hasDigit = /\d/.test(value);
        const hasSpecialChar = /[^a-zA-Z0-9]/.test(value); // 非字母数字字符即为特殊字符
        // 计算包含的字符类型数量
        const charTypesCount = [hasLowercase, hasUppercase, hasDigit, hasSpecialChar].filter(Boolean).length;
        // 至少需要两种字符类型
        return charTypesCount >= 2;
      },
      message: '密码必须包含大小写字母、数字、特殊符号中的至少两种',
      trigger: 'blur'
    }
  ],
  phoneNumber: [
    { required: true, message: '请输入联系电话', trigger: 'blur' },
@@ -59,6 +90,9 @@
  ],
  roleIds: [
    { type: 'array', required: true, message: '请选择角色', trigger: 'change' }
  ],
  sex: [
    { type: 'number', required: true, message: '请选择性别', trigger: 'change' }
  ]
}
@@ -96,7 +130,8 @@
    avatar: '',
    password: '',
    roleIds: null,
    remarks: ''
    remarks: '',
    sex: 1
  }
  fileList.value = []
}
@@ -116,6 +151,7 @@
    preset="card"
    :title="mode === 'edit' ? '编辑账号' : '新增账号'"
    class="w-600px"
     :auto-focus="false"
    :segmented="{
      content: true,
      action: true,
@@ -129,11 +165,11 @@
    >
      <div class="grid grid-cols-2 gap-4">
        <n-form-item label="用户名称" path="nickName">
          <n-input v-model:value="formModel.nickName" placeholder="请输入" clearable />
          <n-input v-model:value="formModel.nickName" placeholder="请输入" clearable maxlength="20"/>
        </n-form-item>
        <n-form-item label="编号" path="uniqueNumber">
          <n-input v-model:value="formModel.uniqueNumber" placeholder="请输入" clearable />
          <n-input v-model:value="formModel.uniqueNumber" placeholder="请输入" clearable maxlength="20"/>
        </n-form-item>
      </div>
@@ -165,13 +201,23 @@
          <n-select v-model:value="formModel.roleIds" :options="roleOptions" placeholder="请选择" clearable multiple/>
        </n-form-item>
      </div>
    <div class="grid grid-cols-2 gap-4">
        <n-form-item label="性别" path="sex">
          <n-radio-group v-model:value="formModel.sex" size="small">
            <n-radio label="男" :value="1">男</n-radio>
            <n-radio label="女" :value="2">女</n-radio>
          </n-radio-group>
        </n-form-item>
      </div>
      <n-form-item label="备注" path="remarks">
        <n-input
          v-model:value="formModel.remarks"
          type="textarea"
          placeholder="请输入"
          :rows="3"
          :maxlength="200"
          show-count
          clearable
        />
      </n-form-item>
src/views/operations/personnel/personnelManagement/components/ResetPasswordModal.vue
@@ -59,7 +59,30 @@
  ],
  newPassword: [
    { required: true, message: '请输入新密码', trigger: 'blur' },
    { min: 6, message: '密码长度至少6位', trigger: 'blur' },
    {
      pattern: /^.{8,20}$/,
      message: '密码长度必须为8-20位字符',
      trigger: 'blur'
    },
    {
      validator: (rule: any, value: string) => {
        if (!value) return true;
        // 检查是否包含大小写字母、数字、特殊符号
        const hasLowercase = /[a-z]/.test(value);
        const hasUppercase = /[A-Z]/.test(value);
        const hasDigit = /\d/.test(value);
        const hasSpecialChar = /[^a-zA-Z0-9]/.test(value); // 非字母数字字符即为特殊字符
        // 计算包含的字符类型数量
        const charTypesCount = [hasLowercase, hasUppercase, hasDigit, hasSpecialChar].filter(Boolean).length;
        // 至少需要两种字符类型
        return charTypesCount >= 2;
      },
      message: '密码必须包含大小写字母、数字、特殊符号中的至少两种',
      trigger: 'blur'
    }
  ],
  confirmPassword: [
    {
src/views/operations/personnel/personnelManagement/detail.vue
@@ -67,8 +67,8 @@
    <n-card style="border-radius: 6px" content-style="padding: 12px">
      <div class="mb-4">
        <div class="text-sm font-medium mb-3">身份证:</div>
        <div class="flex gap-4">
        <div class="text-sm font-medium">身份证:</div>
        <div class="flex gap-4" v-if="personnelInfo.idCardFront || personnelInfo.idCardBack">
          <n-image
            width="128"
            height="80"
@@ -86,10 +86,13 @@
            style="border-radius: 4px"
          />
        </div>
        <div v-else>
          <span>-</span>
        </div>
      </div>
      <div class="mb-4">
        <div class="text-sm font-medium mb-3">驾驶证:</div>
        <div class="text-sm font-medium">驾驶证:</div>
        <n-image
          width="128"
          height="80"
@@ -98,10 +101,13 @@
          object-fit="cover"
          style="border-radius: 4px"
        />
        <div v-else>
          <span>-</span>
        </div>
      </div>
      <div>
        <div class="text-sm font-medium mb-3">无犯罪记录证明:</div>
        <div class="text-sm font-medium">无犯罪记录证明:</div>
        <n-image
          width="128"
          height="80"
@@ -110,6 +116,9 @@
          object-fit="cover"
          style="border-radius: 4px"
        />
        <div v-else>
          <span>-</span>
        </div>
      </div>
    </n-card>
  </div>
src/views/operations/personnel/personnelManagement/index.vue
@@ -4,6 +4,7 @@
import { useRouter } from 'vue-router'
import type { DataTableColumns } from 'naive-ui'
import { NButton, NTag, NDropdown } from 'naive-ui'
import { usePermission } from '@/hooks'
import { getOperationsUserList, exportOperationsUserTemplate, exportOperationsUser, importOperationsUser,
  addOperationsUser, editOperationsUser, updateOperationsUserStatus, deleteOperationsUser } from '@/service/api/attendance'
import { fetchRoleList } from '@/service/api/fleet'
@@ -11,10 +12,12 @@
import SearchPanel from '@/components/custom/SearchPanel.vue'
import PersonnelFormModal from './components/PersonnelFormModal.vue'
import ResetPasswordModal from './components/ResetPasswordModal.vue'
import dayjs from 'dayjs'
const router = useRouter()
const dialog = useDialog()
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const { hasPermission } = usePermission()
const searchModel = ref({
  nickName: '',
@@ -53,6 +56,7 @@
    roleId: null,
    status: null
  }
  getList()
}
const handleExport = () => {
@@ -146,6 +150,7 @@
  {
    label: '编辑',
    key: 'edit',
    show: hasPermission(70115),
    props: {
      onClick: () => handleEdit(row)
    }
@@ -153,6 +158,7 @@
  {
    label: row.status ? '禁用' : '启用',
    key: 'toggle',
    show: hasPermission(70116),
    props: {
      onClick: () => {
        updateOperationsUserStatus({
@@ -170,6 +176,7 @@
  {
    label: '重置密码',
    key: 'password',
    show: hasPermission(70117),
    props: {
      onClick: () => {
        handleResetPassword(row)
@@ -179,11 +186,12 @@
  {
    label: '删除',
    key: 'delete',
    show: hasPermission(70118),
    props: {
      onClick: () => {
        dialog.create({
          title: '确认删除吗?',
          content: '',
          title: '是否确认删除数据?',
          content: `确认删除该人员?此操作不可恢复`,
          positiveText: '删除',
          negativeText: '取消',
          draggable: true,
@@ -265,8 +273,8 @@
      row.status  ? <NTag type="primary" size="small" style={{ borderRadius: '4px' }} >启用</NTag> : <NTag type="error" size="small" style={{ borderRadius: '4px' }} >禁用</NTag>
    )
  },
  { title: '创建时间', key: 'createTime', width: 180 , resizable: true },
  { title: '备注', key: 'remarks', width: 150, resizable: true },
  { title: '创建时间', key: 'createTime', width: 180 , resizable: true, render: (row) => dayjs(row.createTime).format('YYYY-MM-DD HH:mm') },
  { title: '备注', key: 'remarks', width: 150, resizable: true, ellipsis: { tooltip: true }, render: (row) => row.remarks || '-' },
  {
    title: '操作',
    key: 'actions',
@@ -274,7 +282,7 @@
    fixed: 'right',
    render: (row) => (
      <div class="flex gap-3">
        <a style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => router.push(`/operations/personnel/personnelManagement/detail/${row.id}`)}>详情</a>
        <a v-permission="70111" style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => router.push(`/operations/personnel/personnelManagement/detail/${row.id}`)}>详情</a>
        <NDropdown options={moreOptions(row)} trigger="click">
          <a style={{ color: '#0068FF', cursor: 'pointer' }}>更多</a>
        </NDropdown>
@@ -354,10 +362,10 @@
    <n-card class="flex-1 mt-12px">
      <template #header>
        <n-flex justify="flex-end">
          <n-button @click="handleExport" style="width: 84px; height: 32px">导出</n-button>
          <n-button @click="handleImport" style="width: 84px; height: 32px">导入</n-button>
          <n-button @click="handleDownloadTemplate" style="width: 84px; height: 32px">下载模板</n-button>
          <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;"
          <n-button v-permission="70112" @click="handleExport" style="width: 84px; height: 32px">导出</n-button>
          <n-button v-permission="70113" @click="handleImport" style="width: 84px; height: 32px">导入</n-button>
          <n-button v-permission="70112" @click="handleDownloadTemplate" style="width: 84px; height: 32px">下载模板</n-button>
          <n-button v-permission="70114" style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;"
            @click="handleAdd">新增</n-button>
        </n-flex>
      </template>
src/views/operations/taskOrder/taskOrderCenter/components/detailModal.vue
@@ -28,6 +28,7 @@
  formModel.value.id = id
  const res = await getTaskOrderDetail(id)
  if (res.code === 200) {
    console.log(res.data)
    formModel.value = { ...formModel.value, ...res.data }
    if (formModel.value.accidentDetailVO?.vehiclePictures) {
      formModel.value.accidentDetailVO.vehiclePictures = JSON.parse(formModel.value.accidentDetailVO.vehiclePictures)
@@ -208,9 +209,10 @@
            v-if="formModel.detectionDetailVO?.vehiclePictures" />
          <div>
            <div class="plateNumber mb-8px">{{ formModel.detectionDetailVO.plateNumber }}</div>
            <div class="value">{{ formModel.detectionDetailVO.vehicleColor }} | 车身编号:{{
              formModel.detectionDetailVO.vehicleCode }} | VIN码:{{
                formModel.detectionDetailVO.vehicleVin }}</div>
            <div class="value mb-8px">{{ formModel.detectionDetailVO.vehicleColor }} | VIN码:{{
                formModel.detectionDetailVO.vehicleVin }}
            </div>
            <div class="value">车辆位置:{{ formModel.detectionDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="head mb-12px">系统检测</div>
@@ -260,7 +262,7 @@
        </div>
        <div class="mb-16px">任务单号:{{ formModel.detectionDetailVO.id }}</div>
        <div class="mb-16px">操作人:{{ formModel.detectionDetailVO.operationsUserName }}</div>
        <div class="mb-16px">操作时间:{{ dayjs(formModel.detectionDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
        <div class="mb-16px">操作时间:{{ formModel.detectionDetailVO.checkTime ? dayjs(formModel.detectionDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div>
      </div>
    </div>
    <!-- 充电详情 -->
@@ -279,7 +281,7 @@
        </div>
        <div class="mb-16px">任务单号:{{ formModel.chargingDetailVO.id }}</div>
        <div class="mb-16px">开始充电前电量:{{ formModel.chargingDetailVO.startRemainingBattery }}%</div>
        <div class="mb-16px">开始充电时间:{{ dayjs(formModel.chargingDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') }}
        <div class="mb-16px">开始充电时间:{{ formModel.chargingDetailVO.startTime ? dayjs(formModel.chargingDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') : '' }}
        </div>
        <div class="mb-16px">开始充电操作人:{{ formModel.chargingDetailVO.startOperationsUserName }}</div>
        <div class="mb-16px">
@@ -291,7 +293,7 @@
          </div>
        </div>
        <div class="mb-16px">充电后电量:{{ formModel.chargingDetailVO.endRemainingBattery }}%</div>
        <div class="mb-16px">结束充电时间:{{ dayjs(formModel.chargingDetailVO.endTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
        <div class="mb-16px">结束充电时间:{{ formModel.chargingDetailVO.endTime ? dayjs(formModel.chargingDetailVO.endTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div>
        <div class="mb-16px">结束充电操作人:{{ formModel.chargingDetailVO.endOperationsUserName }}</div>
        <div class="mb-16px">
          <div class="mb-8px">结束充电图片:</div>
@@ -310,11 +312,11 @@
          <img :src="formModel.preparationDetailVO.vehiclePictures" class="w-112px h-64px mr-16px"
            v-if="formModel.preparationDetailVO?.vehiclePictures" />
          <div>
            <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{
                formModel.chargingDetailVO.vehicleVin }}
            <div class="plateNumber mb-8px">{{ formModel.preparationDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.preparationDetailVO.vehicleColor }} | VIN码:{{
                formModel.preparationDetailVO.vehicleVin }}
            </div>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
            <div class="value">车辆位置:{{ formModel.preparationDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="mb-16px">
@@ -376,7 +378,7 @@
        </div>
        <div class="mb-16px">任务单号:{{ formModel.preparationDetailVO.id }}</div>
        <div class="mb-16px">操作人:{{ formModel.preparationDetailVO.operationsUserName }}</div>
        <div class="mb-16px">操作时间:{{ dayjs(formModel.preparationDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') }}
        <div class="mb-16px">操作时间:{{ formModel.preparationDetailVO.handleTime ? dayjs(formModel.preparationDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') : '' }}
        </div>
      </div>
    </div>
@@ -387,11 +389,11 @@
          <img :src="formModel.cleanDetailVO.vehiclePictures" class="w-112px h-64px mr-16px"
            v-if="formModel.cleanDetailVO?.vehiclePictures" />
          <div>
            <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{
                formModel.chargingDetailVO.vehicleVin }}
            <div class="plateNumber mb-8px">{{ formModel.cleanDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.cleanDetailVO.vehicleColor }} | VIN码:{{
                formModel.cleanDetailVO.vehicleVin }}
            </div>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
            <div class="value">车辆位置:{{ formModel.cleanDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="mb-16px">
@@ -403,7 +405,7 @@
        </div>
        <div class="mb-16px">任务单号:{{ formModel.cleanDetailVO.id }}</div>
        <div class="mb-16px">操作人:{{ formModel.cleanDetailVO.operationsUserName }}</div>
        <div class="mb-16px">操作时间:{{ dayjs(formModel.cleanDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
        <div class="mb-16px">操作时间:{{ formModel.cleanDetailVO.handleTime ? dayjs(formModel.cleanDetailVO.handleTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div>
      </div>
    </div>
    <!-- 维修详情 -->
@@ -413,14 +415,14 @@
          <img :src="formModel.repairDetailVO.vehiclePictures" class="w-112px h-64px mr-16px"
            v-if="formModel.repairDetailVO?.vehiclePictures" />
          <div>
            <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{
                formModel.chargingDetailVO.vehicleVin }}
            <div class="plateNumber mb-8px">{{ formModel.repairDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.repairDetailVO.vehicleColor }} | VIN码:{{
                formModel.repairDetailVO.vehicleVin }}
            </div>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
            <div class="value">车辆位置:{{ formModel.repairDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="mb-16px">维修停运时间:{{ dayjs(formModel.repairDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
        <div class="mb-16px">维修停运时间:{{ formModel.repairDetailVO.startTime ? dayjs(formModel.repairDetailVO.startTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div>
        <div class="mb-16px">维修原因:{{ formModel.repairDetailVO.repairReason }}</div>
        <div class="mb-16px">
          <div class="label mb-8px">维修前图片:</div>
@@ -429,7 +431,7 @@
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">送修时间:{{ dayjs(formModel.repairDetailVO.sendTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
        <div class="mb-16px">送修时间:{{ formModel.repairDetailVO.sendTime ? dayjs(formModel.repairDetailVO.sendTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div>
        <div class="mb-16px">操作人:{{ formModel.repairDetailVO.sendUserName }}</div>
        <div class="mb-16px">维修总费用:{{ formModel.repairDetailVO.repairMoney }}</div>
        <div class="mb-16px">
@@ -439,7 +441,7 @@
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">取回时间:{{ dayjs(formModel.repairDetailVO.returnTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
        <div class="mb-16px">取回时间:{{ formModel.repairDetailVO.returnTime ? dayjs(formModel.repairDetailVO.returnTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div>
        <div class="mb-16px">操作人:{{ formModel.repairDetailVO.returnUserName }}</div>
        <div class="mb-16px">任务单号:{{ formModel.repairDetailVO.id }}</div>
      </div>
@@ -451,11 +453,11 @@
          <img :src="formModel.routineDetailVO.vehiclePictures" class="w-112px h-64px mr-16px"
            v-if="formModel.routineDetailVO?.vehiclePictures" />
          <div>
            <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{
                formModel.chargingDetailVO.vehicleVin }}
            <div class="plateNumber mb-8px">{{ formModel.routineDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.routineDetailVO.vehicleColor }} | VIN码:{{
                formModel.routineDetailVO.vehicleVin }}
            </div>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
            <div class="value">车辆位置:{{ formModel.routineDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="label mb-16px">任务完成情况:{{ formModel.routineDetailVO.taskDescription }}</div>
@@ -468,7 +470,7 @@
        </div>
        <div class="mb-16px">任务单号:{{ formModel.routineDetailVO.id }}</div>
        <div class="mb-16px">操作人:{{ formModel.routineDetailVO.operationsUserName }}</div>
        <div class="mb-16px">操作时间:{{ dayjs(formModel.routineDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') }}</div>
        <div class="mb-16px">操作时间:{{ formModel.routineDetailVO.checkTime ? dayjs(formModel.routineDetailVO.checkTime).format('YYYY-MM-DD HH:mm:ss') : '' }}</div>
      </div>
    </div>
    <template #action>
src/views/operations/taskOrder/taskOrderCenter/index.vue
@@ -342,6 +342,7 @@
        <NSpace>
          {(row.status === 0 || row.status === 1) && <NButton
            text
             v-permission="7053"
            type='primary'
            onClick={() => modalRef.value.openModal(row.id)}
          >
@@ -349,12 +350,13 @@
          </NButton>}
          <NButton
            text
            v-permission="7054"
            type='primary'
            onClick={() => detailModalRef.value.openModal(row.id)}
          >
            详情
          </NButton>
          <NPopconfirm onPositiveClick={() => handleDeltete(row.id!)}>
          <NPopconfirm  v-permission="7055" onPositiveClick={() => handleDeltete(row.id!)}>
            {{
              default: () => '确认删除',
              trigger: () => <NButton text type="error">删除</NButton>,
@@ -388,8 +390,8 @@
    <n-card style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px">
      <template #header>
        <n-flex justify="flex-end">
          <n-button @click="handleExport" style="width: 84px; height: 32px">导出</n-button>
          <NButton type="primary" secondary @click="handleAdd">
          <n-button  v-permission="7051" @click="handleExport" style="width: 84px; height: 32px">导出</n-button>
          <NButton  v-permission="7052" type="primary" secondary @click="handleAdd">
            新增任务
          </NButton>
        </n-flex>
src/views/service/review/personnel/index.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, h, onMounted } from "vue";
<script setup lang="tsx">
import { ref, onMounted } from "vue";
import type { DataTableColumns } from "naive-ui";
import { NButton, NTag, NAvatar, NImage } from "naive-ui";
import { useBoolean } from "@/hooks";
@@ -64,19 +64,19 @@
    align: "center",
    render: (row) => {
      const displayName = row.nickName || "-";
      return h("div", { class: "flex items-center justify-center gap-2" }, [
        h(
          NAvatar,
          {
            round: true,
            size: 32,
            style: { backgroundColor: "#e0e0e0" },
            src: row.avatar || undefined,
          },
          { default: () => displayName.charAt(0) }
        ),
        h("span", displayName),
      ]);
      return (
        <div class="flex items-center justify-center gap-2">
          <NAvatar
            round
            size={32}
            style={{ backgroundColor: "#e0e0e0" }}
            src={row.avatar || undefined}
          >
            {displayName.charAt(0)}
          </NAvatar>
          <span>{displayName}</span>
        </div>
      );
    },
  },
  {
@@ -100,12 +100,10 @@
      const roleName = Array.isArray(row.roleNames)
        ? row.roleNames.join("、")
        : row.roleNames || "-";
      return h(
        NTag,
        { type: "info", size: "small" },
        {
          default: () => roleName,
        }
      return (
        <NTag type="info" size="small">
          {roleName}
        </NTag>
      );
    },
  },
@@ -117,12 +115,10 @@
    render: (row) => {
      const statusText = auditStatusLabelMap[row.auditStatus];
      const tagType = auditStatusTagTypeMap[row.auditStatus];
      return h(
        NTag,
        { type: tagType as any, size: "small" },
        {
          default: () => statusText,
        }
      return (
        <NTag type={tagType as any} size="small">
          {statusText}
        </NTag>
      );
    },
  },
@@ -135,62 +131,56 @@
      if (row.idCardFront && row.idCardBack) {
        const idCardFront = JSON.parse(row.idCardFront).url;
        const idCardBack = JSON.parse(row.idCardBack).url;
        return h("div", { class: "flex gap-1 justify-center" }, [
          h(NImage, {
            width: 60,
            height: 40,
            src: idCardFront,
            objectFit: "cover",
            style: { borderRadius: "4px" },
            previewSrc: idCardFront,
          }),
          h(NImage, {
            width: 60,
            height: 40,
            src: idCardBack,
            objectFit: "cover",
            style: { borderRadius: "4px" },
            previewSrc: idCardBack,
          }),
        ]);
        return (
          <div class="flex gap-1 justify-center">
            <NImage
              width={60}
              height={40}
              src={idCardFront}
              objectFit="cover"
              style={{ borderRadius: "4px" }}
              previewSrc={idCardFront}
            />
            <NImage
              width={60}
              height={40}
              src={idCardBack}
              objectFit="cover"
              style={{ borderRadius: "4px" }}
              previewSrc={idCardBack}
            />
          </div>
        );
      }
      return h(
        "div",
        {
          class: "flex gap-1 justify-center",
        },
        [
          h(
            "div",
            {
              style: {
                width: "60px",
                height: "40px",
                backgroundColor: "#f0f0f0",
                borderRadius: "4px",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
              },
            },
            "📷"
          ),
          h(
            "div",
            {
              style: {
                width: "60px",
                height: "40px",
                backgroundColor: "#f0f0f0",
                borderRadius: "4px",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
              },
            },
            "📷"
          ),
        ]
      return (
        <div class="flex gap-1 justify-center">
          <div
            style={{
              width: "60px",
              height: "40px",
              backgroundColor: "#f0f0f0",
              borderRadius: "4px",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
            }}
          >
            📷
          </div>
          <div
            style={{
              width: "60px",
              height: "40px",
              backgroundColor: "#f0f0f0",
              borderRadius: "4px",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
            }}
          >
            📷
          </div>
        </div>
      );
    },
  },
@@ -202,19 +192,20 @@
    render: (row) => {
      if (row.driverLicense) {
        const driverLicense = JSON.parse(row.driverLicense).url;
        return h(NImage, {
          width: 60,
          height: 40,
          src: driverLicense,
          objectFit: "cover",
          style: { borderRadius: "4px" },
          previewSrc: driverLicense,
        });
        return (
          <NImage
            width={60}
            height={40}
            src={driverLicense}
            objectFit="cover"
            style={{ borderRadius: "4px" }}
            previewSrc={driverLicense}
          />
        );
      }
      return h(
        "div",
        {
          style: {
      return (
        <div
          style={{
            width: "60px",
            height: "40px",
            backgroundColor: "#f0f0f0",
@@ -223,9 +214,10 @@
            alignItems: "center",
            justifyContent: "center",
            margin: "0 auto",
          },
        },
        "📷"
          }}
        >
          📷
        </div>
      );
    },
  },
@@ -239,19 +231,20 @@
        const certificateOfNoCriminalRecord = JSON.parse(
          row.certificateOfNoCriminalRecord
        ).url;
        return h(NImage, {
          width: 60,
          height: 40,
          src: certificateOfNoCriminalRecord,
          objectFit: "cover",
          style: { borderRadius: "4px" },
          previewSrc: certificateOfNoCriminalRecord,
        });
        return (
          <NImage
            width={60}
            height={40}
            src={certificateOfNoCriminalRecord}
            objectFit="cover"
            style={{ borderRadius: "4px" }}
            previewSrc={certificateOfNoCriminalRecord}
          />
        );
      }
      return h(
        "div",
        {
          style: {
      return (
        <div
          style={{
            width: "60px",
            height: "40px",
            backgroundColor: "#f0f0f0",
@@ -260,9 +253,10 @@
            alignItems: "center",
            justifyContent: "center",
            margin: "0 auto",
          },
        },
        "📷"
          }}
        >
          📷
        </div>
      );
    },
  },
@@ -279,28 +273,28 @@
    align: "center",
    fixed: "right",
    render: (row) => {
      return h("div", { class: "flex items-center justify-center gap-2" }, [
        row.auditStatus === 1
          ? h(
              NButton,
              {
                text: true,
                type: "primary",
                onClick: () => handleApprove(row.id),
              },
              { default: () => "审核" }
            )
          : null,
        h(
          NButton,
          {
            text: true,
            type: "error",
            onClick: () => handleDelete(row.id),
          },
          { default: () => "删除" }
        ),
      ]);
      return (
        <div class="flex items-center justify-center gap-2">
          {row.auditStatus === 1 ? (
            <NButton
              v-permission="80112"
              text
              type="primary"
              onClick={() => handleApprove(row.id)}
            >
              审核
            </NButton>
          ) : null}
          <NButton
            v-permission="80113"
            text
            type="error"
            onClick={() => handleDelete(row.id)}
          >
            删除
          </NButton>
        </div>
      );
    },
  },
];
@@ -492,6 +486,7 @@
    >
      <div class="mb-4 flex justify-end gap-2">
        <n-button
         v-permission="80111"
          style="
            width: 84px;
            height: 32px;
src/views/service/review/policyApplication/index.vue
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="tsx">
import { ref, h, onMounted } from "vue";
import type { DataTableColumns } from "naive-ui";
import { NButton, NTag, NTooltip } from "naive-ui";
@@ -173,28 +173,28 @@
    align: "center",
    fixed: "right",
    render: (row) => {
      return h("div", { class: "flex items-center justify-center gap-2" }, [
        row.auditStatus === 1
          ? h(
              NButton,
              {
                text: true,
                type: "primary",
                onClick: () => handleApprove(row.id),
              },
              { default: () => "审核" }
            )
          : null,
        h(
          NButton,
          {
            text: true,
            type: "error",
            onClick: () => handleDelete(row.id),
          },
          { default: () => "删除" }
        ),
      ]);
      return (
        <div class="flex items-center justify-center gap-2">
          {row.auditStatus === 1 ? (
            <NButton
              v-permission="80122"
              text
              type="primary"
              onClick={() => handleApprove(row.id)}
            >
              审核
            </NButton>
          ) : null}
          <NButton
            v-permission="80123"
            text
            type="error"
            onClick={() => handleDelete(row.id)}
          >
            删除
          </NButton>
        </div>
      );
    },
  },
];
@@ -230,8 +230,8 @@
    positiveText: "确定",
    negativeText: "取消",
    onPositiveClick: async () => {
      const { error } = await deletePolicyApplicationById(id);
      if (!error) {
      const res = await deletePolicyApplicationById(id);
      if (res.code === 200) {
        window.$message?.success("删除成功");
        getPolicyApplicationList();
      } else {
@@ -243,12 +243,12 @@
const handleConfirmReview = async (data: any) => {
  const auditStatus = data?.result === "pass" ? 2 : 3;
  const { error } = await batchAuditPolicyApplication({
  const res = await batchAuditPolicyApplication({
    auditStatus,
    ids: selectedIds.value,
    auditRemarks: data?.auditRemarks || "",
  });
  if (!error) {
  if (res.code === 200) {
    window.$message?.success("审核成功");
    selectedIds.value = [];
    getPolicyApplicationList();
@@ -370,6 +370,7 @@
    >
      <div class="mb-4 flex justify-end gap-2">
        <n-button
        v-permission="80121"
          style="
            width: 84px;
            height: 32px;
@@ -388,7 +389,7 @@
          :data="tableData"
          :loading="loading"
          :scroll-x="1500"
          :row-key="(row: PolicyApplicationItem) => row.id"
          :row-key="(row: any) => row.id"
          @update:checked-row-keys="handleCheck"
        />
        <Pagination :count="count" @change="changePage" />
src/views/service/ticket/feedbackType/index.vue
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="tsx">
import { ref, h, computed, onMounted } from "vue";
import type { DataTableColumns, FormInst, FormRules } from "naive-ui";
import { NButton } from "naive-ui";
@@ -9,6 +9,12 @@
  updateFeedbackType,
  deleteFeedbackType,
} from "@/service/api/serviceCenter";
interface FeedbackTypeItem {
  id: number;
  feedbackType: string;
  createTime?: string;
}
const { bool: loading, setTrue: startLoading, setFalse: endLoading } =
  useBoolean(false);
@@ -57,27 +63,28 @@
    key: "actions",
    width: 160,
    align: "center",
    render: (row) =>
      h("div", { class: "flex items-center justify-center gap-2" }, [
        h(
          NButton,
          {
            text: true,
            type: "primary",
            onClick: () => handleEdit(row),
          },
          { default: () => "编辑" }
        ),
        h(
          NButton,
          {
            text: true,
            type: "error",
            onClick: () => handleDelete(row),
          },
          { default: () => "删除" }
        ),
      ]),
    render: (row) => {
      return (
        <div class="flex items-center justify-center gap-2">
          <NButton
            v-permission="80232"
            text
            type="primary"
            onClick={() => handleEdit(row)}
          >
            编辑
          </NButton>
          <NButton
            v-permission="8023"
            text
            type="error"
            onClick={() => handleDelete(row)}
          >
            删除
          </NButton>
        </div>
      );
    },
  },
];
@@ -103,8 +110,8 @@
  await formRef.value?.validate();
  const payload = { feedbackType: formModel.value.feedbackType };
  if (modalMode.value === "add") {
    const { error } = await saveFeedbackType(payload);
    if (!error) {
    const res = await saveFeedbackType(payload);
    if (res.code === 200) {
      window.$message?.success("新增成功");
      modalVisible.value = false;
      getFeedbackTypeList();
@@ -113,11 +120,11 @@
    }
    return;
  }
  const { error } = await updateFeedbackType({
  const res = await updateFeedbackType({
    id: formModel.value.id as number,
    ...payload,
  });
  if (!error) {
  if (res.code === 200) {
    window.$message?.success("编辑成功");
    modalVisible.value = false;
    getFeedbackTypeList();
@@ -133,8 +140,8 @@
    positiveText: "确定",
    negativeText: "取消",
    onPositiveClick: async () => {
      const { error } = await deleteFeedbackType(row.id);
      if (!error) {
      const res = await deleteFeedbackType(row.id);
      if (res.code === 200) {
        window.$message?.success("删除成功");
        getFeedbackTypeList();
      } else {
@@ -147,7 +154,10 @@
async function getFeedbackTypeList() {
  startLoading();
  try {
    const res = await fetchFeedbackTypeList();
    const res = await fetchFeedbackTypeList({
      pageNum: 1,
      pageSize: 9999,
    });
    if (res.code === 200 && res.data) {
      const list = Array.isArray(res.data)
        ? res.data
@@ -174,6 +184,7 @@
    >
      <div class="mb-4 flex justify-end gap-2">
        <n-button
          v-permission="80221"
          style="
            width: 84px;
            height: 32px;
src/views/service/ticket/workOrder/index.vue
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="tsx">
import { ref, h, onMounted } from "vue";
import type { DataTableColumns } from "naive-ui";
import { NButton } from "naive-ui";
@@ -172,38 +172,43 @@
    width: 260,
    align: "center",
    render: (row) => {
      const buttons = [
        h(
          NButton,
          { text: true, type: "primary", onClick: () => handleView(row) },
          { default: () => "详情" }
        ),
      ];
      if ([1,2].includes(row.handleStatus)) {
        buttons.push(
          h(
            NButton,
            { text: true, type: "primary", onClick: () => handleStart(row) },
            { default: () => "处理" }
          )
        );
      }
      buttons.push(
        h(
          NButton,
          { text: true, type: "primary", onClick: () => handleEdit(row) },
          { default: () => "编辑" }
        ),
        h(
          NButton,
          { text: true, type: "error", onClick: () => handleDelete(row) },
          { default: () => "删除" }
        )
      );
      return h(
        "div",
        { class: "flex items-center justify-center gap-2" },
        buttons
      return (
        <div class="flex items-center justify-center gap-2">
          <NButton
            v-permission="80212"
            text
            type="primary"
            onClick={() => handleView(row)}
          >
            详情
          </NButton>
          {[1, 2].includes(row.handleStatus || 0) ? (
            <NButton
              v-permission="80213"
              text
              type="primary"
              onClick={() => handleStart(row)}
            >
              处理
            </NButton>
          ) : null}
          <NButton
            v-permission="80215"
            text
            type="primary"
            onClick={() => handleEdit(row)}
          >
            编辑
          </NButton>
          <NButton
            v-permission="80216"
            text
            type="error"
            onClick={() => handleDelete(row)}
          >
            删除
          </NButton>
        </div>
      );
    },
  },
@@ -246,11 +251,6 @@
}
function handleStart(row: WorkOrderItem) {
  handleRow.value = row;
  handleModalVisible.value = true;
}
function handleProcess(row: WorkOrderItem) {
  handleRow.value = row;
  handleModalVisible.value = true;
}
@@ -348,8 +348,8 @@
    positiveText: "确定",
    negativeText: "取消",
    onPositiveClick: async () => {
      const { error } = await deleteWorkOrder(row.id);
      if (!error) {
      const res = await deleteWorkOrder(row.id);
      if (res.code === 200) {
        window.$message?.success("删除成功");
        getWorkOrderList();
      } else {
@@ -495,6 +495,7 @@
    >
      <div class="mb-4 flex justify-end gap-2">
        <n-button
          v-permission="80211"
          style="
            width: 96px;
            height: 32px;
src/views/system/accountPermission/permissionGroup/form.vue
@@ -159,30 +159,40 @@
// 虚拟权限数据 - 运维APP
const operationPermissions = ref(transformTreeData([
  {
    id: '10',
    menuName: '车辆管理',
    id: 1,
    menuName: '任务中心',
    type: 'page',
    children: [
      {
        id: '1001',
        menuName: '运营车辆',
        children: [
          { id: '100101', menuName: '查看' },
          { id: '100102', menuName: '编辑' },
        ],
      },
      { id: 3, menuName: '筛选', type: 'button' },
      { id: 4, menuName: '查询', type: 'button' },
      { id: 5, menuName: '到达事故现场', type: 'button' },
      { id: 6, menuName: '处理事故', type: 'button' },
    ],
  },
  {
    id: '11',
    menuName: '任务管理',
    id: 7,
    menuName: '工作台',
    type: 'page',
    children: [
      { id: 9, menuName: '事故上报', type: 'button' },
      { id: 10, menuName: '车辆管理', type: 'button' },
      { id: 11, menuName: '站点点位', type: 'button' },
      {
        id: '1101',
        menuName: '任务列表',
        id: 12,
        menuName: '车辆详情',
        type: 'page',
        children: [
          { id: '110101', menuName: '查看' },
          { id: '110102', menuName: '接单' },
          { id: '110103', menuName: '完成' },
          { id: 13, menuName: '车门解锁', type: 'button' },
          { id: 14, menuName: '后备箱解锁', type: 'button' },
          { id: 15, menuName: '发车/收车', type: 'button' },
          { id: 16, menuName: '接单/测试', type: 'button' },
          { id: 17, menuName: '车辆整备', type: 'button' },
          { id: 18, menuName: '车辆充电', type: 'button' },
          { id: 19, menuName: '车辆检测', type: 'button' },
          { id: 20, menuName: '车辆维修', type: 'button' },
          { id: 21, menuName: '车辆清洁', type: 'button' },
          { id: 22, menuName: '运维记录', type: 'button' },
          { id: 23, menuName: '查看保单', type: 'button' },
        ],
      },
    ],
src/views/transaction/order/orderManagement/index.vue
@@ -29,6 +29,7 @@
const model = ref({ ...initialModel })
function handleResetSearch() {
  model.value = { ...initialModel }
  getList()
}
const pageParams = ref({
  pageNum: 1,
@@ -216,7 +217,7 @@
              关闭订单
            </NButton>
          )}
          {row.status === 6 && (
          {row.status === 8 && (
            <NButton
              text
              type="primary"
@@ -251,7 +252,7 @@
          >
            详情
          </NButton>
          <NButton
          {row.status === 0 && <NButton
            text
            v-permission="60117"
            type="error"
@@ -274,7 +275,7 @@
            }}
          >
            删除
          </NButton>
          </NButton>}
        </NSpace>
      )
    },
@@ -378,7 +379,7 @@
  {
    label: t('transaction.city'),
    path: 'cityCode',
    render: () => <n-cascader v-model:value={model.value.cityCode} options={pcaOptions.value} value-field="code" label-field="name" check-strategy="all" clearable />,
    render: () => <n-cascader v-model:value={model.value.cityCode} options={pcaOptions.value} value-field="code" label-field="name" check-strategy="all" clearable placement="left-start" />,
  },
  {
    label: t('transaction.status'),
@@ -436,7 +437,7 @@
  const response = await exportOrderExcel(params)
  if (response instanceof Blob) {
    downloadFile(response, `订单列表_${new Date().getTime()}.xlsx`)
    message.success('导出成功')
    message.success(`导出成功,共${checkedRowKeysRef.value.length > 0 ? checkedRowKeysRef.value.length : count.value}条数据`)
  }
}
src/views/transaction/pricingManagement/components/TableModal.vue
@@ -243,7 +243,7 @@
        <n-select v-model:value="formModel.cityId" :options="cityList" />
      </n-form-item>
      <n-form-item label="说明" path="remark">
        <n-input v-model:value="formModel.remark" type="textarea" />
        <n-input v-model:value="formModel.remark" type="textarea" :maxlength="200"  show-count/>
      </n-form-item>
      <n-form-item label="计费规则" path="pricingType">
        <n-radio-group v-model:value="formModel.pricingType" name="pricingType">
src/views/transaction/pricingManagement/detail.vue
@@ -18,6 +18,118 @@
  return false
}
const validateTimeRanges = () => {
  // 验证起步价时段
  if (model.value.startingFareJson && model.value.startingFareJson.length > 0) {
    // 检查每个时段的开始时间不能大于等于结束时间
    for (const item of model.value.startingFareJson) {
      if (item.num1 && item.num2) {
        // 将时间字符串转换为分钟数进行比较
        const [startHour, startMinute] = item.num1.split(':').map(Number);
        const [endHour, endMinute] = item.num2.split(':').map(Number);
        const startMinutes = startHour * 60 + startMinute;
        const endMinutes = endHour * 60 + endMinute;
        if (startMinutes >= endMinutes) {
          window.$message.error('起步价时段中,结束时间必须大于开始时间');
          return false;
        }
      }
    }
    // 检查时段之间不能重叠
    for (let i = 0; i < model.value.startingFareJson.length; i++) {
      for (let j = i + 1; j < model.value.startingFareJson.length; j++) {
        const item1 = model.value.startingFareJson[i];
        const item2 = model.value.startingFareJson[j];
        if (item1.num1 && item1.num2 && item2.num1 && item2.num2) {
          const [startHour1, startMinute1] = item1.num1.split(':').map(Number);
          const [endHour1, endMinute1] = item1.num2.split(':').map(Number);
          const [startHour2, startMinute2] = item2.num1.split(':').map(Number);
          const [endHour2, endMinute2] = item2.num2.split(':').map(Number);
          const start1 = startHour1 * 60 + startMinute1;
          const end1 = endHour1 * 60 + endMinute1;
          const start2 = startHour2 * 60 + startMinute2;
          const end2 = endHour2 * 60 + endMinute2;
          // 检查两个时间段是否重叠
          if (start1 < end2 && start2 < end1) {
            window.$message.error('起步价时段不能重叠');
            return false;
          }
        }
      }
    }
  }
  // 验证夜间行驶费用时段
  if (model.value.basicPricingJson1.num14 && model.value.basicPricingJson1.num141) {
    const [startHour, startMinute] = model.value.basicPricingJson1.num14.split(':').map(Number);
    const [endHour, endMinute] = model.value.basicPricingJson1.num141.split(':').map(Number);
    const startMinutes = startHour * 60 + startMinute;
    const endMinutes = endHour * 60 + endMinute;
    if (startMinutes >= endMinutes) {
      window.$message.error('夜间行驶费用时段中,结束时间必须大于开始时间');
      return false;
    }
  }
  // 验证第一个高峰期时段
  if (model.value.basicPricingJson1.num21 && model.value.basicPricingJson1.num211) {
    const [startHour, startMinute] = model.value.basicPricingJson1.num21.split(':').map(Number);
    const [endHour, endMinute] = model.value.basicPricingJson1.num211.split(':').map(Number);
    const startMinutes = startHour * 60 + startMinute;
    const endMinutes = endHour * 60 + endMinute;
    if (startMinutes >= endMinutes) {
      window.$message.error('高峰期费用时段中,结束时间必须大于开始时间');
      return false;
    }
  }
  // 验证第二个高峰期时段
  if (model.value.basicPricingJson1.num22 && model.value.basicPricingJson1.num221) {
    const [startHour, startMinute] = model.value.basicPricingJson1.num22.split(':').map(Number);
    const [endHour, endMinute] = model.value.basicPricingJson1.num221.split(':').map(Number);
    const startMinutes = startHour * 60 + startMinute;
    const endMinutes = endHour * 60 + endMinute;
    if (startMinutes >= endMinutes) {
      window.$message.error('高峰期费用时段中,结束时间必须大于开始时间');
      return false;
    }
  }
  // 检查高峰期时段之间不能重叠
  if (model.value.basicPricingJson1.num21 && model.value.basicPricingJson1.num211 &&
      model.value.basicPricingJson1.num22 && model.value.basicPricingJson1.num221) {
    const [startHour1, startMinute1] = model.value.basicPricingJson1.num21.split(':').map(Number);
    const [endHour1, endMinute1] = model.value.basicPricingJson1.num211.split(':').map(Number);
    const [startHour2, startMinute2] = model.value.basicPricingJson1.num22.split(':').map(Number);
    const [endHour2, endMinute2] = model.value.basicPricingJson1.num221.split(':').map(Number);
    const start1 = startHour1 * 60 + startMinute1;
    const end1 = endHour1 * 60 + endMinute1;
    const start2 = startHour2 * 60 + startMinute2;
    const end2 = endHour2 * 60 + endMinute2;
    // 检查两个高峰期时段是否重叠
    if (start1 < end2 && start2 < end1) {
      window.$message.error('高峰期费用时段不能重叠');
      return false;
    }
  }
  return true;
};
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const route = useRoute()
@@ -114,6 +226,9 @@
}
const handleSubmit = async () => {
  if (!validateTimeRanges()) {
    return;
  }
  await formRef.value?.validate()
  if (type.value === 'add') {
    if (category.value === '2' && hasNullValue(model.value.basicPricingJson2)) {
@@ -143,7 +258,7 @@
        }
      })
    }
    if (hasNullValue(model.value.dynamicPricingJson)) {
    if (model.value.isDynamicPrice === 1 && hasNullValue(model.value.dynamicPricingJson)) {
      window.$message.error('请配置动态定价')
      return
    }
@@ -155,7 +270,6 @@
      id: id.value,
      isDynamicPrice: model.value.isDynamicPrice,
    }
    return
    setPricing(params).then((res) => {
      if (res.code === 200) {
        window.$message.success('设置成功')
@@ -237,7 +351,7 @@
              <div class="pl-4">
                <n-form-item label="起步价设置" path="num1" required>
                  起步价为
                  <n-input-number v-model:value="model.basicPricingJson2.num1" min="0" max="999" :precision="0"
                  <n-input-number v-model:value="model.basicPricingJson2.num1" min="0" max="999" :precision="2"
                    :show-button="false" placeholder="最多3位数" class="mx-3" />
                  元,起步价包含
                  <n-input-number v-model:value="model.basicPricingJson2.num2" min="0" max="999" :precision="0"
@@ -251,7 +365,7 @@
              <div class="pl-4">
                <n-form-item label="里程费设置" path="num4" required>
                  超过起步里程后每公里
                  <n-input-number v-model:value="model.basicPricingJson2.num4" min="0" max="999" :precision="0"
                  <n-input-number v-model:value="model.basicPricingJson2.num4" min="0" max="999" :precision="2"
                    :show-button="false" placeholder="最多3位数" class="mx-3" />
                  元
                </n-form-item>
@@ -259,7 +373,7 @@
              <div class="pl-4">
                <n-form-item label="时长费设置" path="num5" required>
                  超过起步时长后每分钟
                  <n-input-number v-model:value="model.basicPricingJson2.num5" min="0" max="999" :precision="0"
                  <n-input-number v-model:value="model.basicPricingJson2.num5" min="0" max="999" :precision="2"
                    :show-button="false" placeholder="最多3位数" class="mx-3" />
                  元
                </n-form-item>
@@ -282,7 +396,7 @@
                  <div class="pl-20">
                    <n-form-item label="起步价设置" path="num3" required>
                      起步价为
                      <n-input-number v-model:value="item.num3" min="0" max="999" :precision="0" :show-button="false"
                      <n-input-number v-model:value="item.num3" min="0" max="999" :precision="2" :show-button="false"
                        placeholder="最多3位数" class="mx-3" />
                      元,起步价包含
                      <n-input-number v-model:value="item.num4" min="0" max="999" :precision="0" :show-button="false"
@@ -296,7 +410,7 @@
                  <div class="pl-20">
                    <n-form-item label="里程费设置" path="num6" required>
                      超过起步里程后每公里
                      <n-input-number v-model:value="item.num6" min="0" max="999" :precision="0" :show-button="false"
                      <n-input-number v-model:value="item.num6" min="0" max="999" :precision="2" :show-button="false"
                        placeholder="最多3位数" class="mx-3" />
                      元
                    </n-form-item>
@@ -304,7 +418,7 @@
                  <div class="pl-20">
                    <n-form-item label="时长费设置" path="num7" required>
                      超过起步时长后每分钟
                      <n-input-number v-model:value="item.num7" min="0" max="999" :precision="0" :show-button="false"
                      <n-input-number v-model:value="item.num7" min="0" max="999" :precision="2" :show-button="false"
                        placeholder="最多3位数" class="mx-3" />
                      元
                    </n-form-item>
@@ -317,7 +431,7 @@
                  <n-input-number v-model:value="model.basicPricingJson1.num4" min="0" max="999" :precision="0"
                    :show-button="false" placeholder="最多3位数" class="mx-3" />
                  分钟后开始收取等待费,为
                  <n-input-number v-model:value="model.basicPricingJson1.num5" min="0" max="999" :precision="0"
                  <n-input-number v-model:value="model.basicPricingJson1.num5" min="0" max="999" :precision="2"
                    :show-button="false" placeholder="最多3位数" class="mx-3" />
                  元/分,不足一分钟按一分钟计算
                </n-form-item>
@@ -333,7 +447,7 @@
                      <n-input-number v-model:value="model.basicPricingJson1.num7" min="0" max="999" :precision="0"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      公里,超出部分每公里加收
                      <n-input-number v-model:value="model.basicPricingJson1.num8" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num8" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
@@ -345,7 +459,7 @@
                      <n-input-number v-model:value="model.basicPricingJson1.num10" min="0" max="999" :precision="0"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      公里时,超出部分每公里加收
                      <n-input-number v-model:value="model.basicPricingJson1.num11" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num11" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
@@ -354,7 +468,7 @@
                      <n-input-number v-model:value="model.basicPricingJson1.num12" min="0" max="999" :precision="0"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      公里以后,超出部分每公里加收
                      <n-input-number v-model:value="model.basicPricingJson1.num13" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num13" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
@@ -370,26 +484,26 @@
                      <n-time-picker v-model:formatted-value="model.basicPricingJson1.num141" format="HH:mm"
                        value-format="HH:mm" clearable class="w-80px" />
                      点为夜间行车时间,起步价变为
                      <n-input-number v-model:value="model.basicPricingJson1.num15" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num15" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
                    <NSpace align="center">
                      里程费为每公里
                      <n-input-number v-model:value="model.basicPricingJson1.num16" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num16" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元,时长费为每
                      <n-input-number v-model:value="model.basicPricingJson1.num17" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num17" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
                    <NSpace align="center">
                      远途费为每公里加收
                      <n-input-number v-model:value="model.basicPricingJson1.num18" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num18" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      <n-input-number v-model:value="model.basicPricingJson1.num19" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num19" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      <n-input-number v-model:value="model.basicPricingJson1.num20" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num20" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
@@ -410,26 +524,26 @@
                      <n-time-picker v-model:formatted-value="model.basicPricingJson1.num221" format="HH:mm"
                        value-format="HH:mm" clearable class="w-80px" />
                      为高峰期行车时间,起步价变为
                      <n-input-number v-model:value="model.basicPricingJson1.num23" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num23" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
                    <NSpace align="center">
                      里程费为每公里
                      <n-input-number v-model:value="model.basicPricingJson1.num24" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num24" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元,时长费为每
                      <n-input-number v-model:value="model.basicPricingJson1.num25" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num25" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
                    <NSpace align="center">
                      远途费为每公里加收
                      <n-input-number v-model:value="model.basicPricingJson1.num26" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num26" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      <n-input-number v-model:value="model.basicPricingJson1.num27" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num27" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      <n-input-number v-model:value="model.basicPricingJson1.num28" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num28" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
@@ -441,26 +555,26 @@
                  <NSpace vertical :size="16">
                    <NSpace align="center">
                      节假日起步价变为
                      <n-input-number v-model:value="model.basicPricingJson1.num29" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num29" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
                    <NSpace align="center">
                      里程费为每公里
                      <n-input-number v-model:value="model.basicPricingJson1.num30" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num30" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元,时长费为每
                      <n-input-number v-model:value="model.basicPricingJson1.num31" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num31" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
                    <NSpace align="center">
                      远途费为每公里加收
                      <n-input-number v-model:value="model.basicPricingJson1.num32" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num32" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      <n-input-number v-model:value="model.basicPricingJson1.num33" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num33" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      <n-input-number v-model:value="model.basicPricingJson1.num34" min="0" max="999" :precision="0"
                      <n-input-number v-model:value="model.basicPricingJson1.num34" min="0" max="999" :precision="2"
                        :show-button="false" placeholder="最多3位数" class="mr-3" />
                      元
                    </NSpace>
@@ -512,7 +626,7 @@
                    :show-button="false" class="mx-3" />
                  单,价格上浮
                  <n-input-number v-model:value="model.dynamicPricingJson.num5" min="0" :precision="0"
                    :show-button="false" class="mx-3" />%
                    :show-button="false" class="mx-3" max="100"/>%
                </n-form-item>
              </div>
              <div class="pl-20">
@@ -534,7 +648,7 @@
                    :show-button="false" class="mx-3" />
                  km/h视为拥堵,拥堵价格上浮比例:
                  <n-input-number v-model:value="model.dynamicPricingJson.num8" min="0" :precision="0"
                    :show-button="false" class="mx-3" />%
                    :show-button="false" class="mx-3" max="100" />%
                </n-form-item>
              </div>
              <div class="pl-20">
@@ -655,7 +769,7 @@
                <n-form-item label="夜间行驶费用设置" path="basicPricingJson1">
                  <NSpace vertical>
                    <NSpace align="center">
                      {{ model.basicPricingJson1.num14 }} 点为夜间行车时间,起步价变为 {{ model.basicPricingJson1.num15 }} 元
                      {{ model.basicPricingJson1.num14 }}-{{ model.basicPricingJson1.num141 }} 点为夜间行车时间,起步价变为 {{ model.basicPricingJson1.num15 }} 元
                    </NSpace>
                    <NSpace align="center">
                      里程费为每公里 {{ model.basicPricingJson1.num16 }} 元, 时长费为每分钟 {{ model.basicPricingJson1.num17 }} 元
@@ -671,7 +785,7 @@
                <n-form-item label="高峰期费用设置" path="basicPricingJson1">
                  <NSpace vertical>
                    <NSpace align="center">
                      {{ model.basicPricingJson1.num21 }}-{{ model.basicPricingJson1.num22 }} 为高峰期行车时间,起步价变为 {{
                      {{ model.basicPricingJson1.num21 }}-{{ model.basicPricingJson1.num211 }} 和 {{ model.basicPricingJson1.num22 }}-{{ model.basicPricingJson1.num221 }} 为高峰期行车时间,起步价变为 {{
                        model.basicPricingJson1.num23 }} 元
                    </NSpace>
                    <NSpace align="center">
src/views/transaction/pricingManagement/index.vue
@@ -11,6 +11,7 @@
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const { t } = useI18n()
const dialog = useDialog()
const initialModel = {
  vehicleModel: '',
@@ -19,6 +20,7 @@
const model = ref({ ...initialModel })
function handleResetSearch() {
  model.value = { ...initialModel }
  getList()
}
const formRef = ref<FormInst | null>()
@@ -120,12 +122,25 @@
          >
            编辑
          </NButton>
          <NPopconfirm v-permission="6026" onPositiveClick={() => delteteUser(row.id!)}>
            {{
              default: () => '确认删除',
              trigger: () => <NButton text type="error">删除</NButton>,
          <NButton
            text
            v-permission="6026"
            type="error"
            onClick={() => {
              dialog.create({
                title: '是否确认删除数据?',
                content: '确认删除吗该定价配置?删除后不可恢复',
                positiveText: '确定',
                negativeText: '取消',
                draggable: true,
                onPositiveClick: () => {
                  delteteUser(row.id!)
                },
              })
            }}
          </NPopconfirm>
          >
            删除
          </NButton>
        </NSpace>
      )
    },
src/views/transaction/transactionStrategy/index.vue
@@ -26,6 +26,10 @@
  twoPushTime: null, // 二轮推单时间
  threePushDistance: null, // 三轮推单距离
  threePushTime: null, // 三轮推单时间
  fourPushDistance: null, // 四轮推单距离
  fourPushTime: null, // 四轮推单时间
  fivePushDistance: null, // 五轮推单距离
  fivePushTime: null, // 五轮推单时间
  passwordFreeAmount: null, // 免密授权金额
  prepaidLimit: null, // 预付额度 订单大于金额需预付
  ruleType: 1, // 规则类型
@@ -206,18 +210,32 @@
}
async function handleSubmit() {
  startLoading()
  formRef.value?.validate().then(() => {
   try {
    await formRef.value?.validate()
    if (!model.value.cancleCount) {
      message.error('请输入每日取消订单数')
      return
    }
    if (!model.value.onePushDistance || !model.value.onePushTime || !model.value.twoPushDistance || !model.value.twoPushTime || !model.value.threePushDistance || !model.value.threePushTime || !model.value.fourPushDistance || !model.value.fourPushTime || !model.value.fivePushDistance || !model.value.fivePushTime) {
      message.error('请输入播单策略')
      return
    }
    if (!model.value.ruleType) {
      message.error('请选择举手规则')
      return
    }
    if (!model.value.simultaneouslyOrderCount) {
      message.error('请输入同时抢单数')
      return
    }
    // 校验分单倾向配置是否有空数据
    if (data.value.some((item) => item.value === null)) {
      message.error('请配置分单倾向')
      endLoading()
      return
    }
    // 校验权重配置是否有空数据
    if (data1.value.some((item) => item.value === null)) {
      message.error('请配置订单价值权重')
      endLoading()
      return
    }
    // 重组分单倾向配置
@@ -236,14 +254,19 @@
      ...obj,
      ...obj1
    }
    startLoading()
    editTradingStrategy(params).then((res) => {
      if (res.code === 200) {
        window.$message.success('保存成功')
        getDetail()
      }
    }).finally(() => {
      endLoading()
    })
  })
  } catch (error) {
    endLoading()
    console.log('验证失败', error)
  }
}
const getDetail = ()=> {
@@ -388,7 +411,7 @@
                label-style="color: rgba(23, 31, 38, 0.8);">
                <n-input-number v-model:value="model.simultaneouslyOrderCount" min="0" :show-button="false"
                  class="mr-2 w-218px" />
                可同时抢单
                辆车可同时抢单
              </n-form-item>
            </NSpace>
            <NSpace :size="[24, 16]">
src/views/userContent/whitelistManagement/whitelist/index.vue
@@ -214,8 +214,8 @@
  input.onchange = async (e: any) => {
    const file = e.target.files?.[0]
    if (file) {
      const { error } = await importWhitelist(file)
      if (!error) {
      const { code } = await importWhitelist(file)
      if (code === 200) {
        message.success('导入成功')
        getWhitelistList()
      }
@@ -255,8 +255,8 @@
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      const { error } = await freezeWhitelistByIds(selectedRowKeys.value.join(','))
      if (!error) {
      const { code } = await freezeWhitelistByIds(selectedRowKeys.value.join(','))
      if (code === 200) {
        message.success('批量冻结成功')
        selectedRowKeys.value = []
        getWhitelistList()
@@ -285,8 +285,8 @@
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      const { error } = await freezeWhitelistByIds(row.id.toString())
      if (!error) {
      const { code } = await freezeWhitelistByIds(row.id.toString())
      if (code === 200) {
        message.success('冻结成功')
        getWhitelistList()
      }
@@ -302,8 +302,8 @@
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      const { error } = await unfreezeWhitelist(row.id)
      if (!error) {
      const { code } = await unfreezeWhitelist(row.id)
      if (code === 200) {
        message.success('解冻成功')
        getWhitelistList()
      }
@@ -324,8 +324,8 @@
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      const { error } = await deleteWhitelistByIds(row.id.toString())
      if (!error) {
      const { code } = await deleteWhitelistByIds(row.id.toString())
      if (code === 200) {
        message.success('删除成功')
        getWhitelistList()
      }