This commit is contained in:
2025-08-27 19:50:22 +08:00
parent f637e65635
commit 7d6c13e935
12 changed files with 1212 additions and 470 deletions

View File

@ -57,16 +57,15 @@
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" min-width="120" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['materials:materialReceive:query']"
>查看</el-button
>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['materials:materialReceive:remove']"
>删除</el-button
>
</el-tooltip>
<!-- <el-button link type="primary" icon="edit" @click="handleUpdate(scope.row)" v-hasPermi="['materials:materialReceive:edit']"
>修改</el-button
> -->
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['materials:materialReceive:query']"
>查看</el-button
>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['materials:materialReceive:remove']"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
@ -74,7 +73,15 @@
</el-card>
<!-- 添加或修改物料接收单对话框 -->
<el-dialog draggable :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-dialog
:close-on-click-modal="false"
:close-on-press-escape="false"
draggable
:title="dialog.title"
v-model="dialog.visible"
width="800px"
append-to-body
>
<el-form ref="materialReceiveFormRef" :model="form" :rules="rules" label-width="110px">
<el-row>
<el-col :span="12">
@ -122,7 +129,6 @@
:value="item.contractCode"
></el-option>
</el-select>
<!-- <el-input v-model="form.contractName" placeholder="请输入合同名称" /> -->
</el-form-item>
</el-col>
<el-col :span="24">
@ -131,14 +137,13 @@
</el-form-item>
</el-col>
<!-- 数量验收区域修复v-for key问题 -->
<!-- 数量验收区域 -->
<el-col :span="24">
<div class="detail">
<div class="detail-header">
<span>数量验收</span>
<el-button type="primary" v-if="form.materialSource == '1'" link @click="addItem" icon="Plus">添加数量验收</el-button>
</div>
<!-- 关键修复v-for key改为item.id唯一标识而非index -->
<div v-for="(item, index) in form.itemList" :key="item.id" class="detail-item">
<el-row>
<el-col :span="12">
@ -161,21 +166,27 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="数量"
:prop="`itemList.${index}.quantity`"
:rules="{ required: true, message: '数量不能为空', trigger: 'blur' }"
>
<el-input :disabled="form.materialSource == '2'" type="number" v-model="item.quantity" placeholder="请输入数量" />
<el-form-item label="数量" :prop="`itemList.${index}.quantity`" :rules="rules.quantityRule" ref="quantityFormItemRefs[index]">
<el-input
:disabled="form.materialSource == '2'"
type="number"
v-model.number="item.quantity"
placeholder="请输入数量"
min="0"
@input="handleQuantityInput(index)"
@blur="handleQuantityBlur(index)"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="验收"
:prop="`itemList.${index}.acceptedQuantity`"
:rules="{ required: true, message: '验收数量不能为空', trigger: 'blur' }"
>
<el-input type="number" v-model="item.acceptedQuantity" placeholder="请输入验收" />
<el-form-item label="验收" :prop="`itemList.${index}.acceptedQuantity`" :rules="rules.acceptedQuantityRule">
<el-input
type="number"
v-model.number="item.acceptedQuantity"
placeholder="请输入验收"
min="0"
@input="handleAcceptedInput(index)"
/>
</el-form-item>
</el-col>
<el-col :span="12">
@ -184,13 +195,13 @@
:prop="`itemList.${index}.shortageQuantity`"
:rules="{ required: true, message: '缺件数量不能为空', trigger: 'blur' }"
>
<el-input type="number" v-model="item.shortageQuantity" placeholder="自动计算(数量-验收数量)" readonly />
<el-input type="number" min="0" v-model="item.shortageQuantity" placeholder="自动计算" readonly />
<span class="tips">*自动计算数量-验收数量</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-col :span="24">
<el-form-item label="备注" :prop="`itemList.${index}.remark`">
<el-input v-model="item.remark" placeholder="请输入备注" />
<el-input v-model="item.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.itemList.length > 1 && form.materialSource == '1'">
@ -205,26 +216,26 @@
<el-col :span="12">
<el-form-item label="合格证文件" prop="certCountFileId">
<file-upload :isShowTip="false" v-model="form.certCountFileId" />
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.certCountFileId" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出厂报告文件" prop="reportCountFileId">
<file-upload :isShowTip="false" v-model="form.reportCountFileId" />
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.reportCountFileId" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="技术资料文件" prop="techDocCountFileId">
<file-upload :isShowTip="false" v-model="form.techDocCountFileId" />
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.techDocCountFileId" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="厂家资质文件" prop="licenseCountFileId">
<file-upload :isShowTip="false" v-model="form.licenseCountFileId" />
<file-upload :isShowTip="false" :fileType="['pdf', 'png', 'jpg', 'jpeg']" v-model="form.licenseCountFileId" />
</el-form-item>
</el-col>
<el-col :span="24">
<span style="color: #ff0000ab; margin-bottom: 10px; display: block">注意请上传doc/xls/ppt/txt/pdf/png/jpg/jpeg/zip格式文件</span>
<span style="color: #ff0000ab; margin-bottom: 10px; display: block">注意pdf/png/jpg/jpeg格式文件</span>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
@ -258,7 +269,7 @@ import { useUserStoreHook } from '@/store/modules/user';
import wordllReceive from './word/index.vue';
import { listPurchaseDoc, purchaseDocPlanList } from '@/api/materials/purchaseDoc';
import { watch, onMounted, onUnmounted, ref, reactive, computed, toRefs, getCurrentInstance } from 'vue';
import type { ComponentInternalInstance, ElFormInstance, DialogOption } from 'element-plus';
import type { ComponentInternalInstance, ElFormInstance, DialogOption, ElFormItem } from 'element-plus';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { storage_type } = toRefs<any>(proxy?.useDict('storage_type'));
@ -266,8 +277,10 @@ const userStore = useUserStoreHook();
const currentProject = computed(() => userStore.selectedProject);
const wordllReceiveRef = ref<InstanceType<typeof wordllReceive>>();
// 核心修复1存储每个验收条目的watch停止函数,避免内存泄漏
// 存储每个验收条目的watch停止函数
const itemWatchStopFns = ref<Array<() => void>>([]);
// 存储数量表单项的引用,用于手动触发验证
const quantityFormItemRefs = ref<(ElFormItem | null)[]>([]);
// 列表数据
const materialReceiveList = ref<MaterialReceiveVO[]>([]);
@ -285,18 +298,19 @@ const materialReceiveFormRef = ref<ElFormInstance>();
const purchaseDocList = ref([]); // 物资采购单列表
const purchaseMap = new Map(); // 采购单映射id -> 采购单对象)
const contractNameList = ref([]); //合同列表
// 对话框配置
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 生成验收条目唯一ID用于v-for key
// 生成验收条目唯一ID
const generateItemId = () => {
return Date.now() + Math.random().toString(36).substr(2, 9);
};
// 初始化表单数据修复给验收条目添加唯一ID
// 初始化表单数据
const getInitFormData = (): MaterialReceiveForm => {
return {
id: undefined,
@ -323,7 +337,7 @@ const getInitFormData = (): MaterialReceiveForm => {
docCode: undefined,
itemList: [
{
id: generateItemId(), // 新增唯一ID解决v-for渲染问题
id: generateItemId(),
name: undefined,
specification: undefined,
unit: undefined,
@ -357,7 +371,38 @@ const data = reactive({
formCode: [{ required: true, message: '请输入表单编号', trigger: 'blur' }],
docId: [{ required: true, message: '请选择物资采购单', trigger: 'change' }],
supplierUnit: [{ required: true, message: '请输入供货单位', trigger: 'blur' }],
orderingUnit: [{ required: true, message: '请输入订货单位', trigger: 'blur' }]
orderingUnit: [{ required: true, message: '请输入订货单位', trigger: 'blur' }],
// 数量校验规则确保触发时机包含change且类型为number
quantityRule: [
{ required: true, message: '数量不能为空', trigger: ['blur', 'change'] },
{ type: 'number', min: 0, message: '数量不能小于0', trigger: ['blur', 'change'] }
],
// 验收数量规则(允许≤数量)
acceptedQuantityRule: [
{ required: true, message: '验收数量不能为空', trigger: ['blur', 'change'] },
{ type: 'number', min: 0, message: '验收数量不能小于0', trigger: ['blur', 'change'] },
{
validator: (rule, value, callback) => {
const prop = rule.field;
const index = Number(prop.split('.')[1]);
const quantity = Number(form.value.itemList[index].quantity) || 0;
// 数量未填写时不验证大小关系数量有值但验收数量未填时也不阻断由required规则处理
if (form.value.itemList[index].quantity === undefined || form.value.itemList[index].quantity === null) {
callback();
return;
}
// 处理value为undefined/null的情况避免Number(undefined)转为NaN
const acceptedVal = Number(value) || 0;
if (acceptedVal > quantity) {
callback(new Error('验收数量必须小于等于数量'));
} else {
callback();
}
},
trigger: ['blur', 'change']
}
]
}
});
@ -374,25 +419,29 @@ const getList = async () => {
loading.value = false;
}
};
// 获取合同列表数据
const getContractList = async () => {
let res = await getContractNameList(currentProject.value?.id);
contractNameList.value = res.rows;
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置(修复:清理验收条目监听) */
/** 表单重置 */
const reset = () => {
// 停止所有验收条目的watch监听
itemWatchStopFns.value.forEach((stopFn) => stopFn());
itemWatchStopFns.value = [];
form.value = getInitFormData();
materialReceiveFormRef.value?.resetFields();
if (materialReceiveFormRef.value) {
materialReceiveFormRef.value.resetFields();
}
// 重新监听初始条目
if (form.value.itemList.length > 0) {
@ -408,7 +457,9 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
if (queryFormRef.value) {
queryFormRef.value.resetFields();
}
handleQuery();
};
@ -427,24 +478,35 @@ const handleAdd = () => {
form.value.projectName = currentProject.value?.name;
};
/** 修改按钮操作(修复:清理旧监听+添加唯一ID */
/** 修改按钮操作(核心修复:赋值后主动触发验证+数据类型转换 */
const handleUpdate = async (row?: MaterialReceiveVO) => {
reset();
const _id = row?.id || ids.value[0];
try {
const res = await getMaterialReceive(_id);
// 给验收条目补充唯一ID避免后端返回无ID
const formData = res.data;
// 修复1处理itemList数据类型确保数量/验收数量为数字避免字符串与number规则冲突
formData.itemList = formData.itemList.map((item) => ({
...item,
id: item.id || generateItemId()
id: item.id || generateItemId(),
quantity: item.quantity !== undefined ? Number(item.quantity) : undefined, // 转为数字
acceptedQuantity: item.acceptedQuantity !== undefined ? Number(item.acceptedQuantity) : undefined, // 转为数字
shortageQuantity: item.shortageQuantity !== undefined ? Number(item.shortageQuantity) : undefined // 转为数字
}));
Object.assign(form.value, formData);
// 重新监听所有条目
// 修复2重新监听所有条目,并主动触发每个条目的验证(更新验证状态)
form.value.itemList.forEach((_, index) => {
watchItemChanges(index);
// 手动触发当前条目的数量、验收数量、缺件数量验证
if (materialReceiveFormRef.value) {
materialReceiveFormRef.value.validateField(`itemList.${index}.quantity`, () => {});
materialReceiveFormRef.value.validateField(`itemList.${index}.acceptedQuantity`, () => {});
materialReceiveFormRef.value.validateField(`itemList.${index}.shortageQuantity`, () => {});
}
});
dialog.visible = true;
@ -460,13 +522,24 @@ const submitForm = () => {
if (valid) {
buttonLoading.value = true;
try {
// 提交前确保数据类型正确(数字)
const submitForm = {
...form.value,
itemList: form.value.itemList.map((item) => ({
...item,
quantity: Number(item.quantity),
acceptedQuantity: Number(item.acceptedQuantity),
shortageQuantity: Number(item.shortageQuantity)
}))
};
if (form.value.id) {
await updateMaterialReceive({ ...form.value });
await updateMaterialReceive(submitForm);
} else {
form.value.itemList.forEach((item) => {
submitForm.itemList.forEach((item) => {
delete item.id;
});
await addMaterialReceive({ ...form.value });
await addMaterialReceive(submitForm);
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
@ -495,10 +568,10 @@ const handleDelete = async (row?: MaterialReceiveVO) => {
}
};
/** 添加数量验收条目修复添加唯一ID+监听) */
/** 添加数量验收条目 */
const addItem = () => {
const newItem = {
id: generateItemId(), // 唯一ID
id: generateItemId(),
name: undefined,
specification: undefined,
unit: undefined,
@ -508,33 +581,79 @@ const addItem = () => {
remark: undefined
};
form.value.itemList.push(newItem);
// 监听新条目变化
// 监听新条目变化并触发初始验证
watchItemChanges(form.value.itemList.length - 1);
validateQuantityField(form.value.itemList.length - 1);
};
/** 监听验收条目变化,自动计算缺件数量(修复:存储停止函数) */
// 数量输入事件处理
const handleQuantityInput = (index: number) => {
// 确保值为数字类型
if (form.value.itemList[index].quantity !== undefined) {
form.value.itemList[index].quantity = Number(form.value.itemList[index].quantity);
}
// 手动触发验证
validateQuantityField(index);
};
// 数量失焦事件
const handleQuantityBlur = (index: number) => {
validateQuantityField(index);
};
// 验收数量输入事件
const handleAcceptedInput = (index: number) => {
// 确保值为数字类型
if (form.value.itemList[index].acceptedQuantity !== undefined) {
form.value.itemList[index].acceptedQuantity = Number(form.value.itemList[index].acceptedQuantity);
}
// 手动触发相关字段验证
validateQuantityField(index);
};
// 手动验证数量和验收数量字段
const validateQuantityField = (index: number) => {
if (materialReceiveFormRef.value) {
materialReceiveFormRef.value.validateField(`itemList.${index}.quantity`, () => {});
materialReceiveFormRef.value.validateField(`itemList.${index}.acceptedQuantity`, () => {});
materialReceiveFormRef.value.validateField(`itemList.${index}.shortageQuantity`, () => {});
}
};
// 监听条目变化,自动计算缺件数量(修复:计算后触发验证)
const watchItemChanges = (index: number) => {
// 停止该索引已有的监听(避免重复监听)
if (itemWatchStopFns.value[index]) {
itemWatchStopFns.value[index]();
}
// 监听数量和验收数量变化
const stopFn = watch(
() => [form.value.itemList[index].quantity, form.value.itemList[index].acceptedQuantity],
([quantity, acceptedQuantity]) => {
const qty = Number(quantity) || 0;
const acceptedQty = Number(acceptedQuantity) || 0;
let acceptedQty = Number(acceptedQuantity) || 0;
// 仅当验收数量>数量时才修正(允许等于)
if (acceptedQty > qty && qty > 0) {
acceptedQty = qty; // 修正为数量值(最大合法值)
form.value.itemList[index].acceptedQuantity = acceptedQty;
proxy?.$modal.msgWarning(`验收数量不能大于数量,已自动修正为${acceptedQty}`);
}
// 计算缺件数量允许为0
form.value.itemList[index].shortageQuantity = qty - acceptedQty;
// 修复3计算后触发当前条目的验证确保缺件数量状态更新
validateQuantityField(index);
},
{ immediate: true } // 初始时立即计算
{ immediate: true }
);
// 存储停止函数,用于后续删除时清理
itemWatchStopFns.value[index] = stopFn;
};
/** 删除数量验收条目(修复:清理监听+删除条目) */
/** 删除数量验收条目 */
const removeItem = (index: number) => {
if (form.value.itemList.length <= 1) {
proxy?.$modal.msgWarning('至少需要保留一条数量验收记录');
@ -549,6 +668,7 @@ const removeItem = (index: number) => {
// 删除条目和对应的停止函数
form.value.itemList.splice(index, 1);
itemWatchStopFns.value.splice(index, 1);
quantityFormItemRefs.value.splice(index, 1);
};
/** 查看详情 */
@ -564,7 +684,6 @@ const getlistPurchase = async () => {
status: 'finish'
});
purchaseDocList.value = res.rows;
// 构建采购单映射
purchaseDocList.value.forEach((item) => {
purchaseMap.set(item.id, item);
});
@ -573,7 +692,7 @@ const getlistPurchase = async () => {
}
};
/** 通过采购单获取需求信息(修复:清理旧监听+添加新监听 */
/** 通过采购单获取需求信息(修复:数据类型转换 */
const getdemandInfo = async (docId: string) => {
if (!docId) return;
@ -583,25 +702,25 @@ const getdemandInfo = async (docId: string) => {
// 清空旧监听和条目
itemWatchStopFns.value.forEach((stopFn) => stopFn());
itemWatchStopFns.value = [];
quantityFormItemRefs.value = [];
form.value.itemList = [];
// 赋值需求数据并添加监听
// 赋值需求数据并添加监听(确保数量为数字)
res.data.forEach((item, index) => {
const qty = Number(item.demandQuantity) || 0;
const newItem = {
id: generateItemId(), // 唯一ID
id: generateItemId(),
name: item.name,
specification: item.specification,
unit: item.unit,
quantity: qty,
acceptedQuantity: 0,
shortageQuantity: qty, // 初始缺件=数量
quantity: qty, // 确保数字类型
acceptedQuantity: 0, // 初始值为0数字
shortageQuantity: qty, // 初始缺件=数量(数字)
remark: item.remark,
planId: item.id,
id: null // 保留后端需要的空id字段
id: null
};
form.value.itemList.push(newItem);
// 监听当前条目
watchItemChanges(index);
});
}
@ -621,21 +740,25 @@ const handleSelect = (val: string) => {
form.value.materialName = obj.name || '';
}
// 获取采购单对应的需求信息
getdemandInfo(val);
};
/** 核心修复2监听材料来源变化,重置数量验收列表 */
/** 监听材料来源变化,重置数量验收列表 */
watch(
() => form.value.materialSource,
(newSource, oldSource) => {
if (newSource === oldSource) return;
// 1. 停止所有验收条目的监听
// 停止所有验收条目的监听
itemWatchStopFns.value.forEach((stopFn) => stopFn());
itemWatchStopFns.value = [];
// 2. 重置数量验收列表为初始状态1条空记录
quantityFormItemRefs.value = [];
// 清空所有文件上传字段的值
form.value.certCountFileId = undefined; // 合格证文件
form.value.reportCountFileId = undefined; // 出厂报告文件
form.value.techDocCountFileId = undefined; // 技术资料文件
form.value.licenseCountFileId = undefined; // 厂家资质文件
// 重置数量验收列表为初始状态
form.value.itemList = [
{
id: generateItemId(),
@ -649,10 +772,11 @@ watch(
}
];
// 3. 重新监听初始条目
// 重新监听初始条目并触发验证
watchItemChanges(0);
validateQuantityField(0);
// 4. 切换到乙供时,清空采购单相关数据
// 切换到乙供时,清空采购单相关数据
if (newSource === '2') {
form.value.docId = undefined;
form.value.supplierUnit = undefined;
@ -668,9 +792,10 @@ onMounted(() => {
getContractList();
getList();
getlistPurchase();
// 监听初始验收条目
// 监听初始验收条目并触发验证
if (form.value.itemList.length > 0) {
watchItemChanges(0);
validateQuantityField(0);
}
});
@ -689,7 +814,6 @@ const listeningProject = watch(
/** 页面卸载时清理监听 */
onUnmounted(() => {
listeningProject();
// 清理验收条目监听
itemWatchStopFns.value.forEach((stopFn) => stopFn());
});
</script>