zjk
2026-02-28 56cccf63a0748c0cf48237bf935fb04e500a4eea
feat: 排版计划接口对接、任务工单详情接口对接
2个文件已添加
14个文件已修改
1774 ■■■■■ 已修改文件
service.config.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/attendance.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/transaction.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/index.css 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/plan/components/BatchSetModal.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/plan/components/CopyScheduleModal.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/plan/components/EditMonthModal.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/plan/copy.vue 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/plan/edit.vue 394 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/plan/index.vue 362 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/attendance/statistics/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/components/AddFormModal.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/components/detailModal.vue 465 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/index.scss 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/taskOrderCenter/index.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/components/TableModal.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
service.config.ts
@@ -3,7 +3,7 @@
  dev: {
    // url: 'http://localhost:3000',
    url: 'http://221.182.45.100:8000',
    // url: 'http://221.182.45.100:8000',
    // url: 'http://192.168.110.80:8000',
  },
  production: {
    url: 'https://mock.apifox.cn/m1/4071143-0-default',
src/service/api/attendance.ts
@@ -95,6 +95,30 @@
  return method
}
/** 排班管理 */
// 排班管理
export function getSchedulingPageList(params: any) {
  return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/scheduling/pageList', params)
}
// 导出排班
export function exportScheduling(params: any) {
  const method = request.Get<Service.ResponseResult<any>>('/maintenance/api/v1/scheduling/exportExcel?month=' + params.month, {}, { responseType: 'blob' })
  method.meta = { ...method.meta, isBlob: true }
  return method
}
// 保存排班
export function saveScheduling(params: any) {
  return request.Post<Service.ResponseResult<any>>('/maintenance/api/v1/scheduling/saveSchedulingDetail', params)
}
// 获取班次不分页列表
export function getShiftListNoPage() {
  return request.Get<Service.ResponseResult<any>>('/maintenance/xp-shift/api/v1/shift/queryList')
}
/** 运维人员管理 */
src/service/api/transaction.ts
@@ -40,6 +40,11 @@
  return request.Post<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/fleet/list')
}
// 获取可用车辆列表
export function getAvailableVehicleList(orderId: any) {
  return request.Get<Service.ResponseResult<any>>('/vehicleConvoy/api/v1/vehicle/availableList?orderId=' + orderId)
}
/** 订单评价 */
// 获取订单评价列表
src/styles/index.css
@@ -6,6 +6,7 @@
body,
#app {
  height: 100%;
  background-color: #fff;
}
.color-weak {
  filter: invert(80%);
src/views/operations/attendance/plan/components/BatchSetModal.vue
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import dayjs from 'dayjs'
interface ShiftOption {
  name: string
@@ -15,6 +16,9 @@
const props = defineProps<{
  show: boolean
  teamOptions: any
  shifts: any
  selectedMonth: string | null
}>()
const emit = defineEmits<{
@@ -30,19 +34,20 @@
const formRef = ref<FormInst | null>(null)
const formModel = ref<FormModel>({
  team: null,
  shifts: [
    { name: '早', checked: false, dateRange: null },
    { name: '中', checked: false, dateRange: null },
    { name: '晚', checked: false, dateRange: null },
    { name: '全天', checked: false, dateRange: null }
  ]
  shifts: []
})
// 生成1-31的日期选项
const dateOptions = Array.from({ length: 31 }, (_, i) => ({
  label: `${i + 1}`,
  value: i + 1
}))
const dateOptions = computed(() => {
  if (!props.selectedMonth) return []
  const days = dayjs(props.selectedMonth).daysInMonth()
  return Array.from({ length: days }, (_, i) => ({
    label: `${i + 1}`,
    value: i + 1
  }))
})
const rules: FormRules = {
  team: [
@@ -51,11 +56,7 @@
}
// 模拟队伍数据
const teamOptions = [
  { label: '广州白云一队', value: '广州白云一队' },
  { label: '广州白云二队', value: '广州白云二队' },
  { label: '广州南沙三队', value: '广州南沙三队' }
]
const teamOptions = computed(() => props.teamOptions)
const handleCancel = () => {
  show.value = false
@@ -76,22 +77,28 @@
const resetForm = () => {
  formModel.value = {
    team: null,
    shifts: [
      { name: '早', checked: false, dateRange: null },
      { name: '中', checked: false, dateRange: null },
      { name: '晚', checked: false, dateRange: null },
      { name: '全天', checked: false, dateRange: null }
    ]
    shifts: []
  }
}
watch(() => show.value, (newShow) => {
  if (newShow) {
    formModel.value.shifts = props.shifts.map((shift: any) => ({
      ...shift,
      name: shift.shiftName,
      checked: false,
      dateRange: null
    }))
  }
})
</script>
<template>
  <n-modal
    v-model:show="show"
    :mask-closable="false"
    preset="card"
    title="批量设置"
  <n-modal
    v-model:show="show"
    :mask-closable="false"
    preset="card"
    title="批量设置"
    class="w-500px"
    :segmented="{
      content: true,
@@ -106,17 +113,17 @@
      show-require-mark
    >
      <n-form-item label="队伍" path="team">
        <n-select
          v-model:value="formModel.team"
        <n-select
          v-model:value="formModel.team"
          :options="teamOptions"
          placeholder="选择"
        />
      </n-form-item>
      <n-form-item label="班次" path="shifts">
        <div class="w-full flex flex-col gap-3">
          <div
            v-for="shift in formModel.shifts"
        <div class="w-full flex flex-col gap-3" v-if="formModel.shifts.length > 0">
          <div
            v-for="shift in formModel.shifts"
            :key="shift.name"
            class="flex items-center gap-3"
          >
@@ -130,6 +137,7 @@
              :options="dateOptions"
              placeholder="选择"
              multiple
              :max-tag-count="5"
              style="flex: 1"
            />
          </div>
src/views/operations/attendance/plan/components/CopyScheduleModal.vue
@@ -7,7 +7,6 @@
interface FormModel {
  month: number | null
  role: string | null
}
const props = defineProps<{
@@ -26,25 +25,14 @@
const formRef = ref<FormInst | null>(null)
const formModel = ref<FormModel>({
  month: null,
  role: null
  month: null
})
const rules: FormRules = {
  month: [
    { required: true, type: 'number', message: '请选择排班月份', trigger: 'change' }
  ],
  role: [
    { required: true, message: '请选择角色', trigger: 'change' }
  ]
}
// 模拟角色数据
const roleOptions = [
  { label: '网格员', value: '网格员' },
  { label: '管理员', value: '管理员' },
  { label: '巡查员', value: '巡查员' }
]
const handleCancel = () => {
  show.value = false
@@ -54,15 +42,14 @@
const handleConfirm = async () => {
  try {
    await formRef.value?.validate()
    if (formModel.value.month && formModel.value.role) {
    if (formModel.value.month) {
      emit('confirm', formModel.value)
      show.value = false
      // 跳转到复制页面,携带月份和角色参数
      router.push({
        path: '/operations/attendance/plan/copy',
        query: {
        query: {
          month: String(formModel.value.month),
          role: formModel.value.role
        }
      })
      resetForm()
@@ -74,18 +61,17 @@
const resetForm = () => {
  formModel.value = {
    month: null,
    role: null
    month: null
  }
}
</script>
<template>
  <n-modal
    v-model:show="show"
    :mask-closable="false"
    preset="card"
    title="复制排班"
  <n-modal
    v-model:show="show"
    :mask-closable="false"
    preset="card"
    title="复制排班"
    class="w-500px"
    :segmented="{
      content: true,
@@ -100,20 +86,12 @@
      show-require-mark
    >
      <n-form-item label="排班月份" path="month">
        <n-date-picker
          v-model:value="formModel.month"
          type="month"
        <n-date-picker
          v-model:value="formModel.month"
          type="month"
          placeholder="请选择月份"
          style="width: 100%"
          format="yyyy-MM"
        />
      </n-form-item>
      <n-form-item label="角色" path="role">
        <n-select
          v-model:value="formModel.role"
          :options="roleOptions"
          placeholder="请选择角色"
        />
      </n-form-item>
    </n-form>
@@ -121,7 +99,7 @@
    <template #action>
      <n-space justify="end">
        <n-button @click="handleCancel">取消</n-button>
        <n-button type="primary" @click="handleConfirm">排班</n-button>
        <n-button type="primary" @click="handleConfirm">复制</n-button>
      </n-space>
    </template>
  </n-modal>
src/views/operations/attendance/plan/components/EditMonthModal.vue
@@ -31,10 +31,10 @@
const rules: FormRules = {
  month: [
    {
      required: true,
    {
      required: true,
      type: 'number',
      message: '请选择排班月份',
      message: '请选择排班月份',
      trigger: 'change'
    }
  ]
@@ -77,11 +77,11 @@
</script>
<template>
  <n-modal
    v-model:show="show"
    :mask-closable="false"
    preset="card"
    title="编辑排班"
  <n-modal
    v-model:show="show"
    :mask-closable="false"
    preset="card"
    title="编辑排班"
    class="w-500px"
    :segmented="{
      content: true,
@@ -96,9 +96,9 @@
      show-require-mark
    >
      <n-form-item label="排班月份" path="month">
        <n-date-picker
          v-model:value="formModel.month"
          type="month"
        <n-date-picker
          v-model:value="formModel.month"
          type="month"
          placeholder="请选择月份"
          style="width: 100%"
          format="yyyy-MM"
@@ -109,7 +109,7 @@
    <template #action>
      <n-space justify="end">
        <n-button @click="handleCancel">取消</n-button>
        <n-button type="primary" @click="handleConfirm">保存</n-button>
        <n-button type="primary" @click="handleConfirm">排班</n-button>
      </n-space>
    </template>
  </n-modal>
src/views/operations/attendance/plan/copy.vue
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { getSchedulingPageList, getShiftListNoPage, saveScheduling } from '@/service/api/attendance'
import dayjs from 'dayjs'
interface ShiftTag {
  name: string
@@ -15,17 +17,22 @@
}
const route = useRoute()
const total = ref(0)
const pageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
const currentDate = ref('2024/12/01')
const selectedMonth = ref<number>(new Date(2024, 11, 1).valueOf())
const selectedRole = ref<string>('')
const selectedMonth = ref()
const shiftTags = ref<ShiftTag[]>([
  { name: '全班', time: '08:00-16:00', color: '#0068FF', bgColor: '#E6F4FF' },
  { name: '早班', time: '08:00-20:00', color: '#16A60C', bgColor: '#DCF5DD' },
  { name: '中班', time: '16:00-24:00', color: '#FF9000', bgColor: '#FFF8E6' },
  { name: '晚班', time: '19:00-24:00', color: '#764DFF', bgColor: '#F6F0FF' }
])
const shiftTags = ref<any[]>([])
const dayKeyMap = [
  'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
  'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty',
  'twentyOne', 'twentyTwo', 'twentyThree', 'twentyFour', 'twentyFive', 'twentySix', 'twentySeven',
  'twentyEight', 'twentyNine', 'thirty', 'thirtyOne'
]
// 生成日期列表
const generateDates = (year: number, month: number) => {
@@ -43,33 +50,18 @@
  return dates
}
const dates = ref(generateDates(2024, 11))
const dates = ref()
const teamSchedules = ref<TeamSchedule[]>([
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云二队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州南沙三队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }
])
// 初始化一些示例数据
teamSchedules.value[0].schedules[3] = [shiftTags.value[0]]
teamSchedules.value[1].schedules[1] = [shiftTags.value[1]]
const teamSchedules = ref<any[]>([])
const selectedCell = ref<{ teamIndex: number; dateIndex: number } | null>(null)
const selectedShifts = ref<ShiftTag[]>([])
const selectedShifts = ref<any[]>([])
const selectorPosition = ref({ top: 0, left: 0 })
const updateDates = () => {
  const date = new Date(selectedMonth.value)
  const year = date.getFullYear()
  const month = date.getMonth()
  currentDate.value = `${year}/${String(month + 1).padStart(2, '0')}/01`
  dates.value = generateDates(year, month)
  // 重新初始化排班数据
@@ -87,7 +79,24 @@
}
const handleSave = () => {
  console.log('保存')
  const params = {
    month: selectedMonth.value,
    datas: teamSchedules.value.map((item: any) => ({
      fleetId: item.fleetId,
      days: item.schedules.map((shifts: any[], index: number) => {
        return {
          dayTime: `${dayjs(selectedMonth.value).year()}-${dayjs(selectedMonth.value).month() + 1 > 9 ? dayjs(selectedMonth.value).month() + 1 : '0' + (dayjs(selectedMonth.value).month() + 1)}-${index + 1 > 9 ? index + 1 : '0' + (index + 1)}`,
          shiftId: Number(shifts[0]?.id) ? Number(shifts[0]?.id) : null
        }
      })
    }))
  }
  saveScheduling(params).then(res => {
    if (res.code === 200) {
      window.$message.success('排班保存成功')
      getList()
    }
  })
}
const getShiftCount = (schedules: ShiftTag[][], shiftName: string) => {
@@ -100,11 +109,11 @@
const showScheduleModal = ref(false)
const handleCellClick = (teamIndex: number, dateIndex: number, event: MouseEvent) => {
const handleCellClick = (teamIndex: number, dateIndex: number | string , event: MouseEvent) => {
  event.stopPropagation() // 阻止事件冒泡
  selectedCell.value = { teamIndex, dateIndex }
  const currentShifts = teamSchedules.value[teamIndex].schedules[dateIndex]
  selectedCell.value = { teamIndex, dateIndex: Number(dateIndex) }
  const currentShifts = teamSchedules.value[teamIndex].schedules[Number(dateIndex)]
  selectedShifts.value = currentShifts ? [...currentShifts] : []
  // 计算盒子位置
@@ -129,13 +138,23 @@
  })
}
const handleShiftToggle = (shift: ShiftTag) => {
const handleShiftToggle = (shift: any) => {
  shift.name = shift.shiftName
  // 单选模式:直接设置为选中的班次
  selectedShifts.value = [shift]
  // 立即更新到表格
  if (selectedCell.value) {
    teamSchedules.value[selectedCell.value.teamIndex].schedules[selectedCell.value.dateIndex] = [shift]
    teamSchedules.value[selectedCell.value.teamIndex].subtotal = teamSchedules.value[selectedCell.value.teamIndex].subtotal.map((item: any) => {
      if (item.shiftId === shift.id) {
        return {
          ...item,
          count: Number(item.count) + 1
        }
      }
      return item
    })
  }
  // 选择后直接关闭
@@ -159,6 +178,61 @@
  selectedCell.value = null
}
async function getList() {
  const params = {
    ...pageParams.value,
    month: selectedMonth.value,
  }
  try {
    const res = await getSchedulingPageList(params)
    if (res.code === 200) {
      const records = res.data.records || []
      const date = dayjs(selectedMonth.value)
      const daysInMonth = date.daysInMonth()
      teamSchedules.value = records.map((item: any) => {
        const schedules: any[] = []
        for (let i = 0; i < daysInMonth; i++) {
          const key = dayKeyMap[i]
          const dayData = item[key]
          if (dayData && dayData.shiftName) {
            schedules.push([
              {
                id: dayData.shiftId,
                name: dayData.shiftName,
                color: dayData.shiftColor,
                bgColor: dayData.shiftColor + '25',
              }
            ])
          } else {
            schedules.push([])
          }
        }
        return {
          fleetId: item.fleetId,
          team: item.fleetName,
          schedules,
          subtotal: item.subtotal || []
        }
      })
      total.value = res.data.total || teamSchedules.value.length
    }
  } finally {
  }
}
const getShiftList = async () => {
  const res = await getShiftListNoPage()
  if (res.code === 200) {
    shiftTags.value = res.data.map((item: any) => ({
      id: item.id,
      shiftName: item.shiftName,
      time: `${item.startTime}-${item.endTime}`,
      color: item.shiftColor,
      bgColor: item.shiftColor + '25'
    }))
  }
}
onMounted(() => {
  document.addEventListener('click', handleClickOutside)
@@ -167,22 +241,23 @@
  if (monthParam) {
    const monthValue = typeof monthParam === 'string' ? Number(monthParam) : Array.isArray(monthParam) ? Number(monthParam[0]) : monthParam
    if (monthValue && !isNaN(monthValue)) {
      selectedMonth.value = monthValue
      selectedMonth.value = dayjs(monthValue).format('YYYY-MM')
      updateDates()
    }
  }
  // 从路由参数读取角色
  const roleParam = route.query.role
  if (roleParam) {
    selectedRole.value = typeof roleParam === 'string' ? roleParam : Array.isArray(roleParam) ? (roleParam[0] || '') : ''
  }
  getShiftList()
  getList()
})
onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside)
})
function changePage(page: number, size: number) {
  pageParams.value.pageNum = page
  pageParams.value.pageSize = size
  getList()
}
</script>
<template>
@@ -190,15 +265,12 @@
    <n-card style="border-radius: 6px; margin-top: 12px; overflow: hidden;"
      content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;">
      <div class="flex items-center justify-between" style="margin-bottom: 12px;">
        <div class="flex items-center gap-15">
          <div class="text-lg font-bold">角色:{{ selectedRole }}</div>
          <div class="text-lg font-bold">日期:{{ getDisplayDate() }}</div>
        </div>
        <div class="text-lg font-bold">日期:{{ getDisplayDate() }}</div>
      </div>
      <div class="flex items-center justify-between">
        <div class="flex items-center">
          <div v-for="tag in shiftTags" :key="`${tag.name}-${tag.time}`" style="margin-right: 8px;"
          <div v-for="tag in shiftTags" :key="`${tag.shiftName}-${tag.startTime}`" style="margin-right: 8px;"
            class="flex items-center gap-1">
            <span class="rounded text-xs" :style="{
              backgroundColor: tag.bgColor,
@@ -210,7 +282,7 @@
              alignItems: 'center',
              justifyContent: 'center'
            }">
              {{ tag.name }} {{ tag.time }}
              {{ tag.shiftName }} {{ tag.time }}
            </span>
          </div>
        </div>
@@ -218,43 +290,47 @@
          <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button>
        </div>
      </div>
      <div class="schedule-table-container" style="overflow-x: auto; flex: 1;margin-top: 12px;">
        <table class="schedule-table">
          <thead>
            <tr>
              <th class="sticky-col">车队</th>
              <th v-for="(date, index) in dates" :key="index">
                <div>{{ date.month }}/{{ date.day }}</div>
                <div class="text-xs text-gray-500" style="margin-top: 4px;">{{ date.weekDay }}</div>
              </th>
              <th class="summary-col">排班小计</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(schedule, teamIndex) in teamSchedules" :key="teamIndex">
              <td class="sticky-col team-name">{{ schedule.team }}</td>
              <td v-for="(shifts, dateIndex) in schedule.schedules" :key="dateIndex" class="schedule-cell"
                :class="{ 'schedule-cell-selected': selectedCell && selectedCell.teamIndex === teamIndex && selectedCell.dateIndex === dateIndex }"
                @click="handleCellClick(teamIndex, dateIndex, $event)">
                <div class="flex flex-wrap gap-1">
                  <span v-for="(shift, shiftIndex) in shifts" :key="shiftIndex" class="whitespace-nowrap"
                    :style="{ backgroundColor: shift.bgColor, color: shift.color }">
                    {{ shift.name }}
                  </span>
                </div>
              </td>
              <td class="summary-cell">
                <div class="flex flex-wrap justify-center" style="gap: 2px;">
                  <span v-for="tag in shiftTags" :key="`${tag.name}-${tag.time}`" class="summary-tag"
                    :style="{ backgroundColor: tag.bgColor, color: tag.color }">
                    {{ tag.name }} {{ getShiftCount(schedule.schedules, tag.name) }}
                  </span>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <n-scrollbar x-scrollable class="flex-1 my-4">
        <div class="schedule-table-wrapper flex-1">
          <table class="schedule-table">
            <thead>
              <tr>
                <th class="sticky-col">车队</th>
                <th v-for="(date, index) in dates" :key="index">
                  <div>{{ date.month }}/{{ date.day }}</div>
                  <div class="text-xs text-gray-500" style="margin-top: 4px;">{{ date.weekDay }}</div>
                </th>
                <th class="summary-col">排班小计</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(schedule, teamIndex) in teamSchedules" :key="teamIndex">
                <td class="sticky-col team-name">{{ schedule.team }}</td>
                <td v-for="(shifts, dateIndex) in schedule.schedules" :key="dateIndex" class="schedule-cell"
                  :class="{ 'schedule-cell-selected': selectedCell && selectedCell.teamIndex === teamIndex && selectedCell.dateIndex === dateIndex }"
                  @click="handleCellClick(teamIndex, dateIndex, $event)">
                  <div class="flex flex-wrap gap-1">
                    <span v-for="(shift, shiftIndex) in shifts" :key="shiftIndex" class="whitespace-nowrap"
                      :style="{ backgroundColor: shift.bgColor, color: shift.color }">
                      {{ shift.name }}
                    </span>
                  </div>
                </td>
                <td class="summary-cell">
                  <div class="flex flex-wrap justify-center" style="gap: 2px;">
                    <span v-for="tag in schedule.subtotal" :key="`${tag.name}-${tag.time}`" class="summary-tag"
                      :style="{ backgroundColor: tag.shiftColor + '25', color: tag.shiftColor }">
                      {{ tag.shiftName }} {{ tag.count }}
                    </span>
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </n-scrollbar>
      <Pagination :count="total" @change="changePage" />
    </n-card>
    <!-- 排班选择盒子 -->
@@ -271,11 +347,11 @@
        </n-icon>
      </div>
      <div class="schedule-selector-content">
        <div v-for="shift in shiftTags" :key="`${shift.name}-${shift.time}`" class="shift-option"
        <div v-for="shift in shiftTags" :key="`${shift.shiftName}-${shift.time}`" class="shift-option"
          @click="handleShiftToggle(shift)">
          <n-radio :checked="selectedShifts.some(s => s.name === shift.name && s.time === shift.time)" />
          <n-radio :checked="selectedShifts.some(s => s.shiftName === shift.shiftName && s.time === shift.time)" />
          <span class="shift-tag" :style="{ backgroundColor: shift.bgColor, color: shift.color }">
            {{ shift.name }} {{ shift.time }}
            {{ shift.shiftName }} {{ shift.time }}
          </span>
        </div>
      </div>
src/views/operations/attendance/plan/edit.vue
@@ -2,30 +2,26 @@
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import BatchSetModal from './components/BatchSetModal.vue'
interface ShiftTag {
  name: string
  time: string
  color: string
  bgColor: string
}
interface TeamSchedule {
  team: string
  schedules: ShiftTag[][]
}
import { getSchedulingPageList, getShiftListNoPage, saveScheduling } from '@/service/api/attendance'
import dayjs from 'dayjs'
const route = useRoute()
const total = ref(0)
const pageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
const currentDate = ref('2024/12/01')
const selectedMonth = ref<number>(new Date(2024, 11, 1).valueOf())
const selectedMonth = ref()
const shiftTags = ref<ShiftTag[]>([
  { name: '全班', time: '08:00-16:00', color: '#0068FF', bgColor: '#E6F4FF' },
  { name: '早班', time: '08:00-20:00', color: '#16A60C', bgColor: '#DCF5DD' },
  { name: '中班', time: '16:00-24:00', color: '#FF9000', bgColor: '#FFF8E6' },
  { name: '晚班', time: '19:00-24:00', color: '#764DFF', bgColor: '#F6F0FF' }
])
const shiftTags = ref<any[]>([])
const dayKeyMap = [
  'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
  'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty',
  'twentyOne', 'twentyTwo', 'twentyThree', 'twentyFour', 'twentyFive', 'twentySix', 'twentySeven',
  'twentyEight', 'twentyNine', 'thirty', 'thirtyOne'
]
// 生成日期列表
const generateDates = (year: number, month: number) => {
@@ -43,35 +39,20 @@
  return dates
}
const dates = ref(generateDates(2024, 11))
const dates = ref()
const teamSchedules = ref<TeamSchedule[]>([
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云二队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州南沙三队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }
])
// 初始化一些示例数据
teamSchedules.value[0].schedules[3] = [shiftTags.value[0]]
teamSchedules.value[1].schedules[1] = [shiftTags.value[1]]
const teamSchedules = ref<any[]>([])
const selectedCell = ref<{ teamIndex: number; dateIndex: number } | null>(null)
const selectedShifts = ref<ShiftTag[]>([])
const selectedShifts = ref<any[]>([])
const selectorPosition = ref({ top: 0, left: 0 })
const updateDates = () => {
  const date = new Date(selectedMonth.value)
  const year = date.getFullYear()
  const month = date.getMonth()
  currentDate.value = `${year}/${String(month + 1).padStart(2, '0')}/01`
  dates.value = generateDates(year, month)
  // 重新初始化排班数据
  const daysInMonth = new Date(year, month + 1, 0).getDate()
  teamSchedules.value.forEach(schedule => {
@@ -91,58 +72,77 @@
}
const handleSave = () => {
  console.log('保存')
}
const getShiftCount = (schedules: ShiftTag[][], shiftName: string) => {
  let count = 0
  schedules.forEach(shifts => {
    count += shifts.filter(s => s.name === shiftName).length
  const params = {
    month: selectedMonth.value,
    datas: teamSchedules.value.map((item: any) => ({
      fleetId: item.fleetId,
      days: item.schedules.map((shifts: any[], index: number) => {
        return {
          dayTime: `${dayjs(selectedMonth.value).year()}-${dayjs(selectedMonth.value).month() + 1 > 9 ? dayjs(selectedMonth.value).month() + 1 : '0' + (dayjs(selectedMonth.value).month() + 1)}-${index + 1 > 9 ? index + 1 : '0' + (index + 1)}`,
          shiftId: Number(shifts[0]?.id) ? Number(shifts[0]?.id) : null
        }
      })
    }))
  }
  saveScheduling(params).then(res => {
    if (res.code === 200) {
      window.$message.success('排班保存成功')
      getList()
    }
  })
  return count
}
const showScheduleModal = ref(false)
const showBatchSetModal = ref(false)
const handleCellClick = (teamIndex: number, dateIndex: number, event: MouseEvent) => {
const handleCellClick = (teamIndex: number, dateIndex: number | string, event: MouseEvent) => {
  event.stopPropagation() // 阻止事件冒泡
  selectedCell.value = { teamIndex, dateIndex }
  const currentShifts = teamSchedules.value[teamIndex].schedules[dateIndex]
  selectedCell.value = { teamIndex, dateIndex: Number(dateIndex) }
  const currentShifts = teamSchedules.value[teamIndex].schedules[Number(dateIndex)]
  selectedShifts.value = currentShifts ? [...currentShifts] : []
  // 计算盒子位置
  const target = event.currentTarget as HTMLElement
  const rect = target.getBoundingClientRect()
  const boxWidth = 240
  const windowWidth = window.innerWidth
  // 判断右侧是否有足够空间,如果没有则显示在左侧
  let left = rect.right + 2
  if (left + boxWidth > windowWidth) {
    left = rect.left - boxWidth - 10
  }
  selectorPosition.value = {
    top: rect.top,
    left: Math.max(10, left) // 确保不会超出左边界
  }
  nextTick(() => {
    showScheduleModal.value = true
  })
}
const handleShiftToggle = (shift: ShiftTag) => {
const handleShiftToggle = (shift: any) => {
  shift.name = shift.shiftName
  // 单选模式:直接设置为选中的班次
  selectedShifts.value = [shift]
  // 立即更新到表格
  if (selectedCell.value) {
    teamSchedules.value[selectedCell.value.teamIndex].schedules[selectedCell.value.dateIndex] = [shift]
    teamSchedules.value[selectedCell.value.teamIndex].subtotal = teamSchedules.value[selectedCell.value.teamIndex].subtotal.map((item: any) => {
      if (item.shiftId === shift.id) {
        return {
          ...item,
          count: Number(item.count) + 1
        }
      }
      return item
    })
  }
  // 选择后直接关闭
  closeScheduleModal()
}
@@ -150,10 +150,10 @@
// 点击外部关闭选择盒子
const handleClickOutside = (event: MouseEvent) => {
  if (!showScheduleModal.value) return
  const target = event.target as HTMLElement
  const selectorBox = document.querySelector('.schedule-selector-box')
  if (selectorBox && !selectorBox.contains(target)) {
    closeScheduleModal()
  }
@@ -164,156 +164,234 @@
  selectedCell.value = null
}
async function getList() {
  const params = {
    ...pageParams.value,
    month: selectedMonth.value,
  }
  try {
    const res = await getSchedulingPageList(params)
    if (res.code === 200) {
      const records = res.data.records || []
      const date = dayjs(selectedMonth.value)
      const daysInMonth = date.daysInMonth()
      teamSchedules.value = records.map((item: any) => {
        const schedules: any[] = []
        for (let i = 0; i < daysInMonth; i++) {
          const key = dayKeyMap[i]
          const dayData = item[key]
          if (dayData && dayData.shiftName) {
            schedules.push([
              {
                id: dayData.shiftId,
                name: dayData.shiftName,
                color: dayData.shiftColor,
                bgColor: dayData.shiftColor + '25',
              }
            ])
          } else {
            schedules.push([])
          }
        }
        return {
          fleetId: item.fleetId,
          team: item.fleetName,
          schedules,
          subtotal: item.subtotal || []
        }
      })
      total.value = res.data.total || teamSchedules.value.length
    }
  } finally {
  }
}
const getShiftList = async () => {
  const res = await getShiftListNoPage()
  if (res.code === 200) {
    shiftTags.value = res.data.map((item: any) => ({
      id: item.id,
      shiftName: item.shiftName,
      time: `${item.startTime}-${item.endTime}`,
      color: item.shiftColor,
      bgColor: item.shiftColor + '25'
    }))
  }
}
onMounted(() => {
  document.addEventListener('click', handleClickOutside)
  // 从路由参数读取月份
  const monthParam = route.query.month
  if (monthParam) {
    const monthValue = typeof monthParam === 'string' ? Number(monthParam) : Array.isArray(monthParam) ? Number(monthParam[0]) : monthParam
    if (monthValue && !isNaN(monthValue)) {
      selectedMonth.value = monthValue
      selectedMonth.value = dayjs(monthValue).format('YYYY-MM')
      updateDates()
    }
  }
  getShiftList()
  getList()
})
onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside)
})
function changePage(page: number, size: number) {
  pageParams.value.pageNum = page
  pageParams.value.pageSize = size
  getList()
}
const handleBatchConfirm = (data: any) => {
  const fleet = teamSchedules.value.find((item: any) => item.fleetId == data.team)
  data.shifts.forEach((shift: any) => {
    if (shift.checked) {
      shift.dateRange.forEach((date: number) => {
        fleet.schedules[date - 1] = [shift]
      })
    }
  })
  // 重新计算班次统计
  const shiftCountMap: Record<string, number> = {}
  fleet.schedules.forEach((shifts: any[]) => {
    if (shifts && shifts.length > 0) {
      shifts.forEach((shift: any) => {
        const shiftId = String(shift.id)
        shiftCountMap[shiftId] = (shiftCountMap[shiftId] || 0) + 1
      })
    }
  })
  // 更新subtotal数组
  fleet.subtotal = Object.entries(shiftCountMap).map(([shiftId, count]) => {
    const shiftInfo = shiftTags.value.find((tag: any) => tag.id == shiftId)
    console.log('shiftInfo', shiftInfo)
    return {
      shiftId,
      shiftName: shiftInfo?.shiftName,
      count: count,
      bgColor: shiftInfo?.bgColor,
      color: shiftInfo?.color,
      shiftColor: shiftInfo?.color
    }
  })
}
</script>
<template>
  <div class="h-full flex flex-col">
    <n-card style="border-radius: 6px; margin-top: 12px; overflow: hidden;" content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;">
        <div class="flex items-center justify-between" style="margin-bottom: 12px;">
    <n-card style="border-radius: 6px; margin-top: 12px; overflow: hidden;"
      content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;">
      <div class="flex items-center justify-between" style="margin-bottom: 12px;">
        <div class="text-lg font-bold">日期:{{ getDisplayDate() }}</div>
      </div>
      <div class="flex items-center justify-between">
        <div class="flex items-center">
          <div v-for="tag in shiftTags" :key="`${tag.name}-${tag.time}`" style="margin-right: 8px;" class="flex items-center gap-1">
            <span
              class="rounded text-xs"
              :style="{
                backgroundColor: tag.bgColor,
                color: tag.color,
                width: '113px',
                height: '28px',
                borderRadius: '4px',
                display: 'inline-flex',
                alignItems: 'center',
                justifyContent: 'center'
              }"
            >
              {{ tag.name }} {{ tag.time }}
          <div v-for="tag in shiftTags" :key="`${tag.shiftName}-${tag.startTime}`" style="margin-right: 8px;"
            class="flex items-center gap-1">
            <span class="rounded text-xs" :style="{
              backgroundColor: tag.bgColor,
              color: tag.color,
              width: '113px',
              height: '28px',
              borderRadius: '4px',
              display: 'inline-flex',
              alignItems: 'center',
              justifyContent: 'center'
            }">
              {{ tag.shiftName }} {{ tag.time }}
            </span>
          </div>
        </div>
        <div class="flex gap-2">
          <n-button style="width: 84px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF" @click="handleBatchEdit">批量设置</n-button>
          <n-button
            style="width: 84px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF"
            @click="handleBatchEdit">批量设置</n-button>
          <n-button type="primary" style="width: 84px; height: 32px" @click="handleSave">保存</n-button>
        </div>
      </div>
      <div class="schedule-table-container" style="overflow-x: auto; flex: 1;margin-top: 12px;">
        <table class="schedule-table">
          <thead>
            <tr>
              <th class="sticky-col">车队</th>
              <th
                v-for="(date, index) in dates"
                :key="index"
              >
                <div>{{ date.month }}/{{ date.day }}</div>
                <div class="text-xs text-gray-500" style="margin-top: 4px;">{{ date.weekDay }}</div>
              </th>
              <th class="summary-col">排班小计</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(schedule, teamIndex) in teamSchedules" :key="teamIndex">
              <td class="sticky-col team-name">{{ schedule.team }}</td>
              <td
                v-for="(shifts, dateIndex) in schedule.schedules"
                :key="dateIndex"
                class="schedule-cell"
                :class="{ 'schedule-cell-selected': selectedCell && selectedCell.teamIndex === teamIndex && selectedCell.dateIndex === dateIndex }"
                @click="handleCellClick(teamIndex, dateIndex, $event)"
              >
                <div class="flex flex-wrap gap-1">
                  <span
                    v-for="(shift, shiftIndex) in shifts"
                    :key="shiftIndex"
                    class="whitespace-nowrap"
                    :style="{ backgroundColor: shift.bgColor, color: shift.color }"
                  >
                    {{ shift.name }}
                  </span>
                </div>
              </td>
              <td class="summary-cell">
                <div class="flex flex-wrap justify-center" style="gap: 2px;">
                  <span
                    v-for="tag in shiftTags"
                    :key="`${tag.name}-${tag.time}`"
                    class="summary-tag"
                    :style="{ backgroundColor: tag.bgColor, color: tag.color }"
                  >
                    {{ tag.name }} {{ getShiftCount(schedule.schedules, tag.name) }}
                  </span>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <n-scrollbar x-scrollable class="flex-1 my-4">
        <div class="schedule-table-wrapper flex-1">
          <table class="schedule-table">
            <thead>
              <tr>
                <th class="sticky-col">车队</th>
                <th v-for="(date, index) in dates" :key="index">
                  <div>{{ date.month }}/{{ date.day }}</div>
                  <div class="text-xs text-gray-500" style="margin-top: 4px;">{{ date.weekDay }}</div>
                </th>
                <th class="summary-col">排班小计</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(schedule, teamIndex) in teamSchedules" :key="teamIndex">
                <td class="sticky-col team-name">{{ schedule.team }}</td>
                <td v-for="(shifts, dateIndex) in schedule.schedules" :key="dateIndex" class="schedule-cell"
                  :class="{ 'schedule-cell-selected': selectedCell && selectedCell.teamIndex === teamIndex && selectedCell.dateIndex === dateIndex }"
                  @click="handleCellClick(teamIndex, dateIndex, $event)">
                  <div class="flex flex-wrap gap-1">
                    <span v-for="(shift, shiftIndex) in shifts" :key="shiftIndex" class="whitespace-nowrap"
                      :style="{ backgroundColor: shift.bgColor, color: shift.color }">
                      {{ shift.name }}
                    </span>
                  </div>
                </td>
                <td class="summary-cell">
                  <div class="flex flex-wrap justify-center" style="gap: 2px;">
                    <span v-for="tag in schedule.subtotal" :key="`${tag.name}-${tag.time}`" class="summary-tag"
                      :style="{ backgroundColor: tag.shiftColor + '25', color: tag.shiftColor }">
                      {{ tag.shiftName }} {{ tag.count }}
                    </span>
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </n-scrollbar>
      <Pagination :count="total" @change="changePage" />
    </n-card>
    <!-- 排班选择盒子 -->
    <div
      v-if="showScheduleModal"
      class="schedule-selector-box"
      :style="{ top: `${selectorPosition.top}px`, left: `${selectorPosition.left}px` }"
    >
    <div v-if="showScheduleModal" class="schedule-selector-box"
      :style="{ top: `${selectorPosition.top}px`, left: `${selectorPosition.left}px` }">
      <div class="schedule-selector-header">
        <span class="schedule-selector-title">排班选择</span>
        <n-icon size="20" class="cursor-pointer" @click="closeScheduleModal">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
            stroke-linecap="round" stroke-linejoin="round">
            <line x1="18" y1="6" x2="6" y2="18"></line>
            <line x1="6" y1="6" x2="18" y2="18"></line>
          </svg>
        </n-icon>
      </div>
      <div class="schedule-selector-content">
        <div
          v-for="shift in shiftTags"
          :key="`${shift.name}-${shift.time}`"
          class="shift-option"
          @click="handleShiftToggle(shift)"
        >
          <n-radio
            :checked="selectedShifts.some(s => s.name === shift.name && s.time === shift.time)"
          />
          <span
            class="shift-tag"
            :style="{ backgroundColor: shift.bgColor, color: shift.color }"
          >
            {{ shift.name }} {{ shift.time }}
        <div v-for="shift in shiftTags" :key="`${shift.shiftName}-${shift.time}`" class="shift-option"
          @click="handleShiftToggle(shift)">
          <n-radio :checked="selectedShifts.some(s => s.shiftName === shift.shiftName && s.time === shift.time)" />
          <span class="shift-tag" :style="{ backgroundColor: shift.bgColor, color: shift.color }">
            {{ shift.shiftName }} {{ shift.time }}
          </span>
        </div>
      </div>
    </div>
    <BatchSetModal
      v-model:show="showBatchSetModal"
    />
    <BatchSetModal v-model:show="showBatchSetModal"
      :teamOptions="teamSchedules.map(item => ({ label: item.team, value: item.fleetId }))" :shifts="shiftTags"
      :selectedMonth="selectedMonth" @confirm="handleBatchConfirm" />
  </div>
</template>
<style scoped>
.schedule-table-container {
  position: relative;
<style scoped lang="scss">
.schedule-table-wrapper {
  min-width: max-content;
  // position: relative;
  display: inline-block;
  width: 100%;
}
.schedule-table {
src/views/operations/attendance/plan/index.vue
@@ -2,28 +2,29 @@
import { ref } from 'vue'
import EditMonthModal from './components/EditMonthModal.vue'
import CopyScheduleModal from './components/CopyScheduleModal.vue'
import { getSchedulingPageList, getShiftListNoPage, exportScheduling } from '@/service/api/attendance'
import { useBoolean } from '@/hooks'
import { downloadFile } from '@/utils'
import dayjs from 'dayjs'
interface ShiftTag {
  name: string
  time: string
  color: string
  bgColor: string
}
const total = ref(0)
const pageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
interface TeamSchedule {
  team: string
  schedules: (ShiftTag | null)[][]
}
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(false)
const currentDate = ref('2024/12/01')
const selectedMonth = ref<number>(new Date(2024, 11, 1).valueOf())
const selectedMonth = ref()
const shiftTags = ref<ShiftTag[]>([
  { name: '全班', time: '08:00-16:00', color: '#0068FF', bgColor: '#E6F4FF' },
  { name: '早班', time: '08:00-20:00', color: '#16A60C', bgColor: '#DCF5DD' },
  { name: '中班', time: '16:00-24:00', color: '#FF9000', bgColor: '#FFF8E6' },
  { name: '晚班', time: '19:00-24:00', color: '#764DFF', bgColor: '#F6F0FF' }
])
const shiftTags = ref<any[]>([])
const dayKeyMap = [
  'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
  'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty',
  'twentyOne', 'twentyTwo', 'twentyThree', 'twentyFour', 'twentyFive', 'twentySix', 'twentySeven',
  'twentyEight', 'twentyNine', 'thirty', 'thirtyOne'
]
// 生成日期列表
const generateDates = (year: number, month: number) => {
@@ -41,31 +42,23 @@
  return dates
}
const dates = ref(generateDates(2024, 11))
const dates = ref(generateDates(dayjs().year(), dayjs().month()))
const teamSchedules = ref<TeamSchedule[]>([
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云二队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州南沙三队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) },
  { team: '广州白云一队', schedules: Array(31).fill(null).map(() => []) }
])
watch(selectedMonth, () => {
  const date = dayjs(selectedMonth.value)
  dates.value = generateDates(date.year(), date.month())
  // getList()
})
// 初始化一些示例数据
teamSchedules.value[0].schedules[3] = [shiftTags.value[0]]
teamSchedules.value[1].schedules[1] = [shiftTags.value[1]]
const teamSchedules = ref<any[]>([])
const selectedCell = ref<{ teamIndex: number; dateIndex: number } | null>(null)
const selectedShifts = ref<ShiftTag[]>([])
const selectedShifts = ref<any[]>([])
const handleMonthChange = (value: number | null) => {
  if (value) {
    selectedMonth.value = value
    updateDates()
    selectedMonth.value = dayjs(value).format('YYYY-MM')
    // updateDates()
    getList()
  }
}
@@ -73,9 +66,8 @@
  const date = new Date(selectedMonth.value)
  const year = date.getFullYear()
  const month = date.getMonth()
  currentDate.value = `${year}/${String(month + 1).padStart(2, '0')}/01`
  dates.value = generateDates(year, month)
  // 重新初始化排班数据
  const daysInMonth = new Date(year, month + 1, 0).getDate()
  teamSchedules.value.forEach(schedule => {
@@ -87,30 +79,15 @@
  showCopyModal.value = true
}
const handleDownload = () => {
  console.log('下载表')
}
const getShiftCount = (schedules: (ShiftTag | null)[][], shiftName: string) => {
  let count = 0
  schedules.forEach(shifts => {
    if (shifts) {
      count += shifts.filter(s => s.name === shiftName).length
    }
  })
  return count
}
const handleExport = () => {
  console.log('导出')
}
const handleBatchSchedule = () => {
  console.log('批量排班')
}
const handlePublish = () => {
  console.log('发布')
const handleDownload = async () => {
  const params = {
    month: selectedMonth.value,
  }
  const response = await exportScheduling(params)
  if (response instanceof Blob) {
    downloadFile(response, `排班计划列表_${new Date().getTime()}.xlsx`)
    window.$message.success('导出成功')
  }
}
const showScheduleModal = ref(false)
@@ -122,7 +99,7 @@
  selectedShifts.value = [...(teamSchedules.value[teamIndex].schedules[dateIndex] || [])]
}
const handleShiftToggle = (shift: ShiftTag) => {
const handleShiftToggle = (shift: any) => {
  const index = selectedShifts.value.findIndex(s => s.name === shift.name && s.time === shift.time)
  if (index > -1) {
    selectedShifts.value.splice(index, 1)
@@ -142,11 +119,86 @@
  showEditModal.value = true
}
const handleConfirmEdit = (month: number) => {
  selectedMonth.value = month
  updateDates()
}
async function getList() {
  startLoading()
  const params = {
    ...pageParams.value,
    month: selectedMonth.value,
  }
  try {
    const res = await getSchedulingPageList(params)
    if (res.code === 200) {
      const records = res.data.records || []
      const date = dayjs(selectedMonth.value)
      const daysInMonth = date.daysInMonth()
      teamSchedules.value = records.map((item: any) => {
        const schedules: any[] = []
        for (let i = 0; i < daysInMonth; i++) {
          const key = dayKeyMap[i]
          const dayData = item[key]
          if (dayData && dayData.shiftName) {
            schedules.push([
              {
                id: dayData.shiftId,
                name: dayData.shiftName,
                color: dayData.shiftColor,
                bgColor: dayData.shiftColor + '25',
              }
            ])
          } else {
            schedules.push([
              {
                id: 'rest',
                name: '休',
                color: '#999999',
                bgColor: '#F5F5F5',
                isRest: true
              }
            ])
          }
        }
        return {
          fleetId: item.fleetId,
          team: item.fleetName,
          schedules,
          subtotal: item.subtotal || []
        }
      })
      total.value = res.data.total || teamSchedules.value.length
    }
  } finally {
    endLoading()
  }
}
const getShiftList = async () => {
  const res = await getShiftListNoPage()
  if (res.code === 200) {
    shiftTags.value = res.data.map((item: any) => ({
      id: item.shiftId,
      shiftName: item.shiftName,
      time: `${item.startTime}-${item.endTime}`,
      color: item.shiftColor,
      bgColor: item.shiftColor + '25'
    }))
  }
}
onMounted(() => {
  selectedMonth.value = dayjs(new Date()).format('YYYY-MM')
  getShiftList()
  getList()
})
function changePage(page: number, size: number) {
  pageParams.value.pageNum = page
  pageParams.value.pageSize = size
  getList()
}
</script>
@@ -158,123 +210,98 @@
        <div class="text-lg font-bold">排班计划管理</div>
      </div>
      <div class="flex items-center">
      <div class="flex items-center" v-if="shiftTags.length > 0">
        <div class="flex items-center">
          <span class="text-sm">班次设置:</span>
          <div v-for="tag in shiftTags" :key="`${tag.name}-${tag.time}`" style="margin-right: 8px;" class="flex items-center gap-1">
            <span
              class="rounded text-xs"
              :style="{
                backgroundColor: tag.bgColor,
                color: tag.color,
                width: '113px',
                height: '28px',
                borderRadius: '4px',
                display: 'inline-flex',
                alignItems: 'center',
                justifyContent: 'center'
              }"
            >
              {{ tag.name }} {{ tag.time }}
          <div v-for="tag in shiftTags" :key="`${tag.shiftName}-${tag.startTime}`" style="margin-right: 8px;"
            class="flex items-center gap-1">
            <span class="rounded text-xs" :style="{
              backgroundColor: tag.bgColor,
              color: tag.color,
              width: '113px',
              height: '28px',
              borderRadius: '4px',
              display: 'inline-flex',
              alignItems: 'center',
              justifyContent: 'center'
            }">
              {{ tag.shiftName }} {{ tag.time }}
            </span>
          </div>
        </div>
      </div>
    </n-card>
    <n-card style="border-radius: 6px; margin-top: 12px; overflow: hidden;" content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;">
    <n-card style="border-radius: 6px; margin-top: 12px; overflow: hidden;"
      content-style="padding: 12px; height: 100%; display: flex; flex-direction: column;">
      <div class="flex items-center justify-between mb-4">
        <n-date-picker
          v-model:value="selectedMonth"
          placeholder="选择月份"
          type="month"
          style="width: 260px"
          @update:value="handleMonthChange"
        />
        <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;" @click="handleCopy">复制</n-button>
          <n-button style="width: 84px;height: 32px;background-color: #E6F4FF;color: #0068FF;border: 1px solid #7ABDFF;" @click="handleDownload">下载表</n-button>
          <n-button 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;"
            @click="handleDownload">下载表</n-button>
          <n-button type="primary" style="width: 84px; height: 32px" @click="handleEdit">编辑</n-button>
        </div>
      </div>
      <div class="schedule-table-container" style="overflow-x: auto; flex: 1;">
        <table class="schedule-table">
          <thead>
            <tr>
              <th class="sticky-col">车队</th>
              <th
                v-for="(date, index) in dates"
                :key="index"
              >
                <div>{{ date.month }}/{{ date.day }}</div>
                <div class="text-xs text-gray-500" style="margin-top: 4px;">{{ date.weekDay }}</div>
              </th>
              <th class="summary-col">排班小计</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(schedule, teamIndex) in teamSchedules" :key="teamIndex">
              <td class="sticky-col team-name">{{ schedule.team }}</td>
              <td
                v-for="(shifts, dateIndex) in schedule.schedules"
                :key="dateIndex"
                class="schedule-cell"
                @click="handleCellClick(teamIndex, dateIndex)"
              >
                <div class="flex flex-wrap gap-1">
                  <span
                    v-for="(shift, shiftIndex) in shifts"
                    :key="shiftIndex"
                    class="whitespace-nowrap"
                    :style="{ backgroundColor: shift.bgColor, color: shift.color }"
                  >
                    {{ shift.name }}
                  </span>
                </div>
              </td>
              <td class="summary-cell">
                <div class="flex flex-wrap justify-center" style="gap: 2px;">
                  <span
                    v-for="tag in shiftTags"
                    :key="`${tag.name}-${tag.time}`"
                    class="summary-tag"
                    :style="{ backgroundColor: tag.bgColor, color: tag.color }"
                  >
                    {{ tag.name }} {{ getShiftCount(schedule.schedules, tag.name) }}
                  </span>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <n-scrollbar x-scrollable class="flex-1 mb-4">
        <div class="schedule-table-wrapper">
          <table class="schedule-table">
            <thead>
              <tr>
                <th class="sticky-col">车队</th>
                <th v-for="(date, index) in dates" :key="index">
                  <div>{{ date.month }}/{{ date.day }}</div>
                  <div class="text-xs text-gray-500" style="margin-top: 4px;">{{ date.weekDay }}</div>
                </th>
                <th class="summary-col">排班小计</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(schedule, teamIndex) in teamSchedules" :key="teamIndex">
                <td class="sticky-col team-name">{{ schedule.team }}</td>
                <td v-for="(shifts, dateIndex) in schedule.schedules" :key="dateIndex" class="schedule-cell"
                  @click="handleCellClick(teamIndex, dateIndex)">
                  <div class="flex flex-wrap gap-1">
                    <span v-for="(shift, shiftIndex) in shifts" :key="shiftIndex" class="whitespace-nowrap"
                      :style="{ backgroundColor: shift.bgColor, color: shift.color }">
                      {{ shift.name }}
                    </span>
                  </div>
                </td>
                <td class="summary-cell">
                  <div class="flex flex-wrap justify-center" style="gap: 2px;">
                    <span v-for="tag in schedule.subtotal" :key="`${tag.shiftName}-${tag.time}`" class="summary-tag"
                      :style="{ backgroundColor: tag.shiftColor + '25', color: tag.shiftColor }">
                      {{ tag.shiftName }} {{ tag.count }}
                    </span>
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </n-scrollbar>
      <Pagination :count="total" @change="changePage" />
    </n-card>
    <!-- 排班弹窗 -->
    <n-modal v-model:show="showScheduleModal" preset="card" title="排班选择" class="w-400px">
      <div class="flex flex-col gap-3">
        <div
          v-for="shift in shiftTags"
          :key="`${shift.name}-${shift.time}`"
        <div v-for="shift in shiftTags" :key="`${shift.name}-${shift.time}`"
          class="flex items-center justify-between p-3 border rounded cursor-pointer hover:bg-gray-50"
          :class="{ 'border-blue-500 bg-blue-50': selectedShifts.some(s => s.name === shift.name && s.time === shift.time) }"
          @click="handleShiftToggle(shift)"
        >
          @click="handleShiftToggle(shift)">
          <div class="flex items-center gap-2">
            <span
              class="px-3 py-1 rounded text-xs"
              :style="{ backgroundColor: shift.bgColor, color: shift.color }"
            >
            <span class="px-3 py-1 rounded text-xs" :style="{ backgroundColor: shift.bgColor, color: shift.color }">
              {{ shift.name }}
            </span>
            <span class="text-sm">{{ shift.time }}</span>
          </div>
          <n-checkbox
            :checked="selectedShifts.some(s => s.name === shift.name && s.time === shift.time)"
            @click.stop="handleShiftToggle(shift)"
          />
          <n-checkbox :checked="selectedShifts.some(s => s.name === shift.name && s.time === shift.time)"
            @click.stop="handleShiftToggle(shift)" />
        </div>
      </div>
@@ -286,21 +313,18 @@
      </template>
    </n-modal>
    <EditMonthModal
      v-model:show="showEditModal"
      :current-month="selectedMonth"
      @confirm="handleConfirmEdit"
    />
    <EditMonthModal v-model:show="showEditModal" :current-month="selectedMonth" @confirm="handleConfirmEdit" />
    <CopyScheduleModal
      v-model:show="showCopyModal"
    />
    <CopyScheduleModal v-model:show="showCopyModal" />
  </div>
</template>
<style scoped>
.schedule-table-container {
  position: relative;
<style scoped lang="scss">
.schedule-table-wrapper {
  min-width: max-content;
  // position: relative;
  display: inline-block;
  width: 100%;
}
.schedule-table {
src/views/operations/attendance/statistics/index.vue
@@ -30,16 +30,7 @@
  { label: '角色1', value: '角色1' }
]
const statisticsList = ref<any[]>([{
  serialNumber: 1,
  name: '王美丽',
  roleName: '网格员',
  teamName: '队伍1',
  shouldClockInDays: 10,
  clockInDays: 8,
  notClockInDays: 2,
  lateEarly: 1,
}])
const statisticsList = ref<any[]>([])
const total = ref(0)
const pageParams = ref({
@@ -122,7 +113,7 @@
    statisticDate: searchModel.value.statisticDate?.join('-'),
  }
  getClockInStatisticsList(params).then((res) => {
    // statisticsList.value = res.data.records || []
    statisticsList.value = res.data.records || []
    statisticsList.value.forEach((item: any, index: number) => {
      item.serialNumber = index + 1
    })
src/views/operations/taskOrder/taskOrderCenter/components/AddFormModal.vue
@@ -381,9 +381,9 @@
            :max-tag-count="1" />
        </n-form-item>
        <n-form-item label="任务等级" path="taskTypeId">
        <n-form-item label="任务等级" path="taskUrgencyLevel">
          <n-select v-model:value="formModel.taskUrgencyLevel" :options="taskUrgencyLevelOptions" placeholder="请选择"
            clearable />
            clearable  />
        </n-form-item>
        <n-form-item label="车辆" path="vehicleIds">
src/views/operations/taskOrder/taskOrderCenter/components/detailModal.vue
New file
@@ -0,0 +1,465 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { getTaskOrderDetail } from '@/service/api/attendance'
import { NButton, NSpace, NTag } from 'naive-ui'
import dayjs from 'dayjs'
const emit = defineEmits<{
  open: []
  close: []
}>()
const { bool: modalVisible, setTrue: showModal, setFalse: hiddenModal } = useBoolean(false)
const formDefault: any = {
  id: '',
  accidentDetailVO: {},
  chargingDetailVO: {},
  cleanDetailVO: {},
  detectionDetailVO: {},
  preparationDetailVO: {},
  repairDetailVO: {},
  routineDetailVO: {},
}
const formModel = ref({ ...formDefault })
async function openModal(id: string) {
  emit('open')
  formModel.value.id = id
  const res = await getTaskOrderDetail(id)
  if (res.code === 200) {
    formModel.value = { ...formModel.value, ...res.data }
  }
  showModal()
}
function closeModal() {
  formModel.value = { ...formDefault }
  hiddenModal()
  emit('close')
}
defineExpose({
  openModal,
})
const taskUrgencyLevelOptions = [
  { label: '一级', value: 0, type: 'error' },
  { label: '二级', value: 1, type: 'error' },
  { label: '三级', value: 2, type: 'error' },
  { label: '四级', value: 3, type: 'warning' },
]
const statusOptions = [
  { label: '待到达', value: 0, type: 'default' },
  { label: '待处理', value: 1, type: 'warning' },
  { label: '已完成', value: 2, type: 'primary' },
  { label: '已取消', value: 3, type: 'default' },
]
function formatCascadeData(data: any[]): string {
  return data
    .map(parent => {
      const children = parent.child?.map((c: any) => c.errorName).join('、') ?? ''
      return `${parent.errorName}>${children}`
    })
    .join(';')
}
</script>
<template>
  <n-modal v-model:show="modalVisible" :mask-closable="false" preset="card" title="任务详情" class="w-600px" :segmented="{
    content: true,
    action: true,
  }">
    <!-- 事故详情 -->
    <div v-if="formModel.accidentDetailVO?.id">
      <div class="content">
        <!-- 任务信息 -->
        <div>
          <div class="mb-12px flex items-center gap-8px">
            <div class="head">任务信息</div>
            <NTag type="warning" class="w-40px" :bordered="false"
              v-if="formModel.accidentDetailVO.taskUrgencyLevel === 3">{{taskUrgencyLevelOptions.find(it => it.value
                === formModel.accidentDetailVO.taskUrgencyLevel)?.label || ''}}</NTag>
            <NTag type="error" class="w-40px" :bordered="false" v-else>{{taskUrgencyLevelOptions.find(it => it.value
              === formModel.accidentDetailVO.taskUrgencyLevel)?.label || ''}}</NTag>
            <NTag :type="statusOptions.find(it => it.value === formModel.accidentDetailVO.status)?.type" class="w-52px"
              :bordered="false">{{statusOptions.find(it => it.value ===
                formModel.accidentDetailVO.status)?.label || ''}}</NTag>
          </div>
          <div class="mb-16px">{{ formatCascadeData(formModel.accidentDetailVO.errorTypeList) }}</div>
          <div class="mb-16px flex items-center">
            <img :src="formModel.accidentDetailVO.vehiclePictures" class="w-112px h-64px mr-16px"
              v-if="formModel.accidentDetailVO.vehiclePictures" />
            <div>
              <div class="plateNumber mb-8px">{{ formModel.accidentDetailVO.plateNumber }}</div>
              <div class="value">{{ formModel.accidentDetailVO.vehicleColor }} | 车身编号:{{
                formModel.accidentDetailVO.vehicleCode }} | VIN码:{{
                  formModel.accidentDetailVO.vehicleVin }}</div>
            </div>
          </div>
          <div class="mb-16px">
            <div class="label mb-4px">上报地址</div>
            <div class="value">{{ formModel.accidentDetailVO.vehicleLocation }}</div>
          </div>
          <div class="mb-16px">
            <div class="label mb-4px">事故描述</div>
            <div class="value mb-8px">{{ formModel.accidentDetailVO.taskDescription }}</div>
            <div class="imgs">
              <img v-for="(item, idx) in JSON.parse(formModel.accidentDetailVO.taskImage)" :key="idx" :src="item.url"
                class="w-56px h-56px mr-16px" />
            </div>
          </div>
          <div class="mb-16px flex gap-24px">
            <div class="flex items-center flex-1">
              <img :src="formModel.accidentDetailVO.reportUserAvatar" class="w-40px h-40px mr-8px"
                v-if="formModel.accidentDetailVO.reportUserAvatar" />
              <div>
                <div class="label mb-4px">上报人</div>
                <div class="value">{{ formModel.accidentDetailVO.reportUserName }} {{
                  formModel.accidentDetailVO.reportUserPhone }}</div>
              </div>
            </div>
            <div class="flex-1">
              <div class="label mb-4px">上报时间</div>
              <div class="value">{{ formModel.accidentDetailVO.createTime }}</div>
            </div>
          </div>
        </div>
        <!-- 处理情况 -->
        <div>
          <div class="head mb-12px">处理情况</div>
          <div class="mb-16px flex gap-8px">
            人员伤亡情况
            <NTag type="primary" class="w-52px" :bordered="false">伤亡 {{ formModel.accidentDetailVO.deathCount || 0 }}
            </NTag>
            <NTag type="error" class="w-52px" :bordered="false">重伤 {{ formModel.accidentDetailVO.severeCount || 0 }}
            </NTag>
            <NTag type="warning" class="w-52px" :bordered="false">轻伤 {{ formModel.accidentDetailVO.minorCount || 0 }}
            </NTag>
          </div>
          <div class="mb-16px flex gap-8px">
            初版(交警)责任判断
            <NTag type="error" class="w-64px" :bordered="false"
              v-if="formModel.accidentDetailVO.responsibilityJudgment === 1">我方主责</NTag>
            <NTag type="success" class="w-64px" :bordered="false"
              v-if="formModel.accidentDetailVO.responsibilityJudgment === 2">我方无责</NTag>
            <NTag type="primary" class="w-64px" :bordered="false"
              v-if="formModel.accidentDetailVO.responsibilityJudgment === 3">双方同责</NTag>
          </div>
          <div class="mb-16px">
            <div class="label mb-4px">现场情况</div>
            <div class="value mb-8px">{{ formModel.accidentDetailVO.situationDescription }}</div>
            <div class="imgs">
              <img v-for="(item, idx) in JSON.parse(formModel.accidentDetailVO.situationImage)" :key="idx"
                :src="item.url" class="w-56px h-56px mr-16px" />
            </div>
          </div>
          <div class="mb-16px">
            <div class="label mb-4px">处置情况</div>
            <div class="value mb-8px">{{ formModel.accidentDetailVO.disposalDescription }}</div>
            <div class="imgs">
              <img v-for="(item, idx) in JSON.parse(formModel.accidentDetailVO.disposalImage)" :key="idx"
                :src="item.url" class="w-56px h-56px mr-16px" />
            </div>
          </div>
          <div class="mb-16px flex gap-24px">
            <div class="flex items-center flex-1">
              <img :src="formModel.accidentDetailVO.handleUserAvatar" class="w-40px h-40px mr-8px"
                v-if="formModel.accidentDetailVO.handleUserAvatar" />
              <div>
                <div class="label mb-4px">处理人</div>
                <div class="value">{{ formModel.accidentDetailVO.handleUserName }} {{
                  formModel.accidentDetailVO.handleUserPhone }}</div>
              </div>
            </div>
            <div class="flex-1">
              <div class="label mb-4px">处理时间</div>
              <div class="value">{{ formModel.accidentDetailVO.handleTime }}</div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- 检测详情 -->
    <div v-if="formModel.detectionDetailVO?.id">
      <div class="content">
        <div class="mb-16px flex items-center">
          <img :src="formModel.detectionDetailVO.vehiclePictures" class="w-112px h-64px mr-16px"
            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>
        </div>
        <div class="head mb-12px">系统检测</div>
        <div class="label mb-16px">
          <span>{{ formModel.detectionDetailVO.networkDetection === 1 ? '✔' : 'x' }}网络检测</span>
          <span>{{ formModel.detectionDetailVO.versionVerification === 1 ? '✔' : 'x' }}软件版本核对</span>
          <span>{{ formModel.detectionDetailVO.communicationSelfCheck === 1 ? '✔' : 'x' }}通讯自检</span>
          <span>{{ formModel.detectionDetailVO.remoteSelfCheck === 1 ? '✔' : 'x' }}远程控制自检</span>
          <span>{{ formModel.detectionDetailVO.collisionPrevention === 1 ? '✔' : 'x' }}防撞激活</span>
          <span>{{ formModel.detectionDetailVO.sensorSelfCheck === 1 ? '✔' : 'x' }}传感器自检</span>
        </div>
        <div class="head mb-12px">人工检测</div>
        <div class="mb-16px">
          <div class="label mb-8px">传感器清洁:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.detectionDetailVO.sensorCleaningPicture)" :key="idx"
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">车内交互屏检查:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.detectionDetailVO.interactiveScreenPicture)" :key="idx"
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">紧急求助按钮/应答机:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.detectionDetailVO.helpButtonPicture)" :key="idx"
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">交付功能:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.detectionDetailVO.paymentFunctionPicture)" :key="idx"
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">订单系统检查:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.detectionDetailVO.systemCheckPicture)" :key="idx"
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </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>
    </div>
    <!-- 充电详情 -->
    <div v-if="formModel.chargingDetailVO?.id">
      <div class="content">
        <div class="mb-16px flex items-center">
          <img :src="formModel.chargingDetailVO.vehiclePictures" class="w-112px h-64px mr-16px"
            v-if="formModel.chargingDetailVO?.vehiclePictures" />
          <div>
            <div class="plateNumber mb-8px">{{ formModel.chargingDetailVO.plateNumber }}</div>
            <div class="value mb-8px">{{ formModel.chargingDetailVO.vehicleColor }} | VIN码:{{
                formModel.chargingDetailVO.vehicleVin }}
            </div>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
          </div>
        </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>
        <div class="mb-16px">开始充电操作人:{{ formModel.chargingDetailVO.startOperationsUserName }}</div>
        <div class="mb-16px">
          <div class="mb-8px">开始充电图片:</div>
          <div class="imgs">
            <div v-for="(item, idx) in JSON.parse(formModel.chargingDetailVO.startChargingPicture)" :key="idx">
              <img :src="item.url" v-if="item?.url" class="w-56px h-56px mr-16px" />
            </div>
          </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.endOperationsUserName }}</div>
        <div class="mb-16px">
          <div class="mb-8px">结束充电图片:</div>
          <div class="imgs">
            <div v-for="(item, idx) in JSON.parse(formModel.chargingDetailVO.endChargingPicture)" :key="idx">
              <img :src="item.url" v-if="item?.url" class="w-56px h-56px mr-16px" />
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- 整备详情 -->
    <div v-if="formModel.preparationDetailVO?.id">
      <div class="content">
        <div class="mb-16px flex items-center">
          <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>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">胎压:</div>
          <div class="value mb-8px">左前轮:{{ formModel.preparationDetailVO.leftFrontWheel }} 右前轮:{{
            formModel.preparationDetailVO.rightFrontWheel }} 左后轮:{{ formModel.preparationDetailVO.leftRearWheel }}
            右后轮:{{ formModel.preparationDetailVO.rightRearWheel }}</div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">大灯情况:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.preparationDetailVO.headLight)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">转向灯情况:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.preparationDetailVO.turnSignal)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">刹车灯情况:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.preparationDetailVO.brakeLight)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">雾灯情况:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.preparationDetailVO.fogLight)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">车身标识及清洁:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.preparationDetailVO.bodyEmblem)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">舱内清洁及消毒:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.preparationDetailVO.cabinCleaning)" :key="idx"
              :src="item.url" class="w-56px h-56px mr-16px" />
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">遗失物品上报:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.preparationDetailVO.lostItems)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
          <div class="label mt-8px">遗失说明:<div v-html="formModel.preparationDetailVO.lostItemsDetail"></div>
          </div>
        </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>
      </div>
    </div>
    <!-- 洗车详情 -->
    <div v-if="formModel.cleanDetailVO?.id">
      <div class="content">
        <div class="mb-16px flex items-center">
          <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>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="mb-16px">
          <div class="label mb-8px">图片:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.cleanDetailVO.taskImage)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
        </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>
    </div>
    <!-- 维修详情 -->
    <div v-if="formModel.repairDetailVO?.id">
      <div class="content">
        <div class="mb-16px flex items-center">
          <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>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.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.repairReason }}</div>
        <div class="mb-16px">
          <div class="label mb-8px">维修前图片:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.repairDetailVO.startRepairPicture)" :key="idx"
              :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.sendUserName }}</div>
        <div class="mb-16px">维修总费用:{{ formModel.repairDetailVO.repairMoney }}</div>
        <div class="mb-16px">
          <div class="label mb-8px">维修费用明细单:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.repairDetailVO.repairMoneyDetail)" :key="idx"
              :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.returnUserName }}</div>
        <div class="mb-16px">任务单号:{{ formModel.repairDetailVO.id }}</div>
      </div>
    </div>
    <!-- 保养详情 -->
    <div v-if="formModel.routineDetailVO?.id">
      <div class="content">
        <div class="mb-16px flex items-center">
          <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>
            <div class="value">车辆位置:{{ formModel.chargingDetailVO.vehicleLocation }}</div>
          </div>
        </div>
        <div class="label mb-16px">任务完成情况:{{ formModel.routineDetailVO.taskDescription }}</div>
        <div class="mb-16px">
          <div class="label mb-8px">图片:</div>
          <div class="imgs">
            <img v-for="(item, idx) in JSON.parse(formModel.routineDetailVO.taskImage)" :key="idx" :src="item.url"
              class="w-56px h-56px mr-16px" />
          </div>
        </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>
    </div>
    <template #action>
      <n-space justify="end">
        <n-button @click="closeModal">
          返回
        </n-button>
      </n-space>
    </template>
  </n-modal>
</template>
<style scoped lang="scss">
@use '../index.scss';
</style>
src/views/operations/taskOrder/taskOrderCenter/index.scss
New file
@@ -0,0 +1,41 @@
.content {
  .head {
    font-family: PingFang SC;
    font-size: 16px;
    font-weight: 600;
    line-height: 24px;
    letter-spacing: normal;
    color: #171F26;
    position: relative;
    text-indent: 11px;
    &::before {
      content: '';
      position: absolute;
      top: 50%;
      left: 0;
      transform: translateY(-50%);
      width: 3px;
      height: 14px;
      z-index: 0;
      border-radius: 3px;
      background: #0068FF;
    }
  }
  .plateNumber {
    font-size: 20px;
    line-height: 28px;
    font-weight: 600;
  }
  .label {
    color: rgba(23, 31, 38, 0.6);
  }
  .imgs {
    img {
      border-radius: 4px;
    }
  }
}
src/views/operations/taskOrder/taskOrderCenter/index.vue
@@ -7,6 +7,7 @@
import SearchPanel from '@/components/custom/SearchPanel.vue'
import TableModal from './components/TableModal.vue'
import AddFormModal from './components/AddFormModal.vue'
import DetailModal from './components/detailModal.vue'
import dayjs from 'dayjs'
const router = useRouter()
@@ -30,6 +31,8 @@
const formRef = ref<FormInst | null>()
const modalRef = ref()
const detailModalRef = ref()
const showFormModal = ref(false)
const formMode = ref<'add' | 'edit'>('add')
@@ -186,18 +189,56 @@
  formMode.value = 'add'
}
/**
 * 级联选中时自动补充父级 id
 * @param tree 原始树结构
 * @param selectedIds 已选中的 id 列表
 */
function addParentIds(
  tree: any[],
  selectedIds: string[]
): string[] {
  const result = new Set(selectedIds)
  function traverse(nodes: any[], parent?: any) {
    nodes.forEach(node => {
      // 如果当前节点被选中
      if (result.has(node.id) && parent) {
        result.add(parent.id)
      }
      if (node.child && node.child.length) {
        traverse(node.child, node)
      }
    })
  }
  traverse(tree)
  return Array.from(result)
}
const handleConfirmForm = (data: any) => {
  console.log(data)
  const taskImage = data.taskImage.map((item: any) => {
    return {
      id: item.id,
      name: item.name,
      url: item.url
    }
  })
  const params = {
    ...data,
    taskTypeId: data.taskTypeId.join(','),
    taskTypeId: addParentIds(taskErrorIdOptions.value,data.taskTypeId).join(','),
    taskImage: JSON.stringify(taskImage),
  }
    createTaskAccident(params).then((res) => {
      if (res.code === 200) {
        window.$message.success('新增成功')
      }
    }).finally(() => {
      getList()
    })
  createTaskAccident(params).then((res) => {
    if (res.code === 200) {
      window.$message.success('新增成功')
    }
  }).finally(() => {
    getList()
  })
}
@@ -309,7 +350,7 @@
          <NButton
            text
            type='primary'
            onClick={() => modalRef.value.openModal('edit', row.id)}
            onClick={() => detailModalRef.value.openModal(row.id)}
          >
            详情
          </NButton>
@@ -360,6 +401,8 @@
      </NSpace>
    </n-card>
    <TableModal ref="modalRef" @refresh="getList" />
    <AddFormModal v-model:show="showFormModal" :mode="formMode" @confirm="handleConfirmForm" :taskErrorIdOptions="taskErrorIdOptions" :taskUrgencyLevelOptions="taskUrgencyLevelOptions"/>
    <DetailModal ref="detailModalRef" />
    <AddFormModal v-model:show="showFormModal" :mode="formMode" @confirm="handleConfirmForm"
      :taskErrorIdOptions="taskErrorIdOptions" :taskUrgencyLevelOptions="taskUrgencyLevelOptions" />
  </div>
</template>
src/views/transaction/order/orderManagement/components/TableModal.vue
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useBoolean } from '@/hooks'
import { getAvailableVehicleList } from '@/service/api/transaction'
interface Props {
  modalName?: string
@@ -28,6 +29,7 @@
async function openModal(data: any) {
  emit('open')
  await getOrder(data.id)
  showModal()
  formModel.value = { ...data }
}
@@ -58,6 +60,16 @@
}
const options = ref()
const getOrder = async (orderId: number) => {
  const res = await getAvailableVehicleList(orderId)
  if (res.code === 0) {
    options.value = res.data.map((item: any) => ({
      label: item.vehicleName,
      value: item.vehicleId,
    }))
  }
}
</script>
<template>