yn_digital_gadgets_web/src/views/planMange/monthlyPlan/edit.vue

1318 lines
50 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container monthly-plan-edit">
<!-- 顶部返回 + 标题 -->
<el-page-header @back="onBack" :content="pageTitle" class="page-header" />
<el-card shadow="never" class="card-section">
<template #header>
<div class="card-header">基本信息</div>
</template>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="auto"
size="large"
:disabled="isDetail"
>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="运检站">
<el-input v-model="formData.inspectionStationName" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="planMajorId">
<el-select
v-model="formData.planMajorId"
placeholder="请选择专业"
clearable
>
<el-option
:key="item.planMajorId"
:label="item.planMajorName"
:value="item.planMajorId"
v-for="item in majorTypeCategoryOptions"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="业务类型" prop="businessTypeId">
<el-select
v-model="formData.businessTypeId"
placeholder="请选择业务类型"
clearable
>
<el-option
v-for="item in planBusinessTypeOptions"
:key="item.planMajorId"
:label="item.planMajorName"
:value="item.planMajorId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目名称">
<el-input
clearable
disabled
placeholder="请输入项目名称"
v-model.trim="formData.projectName"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="24">
<el-form-item label="工作任务">
<el-input
disabled
maxlength="500"
show-word-limit
type="textarea"
placeholder="请输入作业内容"
v-model.trim="formData.workContent"
:autosize="{ minRows: 4, maxRows: 12 }"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="类别" prop="planCategoryId">
<el-select
placeholder="请选择类别"
v-model="formData.planCategoryId"
clearable
>
<el-option
:key="item.planMajorId"
:value="item.planMajorId"
:label="item.planMajorName"
v-for="item in planCategoryOptions"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<template v-for="(item, index) in formData.workloadList" :key="index">
<el-col :span="8">
<el-form-item
:prop="`workloadList.${index}.workloadCategoryId`"
:class="{ 'hide-required': index !== 0 }"
>
<template #label>
<span v-if="index == 0">工作量</span>
<span v-else style="visibility: hidden">工作量</span>
</template>
<el-select
clearable
placeholder="请选择工作量类别"
v-model="item.workloadCategoryId"
@change="onChangeWorkLoadCategory($event, index)"
>
<el-option
:key="item.workloadCategoryId"
:value="item.workloadCategoryId"
:label="item.workloadCategoryName"
v-for="item in planWorkLoadCategoryOptions"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item
label-width="0"
:prop="`workloadList.${index}.workloadNum`"
>
<el-input
clearable
style="width: 240px"
placeholder="输入数量"
show-word-limit
maxlength="7"
v-model="item.workloadNum"
>
<template #suffix>
<el-button
type="success"
icon="Plus"
size="small"
v-if="index === 0 && !isDetail"
@click="onAddWorkLoad"
/>
<el-button
type="danger"
icon="Delete"
size="small"
v-if="index !== 0 && !isDetail"
@click="onDeleteWorkLoad(index)"
/>
</template>
</el-input>
</el-form-item>
</el-col>
</template>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="基塔数" prop="towerBaseNumber">
<el-input
clearable
show-word-limit
maxlength="7"
placeholder="请输入基塔数"
v-model.trim="formData.towerBaseNumber"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="计划开始时间">
<el-date-picker
v-model="formData.plannedStartTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择计划开始时间"
style="width: 100%"
disabled
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划结束时间">
<el-date-picker
v-model="formData.plannedEndTime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择计划结束时间"
style="width: 100%"
disabled
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item prop="planPersonnelList">
<template #label>
<div style="display: flex; flex-direction: column">
<span>计划投入管理人员</span>
<span
style="
font-size: 12px;
color: #999;
transform: translateY(-20px);
"
>
(职工,劳务派遣,海峡)
</span>
</div>
</template>
<el-input
readonly
placeholder="点击选择管理人员"
@click="onOpenPersonPicker"
v-model="selectedManagerNames"
>
<template #suffix v-if="!isDetail">
<el-icon
class="clickable-suffix"
@click.stop="onOpenPersonPicker"
>
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划投入管理车辆数" prop="planCarNum">
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="请输入车辆数量"
v-model.trim="formData.planCarNum"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="7">
<el-form-item prop="planSkilledWorkerNum">
<template #label>
<div style="display: flex; flex-direction: column">
<span>计划投入熟练工人员数量</span>
<span
style="
font-size: 12px;
color: #999;
transform: translateY(-20px);
"
>
(分包)
</span>
</div>
</template>
<el-input
maxlength="7"
show-word-limit
placeholder="熟练工人员数量"
v-model.trim="formData.planSkilledWorkerNum"
/>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item
label="计划投入工日"
prop="planSkilledWorkerDay"
label-width="120"
>
<el-input
clearable
maxlength="7"
placeholder="工日"
v-model.trim="formData.planSkilledWorkerDay"
/>
</el-form-item>
</el-col>
<el-col :span="7">
<el-form-item prop="planAuxiliaryWorkerNum">
<template #label>
<div style="display: flex; flex-direction: column">
<span>计划投入辅助工人员数量</span>
<span
style="
font-size: 12px;
color: #999;
transform: translateY(-20px);
"
>
(分包)
</span>
</div>
</template>
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="辅助工人员数量"
v-model.trim="formData.planAuxiliaryWorkerNum"
/>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item
label="计划投入工日"
prop="planAuxiliaryWorkerDay"
label-width="120"
>
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="工日"
v-model.trim="formData.planAuxiliaryWorkerDay"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="计划投入分包车辆数" prop="planSubCarNum">
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="请输入分包车辆数量"
v-model.trim="formData.planSubCarNum"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实际工作天数">
<el-input
clearable
maxlength="7"
show-word-limit
placeholder="请输入实际工作天数"
v-model.trim="formData.actualWorkingDay"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 人员排班区域 -->
<el-card shadow="never" class="card-section">
<template #header>
<div class="card-header">人员排班</div>
</template>
<div class="schedule-wrapper">
<div class="schedule-tip" v-if="!isDetail">
<span class="schedule-tip-extra">
仅在“计划开始时间 ~
计划结束时间”范围内可排班;表单中的“计划投入管理人员”只是模板,不会被日历上的增删影响。
</span>
</div>
<div class="schedule-grid">
<div
v-for="day in calendarDays"
:key="day.date"
class="schedule-cell"
:class="{ 'is-disabled': !day.isInRange }"
>
<div class="cell-date">{{ day.label }}</div>
<div class="cell-persons">
<template v-if="day.managers && day.managers.length">
<el-popover
v-if="day.managers.length > maxShowInCell"
placement="top"
trigger="hover"
>
<template #reference>
<div class="person-tags">
<el-tag
size="small"
v-for="person in day.managers.slice(
0,
maxShowInCell,
)"
:key="person.id"
closable
@close.stop="
onRemovePersonFromDay(day.date, person)
"
>
{{ person.name }}
</el-tag>
<span class="more-text">
+{{ day.managers.length - maxShowInCell }}
</span>
</div>
</template>
<div class="popover-list">
<el-tag
size="small"
:key="person.id"
:closable="!isDetail"
v-for="person in day.managers"
@close.stop="onRemovePersonFromDay(day.date, person)"
>
{{ person.name }}
</el-tag>
</div>
</el-popover>
<template v-else>
<div class="person-tags">
<el-tag
size="small"
:key="person.id"
:closable="!isDetail"
v-for="person in day.managers"
@close.stop="onRemovePersonFromDay(day.date, person)"
>
{{ person.name }}
</el-tag>
</div>
</template>
</template>
<span
v-else
class="empty-tip-small"
:class="{ 'is-disabled-text': !day.isInRange }"
>
{{ day.isInRange ? '未安排人员' : '不可排班' }}
</span>
</div>
<div class="cell-actions" v-if="!isDetail">
<el-button
type="primary"
size="small"
plain
:disabled="!day.isInRange || !formData.planPersonnelList.length"
@click="onFillManagersForDay(day.date)"
>
安排人员
</el-button>
<el-button
type="danger"
size="small"
plain
:disabled="!day.isInRange || !day.managers.length"
@click="onClearManagersForDay(day.date)"
>
清除
</el-button>
</div>
</div>
</div>
</div>
</el-card>
<!-- 底部操作按钮(详情模式下隐藏) -->
<div class="page-footer">
<ComButton plain type="info" @click="onBack">
{{ isDetail ? '返回' : '取消' }}
</ComButton>
<ComButton v-if="!isDetail" type="primary" @click="onSubmit">保存</ComButton>
</div>
<!-- 人员选择弹窗(使用封装的 ComDialog -->
<ComDialog :dialog-config="managerDialogConfig" @closeDialogOuter="onCloseDialogOuter">
<template #outerContent>
<el-row :gutter="20">
<el-col :span="16">
<div class="person-search-bar">
<el-input
v-model.trim="managerDialog.keyword"
placeholder="输入姓名搜索"
clearable
prefix-icon="Search"
/>
</div>
<el-table
border
ref="personTableRef"
:data="filteredPersons"
@selection-change="onManagerSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column
show-overflow-tooltip
prop="org"
label="人员所属单位"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="name"
label="姓名"
width="120"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="gender"
label="性别"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="station"
label="岗位"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="position"
label="岗位性质"
align="center"
/>
<el-table-column
show-overflow-tooltip
prop="type"
label="人员分类"
align="center"
/>
</el-table>
</el-col>
<el-col :span="8">
<div class="selected-panel">
<div class="panel-header">
<span>已选人员</span>
<el-button type="text" @click="onClearManagers">清空</el-button>
</div>
<div class="selected-list">
<div>
<el-tag
:key="item.id"
:closable="!isDetail"
class="selected-tag"
@close="onRemoveManager(item)"
v-for="item in managerDialog.selected"
>
{{ item.name }}
</el-tag>
</div>
<div v-if="!managerDialog.selected.length" class="empty-tip">
暂未选择人员
</div>
</div>
</div>
</el-col>
</el-row>
<el-row class="common-btn-row" justify="end" style="margin-top: 16px">
<ComButton plain type="info" @click="managerDialog.visible = false">
取消
</ComButton>
<ComButton type="primary" @click="onConfirmManager">确定</ComButton>
</el-row>
</template>
</ComDialog>
</div>
</template>
<script setup name="MonthlyPlanEdit">
import { ref, reactive, computed, getCurrentInstance, nextTick, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { updatePlanAPI } from '@/api/planMange/plan.js'
import {
getPersonnelCommonListAPI,
getMajorTypeCategorySelectAPI,
getPlanMajorListSelectAPI,
} from '@/api/common.js'
import { updateMonthlyPlanAPI, getMonthlyPlanDetailAPI } from '@/api/planMange/monthlyPlan.js'
import { Search } from '@element-plus/icons-vue'
import { useOptions } from '@/hooks/useOptions'
import ComButton from '@/components/ComButton/index.vue'
import ComDialog from '@/components/ComDialog/index.vue'
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const workLoadCategoryOptions = ref([
{ label: '管理', value: 1 },
{ label: '熟练工', value: 2 },
{ label: '辅助工', value: 3 },
])
// 使用通用 Hook 获取下拉数据(带缓存和自动刷新)
const { options: personnelCommonOptions } = useOptions(
'personnelCommonOptions',
getPersonnelCommonListAPI,
{},
)
// 专业
const { options: majorTypeCategoryOptions } = useOptions(
'majorTypeCategoryOptions',
getMajorTypeCategorySelectAPI,
{
category: 0,
},
)
// 业务类型
const { options: planBusinessTypeOptions } = useOptions(
'planBusinessTypeOptions',
getMajorTypeCategorySelectAPI,
{
category: 1,
},
)
// 类别
const { options: planCategoryOptions } = useOptions(
'planCategoryOptions',
getMajorTypeCategorySelectAPI,
{
category: 2,
},
)
// 工作量类别
const { options: planWorkLoadCategoryOptions } = useOptions(
'planWorkLoadCategoryOptions',
getPlanMajorListSelectAPI,
{},
)
const mode = computed(() => route.query.mode || 'add')
const isDetail = computed(() => mode.value === 'detail')
const pageTitle = computed(() => {
if (mode.value === 'edit') return '编辑月计划'
if (mode.value === 'detail') return '月计划详情'
return '新增月计划'
})
const formRef = ref(null)
// 从路由参数获取禁用字段
const routeParams = computed(() => ({
inspectionStationName: route.query.inspectionStationName || '',
projectName: route.query.projectName || '',
workContent: route.query.workContent || '',
plannedStartTime: route.query.plannedStartTime || '',
plannedEndTime: route.query.plannedEndTime || '',
monthlyPlanId: route.query.monthlyPlanId || null,
}))
const getInitFormData = () => ({
monthlyPlanId: routeParams.value.monthlyPlanId,
// 从路由参数获取的字段(禁用,不传给后端)
inspectionStationName: routeParams.value.inspectionStationName,
projectName: routeParams.value.projectName,
workContent: routeParams.value.workContent,
plannedStartTime: routeParams.value.plannedStartTime,
plannedEndTime: routeParams.value.plannedEndTime,
// 表单字段(需要传给后端)
businessTypeId: null, // 业务类型
planCategoryId: null, // 类别
towerBaseNumber: '', // 基塔数
planPersonnelList: [], // 计划投入管理人员(数组)
planPersonnel: [], // 计划投入管理人员(数组)
planCarNum: '', // 计划投入管理人员车辆数
planSkilledWorkerNum: '', // 计划投入熟练工人员数量
planAuxiliaryWorkerNum: '', // 计划投入辅助工人员数量
planAuxiliaryWorkerDay: '', // 计划投入辅助工人员工日
planSkilledWorkerDay: '', // 计划投入熟练工人员工日
planSubCarNum: '', // 计划投入分包车辆数
actualWorkingDay: '', // 实际工作天数
workloadList: [
{
workloadCategoryId: '', // 工作量类别id
workloadCategoryName: '', // 工作量类别名称
unitPrice: '', // 单价
workloadNum: '', // 工作量
},
],
personnelArrangementList: [], // 人员排班模板 [{day: '', personnelNames: ''}]
})
const formData = ref(getInitFormData())
// 动态生成 rules为 workloadList 的每一项生成校验规则
const rules = computed(() => {
const baseRules = {
planMajorId: [{ required: true, message: '请选择专业', trigger: 'blur' }],
businessTypeId: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
riskLevel: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
planCategoryId: [{ required: true, message: '请选择类别', trigger: 'change' }],
towerBaseNumber: [
{ required: true, message: '请输入基塔数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
],
planPersonnelList: [{ required: true, message: '请选择计划投入管理人员', trigger: 'blur' }],
}
// 为 workloadList 的每一项生成对应的校验规则
// 当 workloadList 长度变化时,会自动重新计算,为每一项生成独立的校验规则
baseRules.workloadList = formData.value.workloadList.map(() => ({
workloadCategoryId: [{ required: true, message: '请选择工作量类别', trigger: 'change' }],
workloadNum: [
{ required: true, message: '请输入工作量', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
],
}))
// 添加其他字段的规则
baseRules.planCarNum = [
{ required: true, message: '请输入管理人员车辆数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planSkilledWorkerNum = [
{ required: true, message: '请输入熟练工人员数量', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planAuxiliaryWorkerNum = [
{ required: true, message: '请输入辅助工人员数量', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planSkilledWorkerDay = [
{ required: true, message: '请输入工日', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planAuxiliaryWorkerDay = [
{ required: true, message: '请输入工日', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.planSubCarNum = [
{ required: true, message: '请输入分包车辆数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
baseRules.actualWorkingDay = [
{ required: true, message: '请输入实际工作天数', trigger: 'blur' },
{ pattern: /^[1-9]\d*$/, message: '请输入正整数', trigger: 'blur' },
]
return baseRules
})
const onBack = () => {
// 关闭当前标签页并跳转到月计划填报页面
proxy.$tab.closeOpenPage({
path: '/plan/monthlyPlan',
})
}
const onSubmit = async () => {
try {
await formRef.value.validate(async (valid, fields) => {
if (!valid) {
// 表单校验未通过时,自动滚动到第一个报错字段
if (fields && Object.keys(fields).length) {
const firstProp = Object.keys(fields)[0]
formRef.value.scrollToField(firstProp)
}
return
}
// 组装参数:排除从路由参数获取的禁用字段
const {
inspectionStationName,
projectName,
workContent,
plannedStartTime,
plannedEndTime,
planPersonnelList,
...submitData
} = formData.value
// 将人员排班数据转换为 personnelArrangementList 格式
submitData.personnelArrangementList = Object.keys(dayAssignments.value).map((date) => {
const managers = dayAssignments.value[date] || []
return {
day: date,
personnelNames: managers.map((p) => p.name).join(','),
}
})
submitData.planPersonnel = planPersonnelList.map((item) => item.id).join(',')
console.log('submitData组装后的参数', submitData)
const result = await updateMonthlyPlanAPI(submitData)
if (result.code === 200) {
proxy.$modal.msgSuccess('编辑成功')
onBack()
}
console.log('result--', result)
})
} catch (error) {
return Promise.reject(error)
}
}
const onAddWorkLoad = () => {
formData.value.workloadList.push({
workloadCategoryId: '',
workloadCategoryName: '',
unitPrice: '',
workloadNum: '',
})
}
const onDeleteWorkLoad = (index) => {
formData.value.workloadList.splice(index, 1)
// 清除被删除项的校验状态
nextTick(() => {
// 由于数组索引变化,清除所有工作量项的校验状态
formData.value.workloadList.forEach((item, idx) => {
formRef.value?.clearValidate([
`workloadList.${idx}.workloadCategoryId`,
`workloadList.${idx}.workloadNum`,
])
})
})
}
// 人员选择弹窗相关
const personTableRef = ref(null)
const managerDialogConfig = reactive({
outerVisible: false,
outerTitle: '选择人员',
outerWidth: '70%',
minHeight: '60vh',
maxHeight: '80vh',
})
const managerDialog = reactive({
visible: false,
keyword: '',
selected: [],
})
// 将公共人员列表转换为表格展示需要的字段结构
const personList = computed(() => {
return (personnelCommonOptions.value || []).map((item) => ({
...item,
org: item.inspectionStationName, // 人员所属单位
gender: item.sex === '1' ? '男' : '女',
station: item.positionName, // 岗位
position: item.personnelNatureName, // 岗位性质
type: item.personnelClassificationName, // 人员分类
}))
})
const filteredPersons = computed(() => {
const list = personList.value
if (!managerDialog.keyword) return list
const keyword = managerDialog.keyword.toLowerCase()
return list.filter((item) => {
const name = (item.name || '').toLowerCase()
const org = (item.org || '').toLowerCase()
const station = (item.station || '').toLowerCase()
return name.includes(keyword) || org.includes(keyword) || station.includes(keyword)
})
})
const selectedManagerNames = computed(() =>
formData.value.planPersonnelList.map((item) => item.name).join('、'),
)
const onOpenPersonPicker = async () => {
managerDialog.visible = true
managerDialogConfig.outerVisible = true
await nextTick()
if (personTableRef.value) {
personTableRef.value.clearSelection()
personList.value.forEach((row) => {
const exists = formData.value.planPersonnelList.find((item) => item.id === row.id)
if (exists) {
personTableRef.value.toggleRowSelection(row, true)
}
})
}
managerDialog.selected = [...formData.value.planPersonnelList]
}
const onManagerSelectionChange = (rows) => {
managerDialog.selected = [...rows]
}
const onRemoveManager = (item) => {
managerDialog.selected = managerDialog.selected.filter((row) => row.id !== item.id)
if (personTableRef.value) {
const target = personList.value.find((row) => row.id === item.id)
if (target) personTableRef.value.toggleRowSelection(target, false)
}
}
const onClearManagers = () => {
managerDialog.selected = []
if (personTableRef.value) personTableRef.value.clearSelection()
}
const onConfirmManager = () => {
formData.value.planPersonnelList = [...managerDialog.selected]
managerDialog.visible = false
managerDialogConfig.outerVisible = false
}
// 关闭弹框统一处理
const onCloseDialogOuter = (visible) => {
managerDialogConfig.outerVisible = visible
}
// 人员排班:按月份生成日期表(自动补全整个月份)
const dayAssignments = ref({}) // { '2025-01-01': [person, ...] }
const maxShowInCell = 5
const calendarDays = computed(() => {
const start = formData.value.plannedStartTime
const end = formData.value.plannedEndTime
if (!start || !end) return []
// 假设后端不会传跨月区间,这里只按开始时间所在月份补全整月
const [yearStr, monthStr] = start.split('-')
const year = Number(yearStr)
const month = Number(monthStr)
if (!year || !month) return []
const daysInMonth = new Date(year, month, 0).getDate()
const pad = (n) => (n < 10 ? `0${n}` : `${n}`)
const list = []
for (let d = 1; d <= daysInMonth; d++) {
const date = `${year}-${pad(month)}-${pad(d)}`
const label = `${month}月${pad(d)}日`
const isInRange = date >= start && date <= end
list.push({
date,
label,
isInRange,
managers: dayAssignments.value[date] || [],
})
}
return list
})
// 将计划投入管理人员模板填充到某一天
const onFillManagersForDay = (date) => {
if (!formData.value.planPersonnelList || !formData.value.planPersonnelList.length) return
dayAssignments.value[date] = formData.value.planPersonnelList.map((p) => ({ ...p }))
}
// 清除某一天的人员
const onClearManagersForDay = (date) => {
if (dayAssignments.value[date]) {
dayAssignments.value[date] = []
}
}
// 从某一天删除单个人员,不影响表单里的模板
const onRemovePersonFromDay = (date, person) => {
const list = dayAssignments.value[date] || []
dayAssignments.value[date] = list.filter((item) => item.id !== person.id)
}
// 工作量类别变化
const onChangeWorkLoadCategory = (event, index) => {
const workloadCategoryId = event
const workloadCategory = planWorkLoadCategoryOptions.value.find(
(item) => item.workloadCategoryId === workloadCategoryId,
)
if (workloadCategory) {
formData.value.workloadList[index].unitPrice = workloadCategory.unitPrice
formData.value.workloadList[index].workloadCategoryName =
workloadCategory.workloadCategoryName
}
// 手动触发该字段的校验,确保校验规则正确执行
nextTick(() => {
formRef.value?.validateField(`workloadList.${index}.workloadCategoryId`)
})
}
// 获取详情
const getDetail = async () => {
const result = await getMonthlyPlanDetailAPI({ monthlyPlanId: route.query.monthlyPlanId })
if (result.code === 200 && result.data) {
const data = result.data
// 1. 基本字段直接赋值
formData.value.monthlyPlanId = data.monthlyPlanId
formData.value.planMajorId = data.planMajorId
formData.value.businessTypeId = data.businessTypeId
formData.value.planCategoryId = data.planCategoryId
formData.value.towerBaseNumber = data.towerBaseNumber || ''
formData.value.planCarNum = data.planCarNum || ''
formData.value.planSkilledWorkerNum = data.planSkilledWorkerNum || ''
formData.value.planAuxiliaryWorkerNum = data.planAuxiliaryWorkerNum || ''
formData.value.planAuxiliaryWorkerDay = data.planAuxiliaryWorkerDay || ''
formData.value.planSkilledWorkerDay = data.planSkilledWorkerDay || ''
formData.value.planSubCarNum = data.planSubCarNum || ''
formData.value.actualWorkingDay = data.actualWorkingDay || ''
formData.value.riskLevel = data.riskLevel
// 2. 从后端返回的数据更新路由参数字段(如果后端返回了这些字段)
if (data.inspectionStationName) {
formData.value.inspectionStationName = data.inspectionStationName
}
if (data.projectName) {
formData.value.projectName = data.projectName
}
if (data.workContent) {
formData.value.workContent = data.workContent
}
if (data.plannedStartTime) {
formData.value.plannedStartTime = data.plannedStartTime
}
if (data.plannedEndTime) {
formData.value.plannedEndTime = data.plannedEndTime
}
// 3. 处理计划投入管理人员:将 planPersonnel 字符串 "1,5,6" 转换为 planPersonnelList 数组
if (data.planPersonnel && data.personneltList && data.personneltList.length > 0) {
const personnelIds = data.planPersonnel.split(',').map((id) => String(id.trim()))
formData.value.planPersonnelList = data.personneltList.filter((person) =>
personnelIds.includes(String(person.id)),
)
} else {
formData.value.planPersonnelList = []
}
// 4. 处理工作量列表
if (data.workloadList && data.workloadList.length > 0) {
formData.value.workloadList = data.workloadList.map((item) => ({
workloadCategoryId: item.workloadCategoryId || '',
workloadCategoryName: item.workloadCategoryName || '',
unitPrice: item.unitPrice || '',
workloadNum: item.workloadNum || '',
}))
} else {
// 如果没有数据,保持默认的一个空项
formData.value.workloadList = [
{
workloadCategoryId: '',
workloadCategoryName: '',
unitPrice: '',
workloadNum: '',
},
]
}
// 5. 处理人员排班:将 personnelArrangementList 转换为 dayAssignments 对象
dayAssignments.value = {}
if (
data.personnelArrangementList &&
data.personnelArrangementList.length > 0 &&
data.personneltList
) {
data.personnelArrangementList.forEach((arrangement) => {
const day = arrangement.day
if (day && arrangement.personnelNames) {
// 将人员名称字符串转换为人员对象数组
const personnelNames = arrangement.personnelNames
.split(',')
.map((name) => name.trim())
const personnelObjects = data.personneltList.filter((person) =>
personnelNames.includes(person.name),
)
if (personnelObjects.length > 0) {
dayAssignments.value[day] = personnelObjects
}
}
})
}
}
}
onMounted(() => {
getDetail()
})
</script>
<style scoped lang="scss">
.monthly-plan-edit {
.page-header {
margin-bottom: 20px;
padding: 16px 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
:deep(.el-page-header__left) {
.el-page-header__back {
padding: 8px;
border-radius: 6px;
transition: all 0.3s ease;
background-color: transparent;
&:hover {
background-color: #f0f2f5;
color: #1677ff;
transform: translateX(-2px);
}
&::before {
font-size: 16px;
font-weight: 600;
}
}
}
:deep(.el-page-header__content) {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin-left: 12px;
}
}
// 隐藏索引不为 0 的工作量项的 required 标记*
:deep(.hide-required) {
&.is-required .el-form-item__label::before {
display: none;
}
}
.card-section {
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
:deep(.el-card__header) {
padding: 16px 20px;
background-color: #fafbfc;
border-bottom: 1px solid #f0f2f5;
}
:deep(.el-card__body) {
padding: 24px 20px;
}
}
.card-header {
font-weight: 600;
font-size: 15px;
color: #1f2937;
position: relative;
padding-left: 12px;
display: flex;
align-items: center;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background: linear-gradient(180deg, #1677ff 0%, #4096ff 100%);
border-radius: 2px;
}
}
.page-footer {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
}
.person-search-bar {
margin-bottom: 8px;
}
.selected-panel {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.selected-list {
flex: 1;
overflow: auto;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.selected-tag {
margin-left: 6px;
margin-bottom: 10px;
}
.empty-tip {
color: #9ca3af;
}
.clickable-suffix {
cursor: pointer;
}
.schedule-wrapper {
.schedule-tip {
display: flex;
flex-direction: column;
font-size: 12px;
color: #6b7280;
margin-bottom: 8px;
.schedule-tip-extra {
margin-top: 2px;
}
}
.schedule-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr)); // 一行 7 个格子
gap: 8px;
}
.schedule-cell {
border: 1px solid #e5e7eb;
border-radius: 4px;
padding: 6px 8px;
min-height: 150px;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #ffffff;
&.is-disabled {
background-color: #f9fafb;
color: #9ca3af;
}
}
.cell-date {
font-size: 12px;
font-weight: 500;
margin-bottom: 4px;
}
.cell-persons {
flex: 1;
// min-height: 52px;
// max-height: 52px;
overflow: hidden;
font-size: 12px;
}
.person-tags {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 4px;
}
.more-text {
font-size: 12px;
color: #2563eb;
margin-left: 2px;
}
.empty-tip-small {
font-size: 12px;
color: #9ca3af;
&.is-disabled-text {
color: #d1d5db;
}
}
.cell-actions {
margin-top: 4px;
display: flex;
justify-content: space-between;
.el-button + .el-button {
margin-left: 4px;
}
}
.popover-list {
.popover-item {
display: flex;
font-size: 12px;
// padding: 2px 6px;
background-color: #f3f4f6;
border-radius: 2px;
gap: 4px;
}
}
}
</style>