feat: 排版计划接口对接、任务工单详情接口对接
| | |
| | | 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', |
| | |
| | | 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') |
| | | } |
| | | |
| | | /** 运维人员管理 */ |
| | | |
| | |
| | | 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) |
| | | } |
| | | |
| | | /** 订单评价 */ |
| | | |
| | | // 获取订单评价列表 |
| | |
| | | body, |
| | | #app { |
| | | height: 100%; |
| | | background-color: #fff; |
| | | } |
| | | .color-weak { |
| | | filter: invert(80%); |
| | |
| | | <script setup lang="ts"> |
| | | import { ref, computed } from 'vue' |
| | | import type { FormInst, FormRules } from 'naive-ui' |
| | | import dayjs from 'dayjs' |
| | | |
| | | interface ShiftOption { |
| | | name: string |
| | |
| | | |
| | | const props = defineProps<{ |
| | | show: boolean |
| | | teamOptions: any |
| | | shifts: any |
| | | selectedMonth: string | null |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | |
| | | 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: [ |
| | |
| | | } |
| | | |
| | | // 模拟队伍数据 |
| | | const teamOptions = [ |
| | | { label: '广州白云一队', value: '广州白云一队' }, |
| | | { label: '广州白云二队', value: '广州白云二队' }, |
| | | { label: '广州南沙三队', value: '广州南沙三队' } |
| | | ] |
| | | const teamOptions = computed(() => props.teamOptions) |
| | | |
| | | const handleCancel = () => { |
| | | show.value = false |
| | |
| | | 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, |
| | |
| | | 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" |
| | | > |
| | |
| | | :options="dateOptions" |
| | | placeholder="选择" |
| | | multiple |
| | | :max-tag-count="5" |
| | | style="flex: 1" |
| | | /> |
| | | </div> |
| | |
| | | |
| | | interface FormModel { |
| | | month: number | null |
| | | role: string | null |
| | | } |
| | | |
| | | const props = defineProps<{ |
| | |
| | | |
| | | 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 |
| | |
| | | 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() |
| | |
| | | |
| | | 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, |
| | |
| | | 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> |
| | |
| | | <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> |
| | |
| | | |
| | | const rules: FormRules = { |
| | | month: [ |
| | | { |
| | | required: true, |
| | | { |
| | | required: true, |
| | | type: 'number', |
| | | message: '请选择排班月份', |
| | | message: '请选择排班月份', |
| | | trigger: 'change' |
| | | } |
| | | ] |
| | |
| | | </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, |
| | |
| | | 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" |
| | |
| | | <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> |
| | |
| | | <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 |
| | |
| | | } |
| | | |
| | | 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) => { |
| | |
| | | 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 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) => { |
| | |
| | | |
| | | 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] : [] |
| | | |
| | | // 计算盒子位置 |
| | |
| | | }) |
| | | } |
| | | |
| | | 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 |
| | | }) |
| | | } |
| | | |
| | | // 选择后直接关闭 |
| | |
| | | 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) |
| | | |
| | |
| | | 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> |
| | |
| | | <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, |
| | |
| | | alignItems: 'center', |
| | | justifyContent: 'center' |
| | | }"> |
| | | {{ tag.name }} {{ tag.time }} |
| | | {{ tag.shiftName }} {{ tag.time }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | |
| | | <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> |
| | | |
| | | <!-- 排班选择盒子 --> |
| | |
| | | </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> |
| | |
| | | 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) => { |
| | |
| | | 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 => { |
| | |
| | | } |
| | | |
| | | 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() |
| | | } |
| | |
| | | // 点击外部关闭选择盒子 |
| | | 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() |
| | | } |
| | |
| | | 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 { |
| | |
| | | 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) => { |
| | |
| | | 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() |
| | | } |
| | | } |
| | | |
| | |
| | | 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 => { |
| | |
| | | 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) |
| | |
| | | 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) |
| | |
| | | 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> |
| | |
| | | <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> |
| | | |
| | |
| | | </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 { |
| | |
| | | { 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({ |
| | |
| | | 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 |
| | | }) |
| | |
| | | :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"> |
| New file |
| | |
| | | <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> |
| New file |
| | |
| | | .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; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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() |
| | |
| | | |
| | | const formRef = ref<FormInst | null>() |
| | | const modalRef = ref() |
| | | const detailModalRef = ref() |
| | | |
| | | const showFormModal = ref(false) |
| | | const formMode = ref<'add' | 'edit'>('add') |
| | | |
| | |
| | | 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() |
| | | }) |
| | | } |
| | | |
| | | |
| | |
| | | <NButton |
| | | text |
| | | type='primary' |
| | | onClick={() => modalRef.value.openModal('edit', row.id)} |
| | | onClick={() => detailModalRef.value.openModal(row.id)} |
| | | > |
| | | 详情 |
| | | </NButton> |
| | |
| | | </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> |
| | |
| | | <script setup lang="ts"> |
| | | import { useBoolean } from '@/hooks' |
| | | import { getAvailableVehicleList } from '@/service/api/transaction' |
| | | |
| | | interface Props { |
| | | modalName?: string |
| | |
| | | |
| | | async function openModal(data: any) { |
| | | emit('open') |
| | | await getOrder(data.id) |
| | | showModal() |
| | | formModel.value = { ...data } |
| | | } |
| | |
| | | } |
| | | |
| | | 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> |