添加基础表维护

This commit is contained in:
lzm
2026-04-25 18:10:45 +08:00
parent 8f9205a6a1
commit 38c634f8de
29 changed files with 4710 additions and 686 deletions

View File

@@ -23,6 +23,7 @@
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="合同金额(元)" prop="contractAmount">
@@ -49,8 +50,9 @@
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-col :span="8">
<el-form-item label="建设单位" prop="constructionUnitName">
<el-input
v-model="formData.constructionUnitName"
@@ -59,14 +61,9 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="8">
<el-form-item label="工程类型" prop="projectType">
<el-select
v-model="formData.projectType"
class="!w-1/1"
clearable
placeholder="请选择工程类型"
>
<el-select v-model="formData.projectType" class="!w-1/1" clearable placeholder="请选择">
<el-option
v-for="item in PROJECT_TYPE_OPTIONS"
:key="item.value"
@@ -76,7 +73,25 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="工程类别" prop="projectCategory">
<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>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="联系人" prop="contactName">
@@ -84,18 +99,19 @@
</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 label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" maxlength="32" placeholder="请输入联系电话" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="合同签订日期" prop="contractSigningDate">
<el-date-picker
v-model="formData.contractSigningDate"
class="!w-1/1"
placeholder="请选择合同签订日期"
placeholder="请选择日期"
type="date"
value-format="YYYY-MM-DD"
/>
@@ -106,34 +122,135 @@
<el-date-picker
v-model="projectStartYearValue"
class="!w-1/1"
placeholder="请选择项目开始年度"
placeholder="请选择年度"
type="year"
value-format="YYYY"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="项目经理" prop="projectManagerName">
<el-input
v-model="formData.projectManagerName"
maxlength="64"
placeholder="请输入项目经理"
/>
<el-form-item label="项目状态" prop="projectStatus">
<el-select
v-model="formData.projectStatus"
class="!w-1/1"
clearable
placeholder="请选择项目状态"
>
<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="engineeringPrincipalName">
<el-form-item :label="statusReasonLabel" :prop="statusReasonProp">
<el-input
v-model="formData.engineeringPrincipalName"
maxlength="64"
placeholder="请输入工程负责人"
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-row>
<el-row :gutter="16">
<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"
>
<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" />
添加项目经理
</el-button>
</div>
</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"
>
<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" />
添加项目负责人
</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">保存</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -143,19 +260,58 @@
<script lang="ts" setup>
import type { FormRules } from 'element-plus'
import * as EmployeeApi from '@/api/tjt/employee'
import * as ProjectApi from '@/api/tjt/project'
import { PROJECT_TYPE_OPTIONS } from '@/views/tjt/shared/planning'
defineOptions({ name: 'TjtProjectForm' })
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
const { t } = useI18n()
const message = useMessage()
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('')
const formType = ref<'create' | 'update'>('create')
const formRef = ref()
const employeeOptions = ref<EmployeeApi.EmployeeSimpleVO[]>([])
const employeeLoading = ref(false)
const createRolePerson = (
roleCode: ProjectApi.ProjectRolePersonVO['roleCode']
): ProjectApi.ProjectRolePersonVO => ({
roleCode,
employeeId: undefined,
employeeName: ''
})
const createFormData = (): ProjectApi.ProjectVO => ({
projectName: '',
@@ -166,14 +322,100 @@ const createFormData = (): ProjectApi.ProjectVO => ({
contactName: '',
contactPhone: '',
contractSigningDate: undefined,
projectManagerName: '',
engineeringPrincipalName: '',
projectType: '',
projectStartYear: new Date().getFullYear()
projectCategory: '',
projectStartYear: new Date().getFullYear(),
projectStatus: '进行中',
pauseReason: '',
terminateReason: '',
innovationOutputRate: DEFAULT_INNOVATION_OUTPUT_RATE,
otherCost: 0,
rolePersons: []
})
const formData = ref<ProjectApi.ProjectVO>(createFormData())
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,
status: '在职',
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 || ''
}
const projectStartYearValue = computed({
get: () => (formData.value.projectStartYear ? String(formData.value.projectStartYear) : undefined),
set: (value?: string) => {
@@ -181,16 +423,61 @@ const projectStartYearValue = computed({
}
})
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 = ''
}
}
)
const formRules = reactive<FormRules>({
projectName: [{ required: true, message: '工程名称不能为空', trigger: 'blur' }],
contractAmount: [{ required: true, message: '合同金额不能为空', trigger: 'blur' }],
totalConstructionArea: [{ required: true, message: '工程总面积不能为空', trigger: 'blur' }],
projectStartYear: [{ required: true, message: '项目开始年度不能为空', trigger: 'change' }]
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'
}
]
})
const open = async (type: string, id?: number) => {
const open = async (type: 'create' | 'update', id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
dialogTitle.value = type === 'create' ? '新增项目' : '编辑项目'
formType.value = type
resetForm()
if (!id) {
@@ -198,7 +485,8 @@ const open = async (type: string, id?: number) => {
}
formLoading.value = true
try {
formData.value = await ProjectApi.getProject(id)
formData.value = normalizeFormData(await ProjectApi.getProject(id))
ensureEmployeeOptions(formData.value.rolePersons)
} finally {
formLoading.value = false
}
@@ -217,12 +505,23 @@ const buildSavePayload = (): ProjectApi.ProjectSaveVO => ({
contactName: formData.value.contactName,
contactPhone: formData.value.contactPhone,
contractSigningDate: formData.value.contractSigningDate,
projectManagerName: formData.value.projectManagerName,
engineeringPrincipalName: formData.value.engineeringPrincipalName,
projectType: formData.value.projectType,
projectCategory: formData.value.projectCategory,
projectStartYear: formData.value.projectStartYear,
projectStatus: formData.value.projectStatus,
pauseReason: formData.value.pauseReason?.trim() || undefined,
terminateReason: formData.value.terminateReason?.trim() || undefined,
finalSettlementAmount: formData.value.finalSettlementAmount,
expectedKValue: formData.value.expectedKValue
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)
})
const submitForm = async () => {
@@ -252,6 +551,22 @@ const submitForm = async () => {
const resetForm = () => {
formData.value = createFormData()
employeeOptions.value = []
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.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>