2026-04-17 18:17:42 +08:00
|
|
|
<template>
|
|
|
|
|
<Dialog v-model="dialogVisible" :title="dialogTitle" width="1080">
|
|
|
|
|
<el-form
|
|
|
|
|
ref="formRef"
|
|
|
|
|
v-loading="formLoading"
|
|
|
|
|
:model="formData"
|
|
|
|
|
:rules="formRules"
|
2026-05-08 17:38:50 +08:00
|
|
|
label-width="130px"
|
2026-04-17 18:17:42 +08:00
|
|
|
>
|
2026-05-08 17:38:50 +08:00
|
|
|
<div class="form-section">
|
|
|
|
|
<div class="form-section-title">项目基础信息</div>
|
|
|
|
|
<el-row :gutter="16">
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<el-form-item label="项目名称" prop="projectName">
|
|
|
|
|
<el-input v-model="formData.projectName" maxlength="200" placeholder="请输入项目名称" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
2026-05-15 17:57:24 +08:00
|
|
|
<el-form-item label="建筑面积(㎡)" prop="totalConstructionArea">
|
2026-05-08 17:38:50 +08:00
|
|
|
<el-input-number
|
|
|
|
|
v-model="formData.totalConstructionArea"
|
|
|
|
|
:min="0"
|
|
|
|
|
:precision="2"
|
|
|
|
|
:step="100"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
controls-position="right"
|
2026-04-25 18:10:45 +08:00
|
|
|
/>
|
2026-05-08 17:38:50 +08:00
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="建设单位" prop="constructionUnitName">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="formData.constructionUnitName"
|
|
|
|
|
maxlength="200"
|
|
|
|
|
placeholder="请输入建设单位"
|
2026-04-17 18:17:42 +08:00
|
|
|
/>
|
2026-05-08 17:38:50 +08:00
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="工程类型" prop="projectType">
|
|
|
|
|
<el-select v-model="formData.projectType" class="!w-1/1" clearable placeholder="请选择">
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="item in PROJECT_TYPE_OPTIONS"
|
|
|
|
|
:key="item.value"
|
|
|
|
|
:label="item.label"
|
|
|
|
|
:value="item.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
2026-05-15 17:57:24 +08:00
|
|
|
<el-form-item label="设计类型" prop="projectCategory">
|
2026-05-08 17:38:50 +08:00
|
|
|
<el-select
|
|
|
|
|
v-model="formData.projectCategory"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
clearable
|
|
|
|
|
placeholder="请选择"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="item in PROJECT_CATEGORY_OPTIONS"
|
|
|
|
|
:key="item.value"
|
|
|
|
|
:label="item.label"
|
|
|
|
|
:value="item.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
<div class="form-section-title">合同与财务信息</div>
|
|
|
|
|
<el-row :gutter="16">
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<el-form-item label="合同状态" prop="contractSignedFlag" required>
|
|
|
|
|
<el-radio-group v-model="formData.contractSignedFlag">
|
|
|
|
|
<el-radio :value="true">已签订</el-radio>
|
|
|
|
|
<el-radio :value="false">未签订</el-radio>
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
2026-05-15 17:57:24 +08:00
|
|
|
<el-form-item label="合同总产值(元)" prop="contractAmount">
|
2026-05-08 17:38:50 +08:00
|
|
|
<el-input-number
|
|
|
|
|
v-model="formData.contractAmount"
|
|
|
|
|
:min="0"
|
|
|
|
|
:precision="2"
|
|
|
|
|
:step="1000"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
controls-position="right"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="签订日期" prop="contractSigningDate">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="formData.contractSigningDate"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
placeholder="请选择日期"
|
|
|
|
|
type="date"
|
|
|
|
|
value-format="YYYY-MM-DD"
|
2026-04-25 18:10:45 +08:00
|
|
|
/>
|
2026-05-08 17:38:50 +08:00
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
<div class="form-section-title">状态与进度</div>
|
|
|
|
|
<el-row :gutter="16">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="项目状态" prop="projectStatus">
|
|
|
|
|
<el-select
|
|
|
|
|
v-model="formData.projectStatus"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
clearable
|
|
|
|
|
placeholder="请选择项目状态"
|
2026-04-25 18:10:45 +08:00
|
|
|
>
|
2026-05-08 17:38:50 +08:00
|
|
|
<el-option
|
|
|
|
|
v-for="item in PROJECT_STATUS_OPTIONS"
|
|
|
|
|
:key="item.value"
|
|
|
|
|
:label="item.label"
|
|
|
|
|
:value="item.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="开始年度" prop="projectStartYear">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="projectStartYearValue"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
placeholder="请选择年度"
|
|
|
|
|
type="year"
|
|
|
|
|
value-format="YYYY"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item :label="statusReasonLabel" :prop="statusReasonProp">
|
|
|
|
|
<el-input
|
|
|
|
|
v-if="formData.projectStatus === '暂停'"
|
|
|
|
|
v-model="formData.pauseReason"
|
|
|
|
|
maxlength="255"
|
|
|
|
|
placeholder="请输入暂停原因"
|
|
|
|
|
/>
|
|
|
|
|
<el-input
|
|
|
|
|
v-else-if="formData.projectStatus === '中止'"
|
|
|
|
|
v-model="formData.terminateReason"
|
|
|
|
|
maxlength="255"
|
|
|
|
|
placeholder="请输入中止原因"
|
|
|
|
|
/>
|
|
|
|
|
<el-input v-else disabled placeholder="进行中或完成状态无需填写原因" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="排序" prop="sortNo">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="formData.sortNo"
|
|
|
|
|
:min="0"
|
|
|
|
|
:precision="0"
|
|
|
|
|
:step="1"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
controls-position="right"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="form-section">
|
|
|
|
|
<div class="form-section-title">人员与建设单位联系信息</div>
|
|
|
|
|
<el-row :gutter="16">
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="建设单位联系人" prop="contactName">
|
|
|
|
|
<el-input v-model="formData.contactName" maxlength="64" placeholder="请输入建设单位联系人" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="建设单位联系电话" prop="contactPhone">
|
|
|
|
|
<el-input v-model="formData.contactPhone" maxlength="32" placeholder="请输入建设单位联系电话" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="项目经理">
|
|
|
|
|
<div class="person-group">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(item, index) in projectManagerPersons"
|
|
|
|
|
:key="`manager-${index}`"
|
|
|
|
|
class="person-row"
|
2026-04-25 18:10:45 +08:00
|
|
|
>
|
2026-05-08 17:38:50 +08:00
|
|
|
<el-select
|
|
|
|
|
:model-value="item.employeeId"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
clearable
|
|
|
|
|
filterable
|
|
|
|
|
remote
|
|
|
|
|
reserve-keyword
|
|
|
|
|
placeholder="请输入项目经理姓名搜索"
|
|
|
|
|
:remote-method="searchEmployees"
|
|
|
|
|
:loading="employeeLoading"
|
|
|
|
|
@change="(value) => handleEmployeeChange(item, value)"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="employee in employeeOptions"
|
|
|
|
|
:key="employee.id"
|
|
|
|
|
:label="employee.officeName ? `${employee.employeeName} / ${employee.officeName}` : employee.employeeName"
|
|
|
|
|
:value="employee.id"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button text type="danger" @click="removeRolePerson(ROLE_PROJECT_MANAGER, index)">
|
|
|
|
|
删除
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button text type="primary" @click="addRolePerson(ROLE_PROJECT_MANAGER)">
|
|
|
|
|
<Icon class="mr-5px" icon="ep:plus" />
|
|
|
|
|
添加项目经理
|
2026-04-25 18:10:45 +08:00
|
|
|
</el-button>
|
|
|
|
|
</div>
|
2026-05-08 17:38:50 +08:00
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
<el-form-item label="工程负责人">
|
|
|
|
|
<div class="person-group">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(item, index) in engineeringPrincipalPersons"
|
|
|
|
|
:key="`principal-${index}`"
|
|
|
|
|
class="person-row"
|
2026-04-25 18:10:45 +08:00
|
|
|
>
|
2026-05-08 17:38:50 +08:00
|
|
|
<el-select
|
|
|
|
|
:model-value="item.employeeId"
|
|
|
|
|
class="!w-1/1"
|
|
|
|
|
clearable
|
|
|
|
|
filterable
|
|
|
|
|
remote
|
|
|
|
|
reserve-keyword
|
|
|
|
|
placeholder="请输入工程负责人姓名搜索"
|
|
|
|
|
:remote-method="searchEmployees"
|
|
|
|
|
:loading="employeeLoading"
|
|
|
|
|
@change="(value) => handleEmployeeChange(item, value)"
|
|
|
|
|
>
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="employee in employeeOptions"
|
|
|
|
|
:key="employee.id"
|
|
|
|
|
:label="employee.officeName ? `${employee.employeeName} / ${employee.officeName}` : employee.employeeName"
|
|
|
|
|
:value="employee.id"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-button
|
|
|
|
|
text
|
|
|
|
|
type="danger"
|
|
|
|
|
@click="removeRolePerson(ROLE_ENGINEERING_PRINCIPAL, index)"
|
|
|
|
|
>
|
|
|
|
|
删除
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button text type="primary" @click="addRolePerson(ROLE_ENGINEERING_PRINCIPAL)">
|
|
|
|
|
<Icon class="mr-5px" icon="ep:plus" />
|
|
|
|
|
添加工程负责人
|
2026-04-25 18:10:45 +08:00
|
|
|
</el-button>
|
|
|
|
|
</div>
|
2026-05-08 17:38:50 +08:00
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
2026-04-17 18:17:42 +08:00
|
|
|
</el-form>
|
2026-04-25 18:10:45 +08:00
|
|
|
|
2026-04-17 18:17:42 +08:00
|
|
|
<template #footer>
|
|
|
|
|
<el-button :disabled="formLoading" type="primary" @click="submitForm">保存</el-button>
|
|
|
|
|
<el-button @click="dialogVisible = false">取消</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
import type { FormRules } from 'element-plus'
|
2026-04-25 18:10:45 +08:00
|
|
|
import * as EmployeeApi from '@/api/tjt/employee'
|
2026-04-17 18:17:42 +08:00
|
|
|
import * as ProjectApi from '@/api/tjt/project'
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'TjtProjectForm' })
|
|
|
|
|
|
2026-04-25 18:10:45 +08:00
|
|
|
const ROLE_PROJECT_MANAGER = 'project_manager'
|
|
|
|
|
const ROLE_ENGINEERING_PRINCIPAL = 'engineering_principal'
|
|
|
|
|
|
|
|
|
|
const PROJECT_TYPE_OPTIONS = [
|
|
|
|
|
{ label: '建筑工程', value: '建筑工程' },
|
|
|
|
|
{ label: '精装工程', value: '精装工程' },
|
|
|
|
|
{ label: '综合工程', value: '综合工程' },
|
|
|
|
|
{ label: '专项设计', value: '专项设计' },
|
|
|
|
|
{ label: 'BIM设计', value: 'BIM设计' },
|
|
|
|
|
{ label: '其他', value: '其他' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const PROJECT_CATEGORY_OPTIONS = [
|
|
|
|
|
{ label: '住宅', value: '住宅' },
|
|
|
|
|
{ label: '公建', value: '公建' },
|
|
|
|
|
{ label: '工业', value: '工业' },
|
|
|
|
|
{ label: '园林景观', value: '园林景观' },
|
|
|
|
|
{ label: '其他', value: '其他' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const PROJECT_STATUS_OPTIONS = [
|
|
|
|
|
{ label: '进行中', value: '进行中' },
|
|
|
|
|
{ label: '完成', value: '完成' },
|
|
|
|
|
{ label: '暂停', value: '暂停' },
|
|
|
|
|
{ label: '中止', value: '中止' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const DEFAULT_INNOVATION_OUTPUT_RATE = 0.01
|
|
|
|
|
|
2026-04-17 18:17:42 +08:00
|
|
|
const { t } = useI18n()
|
|
|
|
|
const message = useMessage()
|
|
|
|
|
|
|
|
|
|
const dialogVisible = ref(false)
|
|
|
|
|
const dialogTitle = ref('')
|
|
|
|
|
const formLoading = ref(false)
|
2026-04-25 18:10:45 +08:00
|
|
|
const formType = ref<'create' | 'update'>('create')
|
2026-04-17 18:17:42 +08:00
|
|
|
const formRef = ref()
|
2026-04-25 18:10:45 +08:00
|
|
|
const employeeOptions = ref<EmployeeApi.EmployeeSimpleVO[]>([])
|
|
|
|
|
const employeeLoading = ref(false)
|
|
|
|
|
|
|
|
|
|
const createRolePerson = (
|
|
|
|
|
roleCode: ProjectApi.ProjectRolePersonVO['roleCode']
|
|
|
|
|
): ProjectApi.ProjectRolePersonVO => ({
|
|
|
|
|
roleCode,
|
|
|
|
|
employeeId: undefined,
|
|
|
|
|
employeeName: ''
|
|
|
|
|
})
|
2026-04-17 18:17:42 +08:00
|
|
|
|
|
|
|
|
const createFormData = (): ProjectApi.ProjectVO => ({
|
|
|
|
|
projectName: '',
|
2026-05-08 17:38:50 +08:00
|
|
|
sortNo: 0,
|
2026-04-17 18:17:42 +08:00
|
|
|
contractSignedFlag: true,
|
|
|
|
|
contractAmount: undefined,
|
|
|
|
|
totalConstructionArea: undefined,
|
|
|
|
|
constructionUnitName: '',
|
|
|
|
|
contactName: '',
|
|
|
|
|
contactPhone: '',
|
|
|
|
|
contractSigningDate: undefined,
|
|
|
|
|
projectType: '',
|
2026-04-25 18:10:45 +08:00
|
|
|
projectCategory: '',
|
|
|
|
|
projectStartYear: new Date().getFullYear(),
|
|
|
|
|
projectStatus: '进行中',
|
|
|
|
|
pauseReason: '',
|
|
|
|
|
terminateReason: '',
|
|
|
|
|
innovationOutputRate: DEFAULT_INNOVATION_OUTPUT_RATE,
|
|
|
|
|
otherCost: 0,
|
|
|
|
|
rolePersons: []
|
2026-04-17 18:17:42 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const formData = ref<ProjectApi.ProjectVO>(createFormData())
|
|
|
|
|
|
2026-04-25 18:10:45 +08:00
|
|
|
const normalizeValue = (value: string | undefined, options: Array<{ value: string }>, fallback = '') =>
|
|
|
|
|
options.find((item) => item.value === value)?.value || fallback
|
|
|
|
|
|
|
|
|
|
const normalizeFormData = (data: ProjectApi.ProjectVO): ProjectApi.ProjectVO => ({
|
|
|
|
|
...createFormData(),
|
|
|
|
|
...data,
|
|
|
|
|
projectType: normalizeValue(data.projectType, PROJECT_TYPE_OPTIONS),
|
|
|
|
|
projectCategory: normalizeValue(data.projectCategory, PROJECT_CATEGORY_OPTIONS),
|
|
|
|
|
projectStatus: normalizeValue(data.projectStatus, PROJECT_STATUS_OPTIONS, '进行中'),
|
|
|
|
|
innovationOutputRate: data.innovationOutputRate ?? DEFAULT_INNOVATION_OUTPUT_RATE,
|
|
|
|
|
otherCost: data.otherCost ?? 0,
|
|
|
|
|
pauseReason: data.pauseReason || '',
|
|
|
|
|
terminateReason: data.terminateReason || '',
|
|
|
|
|
rolePersons: (data.rolePersons || []).map((item) => ({
|
|
|
|
|
roleCode: item.roleCode,
|
|
|
|
|
employeeId: item.employeeId,
|
|
|
|
|
employeeName: item.employeeName,
|
|
|
|
|
sortNo: item.sortNo
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const getRolePersons = (roleCode: ProjectApi.ProjectRolePersonVO['roleCode']) => {
|
|
|
|
|
if (!formData.value.rolePersons) {
|
|
|
|
|
formData.value.rolePersons = []
|
|
|
|
|
}
|
|
|
|
|
return formData.value.rolePersons.filter((item) => item.roleCode === roleCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const projectManagerPersons = computed(() => getRolePersons(ROLE_PROJECT_MANAGER))
|
|
|
|
|
const engineeringPrincipalPersons = computed(() => getRolePersons(ROLE_ENGINEERING_PRINCIPAL))
|
|
|
|
|
|
|
|
|
|
const addRolePerson = (roleCode: ProjectApi.ProjectRolePersonVO['roleCode']) => {
|
|
|
|
|
formData.value.rolePersons = [...(formData.value.rolePersons || []), createRolePerson(roleCode)]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const removeRolePerson = (roleCode: ProjectApi.ProjectRolePersonVO['roleCode'], index: number) => {
|
|
|
|
|
const roleIndexes = (formData.value.rolePersons || [])
|
|
|
|
|
.map((item, itemIndex) => ({ item, itemIndex }))
|
|
|
|
|
.filter(({ item }) => item.roleCode === roleCode)
|
|
|
|
|
const target = roleIndexes[index]
|
|
|
|
|
if (!target) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
formData.value.rolePersons?.splice(target.itemIndex, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ensureEmployeeOptions = (persons?: ProjectApi.ProjectRolePersonVO[]) => {
|
|
|
|
|
const existingIds = new Set(employeeOptions.value.map((item) => item.id))
|
|
|
|
|
;(persons || []).forEach((item) => {
|
|
|
|
|
if (!item.employeeId || existingIds.has(item.employeeId)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
employeeOptions.value.push({
|
|
|
|
|
id: item.employeeId,
|
|
|
|
|
employeeName: item.employeeName || '',
|
|
|
|
|
officeId: undefined,
|
|
|
|
|
officeName: undefined
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const searchEmployees = async (keyword: string) => {
|
|
|
|
|
employeeLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
employeeOptions.value = await EmployeeApi.getEmployeeSimpleList({
|
|
|
|
|
keyword,
|
|
|
|
|
enabledFlag: true
|
|
|
|
|
})
|
|
|
|
|
ensureEmployeeOptions(formData.value.rolePersons)
|
|
|
|
|
} finally {
|
|
|
|
|
employeeLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleEmployeeChange = (item: ProjectApi.ProjectRolePersonVO, employeeId?: number) => {
|
|
|
|
|
const employee = employeeOptions.value.find((option) => option.id === employeeId)
|
|
|
|
|
item.employeeId = employeeId
|
|
|
|
|
item.employeeName = employee?.employeeName || ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 18:17:42 +08:00
|
|
|
const projectStartYearValue = computed({
|
|
|
|
|
get: () => (formData.value.projectStartYear ? String(formData.value.projectStartYear) : undefined),
|
|
|
|
|
set: (value?: string) => {
|
|
|
|
|
formData.value.projectStartYear = value ? Number(value) : undefined
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-25 18:10:45 +08:00
|
|
|
const statusReasonLabel = computed(() =>
|
|
|
|
|
formData.value.projectStatus === '中止' ? '中止原因' : '暂停原因'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const statusReasonProp = computed(() =>
|
|
|
|
|
formData.value.projectStatus === '中止' ? 'terminateReason' : 'pauseReason'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => formData.value.projectStatus,
|
|
|
|
|
(status) => {
|
|
|
|
|
if (status !== '暂停') {
|
|
|
|
|
formData.value.pauseReason = ''
|
|
|
|
|
}
|
|
|
|
|
if (status !== '中止') {
|
|
|
|
|
formData.value.terminateReason = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-17 18:17:42 +08:00
|
|
|
const formRules = reactive<FormRules>({
|
2026-05-08 17:38:50 +08:00
|
|
|
projectName: [{ required: true, message: '项目名称不能为空', trigger: 'blur' }],
|
2026-05-15 17:57:24 +08:00
|
|
|
contractAmount: [{ required: true, message: '合同总产值不能为空', trigger: 'blur' }],
|
|
|
|
|
totalConstructionArea: [{ required: true, message: '建筑面积不能为空', trigger: 'blur' }],
|
2026-04-25 18:10:45 +08:00
|
|
|
projectStartYear: [{ required: true, message: '项目开始年度不能为空', trigger: 'change' }],
|
|
|
|
|
projectStatus: [{ required: true, message: '项目状态不能为空', trigger: 'change' }],
|
|
|
|
|
pauseReason: [
|
|
|
|
|
{
|
|
|
|
|
validator: (_rule, value, callback) => {
|
|
|
|
|
if (formData.value.projectStatus === '暂停' && !String(value || '').trim()) {
|
|
|
|
|
callback(new Error('暂停状态必须填写暂停原因'))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
callback()
|
|
|
|
|
},
|
|
|
|
|
trigger: 'blur'
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
terminateReason: [
|
|
|
|
|
{
|
|
|
|
|
validator: (_rule, value, callback) => {
|
|
|
|
|
if (formData.value.projectStatus === '中止' && !String(value || '').trim()) {
|
|
|
|
|
callback(new Error('中止状态必须填写中止原因'))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
callback()
|
|
|
|
|
},
|
|
|
|
|
trigger: 'blur'
|
|
|
|
|
}
|
|
|
|
|
]
|
2026-04-17 18:17:42 +08:00
|
|
|
})
|
|
|
|
|
|
2026-04-25 18:10:45 +08:00
|
|
|
const open = async (type: 'create' | 'update', id?: number) => {
|
2026-04-17 18:17:42 +08:00
|
|
|
dialogVisible.value = true
|
2026-04-25 18:10:45 +08:00
|
|
|
dialogTitle.value = type === 'create' ? '新增项目' : '编辑项目'
|
2026-04-17 18:17:42 +08:00
|
|
|
formType.value = type
|
|
|
|
|
resetForm()
|
|
|
|
|
if (!id) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
formLoading.value = true
|
|
|
|
|
try {
|
2026-04-25 18:10:45 +08:00
|
|
|
formData.value = normalizeFormData(await ProjectApi.getProject(id))
|
|
|
|
|
ensureEmployeeOptions(formData.value.rolePersons)
|
2026-04-17 18:17:42 +08:00
|
|
|
} finally {
|
|
|
|
|
formLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
defineExpose({ open })
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['success'])
|
|
|
|
|
|
|
|
|
|
const buildSavePayload = (): ProjectApi.ProjectSaveVO => ({
|
|
|
|
|
id: formData.value.id,
|
|
|
|
|
projectName: formData.value.projectName,
|
2026-05-08 17:38:50 +08:00
|
|
|
sortNo: formData.value.sortNo ?? 0,
|
2026-04-17 18:17:42 +08:00
|
|
|
contractSignedFlag: formData.value.contractSignedFlag,
|
|
|
|
|
contractAmount: formData.value.contractAmount,
|
|
|
|
|
totalConstructionArea: formData.value.totalConstructionArea,
|
|
|
|
|
constructionUnitName: formData.value.constructionUnitName,
|
|
|
|
|
contactName: formData.value.contactName,
|
|
|
|
|
contactPhone: formData.value.contactPhone,
|
|
|
|
|
contractSigningDate: formData.value.contractSigningDate,
|
|
|
|
|
projectType: formData.value.projectType,
|
2026-04-25 18:10:45 +08:00
|
|
|
projectCategory: formData.value.projectCategory,
|
2026-04-17 18:17:42 +08:00
|
|
|
projectStartYear: formData.value.projectStartYear,
|
2026-04-25 18:10:45 +08:00
|
|
|
projectStatus: formData.value.projectStatus,
|
|
|
|
|
pauseReason: formData.value.pauseReason?.trim() || undefined,
|
|
|
|
|
terminateReason: formData.value.terminateReason?.trim() || undefined,
|
2026-04-17 18:17:42 +08:00
|
|
|
finalSettlementAmount: formData.value.finalSettlementAmount,
|
2026-04-25 18:10:45 +08:00
|
|
|
innovationOutputRate: formData.value.innovationOutputRate,
|
|
|
|
|
otherCost: formData.value.otherCost,
|
|
|
|
|
rolePersons: (formData.value.rolePersons || [])
|
|
|
|
|
.map((item, index) => ({
|
|
|
|
|
roleCode: item.roleCode,
|
|
|
|
|
employeeId: item.employeeId,
|
|
|
|
|
employeeName: item.employeeName?.trim() || undefined,
|
|
|
|
|
sortNo: index + 1
|
|
|
|
|
}))
|
|
|
|
|
.filter((item) => !!item.employeeId)
|
2026-04-17 18:17:42 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const submitForm = async () => {
|
|
|
|
|
if (!formRef.value) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const valid = await formRef.value.validate()
|
|
|
|
|
if (!valid) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
formLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const payload = buildSavePayload()
|
|
|
|
|
if (formType.value === 'create') {
|
|
|
|
|
await ProjectApi.createProject(payload)
|
|
|
|
|
message.success(t('common.createSuccess'))
|
|
|
|
|
} else {
|
|
|
|
|
await ProjectApi.updateProject(payload)
|
|
|
|
|
message.success(t('common.updateSuccess'))
|
|
|
|
|
}
|
|
|
|
|
dialogVisible.value = false
|
|
|
|
|
emit('success')
|
|
|
|
|
} finally {
|
|
|
|
|
formLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
formData.value = createFormData()
|
2026-04-25 18:10:45 +08:00
|
|
|
employeeOptions.value = []
|
2026-04-17 18:17:42 +08:00
|
|
|
formRef.value?.resetFields()
|
|
|
|
|
}
|
|
|
|
|
</script>
|
2026-04-25 18:10:45 +08:00
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2026-05-08 17:38:50 +08:00
|
|
|
.form-section {
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-section:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-section-title {
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
line-height: 22px;
|
|
|
|
|
color: var(--el-text-color-primary);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-25 18:10:45 +08:00
|
|
|
.person-group {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.person-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) 56px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|