<template>
|
<div>
|
<el-dialog
|
v-model="isShow"
|
title="新增生产订单"
|
width="1500"
|
:close-on-click-modal="false"
|
@close="closeModal"
|
class="production-order-dialog"
|
>
|
<!-- 基本信息 -->
|
<div class="section-card">
|
<div class="section-header">
|
<span class="section-icon">📋</span>
|
<span class="section-title-text">基本信息</span>
|
</div>
|
<el-form label-width="100px" :model="formState" label-position="right" ref="formRef" class="compact-form">
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item
|
label="产品名称"
|
prop="productModelId"
|
:rules="[{ required: true, message: '请选择产品', trigger: 'change' }]"
|
>
|
<el-button type="primary" @click="showProductSelectDialog = true" class="select-btn">
|
{{ formState.productName ? formState.productName : '选择产品' }}
|
</el-button>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="图纸编号" prop="model">
|
<el-input v-model="formState.model" disabled />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="单位" prop="unit">
|
<el-input v-model="formState.unit" disabled />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="需求数量" prop="quantity">
|
<el-input-number v-model="formState.quantity" :step="1" :min="1" style="width: 100%" />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="交付日期" prop="deliveryDate">
|
<el-date-picker
|
v-model="formState.deliveryDate"
|
type="date"
|
placeholder="选择交付日期"
|
style="width: 100%"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="图纸上传">
|
<el-upload
|
v-model:file-list="fileList"
|
:action="upload.url"
|
:headers="upload.headers"
|
:data="upload.data"
|
:on-success="handleDrawingUploadSuccess"
|
:on-remove="handleDrawingRemove"
|
:before-upload="handleDrawingBeforeUpload"
|
:limit="5"
|
accept=".pdf,.jpg,.jpeg,.png,.dwg"
|
list-type="picture-card"
|
>
|
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
<template #tip>
|
<div class="el-upload__tip">
|
支持 pdf、jpg、jpeg、png、dwg 格式,大小不超过 10MB
|
</div>
|
</template>
|
</el-upload>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
</div>
|
|
<!-- 产品选择弹窗 -->
|
<ProductSelectDialog
|
v-model="showProductSelectDialog"
|
@confirm="handleProductSelect"
|
single
|
/>
|
|
<!-- 用料产品选择弹窗 -->
|
<ProductSelectDialog
|
v-model="showMaterialProductDialog"
|
@confirm="handleMaterialProductSelect"
|
/>
|
|
<!-- BOM选择弹窗 -->
|
<el-dialog
|
v-model="showBomDialog"
|
title="选择BOM"
|
width="500"
|
append-to-body
|
>
|
<el-table :data="bomList" border size="small" @row-click="handleBomSelect" highlight-current-row style="margin-bottom: 10px;">
|
<el-table-column type="index" label="序号" width="60" />
|
<el-table-column label="BOM编号" prop="bomNo" min-width="120" />
|
<el-table-column label="版本" prop="version" min-width="80" />
|
<el-table-column label="产品名称" prop="productName" min-width="100" />
|
</el-table>
|
</el-dialog>
|
|
<!-- 工序 -->
|
<div class="section-card">
|
<div class="section-header">
|
<span class="section-icon">🔧</span>
|
<span class="section-title-text">工序</span>
|
<el-button type="primary" size="small" @click="addProductionTask" class="add-btn">
|
<el-icon><Plus /></el-icon> 添加任务
|
</el-button>
|
</div>
|
<div class="table-container">
|
<el-table :data="processRouteItems" border size="small" class="compact-table">
|
<el-table-column type="index" label="序号" width="60" />
|
<el-table-column label="工序名称" min-width="150">
|
<template #default="{ row }">
|
<el-select
|
v-model="row.processId"
|
placeholder="请选择工序"
|
style="width: 100%"
|
@change="(val) => handleProcessChange(val, row)"
|
>
|
<el-option
|
v-for="item in processRouteItemsOptions"
|
:key="item.id"
|
:label="item.processName"
|
:value="item.processId"
|
/>
|
</el-select>
|
</template>
|
</el-table-column>
|
<el-table-column label="报工权限" min-width="180">
|
<template #default="{ row }">
|
<el-select
|
v-model="row.userPower"
|
multiple
|
collapse-tags
|
collapse-tags-tooltip
|
placeholder="请选择报工权限"
|
style="width: 100%"
|
>
|
<el-option
|
v-for="item in userOptions"
|
:key="item.userId"
|
:label="item.nickName"
|
:value="item.nickName"
|
/>
|
</el-select>
|
</template>
|
</el-table-column>
|
<el-table-column label="计划数" min-width="100">
|
<template #default="{ row }">
|
<el-input-number v-model="row.planNum" :min="1" size="small" style="width: 100%" />
|
</template>
|
</el-table-column>
|
<el-table-column label="是否质检" min-width="100">
|
<template #default="{ row }">
|
<el-switch v-model="row.isQuality" :active-value="true" :inactive-value="false" size="small" />
|
</template>
|
</el-table-column>
|
<el-table-column label="计划开始时间" min-width="180">
|
<template #default="{ row }">
|
<el-date-picker
|
v-model="row.planStartTime"
|
type="date"
|
placeholder="选择开始时间"
|
style="width: 100%"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="计划结束时间" min-width="180">
|
<template #default="{ row }">
|
<el-date-picker
|
v-model="row.planEndTime"
|
type="date"
|
placeholder="选择结束时间"
|
style="width: 100%"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" fixed="right" align="center">
|
<template #default="{ $index }">
|
<el-button type="danger" link size="small" @click="removeProcessRouteItem($index)">
|
<el-icon><Delete /></el-icon>
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div v-if="processRouteItems.length === 0" class="empty-tip">
|
<el-empty description="暂无工序,点击上方按钮添加" :image-size="60" />
|
</div>
|
</div>
|
</div>
|
|
<!-- 用料清单 -->
|
<div class="section-card">
|
<div class="section-header">
|
<span class="section-icon">📦</span>
|
<span class="section-title-text">用料清单</span>
|
<el-button type="primary" size="small" @click="addMaterialItem" class="add-btn">
|
<el-icon><Plus /></el-icon> 添加用料
|
</el-button>
|
</div>
|
<div class="table-container">
|
<el-table :data="productStructureRecords" border size="small" class="compact-table">
|
<el-table-column type="index" label="序号" width="50" align="center" />
|
<el-table-column label="图纸编号" prop="model" min-width="120" />
|
<el-table-column label="产品名称" prop="productName" min-width="120" />
|
<el-table-column label="单位产出需要数量" min-width="140">
|
<template #default="{ row }">
|
<el-input-number
|
v-model="row.unitQuantity"
|
:min="0"
|
:precision="2"
|
size="small"
|
style="width: 100%"
|
@change="(val) => handleUnitQuantityChange(val, row)"
|
/>
|
</template>
|
</el-table-column>
|
<el-table-column label="需求数量" min-width="120">
|
<template #default="{ row }">
|
<el-input-number v-model="row.demandedQuantity" :min="0" :precision="2" size="small" style="width: 100%" />
|
</template>
|
</el-table-column>
|
<el-table-column label="单位" min-width="80">
|
<template #default="{ row }">
|
<el-input v-model="row.unit" placeholder="请输入" size="small" />
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="80" fixed="right" align="center">
|
<template #default="{ $index }">
|
<el-button type="danger" link size="small" @click="removeProductStructureRecord($index)">
|
<el-icon><Delete /></el-icon>
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
<div v-if="productStructureRecords.length === 0" class="empty-tip">
|
<el-empty description="暂无用料清单,点击上方按钮添加" :image-size="60" />
|
</div>
|
</div>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button type="primary" @click="handleSubmit">确认</el-button>
|
<el-button @click="closeModal">取消</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import {ref, computed, getCurrentInstance, watch} from "vue";
|
import { Plus, Delete, Upload } from '@element-plus/icons-vue';
|
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
|
import {addProductOrder} from "@/api/productionManagement/productionOrder.js";
|
import {listDeptUserTree} from "@/api/basicData/productProcess.js";
|
import {findProcessRouteItemList} from "@/api/productionManagement/processRouteItem.js";
|
import {listPage as listProductBom} from "@/api/productionManagement/productBom.js";
|
import {listByBomIdIsParent} from "@/api/productionManagement/productStructure.js";
|
import { getToken } from "@/utils/auth.js";
|
|
const props = defineProps({
|
visible: {
|
type: Boolean,
|
required: true,
|
},
|
|
type: {
|
type: String,
|
required: true,
|
default: 'qualified',
|
},
|
});
|
|
const emit = defineEmits(['update:visible', 'completed']);
|
|
// 响应式数据(替代选项式的 data)
|
const formState = ref({
|
productId: undefined,
|
productModelId: undefined,
|
routeId: undefined,
|
productName: "",
|
model: "",
|
unit: "",
|
drawingNumber: "",
|
quantity: 0,
|
deliveryDate: "",
|
tempFileIds: [],
|
salesLedgerFiles: [],
|
});
|
|
// 工序路线明细列表
|
const processRouteItems = ref([]);
|
|
// 物料清单列表
|
const productStructureRecords = ref([]);
|
|
// 用户列表
|
const userOptions = ref([]);
|
|
// 文件列表
|
const fileList = ref([]);
|
|
const upload = reactive({
|
url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
|
headers: { Authorization: "Bearer " + getToken() },
|
data: { type: 14 },
|
});
|
|
const isShow = computed({
|
get() {
|
return props.visible;
|
},
|
set(val) {
|
emit('update:visible', val);
|
},
|
});
|
|
const showProductSelectDialog = ref(false);
|
const showMaterialProductDialog = ref(false);
|
|
// 获取用户列表
|
const fetchUserOptions = () => {
|
listDeptUserTree().then(res => {
|
const users = [];
|
const extractUsers = (nodes) => {
|
nodes.forEach(node => {
|
if (node.userList && node.userList.length > 0) {
|
node.userList.forEach(user => {
|
users.push({
|
userId: user.userId,
|
nickName: user.nickName || user.userName
|
});
|
});
|
}
|
if (node.childrenList && node.childrenList.length > 0) {
|
extractUsers(node.childrenList);
|
}
|
});
|
};
|
extractUsers(res.data || []);
|
userOptions.value = users;
|
});
|
};
|
|
// 组件挂载时获取数据
|
fetchUserOptions();
|
|
let { proxy } = getCurrentInstance()
|
|
const closeModal = () => {
|
// 重置表单数据
|
formState.value = {
|
productId: undefined,
|
productModelId: undefined,
|
routeId: undefined,
|
productName: "",
|
drawingNumber: "",
|
model: "",
|
unit: "",
|
quantity: 0,
|
deliveryDate: "",
|
tempFileIds: [],
|
salesLedgerFiles: [],
|
};
|
// 重置工序路线明细和物料清单
|
processRouteItems.value = [];
|
productStructureRecords.value = [];
|
fileList.value = [];
|
isShow.value = false;
|
};
|
|
// 产品选择处理
|
const handleProductSelect = async (products) => {
|
if (products && products.length > 0) {
|
const product = products[0];
|
formState.value.productId = product.productId;
|
formState.value.productName = product.productName;
|
formState.value.drawingNumber = product.drawingNumber;
|
formState.value.model = product.model;
|
formState.value.productModelId = product.id;
|
formState.value.unit = product.unit;
|
formState.value.routeId = product.routeId;
|
showProductSelectDialog.value = false;
|
|
// 1. 通过产品自带的routeId获取工序列表
|
if (product.routeId) {
|
fetchProcessRouteItems(product.routeId);
|
}
|
|
// 2. 通过产品id查询BOM列表
|
fetchBomList(product.id);
|
|
// 触发表单验证更新
|
proxy.$refs["formRef"]?.validateField('productModelId');
|
}
|
};
|
|
// 工艺路线工序列表
|
const processRouteItemsOptions = ref([]);
|
|
const fetchProcessRouteItems = (routeId) => {
|
processRouteItemsOptions.value = [];
|
findProcessRouteItemList({ routeId: routeId }).then(res => {
|
const items = res.data || [];
|
processRouteItemsOptions.value = items;
|
|
// 自动添加工序
|
processRouteItems.value = items.map(item => ({
|
processId: item.processId,
|
processName: item.processName,
|
productModelId: item.productModelId,
|
userPower: item.userPower ? item.userPower.split(',') : [],
|
planStartTime: "",
|
planEndTime: "",
|
planNum: 1,
|
isQuality: item.isQuality || false,
|
}));
|
});
|
};
|
|
// BOM列表弹窗相关
|
const showBomDialog = ref(false);
|
const bomList = ref([]);
|
const currentProductId = ref(null);
|
|
const fetchBomList = (productModelId) => {
|
currentProductId.value = productModelId;
|
listProductBom({ productModelId: productModelId }).then(res => {
|
const bomData = res.data?.records || [];
|
if (bomData.length === 1) {
|
// 只有一个BOM,直接查询物料清单
|
fetchMaterialList(bomData[0].id);
|
} else if (bomData.length > 1) {
|
// 多个BOM,弹出选择框
|
bomList.value = bomData;
|
showBomDialog.value = true;
|
}
|
});
|
};
|
|
const handleBomSelect = (bom) => {
|
showBomDialog.value = false;
|
fetchMaterialList(bom.id);
|
};
|
|
const fetchMaterialList = (bomId) => {
|
listByBomIdIsParent(bomId).then(res => {
|
const materials = res.data || [];
|
const demandQty = formState.value.quantity || 1;
|
productStructureRecords.value = materials.map(item => ({
|
parentId: item.parentId,
|
productModelId: item.productModelId,
|
model: item.model || '',
|
productName: item.productName || '',
|
productOrderId: undefined,
|
processId: undefined,
|
unitQuantity: item.unitQuantity || 1,
|
demandedQuantity: (item.unitQuantity || 1) * demandQty,
|
unit: item.unit || '',
|
bomId: item.bomId,
|
}));
|
});
|
};
|
|
// 监听物料清单变化,当单位产出需要数量变化时重新计算需求数量
|
watch(() => productStructureRecords.value, (newRecords, oldRecords) => {
|
if (oldRecords && oldRecords.length > 0) {
|
oldRecords.forEach((oldItem, index) => {
|
const newItem = newRecords[index];
|
if (oldItem && newItem && newItem.unitQuantity !== oldItem.unitQuantity) {
|
newItem.demandedQuantity = (newItem.unitQuantity || 1) * (formState.value.quantity || 1);
|
}
|
});
|
}
|
}, { deep: true });
|
|
// 监听需求数量变化,重新计算物料需求数量
|
watch(() => formState.value.quantity, (newQty) => {
|
if (productStructureRecords.value.length > 0 && newQty) {
|
productStructureRecords.value.forEach(item => {
|
item.demandedQuantity = (item.unitQuantity || 1) * newQty;
|
});
|
}
|
});
|
|
// 单位产出需要数量变化处理
|
const handleUnitQuantityChange = (val, row) => {
|
row.demandedQuantity = (val || 1) * (formState.value.quantity || 1);
|
};
|
|
// 工序选择变化处理
|
const handleProcessChange = (processId, row) => {
|
const selectedProcess = processRouteItemsOptions.value.find(item => item.processId === processId);
|
if (selectedProcess) {
|
row.processName = selectedProcess.processName;
|
row.productModelId = selectedProcess.productModelId;
|
row.isQuality = selectedProcess.isQuality || false;
|
// userPower是逗号分隔的用户名,转换为数组
|
if (selectedProcess.userPower) {
|
row.userPower = selectedProcess.userPower.split(',');
|
} else {
|
row.userPower = [];
|
}
|
}
|
};
|
|
// 添加工序
|
const addProductionTask = () => {
|
if (!formState.value.productModelId) {
|
proxy.$modal.msgWarning("请先选择产品");
|
return;
|
}
|
if (processRouteItemsOptions.value.length === 0) {
|
proxy.$modal.msgWarning("请先选择产品以获取工序列表");
|
return;
|
}
|
processRouteItems.value.push({
|
processId: undefined,
|
processName: "",
|
processNo: "",
|
productModelId: undefined,
|
userPower: [],
|
planStartTime: "",
|
planEndTime: "",
|
planNum: 1,
|
isQuality: false,
|
});
|
};
|
|
// 删除工序路线明细
|
const removeProcessRouteItem = (index) => {
|
processRouteItems.value.splice(index, 1);
|
};
|
|
// 添加用料 - 弹出产品选择框
|
const addMaterialItem = () => {
|
showMaterialProductDialog.value = true;
|
};
|
|
// 处理用料产品选择
|
const handleMaterialProductSelect = (products) => {
|
if (products && products.length > 0) {
|
products.forEach(product => {
|
productStructureRecords.value.push({
|
productModelId: product.id,
|
parentId: undefined,
|
productOrderId: undefined,
|
processId: undefined,
|
model: product.model || '',
|
productName: product.productName || '',
|
unitQuantity: 1,
|
demandedQuantity: 1,
|
unit: product.unit,
|
bomId: undefined,
|
});
|
});
|
}
|
showMaterialProductDialog.value = false;
|
};
|
|
// 删除物料清单
|
const removeProductStructureRecord = (index) => {
|
productStructureRecords.value.splice(index, 1);
|
};
|
|
const handleDrawingBeforeUpload = (file) => {
|
const isAllowed = [
|
'application/pdf',
|
'image/jpeg',
|
'image/jpg',
|
'image/png',
|
'application/dwg'
|
].includes(file.type) || file.name.endsWith('.dwg');
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
|
if (!isAllowed) {
|
proxy.$modal.msgError("只能上传 pdf、jpg、jpeg、png、dwg 格式的文件!");
|
return false;
|
}
|
if (!isLt10M) {
|
proxy.$modal.msgError("上传文件大小不能超过 10MB!");
|
return false;
|
}
|
return true;
|
};
|
|
const handleDrawingUploadSuccess = (response, file, fileList) => {
|
console.log('上传成功响应', response);
|
console.log('response.data', response.data);
|
if (response.code === 200) {
|
formState.value.tempFileIds = [response.data?.tempId];
|
formState.value.salesLedgerFiles = [{
|
tempId: response.data?.tempId,
|
originalName: response.data?.originalName || file.name,
|
tempPath: response.data?.tempPath,
|
type: response.data?.type || 14
|
}];
|
proxy.$modal.msgSuccess("上传成功");
|
} else {
|
proxy.$modal.msgError(response.msg || "上传失败");
|
}
|
};
|
|
const handleDrawingRemove = (file) => {
|
formState.value.tempFileIds = [];
|
formState.value.salesLedgerFiles = [];
|
};
|
|
const handleSubmit = () => {
|
proxy.$refs["formRef"].validate(valid => {
|
if (valid) {
|
// 验证是否选择了产品和规格
|
if (!formState.value.productModelId) {
|
proxy.$modal.msgError("请选择产品");
|
return;
|
}
|
if (!formState.value.productModelId) {
|
proxy.$modal.msgError("请选择规格");
|
return;
|
}
|
|
// 处理提交数据 - 将userPower数组转换为逗号分隔的字符串
|
const processedProcessRouteItems = processRouteItems.value.map(item => ({
|
...item,
|
userPower: Array.isArray(item.userPower) ? item.userPower.join(',') : item.userPower
|
}));
|
|
// 组装提交数据
|
const submitData = {
|
...formState.value,
|
processRouteItems: processedProcessRouteItems,
|
productStructureRecords: productStructureRecords.value,
|
files: formState.value.salesLedgerFiles,
|
};
|
|
addProductOrder(submitData).then(res => {
|
// 关闭模态框
|
isShow.value = false;
|
// 告知父组件已完成
|
emit('completed');
|
proxy.$modal.msgSuccess("提交成功");
|
})
|
}
|
})
|
};
|
|
|
defineExpose({
|
closeModal,
|
handleSubmit,
|
isShow,
|
});
|
</script>
|
|
<style scoped>
|
.production-order-dialog :deep(.el-dialog__body) {
|
padding: 15px 20px;
|
max-height: 70vh;
|
overflow-y: auto;
|
}
|
|
.section-card {
|
background: #f8f9fa;
|
border-radius: 8px;
|
padding: 15px;
|
margin-bottom: 15px;
|
}
|
|
.section-header {
|
display: flex;
|
align-items: center;
|
margin-bottom: 15px;
|
padding-bottom: 10px;
|
border-bottom: 1px solid #e4e7ed;
|
}
|
|
.section-icon {
|
font-size: 18px;
|
margin-right: 8px;
|
}
|
|
.section-title-text {
|
font-size: 15px;
|
font-weight: 600;
|
color: #303133;
|
flex: 1;
|
}
|
|
.add-btn {
|
margin-left: auto;
|
}
|
|
.compact-form :deep(.el-form-item) {
|
margin-bottom: 12px;
|
}
|
|
.compact-form :deep(.el-form-item__label) {
|
font-size: 13px;
|
color: #606266;
|
}
|
|
.select-btn {
|
width: 100%;
|
justify-content: flex-start;
|
}
|
|
.table-container {
|
background: #fff;
|
border-radius: 4px;
|
padding: 10px;
|
}
|
|
.compact-table :deep(.el-table__cell) {
|
padding: 4px 0;
|
}
|
|
.compact-table :deep(.el-input__wrapper),
|
.compact-table :deep(.el-input-number) {
|
width: 100%;
|
}
|
|
.empty-tip {
|
padding: 20px 0;
|
}
|
|
.avatar-uploader-icon {
|
font-size: 28px;
|
color: #8c939d;
|
width: 148px;
|
height: 148px;
|
text-align: center;
|
line-height: 148px;
|
}
|
|
:deep(.el-upload--picture-card) {
|
width: 148px;
|
height: 148px;
|
}
|
|
:deep(.el-upload-list__item) {
|
width: 148px;
|
height: 148px;
|
}
|
|
:deep(.el-upload__tip) {
|
font-size: 12px;
|
color: #909399;
|
margin-top: 8px;
|
}
|
|
.upload-inline :deep(.el-upload) {
|
display: inline-flex;
|
}
|
|
.upload-inline :deep(.el-upload__tip) {
|
margin-top: 4px;
|
font-size: 12px;
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: center;
|
gap: 10px;
|
}
|
</style>
|