1318 lines
50 KiB
Vue
1318 lines
50 KiB
Vue
<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>
|