董国庆
2026-03-12 24877ed183b74010d0ddbfa0561a3478ec838478
Merge branch 'main' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web
7个文件已修改
538 ■■■■■ 已修改文件
src/service/api/transaction.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/geofenceManagement/detail.vue 287 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/statistics/detail.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/components/Modal.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/components/TableModal.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/detail.vue 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/transaction.ts
@@ -25,6 +25,11 @@
  return request.Post<Service.ResponseResult<any>>('/order/api/v1/order/reassignById', params)
}
// 订单改价
export function changeAmount(params: any) {
  return request.Post<Service.ResponseResult<any>>('/order/api/v1/order/changeAmount', params)
}
// 获取订单详情
export function getOrderDetail(id: any) {
  return request.Get<Service.ResponseResult<any>>('/order/api/v1/order/queryById?id=' + id)
src/views/businessOperations/geofenceManagement/detail.vue
@@ -24,8 +24,6 @@
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 NEVER_EXPIRE = -1 // -1 永不失效
const IMMEDIATE_EFFECT = -2 // -2 立即生效
const MAX_VERTICES = 16 // 最大顶点数限制
const MAX_AREA = 81 // 最大区域面积限制(单位:平方公里)
let centerPoint = [104.066301, 30.572961]
@@ -79,6 +77,8 @@
  map.on('click', (e: any) => {
    // layerOptions.isActive = false
    console.log('点击事件', e)
    if (isDrawing)
      return
    // 检查是否点击在运营区域(主围栏)内或者点击在禁行区内
    const isPointInFence = fence && (!isDrawing || modalType.value === 'detail') && isPointInPolygon(e.lnglat, fence.getPath())
    const isPointInDisallowedArea = fence && (!isDrawing || modalType.value === 'detail') && disallowedAreaList.value?.some((item: any) => isPointInPolygon(e.lnglat, item.fence?.getPath()))
@@ -93,6 +93,7 @@
          handleDisallowedAreaEdit(disabledArea)
        }
        else {
          console.log('点击在运营区域内')
          // 关闭所有区域的编辑态
          fenceEditor?.close()
          disallowedAreaList.value.forEach((item: any) => item.isActive = false)
@@ -264,7 +265,8 @@
/** 绘制禁行区域多边形围栏 */
function drawDisallowedAreaPolygon(id: any) {
  // 正在绘制或编辑时,禁止再次绘制
  if (isDrawing || isEditing) {
  // if (isDrawing || isEditing) {
  if (isEditing) {
    console.log('正在绘制或编辑禁行区,禁止再次绘制')
    return
  }
@@ -414,11 +416,14 @@
        message.warning('请绘制至少3个点的禁行区')
      }
      isEditing = false
      isDrawing = true
      fenceEditorClosed = true
      setTimeout(() => {
        disallowedArea.isActive = true
        disallowedArea.fence = null
        drawDisallowedAreaPolygon(id)
      }, 100)
      return false
    }
    const area = calcPolygonArea(path)
    console.log('禁行区面积:', path)
@@ -728,13 +733,13 @@
    required: true,
    trigger: ['blur', 'change'],
    validator: (_rule: any, value: any) => {
      // 如果是立即生效,则跳过时间比较验证
      if (formModel.value.effectType === 1 && effectTimeText.value === '立即生效') {
        return true
      }
      // 验证不能为空
      if (value == null) {
        return new Error('请选择生效时间')
      }
      // 如果是立即生效,则跳过时间比较验证
      if (value === IMMEDIATE_EFFECT) {
        return true
      }
      // 如果是具体时间值,则验证不能早于当前时间
      if (typeof value === 'number') {
@@ -764,16 +769,16 @@
    required: true,
    trigger: ['blur', 'change'],
    validator: (_rule: any, value: any) => {
      // 如果是永不失效,则跳过时间比较验证
      if (formModel.value.failureType === 1 && failureTimeText.value === '永不失效') {
        return true
      }
      // 验证不能为空
      if (value == null) {
        return new Error('请选择失效时间')
      }
      // 如果是永不失效,则跳过时间比较验证
      if (value === NEVER_EXPIRE) {
        return true
      }
      // 如果生效时间是立即生效(IMMEDIATE_EFFECT),则失效时间需要大于当前时间
      if (formModel.value.effectTime === IMMEDIATE_EFFECT && typeof value === 'number') {
      if (formModel.value.effectType === 1 && effectTimeText.value === '立即生效' && typeof value === 'number') {
        const currentTime = Date.now()
        if (value <= currentTime) {
          return new Error('失效时间不能小于当前时间')
@@ -849,12 +854,6 @@
    value: 7,
  },
])
const effectiveShortcuts = {
  立即生效: IMMEDIATE_EFFECT,
}
const expireShortcuts = {
  永不失效: NEVER_EXPIRE,
}
// 处理星期选择变化
function handleWeekSelectionChange(selectedValues: number[], item: any) {
@@ -881,14 +880,17 @@
const effectTimeText = ref('')
const failureTimeText = ref('')
const effectTimePicker = ref<any>()
const failureTimePicker = ref<any>()
/** 监听生效时间 */
function handleEffectTime() {
  effectTimeText.value = '立即生效'
  formModel.value.effectType = 1
  effectTimePicker.value?.handlePanelClose()
}
watch(() => formModel.value.effectTime, (newVal) => {
  if (newVal === IMMEDIATE_EFFECT) {
    effectTimeText.value = '立即生效'
    formModel.value.effectType = 1
  }
  else if (typeof newVal === 'number') {
  if (typeof newVal === 'number') {
    effectTimeText.value = dayjs(newVal).format('YYYY-MM-DD HH:mm:ss')
    formModel.value.effectType = 2
  }
@@ -898,12 +900,13 @@
})
/** 监听失效时间 */
function handleFailureTime() {
  failureTimeText.value = '永不失效'
  formModel.value.failureType = 1
  failureTimePicker.value?.handlePanelClose()
}
watch(() => formModel.value.failureTime, (newVal) => {
  if (newVal === NEVER_EXPIRE) {
    failureTimeText.value = '永不失效'
    formModel.value.failureType = 1
  }
  else if (typeof newVal === 'number') {
  if (typeof newVal === 'number') {
    failureTimeText.value = dayjs(newVal).format('YYYY-MM-DD HH:mm:ss')
    formModel.value.failureType = 2
  }
@@ -920,7 +923,11 @@
  groupList.value.push({
    name: `策略组${groupList.value.length + 1}`,
    type: 0,
    time: [],
    time: [{
      startTime: null,
      endTime: null,
      id: 1,
    }],
    edit: false,
    typeStr: [],
    id: groupList.value.length + 1,
@@ -992,12 +999,19 @@
})
// 处理禁行区自定义选择变化
function handleSelectionChange(selectedValues: number[], item: any) {
function handleSelectionChange(selectedValues: any, item: any) {
  if (groupList.value.length > 0) {
    if (selectedValues && (item.typeStr || item.typeStr.length > 0) && item.time.length === 0) {
      item.time.push({
        startTime: null,
        endTime: null,
        id: item.time.length + 1,
      })
    }
    return
  }
  // 如果选中了"每天"(值为0),则只保留"每天"的选项
  if (selectedValues.includes(0)) {
  if (selectedValues.length > 0 && selectedValues.includes(0)) {
    item.typeStr = [0]
  }
  else {
@@ -1013,6 +1027,13 @@
      // 否则保留当前选择
      item.typeStr = selectedValues
    }
  }
  if (selectedValues.length > 0 && (item.typeStr || item.typeStr.length > 0) && item.time.length === 0) {
    item.time.push({
      startTime: null,
      endTime: null,
      id: item.time.length + 1,
    })
  }
}
@@ -1048,6 +1069,55 @@
  disallowedFormModel.value.operationStrategy = []
}
/** 确认禁行区编辑 */
/** 判断禁行区时间是否被禁用 */
function isDisallowedTimeDisabled(hour: number, minute: number | undefined, item: any, type: 'start' | 'end') {
  if (groupList.value.length === 0 || !item.typeStr) {
    return false
  }
  const selectedGroup = groupList.value.find((g: any) => g.id === item.typeStr)
  if (!selectedGroup) {
    return false
  }
  // 计算允许的时间段集合
  const allowedRanges = selectedGroup.time.map((sub: any) => {
    if (!sub.startTime || !sub.endTime)
      return null
    const [startH, startM] = sub.startTime.split(':').map(Number)
    const [endH, endM] = sub.endTime.split(':').map(Number)
    return {
      start: startH * 60 + startM,
      end: endH * 60 + endM,
    }
  }).filter(Boolean)
  if (minute === undefined) {
    // 小时禁用逻辑:如果该小时内没有任何一分钟在允许范围内,则禁用该小时
    // 即:该小时的时间段 [hour*60, (hour+1)*60) 与所有允许时间段都没有交集
    const hourStart = hour * 60
    const hourEnd = (hour + 1) * 60 // 不包含
    const hasIntersection = allowedRanges.some((range: any) => {
      // 判断区间 [hourStart, hourEnd) 与 [range.start, range.end] 是否有交集
      // max(start1, start2) < min(end1, end2)
      // 注意:这里 range.end 是包含的,为了方便比较,视作闭区间
      // 但小时结束是不包含的。
      // 实际上只要 hourStart <= range.end 且 hourEnd > range.start
      return Math.max(hourStart, range.start) < Math.min(hourEnd, range.end + 1)
    })
    return !hasIntersection
  }
  else {
    // 分钟禁用逻辑
    const currentMinute = hour * 60 + minute
    const isAllowed = allowedRanges.some((range: any) => {
      return currentMinute >= range.start && currentMinute <= range.end
    })
    return !isAllowed
  }
}
function handleConfirm() {
  // 校验禁行区的运营策略时段必须在所选运营区策略的时段覆盖内
  if (groupList.value.length > 0) {
@@ -1123,7 +1193,7 @@
/** 添加禁行区 */
function addDisallowedArea() {
  if (disallowedAreaList.value.some((item: any) => item.fence === null)) {
    message.error('请先绘制禁行区')
    message.error('请先绘制或删除当前选中禁行区')
    return
  }
  disallowedAreaList.value.forEach((sub: any) => {
@@ -1133,7 +1203,14 @@
  disallowedAreaList.value.push({
    areaName: `禁行区${disallowedAreaList.value.length + 1}`,
    areaDescription: '',
    operationStrategy: '',
    operationStrategy: [{
      name: `策略组1`,
      type: 0,
      time: [],
      edit: false,
      typeStr: [],
      id: 1,
    }],
    areaStatus: 1,
    isActive: true,
    edit: false,
@@ -1147,6 +1224,10 @@
/** 开启禁行区编辑能力 */
function handleDisallowedAreaEdit(item: any) {
  if (disallowedAreaList.value.some((item: any) => item.fence === null)) {
    message.error('请先绘制或删除当前选中禁行区')
    return
  }
  closeFenceEditor()
  toggleDisallowedArea(item)
  enableDisallowedAreaFenceEdit(item.id)
@@ -1163,10 +1244,16 @@
/** 删除禁行区 */
function removeDisallowedArea(item: any) {
  if (mouseTool?.polygonTemp) {
    mouseTool.polygonTemp.obj.setPath([])
    mouseTool?.close()
    mouseTool = null
    isDrawing = false
  }
  if (item.fence) {
    map.remove(item.fence)
    item.fence.destroy()
    fenceEditor.close()
    fenceEditor?.close()
  }
  disallowedAreaList.value = disallowedAreaList.value.filter((sub: any) => sub.id !== item.id)
}
@@ -1174,13 +1261,12 @@
/** 保存草稿/发布围栏 */
function handleAddFence(type: number) {
  fencePoints = fence.getPath().map((p: any) => [p.lng, p.lat])
  console.log(disallowedAreaList.value)
  if (disallowedAreaList.value.some((item: any) => !item.fence || item.fence === null || item.fence.getPath().map((p: any) => (`${p.lng}_${p.lat}`)).length < 3)) {
    message.error('请先绘制禁行区')
    return
  }
  if (isPolygonSelfIntersect(fencePoints)) {
    message.error('围栏存在自相交,请调整形状')
    message.error('运营区域不允许自相交')
    return
  }
  const fenceArea = renderedPolygons.value.filter((item: any) => item.areaType === 1).find((item: any) => isPolygonIntersectOthers(fencePoints, item.points))
@@ -1200,12 +1286,12 @@
  }
  const disallowedArea = disallowedAreaList.value.find((item: any) => isPolygonSelfIntersect(item.fence.getPath().map((p: any) => [p.lng, p.lat])))
  if (disallowedArea) {
    message.error(`禁行区 ${disallowedArea.areaName} 存在自相交,请调整形状`)
    message.error(`禁行区 ${disallowedArea.areaName} 存在自相交`)
    return
  }
  const disallowedAreaIntersect = disallowedAreaList.value.find((item: any) => isPolygonIntersectOthers(fencePoints, item.fence.getPath().map((p: any) => [p.lng, p.lat])))
  if (disallowedAreaIntersect) {
    message.error(`${disallowedAreaIntersect.areaName} 与运营区相交,请调整形状`)
    message.error(`${disallowedAreaIntersect.areaName} 与运营区相交`)
    return
  }
  // 禁行区不能在运营区外
@@ -1219,6 +1305,40 @@
    return
  }
  formRef.value.validate().then(() => {
    // 验证运营策略重复
    const strategyTypes = new Set<string>()
    for (const item of groupList.value) {
      let typeKey = ''
      if (item.type === 0) { // 工作日
        typeKey = 'workday'
      }
      else if (item.type === 2) { // 非工作日
        typeKey = 'holiday'
      }
      else if (item.type === 1) { // 法定节假日
        typeKey = 'statutoryHoliday'
      }
      else if (item.type === 3) { // 自定义
        if (item.typeStr && item.typeStr.length > 0) {
          const sortedDays = [...item.typeStr].sort((a, b) => a - b).join(',')
          typeKey = `custom-${sortedDays}`
        }
      }
      if (typeKey) {
        if (strategyTypes.has(typeKey)) {
          message.error(`运营策略存在重复:${item.name} 的类型已存在`)
          mainScrollRef.value?.scrollTo({ top: 9999, behavior: 'smooth' })
          return
        }
        strategyTypes.add(typeKey)
      }
      // 验证每天与其他策略互斥
      if (typeKey.includes('custom-0') && groupList.value.length > 1) {
        message.error('选择“每天”时,不能添加其他运营策略')
        mainScrollRef.value?.scrollTo({ top: 9999, behavior: 'smooth' })
        return
      }
    }
    // 验证运营策略时间段
    for (const item of groupList.value) {
      const timeIntervals: { start: string, end: string }[] = []
@@ -1227,12 +1347,14 @@
          // 比较开始时间和结束时间
          if (sub.startTime >= sub.endTime) {
            message.error('运营策略时间段中,结束时间不能小于或等于开始时间')
            mainScrollRef.value?.scrollTo({ top: 9999, behavior: 'smooth' })
            return
          }
          timeIntervals.push({ start: sub.startTime, end: sub.endTime })
        }
        else if (!sub.startTime || !sub.endTime) {
          message.error('运营策略时间段中,开始时间和结束时间不能为空')
          mainScrollRef.value?.scrollTo({ top: 9999, behavior: 'smooth' })
          return
        }
      }
@@ -1242,6 +1364,7 @@
      for (let i = 0; i < timeIntervals.length - 1; i++) {
        if (timeIntervals[i].end > timeIntervals[i + 1].start) {
          message.error('同一个策略的时段不能重叠')
          mainScrollRef.value?.scrollTo({ top: 9999, behavior: 'smooth' })
          return
        }
      }
@@ -1265,7 +1388,8 @@
      id: fenceDetail.value.electronicFenceAreas?.find((item: any) => item.areaType === 1)?.id ?? null,
    }]
    disallowedAreaList.value.forEach((item: any) => {
      const operationStrategy = item.operationStrategy.map((sub: any) => ({
      console.log(item.operationStrategy)
      const operationStrategy = item.operationStrategy?.map((sub: any) => ({
        name: sub.name,
        type: sub.type,
        time: sub.time.map((key: any) => (`${key.startTime}-${key.endTime}`)),
@@ -1336,6 +1460,8 @@
        }).finally(() => endLoading())
      }
    }
  }).catch(() => {
    mainScrollRef.value?.scrollTo({ top: 0, behavior: 'smooth' })
  })
}
@@ -1641,13 +1767,19 @@
          status: fenceDetail.value.status || 1,
          areaType: 1,
          area: fenceDetail.value.area || '',
          effectTime: fenceDetail.value.effectType === 1 ? IMMEDIATE_EFFECT : dayjs(fenceDetail.value.effectTime).valueOf(),
          effectTime: fenceDetail.value.effectType === 1 ? null : dayjs(fenceDetail.value.effectTime).valueOf(),
          effectType: fenceDetail.value.effectType || 2,
          failureTime: fenceDetail.value.failureType === 1 ? NEVER_EXPIRE : dayjs(fenceDetail.value.failureTime).valueOf(),
          failureTime: fenceDetail.value.failureType === 1 ? null : dayjs(fenceDetail.value.failureTime).valueOf(),
          failureType: fenceDetail.value.failureType || 2,
          operationStrategy: fenceDetail.value.operationStrategy || null,
          expireTimeTimestamp: fenceDetail.value.expireTimeTimestamp || null,
          areaStatus: fenceDetail.value.electronicFenceAreas.filter((item: any) => item.areaType === 1)[0].areaStatus || 1,
        }
        if (fenceDetail.value.effectType === 1) {
          effectTimeText.value = '立即生效'
        }
        if (fenceDetail.value.failureType === 1) {
          failureTimeText.value = '永不失效'
        }
        /** 运营区域的运营策略 */
        groupList.value = groupList.value.map((group: any, index: number) => {
@@ -1746,10 +1878,10 @@
    <!-- 地图容器 -->
    <div ref="domRef" class="w-full h-full" :style="drawerVisible ? 'width: calc(100% - 280px);' : ''" />
    <!-- 提示 -->
    <div v-if="modalType === 'add'" class="tips">
    <div v-if="modalType === 'add' && (isDrawing || isEditing) " class="tips">
      {{ !isActiceDisallowedArea ? '请在地图上【非运营区域】绘制多边形区域,双击闭合' : '请在地图上【蓝色运营区域】绘制多边形区域,双击闭合' }}
    </div>
    <div v-if="modalType === 'edit' && fenceDetail?.status !== 2" class="tips">
    <div v-if="modalType === 'edit' && fenceDetail?.status !== 2 && (isDrawing || isEditing)" class="tips">
      {{ !isActiceDisallowedArea ? '【蓝色运营区】可调整区域点位,请勿与其他运营区重叠' : '请在地图上【蓝色运营区域】绘制多边形区域,双击闭合' }}
    </div>
    <!-- 抽屉 -->
@@ -1759,21 +1891,6 @@
        <img v-else src="../../../assets/geofence/foldUp.png" class="icon" @click="toggleDrawer">
      </div>
      <div class="drawer-content">
        <!-- <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'"
              :bordered="false" class="h-24px ml-4px">
              {{ fenceDetail?.status === 0 ? '草稿' : fenceDetail?.status === 1 ? '待生效' : fenceDetail?.status === 2  ? '启用中' : '已停用' }}
            </NTag>
          </div>
          <img src="../../../assets/geofence/close2.png" class="w-24px h-24px ml-auto cursor-pointer" @click="onClose"
            v-if="modalType === 'edit'">
        </div>
        <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 ref="mainScrollRef" class="drawer-main">
          <div class="main-head" :class="{ 'sticky-top': showDrawerHead }">
            <div v-if="modalType === 'add'" class="flex items-center gap-4px mb-16px">
@@ -1984,10 +2101,19 @@
                  </n-input>
                  <div class="position">
                    <n-date-picker
                      v-model:value="formModel.effectTime" type="datetime" clearable
                      class="w-full opacity-0" :shortcuts="effectiveShortcuts" :disabled="isEdit"
                      ref="effectTimePicker" v-model:value="formModel.effectTime" type="datetime"
                      clearable
                      class="w-full opacity-0" :disabled="isEdit"
                      @clear="effectTimeText = ''"
                    />
                    >
                      <template #footer>
                        <div>
                          <NButton text type="primary" @click="handleEffectTime">
                            立即生效
                          </NButton>
                        </div>
                      </template>
                    </n-date-picker>
                  </div>
                  <!-- <n-date-picker panel type="datetime" :shortcuts="effectiveShortcuts"/> -->
                </n-form-item>
@@ -2002,10 +2128,19 @@
                  </n-input>
                  <div class="position">
                    <n-date-picker
                      v-model:value="formModel.failureTime" type="datetime" clearable
                      class="w-full opacity-0" :shortcuts="expireShortcuts" :disabled="isEdit"
                      ref="failureTimePicker" v-model:value="formModel.failureTime" type="datetime"
                      clearable
                      class="w-full opacity-0" :disabled="isEdit"
                      @clear="failureTimeText = ''"
                    />
                    >
                      <template #footer>
                        <div>
                          <NButton text type="primary" @click="handleFailureTime">
                            永不失效
                          </NButton>
                        </div>
                      </template>
                    </n-date-picker>
                  </div>
                </n-form-item>
              </div>
@@ -2060,12 +2195,12 @@
                        <div v-for="sub in item.time" :key="sub.id" class="gap-8px flex items-center time-range">
                          <n-time-picker
                            v-model:formatted-value="sub.startTime" format="HH:mm" value-format="HH:mm"
                            placeholder="请选择" clearable :disabled="isEdit"
                            placeholder="请选择" clearable :disabled="isEdit" :status="sub.startTime ? 'success' : 'error'"
                          />
                          至
                          <n-time-picker
                            v-model:formatted-value="sub.endTime" format="HH:mm" value-format="HH:mm"
                            placeholder="请选择" clearable :disabled="isEdit"
                            placeholder="请选择" clearable :disabled="isEdit" :status="sub.endTime ? 'success' : 'error'"
                          />
                          <img
                            v-if="!isEdit" src="../../../assets/geofence/del.png"
@@ -2263,9 +2398,17 @@
                  </div>
                  <div>
                    <n-select
                      v-if="item.type === 1" v-model:value="item.typeStr" class="flex-1"
                      v-if="item.type === 1 && groupList.length === 0" v-model:value="item.typeStr" class="flex-1"
                      :options="disallowedAreaOptions"
                      :disabled="item.type !== 1" :multiple="groupList.length === 0" :max-tag-count="1" clearable
                      :status="item.typeStr && item.typeStr.length > 0 ? 'success' : 'error'"
                      :disabled="item.type !== 1" multiple :max-tag-count="1" clearable
                      @update:value="(value) => handleSelectionChange(value, item)"
                    />
                    <n-select
                      v-if="item.type === 1 && groupList.length > 0" v-model:value="item.typeStr" class="flex-1"
                      :options="disallowedAreaOptions"
                      :status="item.typeStr ? 'success' : 'error'"
                      :disabled="item.type !== 1" clearable
                      @update:value="(value) => handleSelectionChange(value, item)"
                    />
                  </div>
@@ -2273,12 +2416,16 @@
                    <div v-for="sub in item.time" :key="sub.id" class="gap-8px flex items-center time-range">
                      <n-time-picker
                        v-model:formatted-value="sub.startTime" format="HH:mm" value-format="HH:mm"
                        placeholder="请选择" clearable
                        placeholder="请选择" clearable :status="sub.startTime ? 'success' : 'error'"
                        :is-hour-disabled="(hour: number) => isDisallowedTimeDisabled(hour, undefined, item, 'start')"
                        :is-minute-disabled="(minute: number, hour: number) => isDisallowedTimeDisabled(hour, minute, item, 'start')"
                      />
                      至
                      <n-time-picker
                        v-model:formatted-value="sub.endTime" format="HH:mm" value-format="HH:mm"
                        placeholder="请选择" clearable
                        placeholder="请选择" clearable :status="sub.endTime ? 'success' : 'error'"
                        :is-hour-disabled="(hour: number) => isDisallowedTimeDisabled(hour, undefined, item, 'end')"
                        :is-minute-disabled="(minute: number, hour: number) => isDisallowedTimeDisabled(hour, minute, item, 'end')"
                      />
                      <img
                        src="../../../assets/geofence/del.png"
src/views/operations/attendance/statistics/detail.vue
@@ -80,30 +80,36 @@
  { title: '角色', key: 'roleName', width: 120, resizable: true, },
  { title: '所属队伍', key: 'teamName', width: 120, resizable: true, },
  { title: '日期', key: 'clockTime', width: 120 },
  { title: '班次', key: 'shiftName', width: 120 },
  { title: '上班打卡时间', key: 'clockInTime', minWidth: 120 },
  { title: '班次', key: 'shiftName', width: 160 },
  { title: '上班打卡时间', key: 'clockInTime', width: 160 },
  {
    title: '上班打卡结果',
    key: 'clockInResult',
    minWidth: 120,
    render: (row) => (
      <NTag type={getResultTagType(row.clockInResult)} size="small">
        {row.clockInResult}
      </NTag>
    )
    width: 120,
    render: (row) => {
      if (!row.clockInResult) return ''
      return (
        <NTag type={getResultTagType(row.clockInResult)} size="small">
          {row.clockInResult}
        </NTag>
      )
    }
  },
  { title: '下班打卡时间', key: 'clockOutTime', minWidth: 120 },
  { title: '下班打卡时间', key: 'clockOutTime', width: 160 },
  {
    title: '下班打卡结果',
    key: 'clockOutResult',
     minWidth: 120,
    render: (row) => (
      <NTag type={getResultTagType(row.clockOutResult)} size="small">
        {row.clockOutResult}
      </NTag>
    )
    width: 120,
    render: (row) => {
      if (!row.clockOutResult) return ''
      return (
        <NTag type={getResultTagType(row.clockOutResult)} size="small">
          {row.clockOutResult}
        </NTag>
      )
    }
  },
  { title: '打卡地址', key: 'clockInLocation', minWidth: 200 }
  { title: '打卡地址', key: 'clockInLocation', width: 200, ellipsis: { tooltip: true } }
])
async function getList() {
@@ -114,11 +120,15 @@
    statisticDate: searchModel.value.statisticDate?.map((item: number) => dayjs(item).format('YYYY-MM-DD HH:mm:ss')).join(' - '),
  }
  getClockInStatisticsDetailList(params).then((res) => {
    detailList.value = res.data.records || []
    detailList.value = res.data.list.records || []
    detailList.value.forEach((item: any, index: number) => {
      item.serialNumber = (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1
    })
    total.value = res.data.total || res.data.records?.length
    total.value = res.data.list.total || res.data.list.records?.length
    statistics.value.shouldClockInDays = res.data.shouldClockInCount
    statistics.value.clockInDays = res.data.normalClockInCount
    statistics.value.notClockInDays = res.data.missingClockInCount
    statistics.value.lateEarly = res.data.lateClockInCount
  }).finally(() => {
    endLoading()
  })
@@ -127,10 +137,6 @@
onMounted(() => {
  searchModel.value.appUserId = route.params.id
  getList()
  statistics.value.shouldClockInDays = route.query.shouldClockInDays as string
  statistics.value.clockInDays = route.query.clockInDays as string
  statistics.value.notClockInDays = route.query.notClockInDays as string
  statistics.value.lateEarly = route.query.lateEarly as string
})
function changePage(page: number, size: number) {
@@ -178,7 +184,7 @@
      </template>
      <NSpace vertical :size="12">
        <n-data-table :columns="columns" :data="detailList" :loading="loading" :bordered="false" :header-height="18"
          :row-height="18" />
          :row-height="18" :scroll-x="1500" />
        <Pagination :count="total" :page="pageParams.pageNum" :page-size="pageParams.pageSize" @change="changePage" />
      </NSpace>
    </n-card>
src/views/transaction/order/orderManagement/components/Modal.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import {  } from '@/service/api/transaction'
import { changeAmount } from '@/service/api/transaction'
interface Props {
  modalName?: string
@@ -20,7 +20,8 @@
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const formDefault = {
  orderAmount: '-',
  id: '',
  orderMoney: '-',
  discountAmount: null,
  totalAmount: '-',
}
@@ -30,6 +31,7 @@
  emit('open')
  showModal()
  formModel.value = { ...data }
  formModel.value.totalAmount = data.orderMoney
}
function closeModal() {
@@ -45,16 +47,25 @@
const formRef = ref()
async function submitModal() {
  await formRef.value?.validate()
  startLoading()
  closeModal()
  changeAmount({
    id: formModel.value.id,
    amount: formModel.value.totalAmount,
  }).then((res) => {
    if (res.code === 200) {
      window.$message.success('改价成功')
      closeModal()
    }
  }).finally(() => {
    endLoading()
  })
}
watch(() => formModel.value, (newVal) => {
  if (!newVal.orderAmount || !newVal.discountAmount) {
  if (!newVal.orderMoney || !newVal.discountAmount) {
    return
  }
  formModel.value.totalAmount = `${Number(newVal.orderAmount) - Number(newVal.discountAmount)}`
  formModel.value.totalAmount = `${Number(newVal.orderMoney) - Number(newVal.discountAmount)}`
})
</script>
@@ -65,13 +76,13 @@
      action: true,
    }">
    <n-form ref="formRef" label-placement="left" :model="formModel" require-mark-placement="left">
      <n-form-item :span="1" label="当前订单金额" path="orderAmount">
        {{ formModel.orderAmount }}元
      <n-form-item :span="1" label="当前订单金额:" path="orderMoney">
        {{ formModel.orderMoney }}元
      </n-form-item>
      <n-form-item :span="1" label="优惠金额" path="discountAmount">
        <n-input-number v-model:value="formModel.discountAmount" min="0" :precision="0" :show-button="false" class="mr-2" />元
        <n-input-number v-model:value="formModel.discountAmount" min="0" :precision="2" :show-button="false" class="mr-2" />元
      </n-form-item>
      <n-form-item :span="1" label="优惠后订单金额" path="totalAmount">
      <n-form-item :span="1" label="优惠后订单金额:" path="totalAmount">
        {{ formModel.totalAmount }}元
      </n-form-item>
    </n-form>
src/views/transaction/order/orderManagement/components/TableModal.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { getAvailableVehicleList } from '@/service/api/transaction'
import { getAvailableVehicleList, reassignOrder } from '@/service/api/transaction'
interface Props {
  modalName?: string
@@ -20,16 +20,14 @@
const { bool: submitLoading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const formDefault = {
  name: '',
  city: null,
  remark: '',
  routePlanningRules: '',
  id: '',
  vehicleId: '',
}
const formModel = ref({ ...formDefault })
async function openModal(data: any) {
  emit('open')
  await getOrder(data.id)
  await getList(data.id)
  showModal()
  formModel.value = { ...data }
}
@@ -48,7 +46,17 @@
async function submitModal() {
  await formRef.value?.validate()
  startLoading()
  closeModal()
  reassignOrder({
    orderId: formModel.value.id,
    vehicleId: formModel.value.vehicleId,
  }).then((res: any) => {
    if (res.code === 0) {
      window.$message.success('改派成功')
      closeModal()
    }
  }).finally(() => {
    endLoading()
  })
}
const rules = {
@@ -61,7 +69,7 @@
const options = ref()
const getOrder = async (orderId: number) => {
const getList = async (orderId: number) => {
  const res = await getAvailableVehicleList(orderId)
  if (res.code === 0) {
    options.value = res.data.map((item: any) => ({
@@ -79,8 +87,8 @@
      action: true,
    }">
    <n-form ref="formRef" :rules="rules" label-placement="left" :model="formModel" require-mark-placement="left">
      <n-form-item :span="1" label="改派车辆" path="name">
        <n-select v-model:value="formModel.name" :options="options" />
      <n-form-item :span="1" label="改派车辆" path="vehicleId">
        <n-select v-model:value="formModel.vehicleId" :options="options" />
      </n-form-item>
    </n-form>
    <template #action>
src/views/transaction/order/orderManagement/detail.vue
@@ -4,6 +4,10 @@
import { NButton, NSpace } from 'naive-ui'
import { useTabStore } from '@/store'
const { load } = useScriptTag(
  `https://webapi.amap.com/maps?v=2.0&key=${import.meta.env.VITE_MAP_KEY}&plugin=AMap.PlaceSearch,AMap.Geocoder`,
)
const route = useRoute()
const router = useRouter()
const tabStore = useTabStore()
@@ -18,35 +22,112 @@
}
const model = ref({ ...initialModel })
const mapContainer = ref<HTMLDivElement | null>(null)
let map: any = null
async function initMap() {
  if (!mapContainer.value)
    return
  await load()
  if (typeof (window as any).AMap === 'undefined') {
    return
  }
  map = new AMap.Map(mapContainer.value, {
    zoom: 15,
    center: [116.397428, 39.90923],
    mapStyle: 'amap://styles/whitesmoke',
  })
}
function renderTrack(trackData: any[]) {
  if (!map)
    return
  const path = trackData.map((item: any) => [item.lon, item.lat])
  // 绘制轨迹线
  const polyline = new AMap.Polyline({
    path,
    isOutline: true,
    outlineColor: '#ffeeff',
    borderWeight: 3,
    strokeColor: '#3366FF',
    strokeOpacity: 1,
    strokeWeight: 6,
    strokeStyle: 'solid',
    lineJoin: 'round',
    lineCap: 'round',
    zIndex: 50,
  })
  map.add(polyline)
  // 添加起点标记
  const startPoint = path[0]
  const startMarker = new AMap.Marker({
    position: startPoint,
    icon: new AMap.Icon({
      size: new AMap.Size(25, 34),
      image: '//a.amap.com/jsapi_demos/static/demo-center/icons/dir-marker.png',
      imageSize: new AMap.Size(135, 40),
      imageOffset: new AMap.Pixel(-9, -3),
    }),
    offset: new AMap.Pixel(-13, -30),
    title: '起点',
  })
  map.add(startMarker)
  // 添加终点标记
  const endPoint = path[path.length - 1]
  const endMarker = new AMap.Marker({
    position: endPoint,
    icon: new AMap.Icon({
      size: new AMap.Size(25, 34),
      image: '//a.amap.com/jsapi_demos/static/demo-center/icons/dir-marker.png',
      imageSize: new AMap.Size(135, 40),
      imageOffset: new AMap.Pixel(-95, -3),
    }),
    offset: new AMap.Pixel(-13, -30),
    title: '终点',
  })
  map.add(endMarker)
  // 调整地图视野以适应轨迹
  map.setFitView([polyline, startMarker, endMarker])
}
onMounted(() => {
  if (route.query.orderId) {
    startLoading()
   getOrderDetail(route.query.orderId).then((res) => {
    getOrderDetail(route.query.orderId).then((res) => {
      if (res.code === 200) {
        model.value = res.data || {}
        if (model.value.orderPositionList && model.value.orderPositionList.length > 0) {
          renderTrack(model.value.orderPositionList)
        }
      }
    }).finally(() => {
      endLoading()
    })
  }
  initMap()
})
const handleCancel = () => {
function handleCancel() {
  tabStore.closeTab(route.fullPath)
  router.back()
}
</script>
<template>
  <n-flex class="w-full h-full">
    <n-card>
      <n-form ref="formRef" :model="model" label-placement="left" :show-feedback="false"
        require-mark-placement="left">
      <n-form ref="formRef" :model="model" label-placement="left" :show-feedback="false" require-mark-placement="left">
        <n-spin :show="loading">
          <NSpace vertical :size="16">
            <div class="text-base font-bold">订单详情</div>
            <div class="text-base font-bold">
              订单详情
            </div>
            <n-grid :cols="2" :x-gap="18" class="pl-12">
              <n-form-item-grid-item :span="1" label="下单时间:" path="createTime">
                {{ model.createTime }}
@@ -93,43 +174,53 @@
              <n-form-item-grid-item :span="1" label="下车时间:" path="getoffTime">
                {{ model.getoffTime }}
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="费用明细" path="dropoffTime">
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="费用明细" path="dropoffTime" />
              <n-form-item-grid-item :span="2" label="订单总价:" path="orderMoney">
                {{ model.orderMoney }}
                {{ model.orderMoney }}元
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="起步价:" path="startMoney">
                {{ model.startMoney }}
                {{ model.startMoney }}元
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="等待费用:" path="waitMoney">
                {{ model.waitMoney }}
                {{ model.waitMoney }}元
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="时长费:" path="durationMoney">
                {{ model.durationMoney }}
                {{ model.durationMoney }}元
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="行驶里程:" path="distance">
                {{ model.distance }}
                {{ model.distance }}公里
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="里程费用:" path="mileageMoney">
                {{ model.mileageMoney }}
                {{ model.mileageMoney }}元
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="2" label="远途费:" path="longDistancemoney">
                {{ model.longDistancemoney }}
                {{ model.longDistancemoney }}元
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="1" label="实际支付费用:" path="orderMoney">
                {{ model.orderMoney }}
                {{ model.orderMoney }}元
              </n-form-item-grid-item>
              <n-form-item-grid-item :span="1" label="支付方式:" path="payType">
                {{ model.payType === 1 ? '微信支付' : '支付宝支付' }}
                {{ model.payType === 1 ? '微信小程序支付' : '支付宝支付' }}
              </n-form-item-grid-item>
              <div>线路轨迹</div>
              <div></div>
            </n-grid>
          </NSpace>
        </n-spin>
      </n-form>
      <div v-if="model.orderPositionList && model.orderPositionList.length > 0" class="px-12">
        <div class="h-32px">
          线路轨迹
        </div>
        <div style="width: 100%; height: 400px; position: relative">
          <div ref="mapContainer" style="width: 100%; height: 100%; border-radius: 4px; border: 1px solid #e5e7eb">
            <div class="flex items-center justify-center h-full text-gray-400">
              加载地图中...
            </div>
          </div>
        </div>
      </div>
      <NSpace justify="center" class="w-[50%] mt-12">
        <NButton type='primary' @click="handleCancel">
        <NButton type="primary" @click="handleCancel">
          返回
        </NButton>
      </NSpace>
src/views/transaction/order/orderManagement/index.vue
@@ -190,7 +190,7 @@
              取消订单
            </NButton>
          )}
          {row.status === 5 && (
          {row.status === 6 && (
            <NButton
              text
              v-permission="60114"
@@ -383,7 +383,7 @@
  {
    label: t('transaction.status'),
    path: 'status',
    render: () => <n-select v-model:value={model.value.status} options={statusOptions} />,
    render: () => <n-select v-model:value={model.value.status} options={statusOptions} clearable/>,
  },
  {
    label: t('transaction.orderTime'),