| | |
| | | }); |
| | | } |
| | | |
| | | // 根据BOM ID查询父项产品 |
| | | export function listByBomIdIsParent(bomId) { |
| | | return request({ |
| | | url: "/productStructure/listBybomIdIsParent/" + bomId, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/productStructure", |
| | |
| | | }); |
| | | } |
| | | |
| | | export function startOrPause(data) { |
| | | return request({ |
| | | url: "/productOrder/startOrPause", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // 生产订单-新增 |
| | | export function addProductOrder(data) { |
| | | return request({ |
| | |
| | | <el-table-column type="index" label="序号" width="60" /> |
| | | <el-table-column prop="productName" label="产品大类" min-width="160" /> |
| | | <el-table-column prop="model" label="图纸编号" min-width="200" /> |
| | | <el-table-column prop="drawingNumber" label="规格型号" min-width="160" /> |
| | | <el-table-column prop="unit" label="单位" min-width="160" /> |
| | | </el-table> |
| | | |
| | |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | label: "工艺路线", |
| | | prop: "routeName", |
| | | minWidth: 150, |
| | | }, |
| | | { |
| | | label: "单位", |
| | | prop: "unit", |
| | | minWidth: 100, |
| | |
| | | modelForm.value = { ...data }; |
| | | modelForm.value.tempFileIds = data.tempFileIds || []; |
| | | modelForm.value.salesLedgerFiles = data.salesLedgerFiles || []; |
| | | if (data.salesLedgerFiles && data.salesLedgerFiles.length > 0) { |
| | | drawingFileList.value = data.salesLedgerFiles.map(file => ({ |
| | | name: file.name, |
| | | url: file.url |
| | | })); |
| | | } else if (data.drawingFile) { |
| | | if (data.drawingFile) { |
| | | drawingFileList.value = [{ |
| | | name: data.drawingFile.split('/').pop(), |
| | | url: data.drawingFile |
| | |
| | | text-align: right; |
| | | } |
| | | |
| | | ::v-deep(.row-finished) { |
| | | :deep(.row-finished) { |
| | | background-color: #f6ffed; |
| | | } |
| | | |
| | | ::v-deep(.row-running) { |
| | | :deep(.row-running) { |
| | | background-color: #fffbe6; |
| | | } |
| | | </style> |
| | |
| | | text-align: right; |
| | | } |
| | | |
| | | ::v-deep(.row-abnormal) { |
| | | :deep(.row-abnormal) { |
| | | background-color: #fff5f5; |
| | | } |
| | | </style> |
| | |
| | | color: #333; |
| | | } |
| | | |
| | | ::v-deep(.row-abnormal) { |
| | | :deep(.row-abnormal) { |
| | | background-color: #fff5f5; |
| | | } |
| | | </style> |
| | |
| | | .table_list { |
| | | margin-top: unset; |
| | | } |
| | | ::v-deep(.el-checkbox__label) { |
| | | :deep(.el-checkbox__label) { |
| | | font-weight: bold; |
| | | } |
| | | .empty-tip { |
| | |
| | | <el-button |
| | | icon="Grid" |
| | | @click="toggleView" |
| | | style="margin-right: 10px;" |
| | | > |
| | | 卡片视图 |
| | | </el-button> |
| | | <el-button type="primary" @click="handleAdd">新增</el-button> |
| | | </div> |
| | | </div> |
| | | <el-table |
| | |
| | | {{ getProcessName(scope.row.processId) || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产品名称" prop="productName" min-width="160" /> |
| | | <el-table-column label="图纸编号" prop="model" min-width="140" /> |
| | | <el-table-column label="规格型号" prop="drawingNumber" min-width="160" /> |
| | | <el-table-column label="单位" prop="unit" width="100" /> |
| | | <el-table-column label="是否质检" prop="isQuality" width="100"> |
| | | <template #default="scope"> |
| | | {{scope.row.isQuality ? "是" : "否"}} |
| | |
| | | <el-button |
| | | icon="Menu" |
| | | @click="toggleView" |
| | | style="margin-right: 10px;" |
| | | > |
| | | 表格视图 |
| | | </el-button> |
| | | <el-button type="primary" @click="handleAdd">新增</el-button> |
| | | </div> |
| | | </div> |
| | | <div v-loading="tableLoading" class="card-container"> |
| | |
| | | <div class="card-process-name">{{ getProcessName(item.processId) || '-' }}</div> |
| | | </div> |
| | | |
| | | <!-- 产品信息 --> |
| | | <div class="card-content"> |
| | | <div v-if="item.productName" class="product-info"> |
| | | <div class="product-name">{{ item.productName }}</div> |
| | | <div class="product-model">{{ item.drawingNumber || '-' }}</div> |
| | | <div v-if="item.model" class="product-model"> |
| | | {{ item.model }} |
| | | <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> --> |
| | | </div> |
| | | <el-tag type="primary" class="product-tag" v-if="item.isQuality">质检</el-tag> |
| | | </div> |
| | | <div v-else class="product-info empty">暂无产品信息</div> |
| | | <el-tag type="primary" class="product-tag" v-if="item.isQuality">质检</el-tag> |
| | | </div> |
| | | |
| | | <!-- 操作按钮 --> |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="产品名称" prop="productModelId"> |
| | | <el-button type="primary" @click="showProductSelectDialog = true"> |
| | | {{ form.productName && form.model |
| | | ? `${form.productName} - ${form.model}` |
| | | : '选择产品' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="单位" prop="unit"> |
| | | <el-input |
| | | v-model="form.unit" |
| | | :placeholder="form.productModelId ? '根据选择的产品自动带出' : '请先选择产品'" |
| | | clearable |
| | | :disabled="true" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否质检" prop="isQuality"> |
| | | <el-switch v-model="form.isQuality" :active-value="true" :inactive-value="false"/> |
| | | </el-form-item> |
| | |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 产品选择对话框 --> |
| | | <ProductSelectDialog |
| | | v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | import { findProcessRouteItemList, addOrUpdateProcessRouteItem, sortProcessRouteItem, batchDeleteProcessRouteItem } from "@/api/productionManagement/processRouteItem.js"; |
| | | import { findProductProcessRouteItemList, deleteRouteItem, addRouteItem, addOrUpdateProductProcessRouteItem, sortRouteItem } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { processList } from "@/api/productionManagement/productionProcess.js"; |
| | |
| | | }); |
| | | |
| | | const processOptions = ref([]); |
| | | const showProductSelectDialog = ref(false); |
| | | const staffList = ref([]); |
| | | |
| | | const treeProps = { |
| | |
| | | id: undefined, |
| | | routeId: routeId.value, |
| | | processId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | userPower: [], |
| | | }); |
| | | |
| | | const rules = { |
| | | processId: [{ required: true, message: '请选择工序', trigger: 'change' }], |
| | | productModelId: [{ required: true, message: '请选择产品', trigger: 'change' }], |
| | | }; |
| | | |
| | | // 根据工序ID获取工序名称 |
| | |
| | | id: row.id, |
| | | routeId: routeId.value, |
| | | processId: row.processId, |
| | | productModelId: row.productModelId, |
| | | productName: row.productName || "", |
| | | model: row.model || "", |
| | | unit: row.unit || "", |
| | | isQuality: row.isQuality, |
| | | userPower: userPowerIds, |
| | | }; |
| | |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | // 产品选择 |
| | | const handleProductSelect = (products) => { |
| | | if (products && products.length > 0) { |
| | | const product = products[0]; |
| | | form.value.productModelId = product.id; |
| | | form.value.productName = product.productName; |
| | | form.value.model = product.model; |
| | | form.value.unit = product.unit || ""; |
| | | showProductSelectDialog.value = false; |
| | | // 触发表单验证 |
| | | formRef.value?.validateField('productModelId'); |
| | | } |
| | | }; |
| | | |
| | | // 提交 |
| | | const handleSubmit = () => { |
| | | formRef.value.validate((valid) => { |
| | |
| | | productOrderId: orderId.value, |
| | | productRouteId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | userPower: userPowerNames.join(','), |
| | | dragSort, |
| | |
| | | : addOrUpdateProcessRouteItem({ |
| | | routeId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | userPower: userPowerNames.join(','), |
| | | dragSort, |
| | |
| | | ? addOrUpdateProductProcessRouteItem({ |
| | | id: form.value.id, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | isQuality: form.value.isQuality, |
| | | userPower: userPowerNames.join(','), |
| | | }) |
| | | : addOrUpdateProcessRouteItem({ |
| | | routeId: routeId.value, |
| | | processId: form.value.processId, |
| | | productModelId: form.value.productModelId, |
| | | id: form.value.id, |
| | | isQuality: form.value.isQuality, |
| | | userPower: userPowerNames.join(','), |
| | |
| | | id: undefined, |
| | | routeId: routeId.value, |
| | | processId: undefined, |
| | | productModelId: undefined, |
| | | productName: "", |
| | | model: "", |
| | | unit: "", |
| | | isQuality: false, |
| | | userPower: [], |
| | | }; |
| | | formRef.value?.resetFields(); |
| | | }; |
| | |
| | | |
| | | .process-card { |
| | | flex-shrink: 0; |
| | | width: 220px; |
| | | width: 160px; |
| | | background: #fff; |
| | | border: 1px solid #e4e7ed; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
| | | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); |
| | | padding: 16px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | cursor: move; |
| | | transition: all 0.3s; |
| | | transition: all 0.2s; |
| | | } |
| | | |
| | | .process-card:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | } |
| | | |
| | | .card-header { |
| | |
| | | } |
| | | |
| | | .card-number { |
| | | width: 36px; |
| | | height: 36px; |
| | | line-height: 36px; |
| | | width: 28px; |
| | | height: 28px; |
| | | line-height: 28px; |
| | | border-radius: 50%; |
| | | background: #409eff; |
| | | color: #fff; |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | font-size: 13px; |
| | | margin: 0 auto 8px; |
| | | } |
| | | |
| | | .card-process-name { |
| | | font-size: 14px; |
| | | color: #333; |
| | | color: #303133; |
| | | font-weight: 500; |
| | | word-break: break-all; |
| | | } |
| | |
| | | .card-content { |
| | | flex: 1; |
| | | margin-bottom: 12px; |
| | | min-height: 60px; |
| | | min-height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | |
| | | <div class="app-container"> |
| | | <PageHeader content="产品结构详情"> |
| | | <template #right-button> |
| | | <el-button v-if="dataValue.isEdit && !isOrderPage" |
| | | @click="cancelEdit">取消 |
| | | </el-button> |
| | | <el-button v-if="!dataValue.isEdit && !isOrderPage" |
| | | type="primary" |
| | | @click="dataValue.isEdit = true">编辑 |
| | | </el-button> |
| | | <el-button v-if="dataValue.isEdit && !isOrderPage" |
| | | type="primary" |
| | | @click="cancelEdit">取消 |
| | | @click="dataValue.isEdit = true">点击进行修改 |
| | | </el-button> |
| | | <el-button v-if="!isOrderPage" |
| | | type="primary" |
| | | @click="openBomAddDialog" |
| | | :disabled="!dataValue.isEdit"> |
| | | <el-icon><Document /></el-icon> 按BOM添加 |
| | | </el-button> |
| | | <el-button v-if="!isOrderPage" |
| | | type="success" |
| | | :loading="dataValue.loading" |
| | | @click="submit" |
| | | :disabled="!dataValue.isEdit">确认 |
| | |
| | | label="图纸编号"> |
| | | <template #default="{ row, $index }"> |
| | | <el-form-item v-if="dataValue.isEdit" |
| | | :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]" |
| | | style="margin: 0"> |
| | | :rules="[{ required: true, message: '请选择规格', trigger: ['blur','change'] }]" |
| | | style="margin: 0"> |
| | | <el-select v-model="row.model" |
| | | placeholder="请选择规格" |
| | | clearable |
| | |
| | | </el-form-item> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="drawingNumber" |
| | | label="规格型号" /> |
| | | <el-table-column prop="processName" |
| | | label="消耗工序"> |
| | | label="工序"> |
| | | <template #default="{ row, $index }"> |
| | | <el-form-item v-if="dataValue.isEdit" |
| | | :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]" |
| | | :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选工序', trigger: 'change' }]" |
| | | style="margin: 0"> |
| | | <el-select v-model="row.processId" |
| | | placeholder="请选择" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unitQuantity" |
| | | label="单位产出所需数量"> |
| | | label="单位用量"> |
| | | <template #default="{ row, $index }"> |
| | | <el-form-item v-if="dataValue.isEdit" |
| | | :rules="[{ required: true, message: '请输入单位产出所需数量', trigger: ['blur','change'] }]" |
| | | :rules="[{ required: true, message: '请输入单位用量', trigger: ['blur','change'] }]" |
| | | style="margin: 0"> |
| | | <el-input-number v-model="row.unitQuantity" |
| | | :min="0" |
| | |
| | | </el-form> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="BOM编号" |
| | | <el-table-column v-if="!isOrderPage" |
| | | label="BOM编号" |
| | | prop="bomNo" /> |
| | | <el-table-column label="产品名称" |
| | | prop="productName" /> |
| | | <el-table-column label="图纸编号" |
| | | prop="model" /> |
| | | <el-table-column label="规格型号" |
| | | prop="drawingNumber" /> |
| | | prop="model" /> |
| | | </el-table> |
| | | <product-select-dialog v-if="dataValue.showProductDialog" |
| | | v-model:model-value="dataValue.showProductDialog" |
| | | @confirm="handleProduct" /> |
| | | |
| | | <!-- 按BOM添加弹窗 --> |
| | | <el-dialog v-model="bomAddDialogVisible" title="按BOM添加" width="500px" @close="closeBomAddDialog"> |
| | | <el-form ref="bomAddFormRef" :model="bomAddForm" :rules="bomAddRules" label-width="100px"> |
| | | <el-form-item label="父项产品" prop="parentProductId"> |
| | | <el-select v-model="bomAddForm.parentProductId" placeholder="请选择" clearable filterable |
| | | style="width: 100%" @change="handleBomParentProductChange"> |
| | | <el-option v-for="item in parentProductOptions" :key="item.id" |
| | | :label="`${item.model || item.productCode || item.id} | ${item.productName}`" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <!-- 选中产品后展示产品信息 --> |
| | | <div v-if="selectedBomProduct" class="selected-product-info"> |
| | | <div class="product-info-row"> |
| | | <span class="info-label">产品编号</span> |
| | | <span class="info-value">{{ selectedBomProduct.model || selectedBomProduct.productCode || '-' }}</span> |
| | | </div> |
| | | <div class="product-info-row"> |
| | | <span class="info-label">产品名称</span> |
| | | <span class="info-value">{{ selectedBomProduct.productName || '-' }}</span> |
| | | </div> |
| | | <div class="product-info-row"> |
| | | <span class="info-label">产品规格</span> |
| | | <span class="info-value">{{ selectedBomProduct.spec || selectedBomProduct.drawingNumber || '-' }}</span> |
| | | </div> |
| | | <div class="stock-info-box"> |
| | | <div class="stock-number">{{ selectedBomProduct.stockQuantity || 0 }}</div> |
| | | <div class="stock-label">库存数量(台)</div> |
| | | </div> |
| | | </div> |
| | | <el-form-item label="用量系数" prop="coefficient" style="margin-top: 20px;"> |
| | | <el-input-number v-model="bomAddForm.coefficient" :min="0.01" :precision="2" :step="1" |
| | | controls-position="right" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" @click="submitBomAdd">确定</el-button> |
| | | <el-button @click="closeBomAddDialog">取消</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 高级选择产品弹窗 --> |
| | | <product-select-dialog v-if="showAdvancedProductDialog" v-model:model-value="showAdvancedProductDialog" |
| | | @confirm="handleAdvancedProductSelect" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | reactive, |
| | | ref, |
| | | } from "vue"; |
| | | import { queryList, add } from "@/api/productionManagement/productStructure.js"; |
| | | import { queryList, add, listByBomIdIsParent } from "@/api/productionManagement/productStructure.js"; |
| | | import { listProcessBom } from "@/api/productionManagement/productionOrder.js"; |
| | | import { list } from "@/api/productionManagement/productionProcess"; |
| | | import { productListPage } from "@/api/basicData/product.js"; |
| | | import { listPage as listBomPage } from "@/api/productionManagement/productBom.js"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import { Search, Document } from '@element-plus/icons-vue' |
| | | |
| | | defineComponent({ |
| | | name: "StructureEdit", |
| | |
| | | loading: false, |
| | | isEdit: false, |
| | | }); |
| | | |
| | | // 按BOM添加相关 |
| | | const bomAddDialogVisible = ref(false); |
| | | const bomAddFormRef = ref(); |
| | | const parentProductOptions = ref([]); |
| | | const selectedBomProduct = ref(null); |
| | | const selectedBomTreeData = ref([]); // 保存选中产品时获取的BOM树数据 |
| | | const bomAddForm = reactive({ |
| | | parentProductId: undefined, |
| | | coefficient:1 |
| | | }); |
| | | const bomAddRules = { |
| | | parentProductId: [{ required: true, message: "请选择父项产品", trigger: "change" }], |
| | | coefficient: [{ required: true, message: "请输入用量系数", trigger: "blur" }] |
| | | }; |
| | | |
| | | const tableData = reactive([ |
| | | { |
| | |
| | | isValid = false; |
| | | return; |
| | | } |
| | | if (!isTopLevel && !item.processId) { |
| | | ElMessage.error("请选择消耗工序"); |
| | | isValid = false; |
| | | return; |
| | | } |
| | | if (!item.unitQuantity) { |
| | | ElMessage.error("请输入单位产出所需数量"); |
| | | ElMessage.error("请输入单位用量"); |
| | | isValid = false; |
| | | return; |
| | | } |
| | |
| | | fetchData(); |
| | | }; |
| | | |
| | | // 获取父项产品列表 |
| | | const getParentProductList = async () => { |
| | | try { |
| | | const { data } = await listBomPage({ current: 1, size: 1000 }); |
| | | parentProductOptions.value = data?.records || []; |
| | | } catch (error) { |
| | | console.error("获取父项产品列表失败:", error); |
| | | parentProductOptions.value = []; |
| | | } |
| | | }; |
| | | |
| | | // 打开按BOM添加弹窗 |
| | | const openBomAddDialog = () => { |
| | | bomAddDialogVisible.value = true; |
| | | bomAddForm.parentProductId = undefined; |
| | | bomAddForm.coefficient = 1; |
| | | selectedBomProduct.value = null; |
| | | getParentProductList(); |
| | | }; |
| | | |
| | | // 关闭按BOM添加弹窗 |
| | | const closeBomAddDialog = () => { |
| | | bomAddDialogVisible.value = false; |
| | | bomAddFormRef.value?.resetFields(); |
| | | selectedBomProduct.value = null; |
| | | }; |
| | | |
| | | // 打开高级选择 |
| | | const openAdvancedSelect = () => { |
| | | showAdvancedProductDialog.value = true; |
| | | }; |
| | | |
| | | // 父项产品变更 |
| | | const handleBomParentProductChange = async (val) => { |
| | | if (val) { |
| | | const product = parentProductOptions.value.find(item => item.id === val); |
| | | selectedBomProduct.value = product || null; |
| | | |
| | | // 直接用选中的 BOM ID 调用 listByBomIdIsParent |
| | | const { data: treeData } = await listByBomIdIsParent(val); |
| | | selectedBomTreeData.value = treeData || []; |
| | | // 将二级树中的产品添加到父项产品选项(从第二级开始) |
| | | const addTreeToOptions = (items: any[]) => { |
| | | items.forEach((item: any) => { |
| | | // 跳过第一级,只添加第二级及以下的产品 |
| | | if (item.children && item.children.length > 0) { |
| | | item.children.forEach((child: any) => { |
| | | const exists = parentProductOptions.value.some(opt => opt.id === child.id); |
| | | if (!exists) { |
| | | parentProductOptions.value.push({ |
| | | id: child.id, |
| | | productName: child.productName, |
| | | model: child.model, |
| | | productCode: child.model, |
| | | spec: child.drawingNumber, |
| | | drawingNumber: child.drawingNumber, |
| | | stockQuantity: child.stockQuantity || 0 |
| | | }); |
| | | } |
| | | // 递归添加子项 |
| | | if (child.children && child.children.length > 0) { |
| | | addTreeToOptions([child]); |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | if (selectedBomTreeData.value.length > 0) { |
| | | addTreeToOptions(selectedBomTreeData.value); |
| | | } |
| | | } else { |
| | | selectedBomProduct.value = null; |
| | | selectedBomTreeData.value = []; |
| | | } |
| | | }; |
| | | |
| | | // 提交按BOM添加 |
| | | const submitBomAdd = () => { |
| | | bomAddFormRef.value.validate(async (valid) => { |
| | | if (!valid) return; |
| | | |
| | | const product = parentProductOptions.value.find(item => item.id === bomAddForm.parentProductId); |
| | | if (!product) { |
| | | ElMessage.error("未找到选中的产品"); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | // 使用选择产品时保存的BOM树数据 |
| | | const bomItems = selectedBomTreeData.value || []; |
| | | |
| | | if (bomItems.length === 0) { |
| | | ElMessage.warning("该产品没有BOM信息"); |
| | | return; |
| | | } |
| | | |
| | | // 列表的第一级已经存在,把BOM数据作为第一级的子项(第二级)添加 |
| | | if (dataValue.dataList.length > 0) { |
| | | const firstLevelItem = dataValue.dataList[0]; |
| | | |
| | | // 把BOM数据添加到第一级的children中 |
| | | const addBomItemsRecursively = (items: any[], parentItem: any) => { |
| | | items.forEach((item: any) => { |
| | | const newItem: any = { |
| | | parentId: item.parentId || "", |
| | | parentTempId: parentItem.tempId || "", |
| | | productName: item.productName || "", |
| | | productId: item.productId || item.productModelId || "", |
| | | model: item.model || "", |
| | | productModelId: item.productModelId || "", |
| | | drawingNumber: item.drawingNumber || "", |
| | | processId: item.processId || "", |
| | | processName: item.processName || "", |
| | | unitQuantity: (item.unitQuantity || 0) * bomAddForm.coefficient, |
| | | demandedQuantity: (item.demandedQuantity || 0) * bomAddForm.coefficient, |
| | | unit: item.unit || "", |
| | | children: [], |
| | | tempId: new Date().getTime() + Math.random(), |
| | | }; |
| | | |
| | | // 添加到父项的children |
| | | if (!parentItem.children) { |
| | | parentItem.children = []; |
| | | } |
| | | parentItem.children.push(newItem); |
| | | |
| | | // 递归处理子项 |
| | | if (item.children && item.children.length > 0) { |
| | | addBomItemsRecursively(item.children, newItem); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | addBomItemsRecursively(bomItems, firstLevelItem); |
| | | } |
| | | |
| | | ElMessage.success("添加成功"); |
| | | closeBomAddDialog(); |
| | | } catch (error) { |
| | | console.error("按BOM添加失败:", error); |
| | | ElMessage.error("添加失败"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | onMounted(async () => { |
| | | // 从路由参数回显数据 |
| | | tableData[0].productName = routeProductName.value as string; |
| | |
| | | await fetchProcessOptions(); |
| | | await fetchData(); |
| | | }); |
| | | </script> |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .selected-product-info { |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | padding: 16px; |
| | | margin: 10px 0; |
| | | position: relative; |
| | | } |
| | | |
| | | .product-info-row { |
| | | display: flex; |
| | | margin-bottom: 8px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .product-info-row:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .info-label { |
| | | color: #909399; |
| | | width: 70px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .info-value { |
| | | color: #303133; |
| | | flex: 1; |
| | | } |
| | | |
| | | .stock-info-box { |
| | | position: absolute; |
| | | right: 16px; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | text-align: center; |
| | | } |
| | | |
| | | .stock-number { |
| | | font-size: 28px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | line-height: 1; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .stock-label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div style="text-align: right; margin-bottom: 10px;"> |
| | | <el-button type="primary" @click="handleAdd">新增</el-button> |
| | | <el-button type="info" plain icon="Upload" @click="handleImport" |
| | | v-hasPermi="['product:bom:import']">导入</el-button> |
| | | <el-button type="warning" plain icon="Download" @click="handleExport" :disabled="selectedRows.length !== 1" |
| | | v-hasPermi="['product:bom:export']">导出</el-button> |
| | | <el-button type="primary" @click="handleAdd">新增</el-button> |
| | | <el-button type="danger" plain @click="handleBatchDelete" :disabled="selectedRows.length === 0">删除</el-button> |
| | | </div> |
| | | <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" |
| | |
| | | prop: "productModelName", |
| | | minWidth: 140 |
| | | }, |
| | | { |
| | | label: "规格型号", |
| | | prop: "drawingNumber", |
| | | minWidth: 160 |
| | | }, |
| | | { |
| | | label: "版本号", |
| | | prop: "version", |
| | |
| | | <el-dialog |
| | | v-model="isShow" |
| | | title="新增生产订单" |
| | | width="800" |
| | | width="1500" |
| | | :close-on-click-modal="false" |
| | | @close="closeModal" |
| | | class="production-order-dialog" |
| | | > |
| | | <el-form label-width="140px" :model="formState" label-position="top" ref="formRef"> |
| | | <el-form-item |
| | | label="产品名称" |
| | | prop="productModelId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: '请选择产品', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-button type="primary" @click="showProductSelectDialog = true"> |
| | | {{ formState.productName ? formState.productName : '选择产品' }} |
| | | </el-button> |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="图纸编号" |
| | | prop="productModelName" |
| | | > |
| | | <el-input v-model="formState.productModelName" disabled /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="规格型号" |
| | | prop="drawingNumber" |
| | | > |
| | | <el-input v-model="formState.drawingNumber" disabled /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="单位" |
| | | prop="unit" |
| | | > |
| | | <el-input v-model="formState.unit" disabled /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="工艺路线"> |
| | | <el-select v-model="formState.routeId" |
| | | placeholder="请选择工艺路线" |
| | | style="width: 100%;" |
| | | :loading="bindRouteLoading"> |
| | | <el-option v-for="item in routeOptions" |
| | | :key="item.id" |
| | | :label="`${item.processRouteCode || ''}`" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="需求数量" |
| | | prop="quantity" |
| | | > |
| | | <el-input-number v-model="formState.quantity" :step="1" :min="1" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <!-- 基本信息 --> |
| | | <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 |
| | |
| | | @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%" /> |
| | | </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> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, computed, getCurrentInstance} from "vue"; |
| | | 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, listProcessRoute} from "@/api/productionManagement/productionOrder.js"; |
| | | 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: { |
| | |
| | | productModelId: undefined, |
| | | routeId: undefined, |
| | | productName: "", |
| | | productModelName: "", |
| | | 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({ |
| | |
| | | }); |
| | | |
| | | 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() |
| | | |
| | |
| | | routeId: undefined, |
| | | productName: "", |
| | | drawingNumber: "", |
| | | productModelName: "", |
| | | quantity: '', |
| | | model: "", |
| | | unit: "", |
| | | quantity: 0, |
| | | deliveryDate: "", |
| | | tempFileIds: [], |
| | | salesLedgerFiles: [], |
| | | }; |
| | | // 重置工序路线明细和物料清单 |
| | | processRouteItems.value = []; |
| | | productStructureRecords.value = []; |
| | | fileList.value = []; |
| | | isShow.value = false; |
| | | }; |
| | | |
| | |
| | | formState.value.productId = product.productId; |
| | | formState.value.productName = product.productName; |
| | | formState.value.drawingNumber = product.drawingNumber; |
| | | formState.value.productModelName = product.model; |
| | | formState.value.model = product.model; |
| | | formState.value.productModelId = product.id; |
| | | formState.value.unit = product.unit; |
| | | formState.value.routeId = product.routeId; |
| | | showProductSelectDialog.value = false; |
| | | fetchRouteOptions( product.id); |
| | | |
| | | // 1. 通过产品自带的routeId获取工序列表 |
| | | if (product.routeId) { |
| | | fetchProcessRouteItems(product.routeId); |
| | | } |
| | | |
| | | // 2. 通过产品id查询BOM列表 |
| | | fetchBomList(product.id); |
| | | |
| | | // 触发表单验证更新 |
| | | proxy.$refs["formRef"]?.validateField('productModelId'); |
| | | } |
| | | }; |
| | | |
| | | const routeOptions = ref([]); |
| | | const bindRouteLoading = ref(false); |
| | | const fetchRouteOptions = (productModelId) => { |
| | | formState.value.routeId = undefined; |
| | | routeOptions.value = [] |
| | | bindRouteLoading.value = true; |
| | | listProcessRoute({ productModelId: productModelId }).then(res => { |
| | | routeOptions.value = res.data || []; |
| | | }).finally(() => { |
| | | bindRouteLoading.value = false; |
| | | }) |
| | | } |
| | | // 工艺路线工序列表 |
| | | 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(() => formState.value.quantity, (newQty) => { |
| | | if (productStructureRecords.value.length > 0 && newQty) { |
| | | productStructureRecords.value = productStructureRecords.value.map(item => ({ |
| | | ...item, |
| | | demandedQuantity: (item.unitQuantity || 1) * newQty |
| | | })); |
| | | } |
| | | }); |
| | | |
| | | // 工序选择变化处理 |
| | | 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 => { |
| | |
| | | return; |
| | | } |
| | | |
| | | addProductOrder(formState.value).then(res => { |
| | | // 处理提交数据 - 将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; |
| | | // 告知父组件已完成 |
| | |
| | | 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> |
| | |
| | | <div class="search_form"> |
| | | <el-form :model="searchForm" |
| | | :inline="true"> |
| | | <el-form-item label="客户名称:"> |
| | | <el-input v-model="searchForm.customerName" |
| | | placeholder="请输入" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 160px;" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="合同号:"> |
| | | <el-input v-model="searchForm.salesContractNo" |
| | | placeholder="请输入" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 160px;" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="产品名称:"> |
| | | <el-input v-model="searchForm.productCategory" |
| | | placeholder="请输入" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 160px;" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="规格:"> |
| | | <el-input v-model="searchForm.specificationModel" |
| | | placeholder="请输入" |
| | | clearable |
| | | prefix-icon="Search" |
| | |
| | | |
| | | <new-product-order v-if="isShowNewModal" |
| | | v-model:visible="isShowNewModal" |
| | | type="qualified" |
| | | @completed="handleQuery" /> |
| | | </div> |
| | | </template> |
| | |
| | | productOrderListPage, |
| | | listProcessRoute, |
| | | bindingRoute, |
| | | listProcessBom, delProductOrder, |
| | | listProcessBom, delProductOrder, startOrPause, |
| | | } from "@/api/productionManagement/productionOrder.js"; |
| | | import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import {fileDel} from "@/api/financialManagement/revenueManagement.js"; |
| | |
| | | width: '120px', |
| | | }, |
| | | { |
| | | label: "销售合同号", |
| | | prop: "salesContractNo", |
| | | width: '150px', |
| | | }, |
| | | { |
| | | label: "客户名称", |
| | | prop: "customerName", |
| | | width: '200px', |
| | | }, |
| | | { |
| | | label: "产品名称", |
| | | prop: "productCategory", |
| | | width: '120px', |
| | |
| | | label: "图纸编号", |
| | | prop: "specificationModel", |
| | | width: '160px', |
| | | }, |
| | | { |
| | | label: "规格型号", |
| | | prop: "drawingNumber", |
| | | width: '120px', |
| | | }, |
| | | { |
| | | label: "工艺路线编号", |
| | |
| | | label: "操作", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 200, |
| | | width: 240, |
| | | operation: [ |
| | | { |
| | | name: "开始", |
| | | type: "text", |
| | | showHide: row => row.status === '待生产', |
| | | clickFun: row => { |
| | | handleStartOrPause(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "暂停", |
| | | type: "text", |
| | | showHide: row => row.status === '生产中', |
| | | clickFun: row => { |
| | | handleStartOrPause(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "工艺路线", |
| | | type: "text", |
| | |
| | | }, |
| | | }, |
| | | { |
| | | name: "产品结构", |
| | | name: "物料清单", |
| | | type: "text", |
| | | clickFun: row => { |
| | | showProductStructure(row); |
| | |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | customerName: "", |
| | | salesContractNo: "", |
| | | projectName: "", |
| | | productCategory: "", |
| | | specificationModel: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | |
| | | if (row.isFh) return ''; |
| | | |
| | | const diff = row.deliveryDaysDiff; |
| | | if (diff === undefined || diff === null || diff === '' || diff < 0) return ''; |
| | | |
| | | if (diff === 15) { |
| | | return 'yellow'; |
| | | } else if (diff === 10) { |
| | |
| | | const orderId = row.id; |
| | | try { |
| | | const res = await getOrderProcessRouteMain(orderId); |
| | | const data = res.data || {}; |
| | | if (!data || !data.id) { |
| | | const dataList = res.data || []; |
| | | if (!dataList || dataList.length === 0 || !dataList[0].id) { |
| | | proxy.$modal.msgWarning("未找到关联的工艺路线"); |
| | | return; |
| | | } |
| | | const data = dataList[0]; |
| | | router.push({ |
| | | path: "/productionManagement/processRouteItem", |
| | | query: { |
| | |
| | | } |
| | | }; |
| | | |
| | | const showProductStructure = row => { |
| | | const handleStartOrPause = async (row) => { |
| | | const operation = row.status === '待生产' ? 1 : 2; |
| | | const operationText = operation === 1 ? "开始" : "暂停"; |
| | | try { |
| | | await startOrPause({ id: row.id, operation }); |
| | | proxy.$modal.msgSuccess(`${operationText}成功`); |
| | | getList(); |
| | | } catch (e) { |
| | | console.error(`${operationText}失败:`, e); |
| | | proxy.$modal.msgError(`${operationText}失败`); |
| | | } |
| | | }; |
| | | |
| | | const showProductStructure = async row => { |
| | | let bomNo = row.bomNo || ""; |
| | | if (!bomNo && row.id) { |
| | | try { |
| | | const res = await getOrderProcessRouteMain(row.id); |
| | | const dataList = res.data || []; |
| | | if (dataList && dataList.length > 0 && dataList[0].bomNo) { |
| | | bomNo = dataList[0].bomNo; |
| | | } |
| | | } catch (e) { |
| | | console.error("获取BOM编号失败:", e); |
| | | } |
| | | } |
| | | router.push({ |
| | | path: "/productionManagement/productStructureDetail", |
| | | query: { |
| | | id: row.id, |
| | | bomNo: row.bomNo || "", |
| | | bomNo: bomNo, |
| | | drawingNumber: row.drawingNumber || "", |
| | | productName: row.productCategory || "", |
| | | productModelName: row.specificationModel || "", |
| | |
| | | align-items: start; |
| | | } |
| | | |
| | | ::v-deep .yellow { |
| | | :deep(.yellow) { |
| | | background-color: #FAF0DE; |
| | | } |
| | | |
| | | ::v-deep .pink { |
| | | :deep(.pink) { |
| | | background-color: #FAE1DE; |
| | | } |
| | | |
| | | ::v-deep .red { |
| | | background-color: #f80202; |
| | | :deep(.red) { |
| | | background-color: #FFCCCC; |
| | | } |
| | | |
| | | ::v-deep .purple{ |
| | | :deep(.purple){ |
| | | background-color: #F4DEFA; |
| | | } |
| | | </style> |
| | |
| | | // 监听 record 变化,更新表单数据 |
| | | watch(() => props.record, (newRecord) => { |
| | | if (newRecord && isShow.value) { |
| | | const userPowerNames = newRecord.userPower ? newRecord.userPower.split(',') : []; |
| | | const userPowerIds = userPowerNames.map(name => { |
| | | const user = findUserByName(name); |
| | | return user ? user.id : null; |
| | | }).filter(id => id !== null); |
| | | |
| | | formState.value = { |
| | | id: newRecord.id, |
| | | name: newRecord.name || '', |
| | |
| | | remark: newRecord.remark || '', |
| | | salaryQuota: newRecord.salaryQuota || '', |
| | | isQuality: newRecord.isQuality, |
| | | userPower: userPowerIds, |
| | | userPower: [], |
| | | }; |
| | | |
| | | // 等待 staffList 加载完成后,再查找用户ID |
| | | if (staffList.value.length > 0) { |
| | | setUserPowerIds(newRecord.userPower); |
| | | } else { |
| | | // staffList 还没加载,等待一下再查找 |
| | | const checkStaffList = () => { |
| | | if (staffList.value.length > 0) { |
| | | setUserPowerIds(newRecord.userPower); |
| | | } else { |
| | | setTimeout(checkStaffList, 100); |
| | | } |
| | | }; |
| | | checkStaffList(); |
| | | } |
| | | } |
| | | }, { immediate: true, deep: true }); |
| | | |
| | | // 根据用户名查找用户ID |
| | | const setUserPowerIds = (userPowerStr) => { |
| | | const userPowerNames = userPowerStr ? userPowerStr.split(',') : []; |
| | | const userPowerIds = []; |
| | | const findUserIds = (nodes, targetName) => { |
| | | nodes.forEach(node => { |
| | | if (node.isUser && node.label === targetName) { |
| | | userPowerIds.push(node.id); |
| | | } |
| | | if (node.children && node.children.length > 0) { |
| | | findUserIds(node.children, targetName); |
| | | } |
| | | }); |
| | | }; |
| | | userPowerNames.forEach(name => { |
| | | findUserIds(staffList.value, name); |
| | | }); |
| | | formState.value.userPower = userPowerIds; |
| | | }; |
| | | |
| | | let { proxy } = getCurrentInstance() |
| | | |
| | | const closeModal = () => { |
| | |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "销售合同号", |
| | | prop: "salesContractNo", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "产品名称", |
| | | prop: "productName", |
| | | width: 120, |
| | |
| | | label: "产品图纸编号", |
| | | prop: "productModelName", |
| | | width: 160, |
| | | }, |
| | | { |
| | | label: "产品规格型号", |
| | | prop: "drawingNumber", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "产出数量", |
| | |
| | | prop: "model" |
| | | }, |
| | | { |
| | | label: "规格型号", |
| | | prop: "drawingNumber", |
| | | }, |
| | | { |
| | | label: "单位", |
| | | prop: "unit", |
| | | }, |
| | |
| | | clickFun: row => { |
| | | showReportDialog(row); |
| | | }, |
| | | disabled: row => row.planQuantity <= 0, |
| | | disabled: row => row.status === 1 || row.status === 3 || row.planQuantity <= 0, |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | .justify-between { |
| | | justify-content: space-between; |
| | | } |
| | | ::v-deep(.el-checkbox__label) { |
| | | :deep(.el-checkbox__label) { |
| | | font-weight: bold; |
| | | } |
| | | </style> |
| | |
| | | .table_list { |
| | | margin-top: unset; |
| | | } |
| | | ::v-deep(.el-checkbox__label) { |
| | | :deep(.el-checkbox__label) { |
| | | font-weight: bold; |
| | | } |
| | | .actions { |
| | |
| | | margin-left: 10px; |
| | | } |
| | | |
| | | ::v-deep .yellow { |
| | | :deep(.yellow) { |
| | | background-color: #FAF0DE; |
| | | } |
| | | |
| | | ::v-deep .pink { |
| | | :deep(.pink) { |
| | | background-color: #FAE1DE; |
| | | } |
| | | |
| | | ::v-deep .red { |
| | | :deep(.red) { |
| | | background-color: #FAE1DE; |
| | | } |
| | | |
| | | ::v-deep .purple{ |
| | | :deep(.purple){ |
| | | background-color: #F4DEFA; |
| | | } |
| | | |