482 lines
15 KiB
Vue
482 lines
15 KiB
Vue
<template>
|
||
<el-dialog
|
||
v-model="visible"
|
||
title="履约验收"
|
||
width="75%"
|
||
:close-on-click-modal="false"
|
||
destroy-on-close
|
||
class="purchasing-accept-modal"
|
||
@close="handleClose"
|
||
>
|
||
<div v-loading="loading" class="modal-body" :key="String(purchaseId)">
|
||
<div class="main-tabs">
|
||
<div class="main-tab-nav">
|
||
<div
|
||
class="main-tab-item"
|
||
:class="{ active: mainTab === 'common' }"
|
||
@click="mainTab = 'common'"
|
||
>
|
||
公共信息
|
||
</div>
|
||
<div
|
||
class="main-tab-item"
|
||
:class="{ active: mainTab === 'batch' }"
|
||
@click="mainTab = 'batch'"
|
||
>
|
||
{{ 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"
|
||
:purchase-id="purchaseId"
|
||
:project-name="applyInfo?.projectName"
|
||
:dept-name="applyInfo?.deptName"
|
||
/>
|
||
</div>
|
||
<div v-show="mainTab === 'batch'" class="tab-content">
|
||
<div v-if="batches.length > 0">
|
||
<div v-show="commonForm?.isInstallment !== '0'" class="batch-tabs">
|
||
<div
|
||
v-for="b in batches"
|
||
:key="b.id"
|
||
class="batch-tab-item"
|
||
:class="{ active: String(b.batch) === activeTab, disabled: !canEditBatch(b.batch) }"
|
||
@click="canEditBatch(b.batch) && (activeTab = String(b.batch))"
|
||
>
|
||
<span>第{{ b.batch }}期</span>
|
||
<el-tag v-if="isBatchCompleted(b)" type="success" size="small">已填</el-tag>
|
||
<el-tag v-else-if="!canEditBatch(b.batch)" type="info" size="small">需先完成上一期</el-tag>
|
||
</div>
|
||
</div>
|
||
<div class="batch-panel">
|
||
<AcceptBatchForm
|
||
v-for="b in batches"
|
||
v-show="String(b.batch) === activeTab"
|
||
:key="b.id"
|
||
:ref="(el) => setBatchFormRef(b.batch, el)"
|
||
:model-value="batchForms[b.batch]"
|
||
:readonly="false"
|
||
:purchase-id="String(purchaseId)"
|
||
:project-type="acceptProjectType"
|
||
:budget="applyInfo?.budget"
|
||
:batch-num="b.batch"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div v-else class="tip-box">
|
||
<el-alert type="info" :closable="false" show-icon>
|
||
请先在「公共信息」中填写并点击「保存公共配置」,系统将按分期次数自动生成验收批次
|
||
</el-alert>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<span>
|
||
<el-button @click="handleClose">关 闭</el-button>
|
||
<el-button
|
||
v-if="mainTab === 'common' || batches.length === 0"
|
||
type="primary"
|
||
@click="saveCommonConfig"
|
||
:loading="saving"
|
||
>
|
||
保存公共配置
|
||
</el-button>
|
||
<el-button
|
||
v-else-if="mainTab === 'batch' && activeBatchId"
|
||
type="primary"
|
||
@click="saveCurrentBatch"
|
||
:loading="saving"
|
||
>
|
||
保存第{{ activeTab }}期
|
||
</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, nextTick } from 'vue'
|
||
import { useMessage } from '/@/hooks/message'
|
||
import {
|
||
saveCommonConfig as apiSaveCommonConfig,
|
||
getCommonConfigWithBatches,
|
||
updateBatch,
|
||
getDetail,
|
||
} from '/@/api/purchase/purchasingAccept'
|
||
import AcceptCommonForm from './AcceptCommonForm.vue'
|
||
import AcceptBatchForm from './AcceptBatchForm.vue'
|
||
|
||
const emit = defineEmits(['refresh'])
|
||
|
||
const visible = ref(false)
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const purchaseId = ref<string | number>('')
|
||
const applyInfo = ref<any>(null)
|
||
const rowProjectType = ref<string>('A')
|
||
const batches = ref<any[]>([])
|
||
const mainTab = ref('common')
|
||
const activeTab = ref('1')
|
||
const commonFormRef = ref()
|
||
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 batchSavedFlags = ref<Record<number, boolean>>({})
|
||
|
||
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 || ''
|
||
})
|
||
|
||
/** 项目类型 A:货物 B:工程 C:服务,用于批次表单模版下载 */
|
||
const acceptProjectType = computed(() => applyInfo.value?.projectType || rowProjectType.value || 'A')
|
||
|
||
/** 是否允许编辑该期:第 1 期始终可编辑;第 N 期仅当第 1~N-1 期均已保存后才可编辑 */
|
||
const canEditBatch = (batch: number) => {
|
||
if (batch === 1) return true
|
||
for (let i = 1; i < batch; i++) {
|
||
if (!batchSavedFlags.value[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/** 该期是否已保存(用于 tab 上显示“已填”标签) */
|
||
const isBatchCompleted = (b: any) => {
|
||
return !!batchSavedFlags.value[b.batch]
|
||
}
|
||
|
||
const isBatchCompletedByIdx = (batch: number) => {
|
||
return !!batchSavedFlags.value[batch]
|
||
}
|
||
|
||
const loadData = async () => {
|
||
if (!purchaseId.value) return
|
||
const currentId = String(purchaseId.value)
|
||
loading.value = true
|
||
try {
|
||
const configRes = await getCommonConfigWithBatches(currentId)
|
||
// 防止快速切换:若已打开其他申请单,忽略本次结果
|
||
if (String(purchaseId.value) !== currentId) return
|
||
|
||
const config = configRes?.data
|
||
|
||
if (config?.common) {
|
||
applyInfo.value = config.common
|
||
// 采购人员和资产管理员始终回填
|
||
commonForm.value.purchaserId = config.common.purchaserId || ''
|
||
commonForm.value.purchaserName = config.common.purchaserName || ''
|
||
commonForm.value.assetAdminId = config.common.assetAdminId || ''
|
||
commonForm.value.assetAdminName = config.common.assetAdminName || ''
|
||
|
||
// 其他字段仅当存在已保存批次时回填
|
||
if (config?.batches?.length) {
|
||
Object.assign(commonForm.value, {
|
||
hasContract: config.common.hasContract || '0',
|
||
contractId: config.common.contractId || '',
|
||
isInstallment: config.common.isInstallment || '0',
|
||
totalPhases: config.common.totalPhases || 1,
|
||
supplierName: config.common.supplierName || '',
|
||
supplierContact: config.common.supplierContact || '',
|
||
transactionAmount: config.common.transactionAmount || null,
|
||
})
|
||
}
|
||
}
|
||
|
||
if (config?.batches?.length) {
|
||
batches.value = config.batches.sort((a: any, b: any) => (a.batch || 0) - (b.batch || 0))
|
||
activeTab.value = String(batches.value[0]?.batch || '1')
|
||
mainTab.value = 'batch'
|
||
for (const b of batches.value) {
|
||
if (!batchForms[b.batch]) batchForms[b.batch] = {}
|
||
}
|
||
await loadBatchDetails()
|
||
if (String(purchaseId.value) !== currentId) return
|
||
} else {
|
||
batches.value = []
|
||
}
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '加载失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const loadBatchDetails = async () => {
|
||
for (const b of batches.value) {
|
||
batchSavedFlags.value[b.batch] = false
|
||
}
|
||
for (const b of batches.value) {
|
||
try {
|
||
const res = await getDetail(String(purchaseId.value), b.batch)
|
||
const d = res?.data
|
||
if (d?.accept) {
|
||
// 仅当该期在服务端有验收日期时才视为已保存
|
||
const hasSaved = !!d.accept.acceptDate
|
||
batchSavedFlags.value[b.batch] = hasSaved
|
||
// 优先使用 templateFiles(包含id和fileTitle),否则降级使用 templateFileIds
|
||
let fileIdsStr = ''
|
||
if (d.accept.templateFiles && d.accept.templateFiles.length > 0) {
|
||
// 使用 templateFiles,格式为 {id: string, fileTitle: string}[]
|
||
fileIdsStr = d.accept.templateFiles.map((f: any) => f.id).join(',')
|
||
} else if (d.accept.templateFileIds) {
|
||
// 降级使用 templateFileIds
|
||
const fileIds = d.accept.templateFileIds
|
||
fileIdsStr = Array.isArray(fileIds) ? fileIds.join(',') : (fileIds || '')
|
||
}
|
||
batchForms[b.batch] = {
|
||
acceptType: '2', // 固定为上传模式
|
||
acceptDate: d.accept.acceptDate || '',
|
||
remark: d.accept.remark || '',
|
||
templateFileIds: fileIdsStr,
|
||
// 保存文件信息用于显示
|
||
_templateFiles: d.accept.templateFiles || [],
|
||
}
|
||
// 通知子组件初始化数据
|
||
await nextTick()
|
||
const batchFormRef = batchFormRefMap.value[b.batch]
|
||
if (batchFormRef?.initData) {
|
||
batchFormRef.initData()
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
|
||
const saveCommonConfig = async () => {
|
||
const formRef = commonFormRef.value
|
||
const valid = await formRef?.validate?.().catch(() => false)
|
||
if (!valid) return
|
||
// 直接从子组件 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
|
||
}
|
||
saving.value = true
|
||
try {
|
||
await apiSaveCommonConfig({
|
||
purchaseId: String(purchaseId.value),
|
||
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 ?? ''),
|
||
transactionAmount: form.transactionAmount ?? null,
|
||
})
|
||
useMessage().success('保存成功')
|
||
await loadData()
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
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 formData = batchFormRef?.getFormData?.() || batchFormRef?.form
|
||
if (!formData) return
|
||
|
||
if (!formData.acceptDate) {
|
||
useMessage().error('请选择验收日期')
|
||
return
|
||
}
|
||
|
||
// templateFileIds: 提取ID数组
|
||
let fileIds: string[] = []
|
||
if (formData.templateFileIds) {
|
||
if (Array.isArray(formData.templateFileIds)) {
|
||
fileIds = formData.templateFileIds.map((item: any) => {
|
||
if (typeof item === 'string') return item
|
||
if (item && item.id) return item.id
|
||
return null
|
||
}).filter(Boolean)
|
||
} else if (typeof formData.templateFileIds === 'string') {
|
||
fileIds = formData.templateFileIds.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||
}
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
await updateBatch({
|
||
id: b.id,
|
||
purchaseId: String(purchaseId.value),
|
||
acceptType: '2', // 固定为上传模式
|
||
acceptDate: formData.acceptDate,
|
||
remark: formData.remark,
|
||
templateFileIds: fileIds,
|
||
})
|
||
useMessage().success('保存成功')
|
||
batchSavedFlags.value[curBatch] = true
|
||
await loadData()
|
||
} catch (e: any) {
|
||
useMessage().error(e?.msg || '保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const handleClose = () => {
|
||
visible.value = false
|
||
emit('refresh')
|
||
}
|
||
|
||
const DEFAULT_COMMON_FORM = {
|
||
hasContract: '0',
|
||
contractId: '',
|
||
isInstallment: '0',
|
||
totalPhases: 1,
|
||
supplierName: '',
|
||
supplierContact: '',
|
||
purchaserId: '',
|
||
purchaserName: '',
|
||
assetAdminId: '',
|
||
assetAdminName: '',
|
||
transactionAmount: null,
|
||
}
|
||
|
||
/** 将弹窗内所有内容恢复为初始空值(替换整个对象以确保引用变化) */
|
||
const resetAllToDefault = () => {
|
||
openToken.value++
|
||
commonForm.value = { ...DEFAULT_COMMON_FORM }
|
||
applyInfo.value = null
|
||
mainTab.value = 'common'
|
||
activeTab.value = '1'
|
||
batchFormRefMap.value = {}
|
||
batches.value = []
|
||
Object.keys(batchForms).forEach((k) => delete batchForms[Number(k)])
|
||
batchSavedFlags.value = {}
|
||
}
|
||
|
||
const open = async (row: any) => {
|
||
purchaseId.value = row?.id ?? ''
|
||
rowProjectType.value = row?.projectType || 'A'
|
||
|
||
// 1. 先将弹窗内所有内容恢复为初始空值
|
||
resetAllToDefault()
|
||
|
||
// 2. 显示弹窗并开启 loading,避免接口返回前展示旧数据
|
||
visible.value = true
|
||
loading.value = true
|
||
|
||
// 3. 等待 Vue 完成渲染,确保子组件已接收并展示空值
|
||
await nextTick()
|
||
await nextTick()
|
||
|
||
// 4. 再进行接口查询并覆盖
|
||
await loadData()
|
||
}
|
||
|
||
defineExpose({ open })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.modal-body {
|
||
padding: 0;
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
}
|
||
.main-tab-nav {
|
||
display: flex;
|
||
gap: 4px;
|
||
margin-bottom: 16px;
|
||
border-bottom: 1px solid var(--el-border-color);
|
||
}
|
||
.main-tab-item {
|
||
padding: 12px 20px;
|
||
cursor: pointer;
|
||
color: var(--el-text-color-regular);
|
||
border-bottom: 2px solid transparent;
|
||
margin-bottom: -1px;
|
||
transition: all 0.2s;
|
||
}
|
||
.main-tab-item:hover {
|
||
color: var(--el-color-primary);
|
||
}
|
||
.main-tab-item.active {
|
||
color: var(--el-color-primary);
|
||
font-weight: 600;
|
||
border-bottom-color: var(--el-color-primary);
|
||
}
|
||
.main-tab-content {
|
||
padding-top: 4px;
|
||
}
|
||
.tab-content {
|
||
min-height: 200px;
|
||
display: block;
|
||
}
|
||
.tip-box {
|
||
padding: 20px 0;
|
||
}
|
||
.batch-tabs {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.batch-tab-item {
|
||
padding: 8px 16px;
|
||
border: 1px solid var(--el-border-color);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s;
|
||
}
|
||
.batch-tab-item:hover:not(.disabled) {
|
||
border-color: var(--el-color-primary);
|
||
color: var(--el-color-primary);
|
||
}
|
||
.batch-tab-item.active {
|
||
background: var(--el-color-primary);
|
||
border-color: var(--el-color-primary);
|
||
color: #fff;
|
||
}
|
||
.batch-tab-item.disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
.batch-panel {
|
||
min-height: 200px;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* 弹窗横向滚动修复,需非 scoped 以影响 el-dialog */
|
||
.purchasing-accept-modal .el-dialog__body {
|
||
overflow-x: hidden;
|
||
}
|
||
</style>
|