gaoluyang
2 天以前 36b909e117c3ccc22dd266a94479e2a02335d261
src/views/productionManagement/productionOrder/New.vue
@@ -3,66 +3,87 @@
    <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="drawingNumber"
        >
          <el-input v-model="formState.drawingNumber"  disabled />
        </el-form-item>
        <el-form-item
            label="规格"
            prop="productModelName"
        >
          <el-input v-model="formState.productModelName"  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
@@ -70,6 +91,167 @@
          @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>
@@ -81,9 +263,15 @@
</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: {
@@ -106,10 +294,31 @@
  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({
@@ -122,6 +331,34 @@
});
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()
@@ -133,9 +370,17 @@
    routeId: undefined,
    productName: "",
    drawingNumber: "",
    productModelName: "",
    quantity: '',
    model: "",
    unit: "",
    quantity: 0,
    deliveryDate: "",
    tempFileIds: [],
    salesLedgerFiles: [],
  };
  // 重置工序路线明细和物料清单
  processRouteItems.value = [];
  productStructureRecords.value = [];
  fileList.value = [];
  isShow.value = false;
};
@@ -146,28 +391,219 @@
    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 => {
@@ -182,7 +618,21 @@
        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;
        // 告知父组件已完成
@@ -200,3 +650,115 @@
  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>