buhuazhen
2026-05-12 181002704e5df616539e6250f975fe7de783b6e2
feat(productionOrder): 新增修改工艺路线功能,优化绑定弹窗逻辑

生产订单列表新增修改工艺路线操作列,按hasProduct控制显示隐藏
重构工艺绑定弹窗,支持查看、编辑、新增三种交互模式
新增材料选型缓存机制,减少重复接口请求
修复表单字段匹配与id传递问题,统一数据处理逻辑
优化弹窗内表单的只读展示样式,提升交互体验
已修改2个文件
211 ■■■■ 文件已修改
src/views/productionManagement/productionOrder/BindRouteDialog.vue 174 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/BindRouteDialog.vue
@@ -62,7 +62,7 @@
        <el-table-column label="材料名称">
          <template #default="{ row, $index }">
            <el-tree-select
                v-if="!isDetail"
                v-if="props.type === 'add'"
                v-model="row.productById"
                placeholder="请选择"
                clearable
@@ -82,7 +82,7 @@
          </template>
          <template #default="{ row }">
            <el-select
                v-if="!isDetail"
                v-if="props.type === 'add'"
                v-model="row.productModelId"
                placeholder="请选择规格"
                filterable
@@ -97,8 +97,7 @@
                  :value="item.id"
              />
            </el-select>
            <el-text class="mx-1" v-else>Default</el-text>
<!--            <span v-else>{{ row.model }}</span>-->
            <span v-else>{{ row.model }}</span>
          </template>
        </el-table-column>
        <el-table-column label="数量">
@@ -108,8 +107,9 @@
          </template>
        </el-table-column>
        <el-table-column label="计量单位">
          <template #default="{ row }">
            <el-input v-model="row.unit" placeholder="计量单位" :disabled="isDetail"/>
          <template  #default="{ row }">
            <el-input  v-if="props.type === 'add'" v-model="row.unit" placeholder="计量单位" :disabled="isDetail"/>
            <span v-else>{{ row.unit }}</span>
          </template>
        </el-table-column>
        <el-table-column label="单价">
@@ -461,6 +461,7 @@
const filePreviewRef = ref()
const formData = reactive({
  id: null,
  productOrderList: null,
  salesLedgerId: null,
  productOrderId: null,
@@ -514,6 +515,8 @@
  ],
  materialInfo: [
    {
      parentId: "",
      productById: "",
      productId: "",
      name: "",
      productModelId: "",
@@ -562,6 +565,7 @@
const cloneDeep = (val) => JSON.parse(JSON.stringify(val))
const createDefaultFormData = () => ({
  id: null,
  productOrderList: null,
  salesLedgerId: null,
  productOrderId: null,
@@ -615,6 +619,8 @@
  ],
  materialInfo: [
    {
      parentId: "",
      productById: "",
      productId: "",
      name: "",
      productModelId: "",
@@ -703,6 +709,7 @@
  }
  Object.keys(formData).forEach((key) => {
    if (key === 'id') return
    if (source[key] !== undefined) {
      formData[key] = Array.isArray(source[key]) ? cloneDeep(source[key]) : source[key]
    }
@@ -721,6 +728,20 @@
        }))
  }
  if (Array.isArray(formData.materialInfo)) {
    formData.materialInfo = formData.materialInfo.map((m) => {
      const parentId = m?.parentId ? String(m.parentId) : ""
      const productById = m?.productById ? String(m.productById) : ""
      const name = m?.name ?? ""
      return {
        ...m,
        parentId,
        productById: productById || parentId || "",
        name: name || (parentId ? findProductLabelById(productOptions.value, parentId) : "")
      }
    })
  }
  // 兼容 index.vue 里常用字段名与弹窗字段名不一致的情况
  if (source.productName === undefined && source.productCategory !== undefined) {
    formData.productName = source.productCategory
@@ -736,6 +757,16 @@
  }
  if (source.productOrderId === undefined && source.id !== undefined) {
    formData.productOrderId = source.id
  }
  // 编辑/查看时需要把 productionProductInput 的 id 带回去(后端 save 走更新)
  // 列表行里一般是 printId;详情接口一般返回 { id, productOrderId, ... }
  if (source.printId !== undefined && source.printId !== null && source.printId !== '') {
    formData.id = source.printId
  } else if (source.productionProductInputId !== undefined && source.productionProductInputId !== null && source.productionProductInputId !== '') {
    formData.id = source.productionProductInputId
  } else if (source.productOrderId !== undefined && source.id !== undefined && source.id !== null && source.id !== '') {
    formData.id = source.id
  }
  introductionLetterList.value = String(formData.introductionLetter || "")
@@ -767,6 +798,7 @@
    (val) => {
      mergeRowDataToForm(val)
      getProductOrder()
      Promise.resolve().then(() => hydrateMaterialInfo())
    },
    {immediate: true, deep: true}
)
@@ -793,14 +825,14 @@
const convertProductOptions = (data) => {
  return data.map(item => ({
    label: item.label || item.productName || item.name || "",
    value: item.id,
    value: String(item.id ?? ""),
    children: item.children?.length ? convertProductOptions(item.children) : undefined
  }))
}
const findProductLabelById = (options, productId) => {
  for (const item of options) {
    if (item.value === productId) {
    if (String(item.value) === String(productId)) {
      return item.label
    }
    if (item.children?.length) {
@@ -813,10 +845,69 @@
  return ""
}
const normalizeId = (v) => {
  if (v === 0 || v === "0") return "0"
  if (v === null || v === undefined) return ""
  const s = String(v)
  return s === "null" || s === "undefined" ? "" : s
}
const materialModelOptionsCache = new Map()
const normalizeModelOptions = (res) => {
  return Array.isArray(res) ? res :
      Array.isArray(res?.data) ? res.data :
          Array.isArray(res?.rows) ? res.rows :
              Array.isArray(res?.data?.records) ? res.data.records :
                  []
}
const getModelOptionsByParentId = async (parentId) => {
  const key = normalizeId(parentId)
  if (!key) return []
  if (materialModelOptionsCache.has(key)) return materialModelOptionsCache.get(key)
  const res = await modelList({id: key})
  const options = normalizeModelOptions(res)
  materialModelOptionsCache.set(key, options)
  return options
}
const hydrateMaterialInfo = async () => {
  const rows = Array.isArray(formData.materialInfo) ? formData.materialInfo : []
  await Promise.all(rows.map(async (row) => {
    if (!row || typeof row !== 'object') return
    const parentId = normalizeId(row.parentId || row.productById)
    if (!parentId) return
    row.parentId = parentId
    row.productById = parentId
    if (!row.name) {
      row.name = findProductLabelById(productOptions.value, parentId)
    }
    if (!Array.isArray(row.modelOptions) || row.modelOptions.length === 0) {
      row.modelOptions = await getModelOptionsByParentId(parentId)
    }
    const productModelId = normalizeId(row.productModelId)
    if (productModelId && Array.isArray(row.modelOptions) && row.modelOptions.length) {
      const currentModel = row.modelOptions.find(item => normalizeId(item?.id) === productModelId)
      if (currentModel) {
        row.model = currentModel?.model || row.model || ""
        row.unit = currentModel?.unit || row.unit || ""
        if (!row.productId) {
          row.productId = currentModel?.id || ""
        }
      }
    }
  }))
}
const getMaterialProductOptions = () => {
  productTreeList().then(res => {
    const rawData = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []
    productOptions.value = convertProductOptions(rawData)
    Promise.resolve().then(() => hydrateMaterialInfo())
  })
}
@@ -848,26 +939,31 @@
}
const getModels = async (val, row, index) => {
  const targetRow = formData.materialInfo[index]
  row.productId = val || ""
  row.productById = val || ""
  row.name = val ? findProductLabelById(productOptions.value, val) : ""
  const targetRow = formData.materialInfo[index] || {}
  const parentId = val ? String(val) : ""
  const name = parentId ? findProductLabelById(productOptions.value, parentId) : ""
  row.productModelId = ""
  row.model = ""
  row.unit = ""
  row.modelOptions = []
  if (!val) return
  const res = await modelList({id: val})
  formData.materialInfo[index] = {
  const baseRow = {
    ...targetRow,
    modelOptions: Array.isArray(res) ? res :
        Array.isArray(res?.data) ? res.data :
            Array.isArray(res?.rows) ? res.rows :
                Array.isArray(res?.data?.records) ? res.data.records :
                    []
    parentId,
    productId: "",
    productById: parentId,
    name,
    productModelId: "",
    model: "",
    unit: "",
    modelOptions: []
  }
  formData.materialInfo[index] = baseRow
  if (!parentId) return
  const modelOptions = await getModelOptionsByParentId(parentId)
  formData.materialInfo[index] = {
    ...baseRow,
    modelOptions
  }
  // row.modelOptions = Array.isArray(res) ? res :
  //     Array.isArray(res?.data) ? res.data :
@@ -889,6 +985,8 @@
const addMaterialRow = () => {
  formData.materialInfo.push({
    id: Date.now().toString(),
    parentId: "",
    productById: "",
    productId: "",
    name: "",
    productModelId: "",
@@ -957,24 +1055,24 @@
  const materialRows = Array.isArray(formData.materialInfo) ? formData.materialInfo : []
  for (let i = 0; i < materialRows.length; i++) {
    const row = materialRows[i] || {}
    if (!row.productId) {
    if (!row.productModelId) {
      ElMessage.warning(`材料信息第${i + 1}行:    规格必填`)
      return
    }
  }
  const rows = Array.isArray(formData.processContent) ? formData.processContent : []
  for (let i = 0; i < rows.length; i++) {
    const row = rows[i] || {}
    if (!row.deviceId) {
      ElMessage.warning(`工艺加工第${i + 1}行:机台必填`)
      return
    }
    if (!Array.isArray(row.reportUserIds) || row.reportUserIds.length === 0) {
      ElMessage.warning(`工艺加工第${i + 1}行:报工人必填`)
      return
    }
  }
  // for (let i = 0; i < rows.length; i++) {
  //   const row = rows[i] || {}
  //   if (!row.deviceId) {
  //     ElMessage.warning(`工艺加工第${i + 1}行:机台必填`)
  //     return
  //   }
  //   if (!Array.isArray(row.reportUserIds) || row.reportUserIds.length === 0) {
  //     ElMessage.warning(`工艺加工第${i + 1}行:报工人必填`)
  //     return
  //   }
  // }
  const payload = cloneDeep(formData)
  delete payload.productOrderList
  if (Array.isArray(payload.materialInfo)) {
src/views/productionManagement/productionOrder/index.vue
@@ -111,7 +111,11 @@
const BindRouteDialogRef = ref(null)
const handleBindRouteSubmit = async (data) => {
  const res = await saveProductionProductInput(data)
  const payload = { ...(data || {}) }
  if (bindDialogType.value === "edit" && !payload.id) {
    payload.id = rowData.value?.printId || rowData.value?.id || null
  }
  const res = await saveProductionProductInput(payload)
  if (res.code === 200) {
    proxy.$modal.msgSuccess("绑定成功");
    bindRouteDialogVisible.value = false
@@ -229,9 +233,17 @@
      {
        name: "查看工艺路线",
        type: "text",
        showHide: row => row.printId,
        showHide: row => row.printId && Number(row?.hasProduct) > 0,
        clickFun: row => {
          openBindRouteDialog(row, "view");
        },
      },
      {
        name: "修改工艺路线",
        type: "text",
        showHide: row => row.printId && !(Number(row?.hasProduct) > 0),
        clickFun: row => {
          openBindRouteDialog(row, "edit");
        },
      },
      {
@@ -324,15 +336,24 @@
  bindRouteLoading.value = true;
  try {
    BindRouteDialogRef.value?.resetForm?.()
    if (type === "view") {
      bindDialogType.value = "detail"
    if (type === "view" || type === "edit") {
      bindDialogType.value = type === "view" ? "detail" : "edit"
      const res = await viewGetByProductWordId(row.id)
      if (res?.cuttingFileVo?.id == null) {
        res.cuttingFileVo = []
      const detail = res?.data || res || {}
      if (detail?.cuttingFileVo?.id == null) {
        detail.cuttingFileVo = []
      } else {
        res.cuttingFileVo = [res.cuttingFileVo]
        detail.cuttingFileVo = [detail.cuttingFileVo]
      }
      rowData.value = res?.data || res
      if (detail.productOrderId == null) {
        detail.productOrderId = row.id
      }
      if (type === "edit" && (detail.id == null || detail.id === "") && row?.printId) {
        detail.id = row.printId
      }
      rowData.value = detail
    } else {
      bindDialogType.value = "add"
      rowData.value = deepClone( row)