履约验收

This commit is contained in:
吴红兵
2026-02-06 15:42:51 +08:00
parent 0704b79b32
commit 591bad2318
3 changed files with 238 additions and 111 deletions

View File

@@ -1,16 +1,16 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px" class="accept-batch-form">
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px" >
<el-row :gutter="24">
<el-col :span="12">
<el-col :span="12" class="mb20">
<el-form-item label="验收方式" prop="acceptType">
<el-radio-group v-model="form.acceptType" :disabled="readonly">
<el-radio label="1">填写履约验收评价表</el-radio>
<el-radio label="2" :disabled="!canFill">上传履约验收评价表</el-radio>
<el-radio label="1" :disabled="!canFill">填写履约验收评价表</el-radio>
<el-radio label="2">上传履约验收评价表</el-radio>
</el-radio-group>
<div v-if="!canFill" class="el-form-item__tip">金额30万仅支持上传模版</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="12" class="mb20">
<el-form-item label="验收日期" prop="acceptDate">
<el-date-picker
v-model="form.acceptDate"
@@ -26,12 +26,13 @@
<!-- 填报方式验收内容表格 -->
<template v-if="form.acceptType === '1' && canFill">
<el-col :span="24">
<el-col :span="12" class="mb20">
<el-form-item label="验收内容" prop="acceptContents">
<el-table :data="form.acceptContents" border size="small" max-height="260" class="accept-content-table">
<el-table :data="form.acceptContents" border size="small" class="accept-content-table">
<el-table-column prop="itemName" label="验收项" >
<template #default="{ row }">
{{row.itemName}}
<div>
<span>{{row.itemName}}</span>
<el-input
v-if="row.type === 'input'"
v-model="row.remark"
@@ -39,10 +40,12 @@
size="small"
:disabled="readonly"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="isQualified" label="合格/不合格" width="140" align="center">
<el-table-column prop="isQualified" label="合格/不合格" align="center">
<template #default="{ row }">
<el-radio-group v-model="row.isQualified" size="small" :disabled="readonly">
<el-radio label="1">合格</el-radio>
@@ -58,7 +61,7 @@
<!-- 上传方式 -->
<template v-if="form.acceptType === '2'">
<el-col :span="24">
<el-col :span="12">
<el-form-item label="履约验收模版" prop="templateFileIds">
<UploadFile
v-model="templateFileIdsStr"
@@ -72,7 +75,7 @@
</template>
<!-- 验收小组 -->
<el-col :span="24">
<el-col :span="12">
<el-form-item label="验收小组" prop="acceptTeam">
<div class="team-list">
<div v-for="(m, idx) in form.acceptTeam" :key="idx" class="team-row">
@@ -82,7 +85,30 @@
</div>
<el-button v-if="!readonly" type="primary" link size="small" @click="addTeam">+ 增加成员</el-button>
</div>
<div class="el-form-item__tip">至少3人且为单数</div>
<div class="el-form-item__tip">
至少3人且为单数
<template v-if="(previousBatchesTeams || []).length > 0">
<span class="copy-from-inline">
从往期带入
<el-select
v-model="copyFromBatch"
placeholder="同第N期"
clearable
size="small"
style="width: 100px"
@change="onCopyFromBatch"
>
<el-option
v-for="item in (previousBatchesTeams || [])"
:key="item.batch"
:label="`同第${item.batch}期`"
:value="item.batch"
/>
</el-select>
</span>
</template>
</div>
</el-form-item>
</el-col>
@@ -106,14 +132,17 @@ const props = withDefaults(
readonly?: boolean
purchaseId?: string
acceptanceItems?: any[]
batchNum?: number
previousBatchesTeams?: { batch: number; team: any[] }[]
}>(),
{ readonly: false, canFill: true, purchaseId: '' }
{ readonly: false, canFill: true, purchaseId: '', batchNum: 1, previousBatchesTeams: () => [] }
)
const emit = defineEmits(['update:modelValue'])
const formRef = ref<FormInstance>()
const templateFileIdsStr = ref('')
const copyFromBatch = ref<number | null>(null)
const form = reactive({
acceptType: '1',
@@ -129,8 +158,20 @@ const form = reactive({
...props.modelValue,
})
watch(() => props.modelValue, (val) => Object.assign(form, val || {}), { deep: true })
watch(() => props.modelValue, (val) => {
Object.assign(form, val || {})
// 金额≥30万时强制为上传方式
if (!props.canFill && form.acceptType === '1') {
form.acceptType = '2'
}
}, { deep: true })
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
// 金额≥30万时默认选中上传方式
watch(() => props.canFill, (val) => {
if (!val && form.acceptType === '1') {
form.acceptType = '2'
}
}, { immediate: true })
watch(() => props.acceptanceItems, (items) => {
if (items?.length && form.acceptContents.length === 0) {
@@ -163,6 +204,19 @@ const removeTeam = (idx: number) => {
form.acceptTeam.splice(idx, 1)
}
const onCopyFromBatch = (n: number | null) => {
if (!n) return
const item = props.previousBatchesTeams?.find((x) => x.batch === n)
if (item?.team?.length) {
form.acceptTeam = item.team.map((m: any) => ({
name: m.name || '',
deptCode: m.deptCode || '',
deptName: m.deptName || '',
}))
}
copyFromBatch.value = null
}
const rules: FormRules = {
acceptType: [{ required: true, message: '请选择验收方式', trigger: 'change' }],
acceptDate: [{ required: true, message: '请选择验收日期', trigger: 'change' }],
@@ -174,26 +228,16 @@ defineExpose({ validate, form })
</script>
<style scoped>
.accept-batch-form :deep(.el-form-item) {
margin-bottom: 24px;
.mb20 {
margin-bottom: 20px;
}
.accept-content-table :deep(.el-table__body td) {
padding: 10px 0;
width: 200px;
}
.team-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.team-row {
display: flex;
.copy-from-inline {
display: inline-flex;
align-items: center;
gap: 12px;
gap: 6px;
margin-left: 4px;
}
.el-form-item__tip {
font-size: 12px;
color: #909399;
margin-top: 6px;
.copy-from-inline :deep(.el-select) {
vertical-align: middle;
}
</style>

View File

@@ -1,17 +1,17 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px" class="accept-common-form compact-form">
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-row :gutter="24">
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="项目名称">
<el-input :model-value="projectName || form.projectName" readonly placeholder="-" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="需求部门">
<el-input :model-value="deptName || form.deptName" readonly placeholder="-" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="是否签订合同" prop="hasContract">
<el-radio-group v-model="form.hasContract">
<el-radio label="0"></el-radio>
@@ -19,12 +19,12 @@
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.hasContract === '1'">
<el-col :span="8" class="mb20" v-if="form.hasContract === '1'">
<el-form-item label="合同" prop="contractId">
<el-input v-model="form.contractId" placeholder="请选择合同(待对接合同接口)" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="是否分期验收" prop="isInstallment">
<el-radio-group v-model="form.isInstallment">
<el-radio label="0"></el-radio>
@@ -32,27 +32,27 @@
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.isInstallment === '1'">
<el-col :span="8" class="mb20" v-if="form.isInstallment === '1'">
<el-form-item label="分期次数" prop="totalPhases">
<el-input-number v-model="form.totalPhases" :min="1" :max="99" placeholder="请输入" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="供应商名称" prop="supplierName">
<el-input v-model="form.supplierName" placeholder="选择合同后自动带出" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="供应商联系人及电话" prop="supplierContact">
<el-input v-model="form.supplierContact" placeholder="请输入" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="采购人员" prop="purchaserId">
<org-selector v-model:orgList="purchaserList" type="user" :multiple="false" @update:orgList="onPurchaserChange" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" class="mb20">
<el-form-item label="资产管理员" prop="assetAdminId">
<org-selector v-model:orgList="assetAdminList" type="user" :multiple="false" @update:orgList="onAssetAdminChange" />
</el-form-item>
@@ -69,6 +69,8 @@ const props = defineProps<{
modelValue: Record<string, any>
projectName?: string
deptName?: string
/** 每次打开弹窗时变化,用于强制重置内部 form */
resetKey?: number
}>()
const emit = defineEmits(['update:modelValue'])
@@ -91,9 +93,8 @@ const form = reactive({
...props.modelValue,
})
watch(() => props.modelValue, (val) => {
const syncFormFromModel = (val: Record<string, any> | undefined) => {
Object.assign(form, val || {})
// 人员选择回显
if (form.purchaserId && form.purchaserName) {
purchaserList.value = [{ id: form.purchaserId, name: form.purchaserName, type: 'user' }]
} else {
@@ -104,7 +105,11 @@ watch(() => props.modelValue, (val) => {
} else {
assetAdminList.value = []
}
}, { deep: true, immediate: true })
}
watch(() => props.modelValue, syncFormFromModel, { deep: true, immediate: true })
// resetKey 变化时强制用 modelValue 覆盖内部 form
watch(() => props.resetKey, () => syncFormFromModel(props.modelValue))
watch(form, () => emit('update:modelValue', { ...form }), { deep: true })
const onPurchaserChange = (list: any[]) => {
@@ -140,26 +145,7 @@ defineExpose({ validate, form })
</script>
<style scoped>
.accept-common-form {
padding: 0 4px;
}
.accept-common-form :deep(.el-form-item) {
margin-bottom: 24px;
}
/* 紧凑表单样式 */
.compact-form {
:deep(.el-form-item) {
margin-bottom: 16px;
}
:deep(.el-form-item__label) {
padding-right: 12px;
font-size: 14px;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner) {
font-size: 14px;
}
.mb20 {
margin-bottom: 20px;
}
</style>

View File

@@ -2,13 +2,13 @@
<el-dialog
v-model="visible"
title="履约验收"
width="85%"
width="75%"
:close-on-click-modal="false"
destroy-on-close
class="purchasing-accept-modal"
@close="handleClose"
>
<div v-loading="loading" class="modal-body">
<div v-loading="loading" class="modal-body" :key="String(purchaseId)">
<div class="main-tabs">
<div class="main-tab-nav">
<div
@@ -23,12 +23,14 @@
:class="{ active: mainTab === 'batch' }"
@click="mainTab = 'batch'"
>
分期验收{{ batches.length > 0 ? ` (${batches.length})` : '' }}
{{ commonForm?.isInstallment === '0' ? '验收' : '分期验收' }}{{ commonForm?.isInstallment !== '0' && batches.length > 0 ? ` (${batches.length})` : '' }}
</div>
</div>
<div class="main-tab-content">
<div v-show="mainTab === 'common'" class="tab-content">
<AcceptCommonForm
:key="`${purchaseId}-${openToken}`"
:reset-key="openToken"
ref="commonFormRef"
v-model="commonForm"
:project-name="applyInfo?.projectName"
@@ -37,7 +39,7 @@
</div>
<div v-show="mainTab === 'batch'" class="tab-content">
<div v-if="batches.length > 0">
<div class="batch-tabs">
<div v-show="commonForm?.isInstallment !== '0'" class="batch-tabs">
<div
v-for="b in batches"
:key="b.id"
@@ -55,11 +57,14 @@
v-for="b in batches"
v-show="String(b.batch) === activeTab"
:key="b.id"
:ref="(el) => setBatchFormRef(b.batch, el)"
v-model="batchForms[b.batch]"
:can-fill="canFillForm"
:readonly="false"
:purchase-id="String(purchaseId)"
:acceptance-items="acceptanceItems"
:batch-num="b.batch"
:previous-batches-teams="getPreviousBatchesTeams(b.batch)"
/>
</div>
</div>
@@ -125,14 +130,33 @@ const batches = ref<any[]>([])
const mainTab = ref('common')
const activeTab = ref('1')
const commonFormRef = ref()
const commonForm = reactive<Record<string, any>>({})
const batchFormRefMap = ref<Record<number, any>>({})
/** 使用 ref 并在每次打开时替换整个对象,确保子组件能感知引用变化并清空 */
const commonForm = ref<Record<string, any>>({})
/** 每次打开自增,用于强制 AcceptCommonForm 重新挂载,确保公共信息彻底清空 */
const openToken = ref(0)
const batchForms = reactive<Record<number, any>>({})
const setBatchFormRef = (batch: number, el: any) => {
if (el) batchFormRefMap.value[batch] = el
}
const activeBatchId = computed(() => {
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
return b?.id || ''
})
const getPreviousBatchesTeams = (batchNum: number) => {
const list: { batch: number; team: any[] }[] = []
for (let n = 1; n < batchNum; n++) {
const team = batchForms[n]?.acceptTeam
if (Array.isArray(team) && team.length > 0) {
list.push({ batch: n, team })
}
}
return list
}
const canEditBatch = (batch: number) => {
if (batch === 1) return true
for (let i = 1; i < batch; i++) {
@@ -142,7 +166,8 @@ const canEditBatch = (batch: number) => {
}
const isBatchCompleted = (b: any) => {
return !!(b.acceptType && b.acceptDate)
const form = batchForms[b.batch]
return !!(form?.acceptType && form?.acceptDate)
}
const isBatchCompletedByIdx = (batch: number) => {
@@ -152,17 +177,24 @@ const isBatchCompletedByIdx = (batch: number) => {
const loadData = async () => {
if (!purchaseId.value) return
const currentId = String(purchaseId.value)
loading.value = true
try {
const [configRes, canFillRes] = await Promise.all([
getCommonConfigWithBatches(String(purchaseId.value)),
apiCanFillForm(String(purchaseId.value)),
getCommonConfigWithBatches(currentId),
apiCanFillForm(currentId),
])
// 防止快速切换:若已打开其他申请单,忽略本次结果
if (String(purchaseId.value) !== currentId) return
const config = configRes?.data
canFillForm.value = !!canFillRes?.data
if (config?.common) {
Object.assign(commonForm, {
applyInfo.value = config.common
// 仅当存在已保存批次时,才用接口数据回填公共信息;否则保持 open() 中的默认清空值
if (config?.batches?.length) {
Object.assign(commonForm.value, {
hasContract: config.common.hasContract || '0',
contractId: config.common.contractId || '',
isInstallment: config.common.isInstallment || '0',
@@ -174,13 +206,14 @@ const loadData = async () => {
assetAdminId: config.common.assetAdminId || '',
assetAdminName: config.common.assetAdminName || '',
})
applyInfo.value = config.common
}
}
const projectType = applyInfo.value?.projectType || rowProjectType.value || 'A'
const typeMap: Record<string, string> = { A: 'A', B: 'B', C: 'C' }
const at = typeMap[projectType] || 'A'
const itemsRes = await getAcceptanceItems(at)
if (String(purchaseId.value) !== currentId) return
acceptanceItems.value = itemsRes?.data || []
if (config?.batches?.length) {
@@ -191,6 +224,7 @@ const loadData = async () => {
if (!batchForms[b.batch]) batchForms[b.batch] = {}
}
await loadBatchDetails()
if (String(purchaseId.value) !== currentId) return
} else {
batches.value = []
}
@@ -252,9 +286,13 @@ const loadBatchDetails = async () => {
}
const saveCommonConfig = async () => {
const valid = await commonFormRef.value?.validate?.().catch(() => false)
const formRef = commonFormRef.value
const valid = await formRef?.validate?.().catch(() => false)
if (!valid) return
if (commonForm.isInstallment === '1' && (!commonForm.totalPhases || commonForm.totalPhases < 1)) {
// 直接从子组件 form 读取,确保拿到用户填写的最新值(避免 v-model 同步延迟)
const form = formRef?.form || commonForm.value
const isInstallment = form.isInstallment === '1' || form.isInstallment === 1
if (isInstallment && (!form.totalPhases || form.totalPhases < 1)) {
useMessage().error('请填写分期次数')
return
}
@@ -262,16 +300,16 @@ const saveCommonConfig = async () => {
try {
await apiSaveCommonConfig({
purchaseId: String(purchaseId.value),
hasContract: commonForm.hasContract,
contractId: commonForm.contractId,
isInstallment: commonForm.isInstallment,
totalPhases: commonForm.isInstallment === '1' ? commonForm.totalPhases : 1,
supplierName: commonForm.supplierName,
supplierContact: commonForm.supplierContact,
purchaserId: commonForm.purchaserId,
purchaserName: commonForm.purchaserName,
assetAdminId: commonForm.assetAdminId,
assetAdminName: commonForm.assetAdminName,
hasContract: form.hasContract ?? '0',
contractId: form.contractId ?? '',
isInstallment: form.isInstallment ?? '0',
totalPhases: isInstallment ? (Number(form.totalPhases) || 1) : 1,
supplierName: String(form.supplierName ?? ''),
supplierContact: String(form.supplierContact ?? ''),
purchaserId: String(form.purchaserId ?? ''),
purchaserName: String(form.purchaserName ?? ''),
assetAdminId: String(form.assetAdminId ?? ''),
assetAdminName: String(form.assetAdminName ?? ''),
})
useMessage().success('保存成功')
await loadData()
@@ -283,11 +321,25 @@ const saveCommonConfig = async () => {
}
const saveCurrentBatch = async () => {
const curBatch = Number(activeTab.value)
const batchFormRef = batchFormRefMap.value[curBatch]
const valid = await batchFormRef?.validate?.().catch(() => false)
if (!valid) return
const b = batches.value.find((x: any) => String(x.batch) === activeTab.value)
if (!b?.id) return
const form = batchForms[Number(activeTab.value)]
const form = batchForms[curBatch]
if (!form) return
if (!form.acceptType) {
useMessage().error('请选择验收方式')
return
}
if (!form.acceptDate) {
useMessage().error('请选择验收日期')
return
}
const team = (form.acceptTeam || []).filter((m: any) => m?.name)
if (team.length < 3 || team.length % 2 === 0) {
useMessage().error('验收小组至少3人且为单数')
@@ -320,13 +372,50 @@ const handleClose = () => {
emit('refresh')
}
const open = (row: any) => {
const DEFAULT_COMMON_FORM = {
hasContract: '0',
contractId: '',
isInstallment: '0',
totalPhases: 1,
supplierName: '',
supplierContact: '',
purchaserId: '',
purchaserName: '',
assetAdminId: '',
assetAdminName: '',
}
/** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */
const resetAllToDefault = () => {
openToken.value++
commonForm.value = { ...DEFAULT_COMMON_FORM }
applyInfo.value = null
mainTab.value = 'common'
activeTab.value = '1'
batchFormRefMap.value = {}
batches.value = []
acceptanceItems.value = []
canFillForm.value = true
Object.keys(batchForms).forEach((k) => delete batchForms[Number(k)])
}
const open = async (row: any) => {
purchaseId.value = row?.id ?? ''
rowProjectType.value = row?.projectType || 'A'
// 1. 先将弹窗内所有内容恢复为初始空值
resetAllToDefault()
// 2. 显示弹窗并开启 loading避免接口返回前展示旧数据
visible.value = true
batches.value = []
Object.keys(batchForms).forEach((k) => delete batchForms[Number(k)])
nextTick(() => loadData())
loading.value = true
// 3. 等待 Vue 完成渲染,确保子组件已接收并展示空值
await nextTick()
await nextTick()
// 4. 再进行接口查询并覆盖
await loadData()
}
defineExpose({ open })
@@ -337,6 +426,7 @@ defineExpose({ open })
padding: 0;
max-height: 70vh;
overflow-y: auto;
overflow-x: hidden;
}
.main-tab-nav {
display: flex;
@@ -403,3 +493,10 @@ defineExpose({ open })
min-height: 200px;
}
</style>
<style>
/* 弹窗横向滚动修复,需非 scoped 以影响 el-dialog */
.purchasing-accept-modal .el-dialog__body {
overflow-x: hidden;
}
</style>