Files
school-developer/src/views/purchase/purchasingrequisition/accept/PurchasingAcceptModal.vue
2026-03-19 15:55:37 +08:00

482 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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 期仅当第 1N-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>