zhangwencui
昨天 ec5bd9a7abba71e9234fa0d327e8261f89723603
工艺路线替换回去,参数功能添加、dist.js修改字典增加id
已添加6个文件
已修改3个文件
7727 ■■■■■ 文件已修改
src/components/ProcessParamListDialog.vue 631 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/Edit.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/ItemsForm.vue 531 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 2567 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index2.vue 2417 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 1024 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProcessParamListDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,631 @@
<template>
  <el-dialog v-model="visible"
             :title="title"
             width="800px"
             destroy-on-close>
    <div class="param-list-container">
      <div class="params-header">
        <span>参数列表</span>
        <el-button v-if="editable"
                   type="primary"
                   link
                   size="small"
                   @click="handleAddParam">
          <el-icon>
            <Plus />
          </el-icon>新增
        </el-button>
      </div>
      <div class="params-list">
        <div v-for="param in paramList"
             :key="param.id"
             class="param-item">
          <div class="param-info">
            <span class="param-code">{{ param.paramName }}</span>
            <span v-if="param.valueMode == 1"
                  class="param-value">
              æ ‡å‡†å€¼ï¼š{{ param.standardValue || "-" }} {{ param.unit }}
            </span>
            <span v-else
                  class="param-value">
              æ ‡å‡†å€¼ï¼š{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}
            </span>
          </div>
          <div class="param-actions">
            <el-button v-if="editable"
                       link
                       type="primary"
                       size="small"
                       @click="handleEditParam(param)">
              ç¼–辑
            </el-button>
            <el-button v-if="editable"
                       link
                       type="danger"
                       size="small"
                       @click="handleDeleteParam(param)">
              åˆ é™¤
            </el-button>
          </div>
        </div>
        <el-empty v-if="!paramList || paramList.length === 0"
                  description="暂无参数"
                  :image-size="50" />
      </div>
    </div>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="selectParamDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
        <!-- å·¦ä¾§å‚数列表 -->
        <div class="param-list-area">
          <div class="area-title">可选参数</div>
          <div class="search-box">
            <el-input v-model="paramSearchKeyword"
                      placeholder="请输入参数名称搜索"
                      clearable
                      size="small"
                      @input="getBaseParamListData">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredParamList"
                    height="400"
                    border
                    highlight-current-row
                    @current-change="handleSelectParam">
            <el-table-column prop="paramName"
                             label="参数名称" />
            <el-table-column prop="paramType"
                             label="参数类型">
              <template #default="scope">
                <el-tag size="small"
                        :type="getParamTypeTag(scope.row.paramType)">{{ getParamTypeText(scope.row.paramType) }}</el-tag>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µæŽ§ä»¶ -->
          <div class="pagination-container"
               style="margin-top: 10px;">
            <el-pagination v-model:current-page="paramPage.current"
                           v-model:page-size="paramPage.size"
                           :page-sizes="[10, 20, 50, 100]"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="paramPage.total"
                           @size-change="getBaseParamListData"
                           @current-change="getBaseParamListData"
                           size="small" />
          </div>
        </div>
        <!-- å³ä¾§å‚数详情 -->
        <div class="param-detail-area">
          <div class="area-title">参数详情</div>
          <el-form v-if="selectedParam"
                   :model="selectedParam"
                   label-width="100px"
                   class="param-detail-form">
            <el-form-item label="参数名称">
              <span class="detail-text">{{ selectedParam.paramName }}</span>
            </el-form-item>
            <el-form-item label="参数模式">
              <el-tag size="small"
                      :type="selectedParam.valueMode == '1' ? 'success' : 'warning'">
                {{ selectedParam.valueMode == '1' ? '单值' : '区间' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数类型">
              <el-tag size="small"
                      :type="getParamTypeTag(selectedParam.paramType)">{{ getParamTypeText(selectedParam.paramType) }}</el-tag>
            </el-form-item>
            <el-form-item label="参数格式">
              <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
            </el-form-item>
            <el-form-item label="单位">
              <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
            </el-form-item>
            <el-form-item label="标准值"
                          v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.standardValue"
                        type="number"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="最小值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.minValue"
                        type="number"
                        placeholder="请输入最小值" />
            </el-form-item>
            <el-form-item label="最大值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.maxValue"
                        type="number"
                        placeholder="请输入最大值" />
            </el-form-item>
            <el-form-item label="排序">
              <el-input v-model="selectedParam.sort"
                        type="number"
                        placeholder="请输入排序" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择参数"
                    :image-size="100" />
        </div>
      </div>
      <template #footer>
        <el-button @click="selectParamDialogVisible = false">取消</el-button>
        <el-button type="primary"
                   @click="handleParamSelectSubmit">确定</el-button>
      </template>
    </el-dialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
               width="600px">
      <el-form :model="editParamForm"
               :rules="editParamRules"
               ref="editParamFormRef"
               label-width="120px">
        <el-form-item label="参数名称">
          <span class="detail-text">{{ editParamForm.paramName }}</span>
        </el-form-item>
        <el-form-item label="参数模式">
          <el-tag size="small"
                  :type="editParamForm.valueMode == '1' ? 'success' : 'warning'">
            {{ editParamForm.valueMode == '1' ? '单值' : '区间' }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数类型">
          <el-tag size="small"
                  :type="getParamTypeTag(editParamForm.paramType)">
            {{ getParamTypeText(editParamForm.paramType) }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数格式">
          <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
        </el-form-item>
        <el-form-item label="单位">
          <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
        </el-form-item>
        <el-form-item label="标准值"
                      v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    type="number"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="最小值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="minValue">
          <el-input v-model="editParamForm.minValue"
                    type="number"
                    placeholder="请输入最小值" />
        </el-form-item>
        <el-form-item label="最大值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="maxValue">
          <el-input v-model="editParamForm.maxValue"
                    type="number"
                    placeholder="请输入最大值" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="editParamDialogVisible = false">取消</el-button>
        <el-button type="primary"
                   @click="handleEditParamSubmit">确定</el-button>
      </template>
    </el-dialog>
  </el-dialog>
</template>
<script setup>
  import { ref, computed, watch } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { Plus, Search } from "@element-plus/icons-vue";
  import {
    delProcessRouteItemParam,
    editProcessRouteItemParam,
    addProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "参数列表",
    },
    routeId: {
      type: Number,
      default: 0,
    },
    process: {
      type: Object,
      default: () => ({}),
    },
    paramList: {
      type: Array,
      default: () => [],
    },
    editable: {
      type: Boolean,
      default: true,
    },
  });
  const emit = defineEmits(["update:modelValue", "refresh"]);
  const visible = computed({
    get: () => props.modelValue,
    set: value => emit("update:modelValue", value),
  });
  // å“åº”式数据
  const selectParamDialogVisible = ref(false);
  const editParamDialogVisible = ref(false);
  const paramSearchKeyword = ref("");
  const selectedParam = ref(null);
  const filteredParamList = ref([]);
  const paramPage = ref({
    current: 1,
    size: 10,
    total: 0,
  });
  const editParamForm = ref({
    id: null,
    processId: null,
    paramId: null,
    paramName: "",
    valueMode: "1",
    standardValue: null,
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
    paramType: null,
    paramFormat: "",
    unit: "",
  });
  const editParamRules = ref({
    standardValue: [{ required: true, message: "请输入标准值", trigger: "blur" }],
    minValue: [{ required: true, message: "请输入最小值", trigger: "blur" }],
    maxValue: [{ required: true, message: "请输入最大值", trigger: "blur" }],
  });
  const editParamFormRef = ref(null);
  // æ–°å¢žå‚æ•°
  const handleAddParam = () => {
    selectedParam.value = null;
    paramSearchKeyword.value = "";
    paramPage.current = 1;
    // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
    getBaseParamListData();
    selectParamDialogVisible.value = true;
  };
  // ç¼–辑参数
  const handleEditParam = param => {
    editParamForm.value = {
      id: param.id,
      processId: props.process.id,
      paramId: param.paramId,
      paramName: param.parameterName || param.paramName,
      valueMode: param.parameterType2 || param.valueMode || "1",
      standardValue: param.standardValue,
      minValue: param.minValue,
      maxValue: param.maxValue,
      sort: param.sort || 1,
      isRequired: param.isRequired || 0,
      paramType: param.parameterType || param.paramType,
      paramFormat: param.parameterFormat || param.paramFormat,
      unit: param.unit || param.unit,
    };
    editParamDialogVisible.value = true;
  };
  // åˆ é™¤å‚æ•°
  const handleDeleteParam = param => {
    ElMessageBox.confirm("确定要删除该参数吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // è°ƒç”¨API删除参数
        delProcessRouteItemParam(param.id)
          .then(res => {
            ElMessage.success("删除成功");
            emit("refresh");
          })
          .catch(err => {
            ElMessage.error("删除参数失败");
            console.error("删除参数失败:", err);
          });
      })
      .catch(() => {});
  };
  // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
  const getBaseParamListData = () => {
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // é€‰æ‹©å‚æ•°
  const handleSelectParam = param => {
    selectedParam.value = param;
  };
  // æäº¤é€‰æ‹©å‚æ•°
  const handleParamSelectSubmit = () => {
    if (!selectedParam.value) {
      ElMessage.warning("请先选择一个参数");
      return;
    }
    if (!props.process || !props.process.id) {
      ElMessage.error("工艺路线项目信息不完整");
      return;
    }
    // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
    const isNumericMode = selectedParam.value.valueMode === 1;
    // è°ƒç”¨API新增参数
    addProcessRouteItemParam({
      routeItemId: props.process.id,
      paramId: selectedParam.value.id,
      standardValue: isNumericMode ? selectedParam.value.standardValue || "" : "",
      minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
      maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
      isRequired: selectedParam.value.isRequired || 0,
      sort: selectedParam.value.sort || 1,
    })
      .then(res => {
        if (res.code === 200) {
          ElMessage.success("添加参数成功");
          selectParamDialogVisible.value = false;
          emit("refresh");
        } else {
          ElMessage.error(res.msg || "添加参数失败");
        }
      })
      .catch(err => {
        ElMessage.error("添加参数失败");
        console.error("添加参数失败:", err);
      });
  };
  // æäº¤ç¼–辑参数
  const handleEditParamSubmit = () => {
    if (!editParamFormRef.value) return;
    editParamFormRef.value.validate(valid => {
      if (valid) {
        // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
        const isNumericMode = editParamForm.value.valueMode == 1;
        // è°ƒç”¨API修改参数
        editProcessRouteItemParam({
          id: editParamForm.value.id,
          routeItemId: props.process.id,
          paramId: editParamForm.value.paramId,
          standardValue: isNumericMode
            ? editParamForm.value.standardValue || ""
            : "",
          minValue: isNumericMode ? editParamForm.value.minValue || 0 : null,
          maxValue: isNumericMode ? editParamForm.value.maxValue || 0 : null,
          isRequired: editParamForm.value.isRequired || 0,
        })
          .then(res => {
            if (res.code === 200) {
              ElMessage.success("编辑成功");
              editParamDialogVisible.value = false;
              emit("refresh");
            } else {
              ElMessage.error(res.msg || "编辑失败");
            }
          })
          .catch(err => {
            ElMessage.error("编辑参数失败");
            console.error("编辑参数失败:", err);
          });
      }
    });
  };
  // èŽ·å–å‚æ•°ç±»åž‹æ ‡ç­¾
  const getParamTypeTag = type => {
    const typeMap = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return typeMap[type] || "default";
  };
  // èŽ·å–å‚æ•°ç±»åž‹æ–‡æœ¬
  const getParamTypeText = type => {
    const typeMap = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return typeMap[type] || type;
  };
  watch(
    () => props.modelValue,
    newVal => {
      if (!newVal) {
        // å¼¹çª—关闭时重置数据
        selectParamDialogVisible.value = false;
        editParamDialogVisible.value = false;
        selectedParam.value = null;
        paramSearchKeyword.value = "";
        paramPage.current = 1;
        filteredParamList.value = [];
        editParamForm.value = {
          id: null,
          processId: null,
          paramId: null,
          paramName: "",
          valueMode: "1",
          standardValue: null,
          minValue: null,
          maxValue: null,
          sort: 1,
          isRequired: 0,
          paramType: null,
          paramFormat: "",
          unit: "",
        };
      }
    }
  );
</script>
<style scoped>
  .param-list-container {
    padding: 10px 0;
  }
  .params-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
    padding-bottom: 10px;
    border-bottom: 1px solid #e4e7ed;
  }
  .params-header span {
    font-size: 16px;
    font-weight: 500;
    color: #303133;
  }
  .params-list {
    max-height: 400px;
    overflow-y: auto;
  }
  .param-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    margin-bottom: 8px;
    background-color: #f9f9f9;
    border-radius: 4px;
    transition: all 0.3s ease;
  }
  .param-item:hover {
    background-color: #ecf5ff;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  .param-info {
    display: flex;
    align-items: center;
    gap: 20px;
    flex: 1;
  }
  .param-code {
    font-weight: 500;
    color: #303133;
    min-width: 120px;
  }
  .param-value {
    color: #606266;
    font-size: 14px;
  }
  .param-actions {
    display: flex;
    gap: 10px;
  }
  /* æ»šåŠ¨æ¡æ ·å¼ */
  .params-list::-webkit-scrollbar {
    width: 6px;
  }
  .params-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 3px;
  }
  .params-list::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
  }
  .params-list::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  /* é€‰æ‹©å‚数对话框样式 */
  .param-select-container {
    display: flex;
    gap: 20px;
  }
  .param-list-area {
    flex: 1;
    min-width: 400px;
  }
  .param-detail-area {
    flex: 1;
    min-width: 300px;
  }
  .area-title {
    font-size: 14px;
    font-weight: 500;
    margin-bottom: 10px;
    color: #303133;
  }
  .search-box {
    display: flex;
    gap: 10px;
    margin-bottom: 10px;
  }
  .param-detail-form {
    background: #f9f9f9;
    padding: 15px;
    border-radius: 4px;
  }
  .detail-text {
    font-weight: 500;
  }
</style>
src/utils/dict.js
@@ -14,7 +14,7 @@
        res.value[dictType] = dicts
      } else {
        getDicts(dictType).then(resp => {
          res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
          res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue,id: p.dictCode, elTagType: p.listClass, elTagClass: p.cssClass }))
          useDictStore().setDict(dictType, res.value[dictType])
        })
      }
src/views/productionManagement/processRoute/Edit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,257 @@
<template>
  <div>
    <el-dialog v-model="isShow"
               title="编辑工艺路线"
               width="400"
               @close="closeModal">
      <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.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="BOM"
                      prop="bomId"
                      :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]">
          <el-select v-model="formState.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!formState.productModelId || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
                       :label="item.bomNo || `BOM-${item.id}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注"
                      prop="description">
          <el-input v-model="formState.description"
                    type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog v-model="showProductSelectDialog"
                           @confirm="handleProductSelect"
                           single />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import {
    ref,
    computed,
    getCurrentInstance,
    onMounted,
    nextTick,
    watch,
  } from "vue";
  import { update } from "@/api/productionManagement/processRoute.js";
  import { getByModel } from "@/api/productionManagement/productBom.js";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  const props = defineProps({
    visible: {
      type: Boolean,
      required: true,
    },
    record: {
      type: Object,
      required: true,
    },
  });
  const emit = defineEmits(["update:visible", "completed"]);
  // å“åº”式数据(替代选项式的 data)
  const formState = ref({
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
    bomId: undefined,
    description: "",
  });
  const isShow = computed({
    get() {
      return props.visible;
    },
    set(val) {
      emit("update:visible", val);
    },
  });
  const showProductSelectDialog = ref(false);
  const bomOptions = ref([]);
  let { proxy } = getCurrentInstance();
  const closeModal = () => {
    isShow.value = false;
  };
  // è®¾ç½®è¡¨å•数据
  const setFormData = () => {
    if (props.record) {
      formState.value = {
        ...props.record,
        productId: props.record.productId,
        productModelId: props.record.productModelId,
        productName: props.record.productName || "",
        // æ³¨æ„ï¼šrecord中的字段是model,需要映射到productModelName
        productModelName:
          props.record.model || props.record.productModelName || "",
        bomId: props.record.bomId,
        description: props.record.description || "",
      };
      // å¦‚果有产品型号ID,加载BOM列表
      if (props.record.productModelId) {
        loadBomList(props.record.productModelId);
      }
    }
  };
  // åŠ è½½BOM列表
  const loadBomList = async productModelId => {
    if (!productModelId) {
      bomOptions.value = [];
      return;
    }
    try {
      const res = await getByModel(productModelId);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === "object") {
        bomList = [res];
      }
      bomOptions.value = bomList;
    } catch (error) {
      console.error("加载BOM列表失败:", error);
      bomOptions.value = [];
    }
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      // å…ˆæŸ¥è¯¢BOM列表(必选)
      try {
        const res = await getByModel(product.id);
        // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
        let bomList = [];
        if (Array.isArray(res)) {
          bomList = res;
        } else if (res && res.data) {
          bomList = Array.isArray(res.data) ? res.data : [res.data];
        } else if (res && typeof res === "object") {
          bomList = [res];
        }
        if (bomList.length > 0) {
          formState.value.productModelId = product.id;
          formState.value.productName = product.productName;
          formState.value.productModelName = product.model;
          // å¦‚果当前选择的BOM不在新列表中,则重置BOM选择
          const currentBomExists = bomList.some(
            bom => bom.id === formState.value.bomId
          );
          if (!currentBomExists) {
            formState.value.bomId = undefined;
          }
          bomOptions.value = bomList;
          showProductSelectDialog.value = false;
          // è§¦å‘表单验证更新
          proxy.$refs["formRef"]?.validateField("productModelId");
        } else {
          proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
        }
      } catch (error) {
        // å¦‚果接口返回404或其他错误,说明没有BOM
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    }
  };
  const handleSubmit = () => {
    proxy.$refs["formRef"].validate(valid => {
      if (valid) {
        // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
        if (!formState.value.productModelId) {
          proxy.$modal.msgError("请选择产品");
          return;
        }
        if (!formState.value.bomId) {
          proxy.$modal.msgError("请选择BOM");
          return;
        }
        update(formState.value).then(res => {
          // å…³é—­æ¨¡æ€æ¡†
          isShow.value = false;
          // å‘ŠçŸ¥çˆ¶ç»„件已完成
          emit("completed");
          proxy.$modal.msgSuccess("提交成功");
        });
      }
    });
  };
  defineExpose({
    closeModal,
    handleSubmit,
    isShow,
  });
  // ç›‘听弹窗打开,初始化表单数据
  watch(
    () => props.visible,
    visible => {
      if (visible && props.record) {
        nextTick(() => {
          setFormData();
        });
      }
    },
    { immediate: true }
  );
  onMounted(() => {
    if (props.visible && props.record) {
      setFormData();
    }
  });
</script>
src/views/productionManagement/processRoute/ItemsForm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,531 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="工艺路线项目"
        width="800px"
        @close="closeModal"
    >
      <div class="operate-button">
        <el-button
            type="primary"
            @click="isShowProductSelectDialog = true"
            class="mb5"
            style="margin-bottom: 10px;"
        >
          é€‰æ‹©äº§å“
        </el-button>
        <el-switch
            v-model="isTable"
            inline-prompt
            active-text="表格"
            inactive-text="列表"
            @change="handleViewChange"
        />
      </div>
      <el-table
          v-if="isTable"
          ref="multipleTable"
          v-loading="tableLoading"
          border
          :data="routeItems"
          :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
          row-key="id"
          tooltip-effect="dark"
          class="lims-table"
          style="cursor: move;"
      >
        <el-table-column align="center" label="序号" width="60">
          <template #default="scope">
            {{ scope.$index + 1 }}
          </template>
        </el-table-column>
        <el-table-column
            v-for="(item, index) in tableColumn"
            :key="index"
            :label="item.label"
            :width="item.width"
            show-overflow-tooltip
        >
          <template #default="scope" v-if="item.dataType === 'action'">
            <el-button
                v-for="(op, opIndex) in item.operation"
                :key="opIndex"
                :type="op.type"
                :link="op.link"
                size="small"
                @click.stop="op.clickFun(scope.row)"
            >
              {{ op.name }}
            </el-button>
          </template>
          <template #default="scope" v-else>
            <template v-if="item.prop === 'processId'">
              <el-select
                  v-model="scope.row[item.prop]"
                  style="width: 100%;"
                  @mousedown.stop
              >
                <el-option
                    v-for="process in processOptions"
                    :key="process.id"
                    :label="process.name"
                    :value="process.id"
                />
              </el-select>
            </template>
            <template v-else>
              {{ scope.row[item.prop] || '-' }}
            </template>
          </template>
        </el-table-column>
      </el-table>
      <!-- ä½¿ç”¨æ™®é€šdiv替代el-steps -->
      <div
          v-else
          ref="stepsContainer"
          class="mb5 custom-steps"
          style="padding: 10px 0; display: flex; flex-wrap: nowrap; gap: 20px; align-items: flex-start;"
      >
        <div
            v-for="(item, index) in routeItems"
            :key="item.id"
            class="custom-step draggable-step"
            :data-id="item.id"
            style="cursor: move; flex: 0 0 auto; min-width: 220px;"
        >
          <div class="step-content">
            <div class="step-number">{{ index + 1 }}</div>
            <el-card
                :header="item.productName"
                class="step-card"
                style="cursor: move;"
            >
              <div class="step-card-content">
                <p>{{ item.model }}</p>
                <p>{{ item.unit }}</p>
                <el-select
                    v-model="item.processId"
                    style="width: 100%;"
                    @mousedown.stop
                >
                  <el-option
                      v-for="process in processOptions"
                      :key="process.id"
                      :label="process.name"
                      :value="process.id"
                  />
                </el-select>
              </div>
              <template #footer>
                <div class="step-card-footer">
                  <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">删除</el-button>
                </div>
              </template>
            </el-card>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <ProductSelectDialog
        v-model="isShowProductSelectDialog"
        @confirm="handelSelectProducts"
    />
  </div>
</template>
<script setup>
import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
import { findProcessRouteItemList, addOrUpdateProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
import { processList } from "@/api/productionManagement/productionProcess.js";
import Sortable from 'sortablejs';
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
    default: false
  },
  record: {
    type: Object,
    required: true,
    default: () => ({})
  }
});
const emit = defineEmits(['update:visible', 'completed']);
const processOptions = ref([]);
const tableLoading = ref(false);
const isShowProductSelectDialog = ref(false);
const routeItems = ref([]);
let tableSortable = null;
let stepsSortable = null;
const multipleTable = ref(null);
const stepsContainer = ref(null);
const isTable = ref(true);
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  }
});
const tableColumn = ref([
  { label: "产品名称", prop: "productName", width: 180 },
  { label: "规格名称", prop: "model", width: 150 },
  { label: "单位", prop: "unit", width: 80 },
  { label: "工序名称", prop: "processId", width: 180 },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 100,
    operation: [
      {
        name: "删除",
        type: "danger",
        link: true,
        clickFun: (row) => {
          const idx = routeItems.value.findIndex(item => item.id === row.id);
          if (idx > -1) {
            removeItem(idx)
          }
        }
      }
    ]
  }
]);
const removeItem = (index) => {
  routeItems.value.splice(index, 1);
  nextTick(() => initSortable());
};
const removeItemByID = (id) => {
  const idx = routeItems.value.findIndex(item => item.id === id);
  if (idx > -1) {
    routeItems.value.splice(idx, 1);
    nextTick(() => initSortable());
  }
};
const closeModal = () => {
  isShow.value = false;
};
const handelSelectProducts = (products) => {
  destroySortable();
  const newData = products.map(({ id, ...product }) => ({
    ...product,
    productModelId: id,
    routeId: props.record.id,
    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
    processId: undefined
  }));
  console.log('选择产品前数组:', routeItems.value);
  routeItems.value.push(...newData);
  routeItems.value = [...routeItems.value];
  console.log('选择产品后数组:', routeItems.value);
  // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
  nextTick(() => {
    // å¼ºåˆ¶é‡æ–°æ¸²æŸ“组件
    if (proxy?.$forceUpdate) {
      proxy.$forceUpdate();
    }
    const temp = [...routeItems.value];
    routeItems.value = [];
    nextTick(() => {
      routeItems.value = temp;
      initSortable();
    });
  });
};
const findProcessRouteItems = () => {
  tableLoading.value = true;
  findProcessRouteItemList({ routeId: props.record.id })
      .then(res => {
        tableLoading.value = false;
        routeItems.value = res.data.map(item => ({
          ...item,
          processId: item.processId === 0 ? undefined : item.processId
        }));
        // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM完全渲染
        nextTick(() => {
          setTimeout(() => initSortable(), 100);
        });
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
      });
};
const findProcessList = () => {
  processList({})
      .then(res => {
        processOptions.value = res.data;
      })
      .catch(err => {
        console.error("获取工序失败:", err);
      });
};
const { proxy } = getCurrentInstance() || {};
const handleSubmit = () => {
  const hasEmptyProcess = routeItems.value.some(item => !item.processId);
  if (hasEmptyProcess) {
    proxy?.$modal?.msgError("请为所有项目选择工序");
    return;
  }
  addOrUpdateProcessRouteItem({
    routeId: props.record.id,
    processRouteItem: routeItems.value.map(({ id, ...item }) => item)
  })
      .then(res => {
        isShow.value = false;
        emit('completed');
        proxy?.$modal?.msgSuccess("提交成功");
      })
      .catch(err => {
        proxy?.$modal?.msgError(`提交失败:${err.msg || "网络异常"}`);
      });
};
const destroySortable = () => {
  if (tableSortable) {
    tableSortable.destroy();
    tableSortable = null;
  }
  if (stepsSortable) {
    stepsSortable.destroy();
    stepsSortable = null;
  }
};
const initSortable = () => {
  destroySortable();
  if (isTable.value) {
    if (!multipleTable.value) return;
    const tbody = multipleTable.value.$el.querySelector('.el-table__body tbody') ||
        multipleTable.value.$el.querySelector('.el-table__body-wrapper > table > tbody');
    if (!tbody) return;
    tableSortable = new Sortable(tbody, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      handle: '.el-table__row',
      filter: '.el-button, .el-select',
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序,与表格模式保持一致
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
        console.log('排序后数组:', routeItems.value);
      }
    });
  } else {
    if (!stepsContainer.value) return;
    // ä¿®æ”¹ï¼šç›´æŽ¥ä½¿ç”¨stepsContainer.value作为拖拽容器
    const stepsList = stepsContainer.value;
    if (!stepsList) {
      console.warn('未找到步骤条拖拽容器');
      return;
    }
    // ä¿®æ”¹ï¼šç®€åŒ–拖拽配置
    stepsSortable = new Sortable(stepsList, {
      animation: 150,
      ghostClass: 'sortable-ghost',
      draggable: '.draggable-step', // å¯æ‹–拽元素
      handle: '.draggable-step, .step-card', // æ‹–拽手柄
      filter: '.el-button, .el-select, .el-input', // è¿‡æ»¤æŒ‰é’®/选择器
      forceFallback: true,
      fallbackClass: 'sortable-fallback',
      preventOnFilter: true,
      scroll: true,
      scrollSensitivity: 30,
      scrollSpeed: 10,
      bubbleScroll: true,
      onEnd: (evt) => {
        if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
        // ä½¿ç”¨æ•°ç»„ splice æ–¹æ³•重新排序
        const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
        routeItems.value.splice(evt.newIndex, 0, moveItem);
        routeItems.value = [...routeItems.value];
      }
    });
    // è°ƒè¯•:打印容器和实例,确认绑定成功
    console.log('步骤条拖拽容器:', stepsList);
    console.log('Sortable实例:', stepsSortable);
  }
};
const handleViewChange = () => {
  destroySortable();
  // å»¶è¿Ÿåˆå§‹åŒ–,确保视图切换后DOM完全渲染
  nextTick(() => {
    setTimeout(() => initSortable(), 100);
  });
};
onMounted(() => {
  findProcessRouteItems();
  findProcessList();
});
onUnmounted(() => {
  destroySortable();
});
defineExpose({
  closeModal,
  handleSubmit,
  isShow
});
</script>
<style scoped>
:deep(.sortable-ghost) {
  opacity: 0.6;
  background-color: #f5f7fa !important;
}
:deep(.el-table__row) {
  transition: background-color 0.2s;
}
:deep(.el-table__row:hover) {
  background-color: #f9fafc !important;
}
:deep(.el-card__footer){
  padding: 0 !important;
}
.operate-button {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤æ¡å®¹å™¨æ ·å¼ */
.custom-steps {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 20px;
  min-height: 100px;
}
/* ä¿®æ”¹ï¼šè‡ªå®šä¹‰æ­¥éª¤é¡¹æ ·å¼ */
.custom-step {
  cursor: move !important;
  padding: 8px;
  position: relative;
  transition: all 0.2s ease;
  flex: 0 0 auto;
  min-width: 220px;
  touch-action: none;
}
/* æ‹–拽悬浮样式,提示可拖拽 */
.custom-step:hover {
  background-color: rgba(64, 158, 255, 0.05);
  transform: translateY(-2px);
}
.sortable-ghost {
  opacity: 0.4;
  background-color: #f5f7fa !important;
  border: 2px dashed #409eff;
  margin: 10px;
  transform: scale(1.02);
}
.sortable-fallback {
  opacity: 0.9;
  background-color: #f5f7fa;
  border: 1px solid #409eff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  transform: rotate(2deg);
  margin: 10px;
}
.step-card {
  cursor: move !important;
  transition: box-shadow 0.2s ease;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: auto;
  margin: 10px;
  height: 240px;
}
.step-card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.step-content {
  width: 220px;
  user-select: none;
}
.step-card-content {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.step-card-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 10px;
}
/* è‡ªå®šä¹‰åºå·æ ·å¼ä¼˜åŒ– */
.step-number {
  font-weight: bold;
  text-align: center;
  width: 36px;
  height: 36px;
  line-height: 36px;
  margin: 0 auto 10px;
  background: #409eff;
  color: #fff;
  border-radius: 50%;
  font-size: 14px;
}
</style>
src/views/productionManagement/processRoute/New.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="新增工艺路线"
        width="400"
        @close="closeModal"
    >
      <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.productModelName
              ? `${formState.productName} - ${formState.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item
            label="BOM"
            prop="bomId"
            :rules="[
                {
                required: true,
                message: '请选择BOM',
                trigger: 'change',
              }
            ]"
        >
          <el-select
              v-model="formState.bomId"
              placeholder="请选择BOM"
              clearable
              :disabled="!formState.productModelId || bomOptions.length === 0"
              style="width: 100%"
          >
            <el-option
                v-for="item in bomOptions"
                :key="item.id"
                :label="item.bomNo || `BOM-${item.id}`"
                :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="备注" prop="description">
          <el-input v-model="formState.description" type="textarea" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog
          v-model="showProductSelectDialog"
          @confirm="handleProductSelect"
          single
      />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, computed, getCurrentInstance} from "vue";
import {add} from "@/api/productionManagement/processRoute.js";
import {getByModel} from "@/api/productionManagement/productBom.js";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  productId: undefined,
  productModelId: undefined,
  productName: "",
  productModelName: "",
  bomId: undefined,
  description: '',
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
const showProductSelectDialog = ref(false);
const bomOptions = ref([]);
let { proxy } = getCurrentInstance()
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  formState.value = {
    productId: undefined,
    productModelId: undefined,
    productName: "",
    productModelName: "",
    bomId: undefined,
    description: '',
  };
  bomOptions.value = [];
  isShow.value = false;
};
// äº§å“é€‰æ‹©å¤„理
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    // å…ˆæŸ¥è¯¢BOM列表(必选)
    try {
      const res = await getByModel(product.id);
      // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
      let bomList = [];
      if (Array.isArray(res)) {
        bomList = res;
      } else if (res && res.data) {
        bomList = Array.isArray(res.data) ? res.data : [res.data];
      } else if (res && typeof res === 'object') {
        bomList = [res];
      }
      if (bomList.length > 0) {
        formState.value.productModelId = product.id;
        formState.value.productName = product.productName;
        formState.value.productModelName = product.model;
        formState.value.bomId = undefined; // é‡ç½®BOM选择
        bomOptions.value = bomList;
        showProductSelectDialog.value = false;
        // è§¦å‘表单验证更新
        proxy.$refs["formRef"]?.validateField('productModelId');
      } else {
        proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
      }
    } catch (error) {
      // å¦‚果接口返回404或其他错误,说明没有BOM
      proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
    }
  }
};
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’ŒBOM
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
      }
      if (!formState.value.bomId) {
        proxy.$modal.msgError("请选择BOM");
        return;
      }
      add(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
    }
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
</script>
src/views/productionManagement/processRoute/index.vue
@@ -1,1201 +1,232 @@
<template>
  <div class="app-container">
    <div class="route-header">
      <div class="add-route-btn"
           @click="handleAddRoute">
        <el-icon>
          <Plus />
        </el-icon>
        <span>新增工艺路线</span>
      </div>
    <div class="search_form">
      <el-form :model="searchForm"
               :inline="true">
        <el-form-item label="规格名称:">
          <el-input v-model="searchForm.model"
                    placeholder="请输入"
                    clearable
                    prefix-icon="Search"
                    style="width: 200px;"
                    @change="handleQuery" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
        </el-form-item>
      </el-form>
    </div>
    <div class="route-card-list">
      <div v-for="route in routeList"
           :key="route.id"
           class="route-card">
        <div class="card-header">
          <div class="route-info">
            <span class="route-name"><el-icon style="margin-right: 8px;line-height: 30px;">
                <ScaleToOriginal />
              </el-icon>{{route.routeCode }}<el-tag style="margin-left: 8px"
                      :type="!route.status ? 'warning' : 'success'">{{ !route.status ? '草稿' : '批准' }}</el-tag></span>
            <!-- <span class="route-code">{{ route.routeCode }}</span> -->
          </div>
          <div class="route-actions">
            <el-button v-if="!route.status"
                       link
                       type="success"
                       @click="handleApproveRoute(route)">
              <el-icon>
                <Check />
              </el-icon>
              æ‰¹å‡†
            </el-button>
            <el-button v-if="route.status"
                       link
                       type="warning"
                       @click="handleRevokeApproveRoute(route)">
              <el-icon>
                <Close />
              </el-icon>
              æ’¤é”€æ‰¹å‡†
            </el-button>
            <el-button link
                       type="primary"
                       @click="handleEditRoute(route)">
              <el-icon>
                <Edit />
              </el-icon>
              ç¼–辑
            </el-button>
            <el-button link
                       type="danger"
                       @click="handleDeleteRoute(route)">
              <el-icon>
                <Delete />
              </el-icon>
              åˆ é™¤
            </el-button>
          </div>
        </div>
        <div class="card-body">
          <div class="route-meta">
            <span class="meta-item">
              <el-icon>
                <Box />
              </el-icon>
              <span class="meta-label">产品:</span>
              <span class="meta-value">{{ route.productName }} - {{ route.productModelName }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">BOM:</span>
              <span class="meta-value">{{ route.bomNo || '-' }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">备注:</span>
              <span class="meta-value">{{ route.description || '暂无描述' }}</span>
            </span>
          </div>
          <div class="expand-btn-wrapper">
            <el-button class="expand-btn"
                       :class="{ expanded: route.expanded }"
                       type="primary"
                       text
                       @click="toggleExpand(route)">
              <span class="btn-text">{{ route.expanded ? '收起工序路线' : '展开工序路线' }}</span>
              <el-icon class="expand-icon">
                <component :is="route.expanded ? 'ArrowUp' : 'ArrowDown'" />
              </el-icon>
            </el-button>
          </div>
        </div>
        <div v-if="route.expanded"
             class="process-route">
          <div class="process-flow">
            <div v-for="(process, index) in route.processList"
                 :key="process.id"
                 class="process-flow-item"
                 draggable="true"
                 @dragstart="handleDragStart($event, index, route.id)"
                 @dragover="handleDragOver($event)"
                 @drop="handleDrop($event, index, route.id)"
                 @dragend="handleDragEnd">
              <div class="process-node"
                   :class="{ expanded: process.expanded }">
                <div class="process-node-header">
                  <div class="process-number">{{ index + 1 }}</div>
                  <div class="process-actions">
                    <el-button link
                               type="primary"
                               @click="handleEditProcessSelect(route, index, process)">
                      <el-icon>
                        <Edit />
                      </el-icon>
                    </el-button>
                    <el-button link
                               type="danger"
                               @click="handleDeleteProcess(route.id, process)">
                      <el-icon>
                        <Delete />
                      </el-icon>
                    </el-button>
                  </div>
                </div>
                <div class="process-node-body">
                  <!-- <div class="process-code">{{ process.processId }}</div> -->
                  <div class="process-name">{{ process.processName }}</div>
                  <!-- <div class="process-desc">{{ process.remark || '暂无描述' }}</div> -->
                </div>
                <div class="process-node-footer">
                  <!-- <el-tag size="small"
                          :type="process.status === '1' ? 'success' : 'info'">
                    {{ process.status === '1' ? '启用' : '停用' }}
                  </el-tag> -->
                  <el-button type="primary"
                             link
                             size="small"
                             @click="toggleProcessParams(process)">
                    {{ process.expanded ? '收起参数' : '展开参数' }}
                    ({{ process.paramCount }})
                  </el-button>
                </div>
                <div v-if="process.expanded"
                     class="process-params-section">
                  <div class="params-header">
                    <span>参数列表</span>
                    <el-button type="primary"
                               link
                               size="small"
                               @click="handleAddParam(route.id, process)">
                      <el-icon>
                        <Plus />
                      </el-icon>新增
                    </el-button>
                  </div>
                  <div class="params-list">
                    <div v-for="param in process.paramList"
                         :key="param.id"
                         class="param-item">
                      <div class="param-info">
                        <span class="param-code">{{ param.paramName }}</span>
                        <!-- <span class="param-name">{{ param.paramName }}</span> -->
                        <!-- <el-tag size="small"
                                style="margin-right: 20px;"
                                :type="getParamTypeTag(param.parameterType)">
                          {{ param.parameterType }}
                        </el-tag> -->
                        <span v-if="param.valueMode==1"
                              class="param-value">标准值:{{ param.standardValue || "-" }} {{ param.unit }}</span>
                        <span v-else
                              class="param-value">标准值:{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}</span>
                      </div>
                      <div class="param-actions">
                        <el-button link
                                   type="primary"
                                   size="small"
                                   @click="handleEditParam(route.id, process, param)">
                          ç¼–辑
                        </el-button>
                        <el-button link
                                   type="danger"
                                   size="small"
                                   @click="handleDeleteParam(route.id, process, param)">
                          åˆ é™¤
                        </el-button>
                      </div>
                    </div>
                    <el-empty v-if="!process.paramList || process.paramList.length === 0"
                              description="暂无参数"
                              :image-size="50" />
                  </div>
                </div>
              </div>
              <div v-if="index < route.processList.length - 1"
                   class="flow-arrow">
                <el-icon>
                  <Right />
                </el-icon>
              </div>
            </div>
            <div class="add-process-node"
                 @click="handleSelectProcess(route, index)">
              <el-icon>
                <Plus />
              </el-icon>
              <span>新增工序</span>
            </div>
          </div>
        </div>
    <div class="table_list">
      <div style="text-align: right"
           class="mb10">
        <el-button type="primary"
                   @click="showNewModal">新增工艺路线</el-button>
        <el-button type="danger"
                   @click="handleDelete"
                   :disabled="selectedRows.length === 0"
                   plain>删除工艺路线</el-button>
      </div>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
                :total="page.total" />
    </div>
    <!-- åˆ†é¡µæŽ§ä»¶ -->
    <div class="pagination-container">
      <el-pagination v-model:current-page="routePage.current"
                     v-model:page-size="routePage.size"
                     :page-sizes="[10, 20, 50, 100]"
                     layout="total, sizes, prev, pager, next, jumper"
                     :total="routePage.total"
                     @size-change="handleRouteSizeChange"
                     @current-change="handleRouteCurrentChange" />
    </div>
    <!-- å·¥è‰ºè·¯çº¿æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="routeDialogVisible"
               :title="isRouteEdit ? '编辑工艺路线' : '新增工艺路线'"
               width="500px">
      <el-form :model="routeForm"
               :rules="routeRules"
               ref="routeFormRef"
               label-width="120px">
        <el-form-item label="产品名称"
                      prop="productModelId">
          <el-button type="primary"
                     @click="handleProcessProductSelectClick2">
            {{ routeForm.productName && routeForm.productModelName
              ? `${routeForm.productName} - ${routeForm.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="BOM"
                      prop="bomId">
          <el-select v-model="routeForm.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!routeForm.productModelId || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
                       :label="item.bomNo || `BOM-${item.id}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="路线编码"
                      prop="routeCode">
          <el-input v-model="routeForm.routeCode"
                    disabled
                    placeholder="自动生成" />
        </el-form-item>
        <el-form-item label="备注"
                      prop="description">
          <el-input v-model="routeForm.description"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入路线描述" />
        </el-form-item>
        <!-- <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="routeForm.status">
            <el-radio label="1">启用</el-radio>
            <el-radio label="0">停用</el-radio>
          </el-radio-group>
        </el-form-item> -->
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="routeDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleRouteSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¼¹çª— -->
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
    <!-- å·¥åºæ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="processDialogVisible"
               :title="isProcessEdit ? '编辑工序' : '新增工序'"
               width="500px">
      <el-form :model="processForm"
               :rules="processRules"
               ref="processFormRef"
               label-width="120px">
        <el-form-item label="工序编码"
                      prop="no">
          <el-input v-model="processForm.no"
                    placeholder="请输入工序编码" />
        </el-form-item>
        <el-form-item label="工序名称"
                      prop="name">
          <el-input v-model="processForm.name"
                    placeholder="请输入工序名称" />
        </el-form-item>
        <el-form-item label="工序描述"
                      prop="remark">
          <el-input v-model="processForm.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入工序描述" />
        </el-form-item>
        <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="processForm.status">
            <el-radio :label="true">启用</el-radio>
            <el-radio :label="false">停用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="processDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleProcessSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å·¥åºå¯¹è¯æ¡† -->
    <el-dialog v-model="selectProcessDialogVisible"
               title="选择工序"
               width="1000px">
      <div class="process-select-container">
        <!-- å·¦ä¾§å·¥åºåˆ—表 -->
        <div class="process-list-area">
          <div class="area-title">可选工序</div>
          <div class="search-box">
            <el-input v-model="processSearchKeyword"
                      placeholder="请输入工序名称搜索"
                      clearable
                      size="small"
                      @input="handleProcessSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredProcessList"
                    height="360"
                    border
                    highlight-current-row
                    @current-change="handleProcessSelect">
            <el-table-column prop="no"
                             label="工序编号"
                             width="100" />
            <el-table-column prop="name"
                             label="工序名称" />
            <el-table-column prop="remark"
                             label="工序描述" />
            <el-table-column prop="status"
                             label="状态"
                             width="80">
              <template #default="scope">
                <el-tag size="small"
                        :type="scope.row.status ? 'success' : 'info'">
                  {{ scope.row.status ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <!-- å³ä¾§å·¥åºè¯¦æƒ… -->
        <div class="process-detail-area">
          <div class="area-title">工序详情</div>
          <el-form v-if="selectedProcessItem"
                   :model="processForm"
                   label-width="100px"
                   class="process-detail-form">
            <el-form-item label="工序编号">
              <span class="detail-text">{{ selectedProcessItem.no }}</span>
            </el-form-item>
            <el-form-item label="工序名称">
              <span class="detail-text">{{ selectedProcessItem.name }}</span>
            </el-form-item>
            <el-form-item label="工序描述">
              <span class="detail-text">{{ selectedProcessItem.remark || '-' }}</span>
            </el-form-item>
            <el-form-item label="状态">
              <el-tag size="small"
                      :type="selectedProcessItem.status ? 'success' : 'info'">
                {{ selectedProcessItem.status ? '启用' : '停用' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="是否质检">
              <el-tag size="small"
                      :type="selectedProcessItem.isQuality ? 'success' : 'info'">
                {{ selectedProcessItem.isQuality ? '质检' : '非质检' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="产品名称"
                          prop="productModelId">
              <el-button type="primary"
                         @click="handleProcessProductSelectClick">
                {{ processForm.productName && processForm.model
                  ? `${processForm.productName} - ${processForm.model}`
                  : '选择产品' }}
              </el-button>
            </el-form-item>
            <el-form-item label="单位"
                          prop="unit">
              <el-input v-model="processForm.unit"
                        :placeholder="processForm.productModelId ? '根据选择的产品自动带出' : '请先选择产品' "
                        clearable
                        :disabled="true" />
            </el-form-item>
            <el-form-item label="是否质检"
                          prop="isQuality">
              <el-switch v-model="processForm.isQuality"
                         :active-value="true"
                         inactive-value="false" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择工序" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectProcessDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedProcessItem || !processForm.productModelId"
                     @click="handleProcessSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- å‚数新增/编辑对话框 -->
    <el-dialog v-model="paramDialogVisible"
               :title="isParamEdit ? '编辑参数' : '新增参数'"
               width="500px">
      <el-form :model="paramForm"
               :rules="paramRules"
               ref="paramFormRef"
               label-width="120px">
        <el-form-item label="参数编号"
                      prop="parameterCode">
          <el-input v-model="paramForm.parameterCode"
                    placeholder="请输入参数编号" />
        </el-form-item>
        <el-form-item label="参数名称"
                      prop="parameterName">
          <el-input v-model="paramForm.parameterName"
                    placeholder="请输入参数名称" />
        </el-form-item>
        <el-form-item label="参数模式"
                      prop="parameterType2">
          <el-select v-model="paramForm.parameterType2"
                     placeholder="请选择参数模式">
            <el-option label="单值"
                       value="1" />
            <el-option label="区间"
                       value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="参数类型"
                      prop="parameterType">
          <el-select v-model="paramForm.parameterType"
                     @change="handleParamTypeChange"
                     placeholder="请选择参数类型">
            <el-option label="数值格式"
                       value="数值格式" />
            <el-option label="文本格式"
                       value="文本格式" />
            <el-option label="下拉选项"
                       value="下拉选项" />
            <el-option label="时间格式"
                       value="时间格式" />
          </el-select>
        </el-form-item>
        <el-form-item v-if="paramForm.parameterType === '下拉选项'"
                      label="数据字典"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择数据字典">
            <el-option v-for="item in dictTypes"
                       :key="item.dictType"
                       :label="item.dictName"
                       :value="item.dictType" />
          </el-select>
        </el-form-item>
        <el-form-item v-else-if="paramForm.parameterType === '时间格式'"
                      label="时间格式"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择时间格式">
            <el-option label="YYYY-MM-DD HH:mm:ss"
                       value="YYYY-MM-DD HH:mm:ss" />
            <el-option label="YYYY-MM-DD"
                       value="YYYY-MM-DD" />
          </el-select>
        </el-form-item>
        <el-form-item v-else
                      label="参数格式"
                      prop="parameterFormat">
          <el-input v-model="paramForm.parameterFormat"
                    placeholder="请输入参数格式" />
        </el-form-item>
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="paramForm.standardValue"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="单位"
                      prop="unit">
          <el-input v-model="paramForm.unit"
                    placeholder="请输入单位" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="paramDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="selectParamDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
        <!-- å·¦ä¾§å‚数列表 -->
        <div class="param-list-area">
          <div class="area-title">可选参数</div>
          <div class="search-box">
            <el-input v-model="paramSearchKeyword"
                      placeholder="请输入参数名称搜索"
                      clearable
                      size="small"
                      @input="handleParamSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredParamList"
                    height="300"
                    border
                    highlight-current-row
                    @current-change="handleParamSelect">
            <el-table-column prop="paramName"
                             label="参数名称" />
            <el-table-column prop="paramType"
                             label="参数类型">
              <template #default="scope">
                <el-tag size="small"
                        :type="getParamTypeTag(scope.row.paramType)">
                  {{ getParamTypeText(scope.row.paramType) }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µæŽ§ä»¶ -->
          <div class="pagination-container"
               style="margin-top: 10px;">
            <el-pagination v-model:current-page="paramPage.current"
                           v-model:page-size="paramPage.size"
                           :page-sizes="[10, 20, 50, 100]"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="paramPage.total"
                           @size-change="handleParamSizeChange"
                           @current-change="handleParamCurrentChange"
                           size="small" />
          </div>
        </div>
        <!-- å³ä¾§å‚数详情 -->
        <div class="param-detail-area">
          <div class="area-title">参数详情</div>
          <el-form v-if="selectedParam"
                   :model="selectedParam"
                   label-width="100px"
                   class="param-detail-form">
            <el-form-item label="参数名称">
              <span class="detail-text">{{ selectedParam.paramName }}</span>
            </el-form-item>
            <el-form-item label="参数模式">
              <el-tag size="small"
                      :type="selectedParam.valueMode == '1' ? 'success' : 'warning'">
                {{ selectedParam.valueMode == '1' ? '单值' : '区间' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数类型">
              <el-tag size="small"
                      :type="getParamTypeTag(selectedParam.paramType)">
                {{ getParamTypeText(selectedParam.paramType) }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数格式">
              <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
            </el-form-item>
            <el-form-item label="单位">
              <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
            </el-form-item>
            <el-form-item label="标准值"
                          v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.standardValue"
                        type="number"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="最小值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.minValue"
                        type="number"
                        placeholder="请输入最小值" />
            </el-form-item>
            <el-form-item label="最大值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.maxValue"
                        type="number"
                        placeholder="请输入最大值" />
            </el-form-item>
            <el-form-item label="排序">
              <el-input v-model="selectedParam.sort"
                        type="number"
                        placeholder="请输入排序" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired"
                         :active-value="1"
                         :inactive-value="0" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择参数" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedParam"
                     @click="handleParamSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
               width="600px">
      <el-form :model="editParamForm"
               :rules="editParamRules"
               ref="editParamFormRef"
               label-width="120px">
        <el-form-item label="参数名称">
          <span class="detail-text">{{ editParamForm.paramName }}</span>
        </el-form-item>
        <el-form-item label="参数模式">
          <el-tag size="small"
                  :type="editParamForm.valueMode == '1' ? 'success' : 'warning'">
            {{ editParamForm.valueMode == '1' ? '单值' : '区间' }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数类型">
          <el-tag size="small"
                  :type="getParamTypeTag(editParamForm.paramType)">
            {{ getParamTypeText(editParamForm.paramType) }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数格式">
          <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
        </el-form-item>
        <el-form-item label="单位">
          <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
        </el-form-item>
        <el-form-item label="标准值"
                      v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    type="number"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="最小值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="minValue">
          <el-input v-model="editParamForm.minValue"
                    type="number"
                    placeholder="请输入最小值" />
        </el-form-item>
        <el-form-item label="最大值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="maxValue">
          <el-input v-model="editParamForm.maxValue"
                    type="number"
                    placeholder="请输入最大值" />
        </el-form-item>
        <el-form-item label="排序"
                      prop="sort">
          <el-input v-model="editParamForm.sort"
                    type="number"
                    placeholder="请输入排序" />
        </el-form-item>
        <el-form-item label="是否必填"
                      prop="isRequired">
          <el-switch v-model="editParamForm.isRequired"
                     :active-value="1"
                     :inactive-value="0" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="editParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleEditParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <new-process v-if="isShowNewModal"
                 v-model:visible="isShowNewModal"
                 @completed="getList" />
    <edit-process v-if="isShowEditModal"
                  v-model:visible="isShowEditModal"
                  :record="record"
                  @completed="getList" />
    <route-item-form v-if="isShowItemModal"
                     v-model:visible="isShowItemModal"
                     :record="record"
                     @completed="getList" />
  </div>
</template>
<script setup>
  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
  import NewProcess from "@/views/productionManagement/processRoute/New.vue";
  import EditProcess from "@/views/productionManagement/processRoute/Edit.vue";
  import RouteItemForm from "@/views/productionManagement/processRoute/ItemsForm.vue";
  import {
    Plus,
    Edit,
    Delete,
    ArrowUp,
    ArrowDown,
    Right,
    Search,
    Check,
    Close,
    Box,
    Document,
  } from "@element-plus/icons-vue";
  import { listType } from "@/api/system/dict/type";
  import { getByModel } from "@/api/productionManagement/productBom.js";
  import { add, update, del } from "@/api/productionManagement/processRoute.js";
  import {
    addOrUpdateProcessRouteItem,
    batchDeleteProcessRouteItem,
    sortProcessRouteItem,
    findProcessRouteItemList,
    getProcessParamList,
    addProcessRouteItemParam,
    editProcessRouteItemParam,
    delProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import { list as getProcessListApi } from "@/api/productionManagement/productionProcess.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
    listPage,
    del,
    update,
  } from "@/api/productionManagement/processRoute.js";
  import { useRouter } from "vue-router";
  import { ElMessageBox, ElMessage } from "element-plus";
  // å·¥è‰ºè·¯çº¿åˆ—表
  const routeList = ref([]);
  const dictTypes = ref([]);
  // å·¥è‰ºè·¯çº¿åˆ†é¡µ
  const routePage = reactive({
  const router = useRouter();
  const data = reactive({
    searchForm: {
      model: "",
    },
  });
  const { searchForm } = toRefs(data);
  const tableColumn = ref([
    {
      label: "工艺路线编号",
      prop: "processRouteCode",
    },
    {
      label: "产品名称",
      prop: "productName",
    },
    {
      label: "规格名称",
      prop: "model",
    },
    {
      label: "BOM编号",
      prop: "bomNo",
    },
    {
      label: "描述",
      prop: "description",
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 280,
      operation: [
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            showEditModal(row);
          },
        },
        {
          name: "路线项目",
          type: "text",
          clickFun: row => {
            showItemModal(row);
          },
        },
        {
          name: "批准",
          type: "primary",
          text: true,
          showHide: row => {
            return !row.status;
          },
          clickFun: row => {
            handleApproveRoute(row);
          },
        },
        {
          name: "取消批准",
          type: "warning",
          text: true,
          showHide: row => {
            return row.status;
          },
          clickFun: row => {
            handleRevokeApproveRoute(row);
          },
        },
      ],
    },
  ]);
  const tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const isShowNewModal = ref(false);
  const isShowEditModal = ref(false);
  const isShowItemModal = ref(false);
  const record = ref({});
  const page = reactive({
    current: 1,
    size: 10,
    size: 100,
    total: 0,
  });
  // èŽ·å–å…¨å±€å®žä¾‹
  const { proxy } = getCurrentInstance();
  // äº§å“é€‰æ‹©å’ŒBOM相关
  const showProductSelectDialog = ref(false);
  const bomOptions = ref([]);
  // å·¥è‰ºè·¯çº¿å¯¹è¯æ¡†
  const routeDialogVisible = ref(false);
  const isRouteEdit = ref(false);
  const routeFormRef = ref(null);
  const routeForm = reactive({
    id: null,
    productModelId: null,
    productName: "",
    productModelName: "",
    bomId: null,
    routeCode: "",
    description: "",
    status: true,
  });
  const routeRules = {
    productModelId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
    bomId: [{ required: true, message: "请选择BOM", trigger: "change" }],
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  // å·¥åºå¯¹è¯æ¡†
  const processDialogVisible = ref(false);
  const isProcessEdit = ref(false);
  const processFormRef = ref(null);
  const currentRouteId = ref(null);
  const processForm = reactive({
    id: null,
    no: "",
    name: "",
    remark: "",
    status: true,
  });
  const processRules = {
    no: [{ required: true, message: "请输入工序编码", trigger: "blur" }],
    name: [{ required: true, message: "请输入工序名称", trigger: "blur" }],
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†
  const selectProcessDialogVisible = ref(false);
  const availableProcessList = ref([]);
  const filteredProcessList = ref([]);
  const selectedProcessItem = ref(null);
  const processSearchKeyword = ref("");
  const currentRouteIndex = ref(null);
  // å‚数对话框
  const paramDialogVisible = ref(false);
  const isParamEdit = ref(false);
  const paramFormRef = ref(null);
  const currentProcessId = ref(null);
  const paramForm = reactive({
    id: null,
    parameterCode: "",
    parameterName: "",
    parameterType2: "1",
    parameterType: "",
    parameterFormat: "",
    standardValue: "",
    unit: "",
  });
  const paramRules = {
    parameterCode: [
      { required: true, message: "请输入参数编号", trigger: "blur" },
    ],
    parameterName: [
      { required: true, message: "请输入参数名称", trigger: "blur" },
    ],
    parameterType: [
      { required: true, message: "请选择参数类型", trigger: "change" },
    ],
  };
  // é€‰æ‹©å‚数对话框
  const selectParamDialogVisible = ref(false);
  const availableParamList = ref([]);
  const filteredParamList = ref([]);
  const selectedParam = ref(null);
  const paramSearchKeyword = ref("");
  // å¯é€‰å‚数分页
  const paramPage = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // ç¼–辑参数对话框
  const editParamDialogVisible = ref(false);
  const editParamFormRef = ref(null);
  const editParamForm = reactive({
    id: null,
    processId: null,
    paramId: null,
    paramName: "",
    valueMode: "1",
    standardValue: null,
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
  });
  const editParamRules = reactive({
    standardValue: [
      {
        required: true,
        message: "请输入标准值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入标准值"));
          } else {
            callback();
          }
        },
      },
    ],
    minValue: [
      {
        required: true,
        message: "请输入最小值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最小值"));
          } else {
            callback();
          }
        },
      },
    ],
    maxValue: [
      {
        required: true,
        message: "请输入最大值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最大值"));
          } else {
            callback();
          }
        },
      },
    ],
    sort: [
      {
        required: true,
        message: "请输入排序",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入排序"));
          } else if (isNaN(value) || value < 1) {
            callback(new Error("排序必须是大于0的整数"));
          } else {
            callback();
          }
        },
      },
    ],
  });
  // æ‹–拽相关
  const draggedItem = ref(null);
  const draggedRouteId = ref(null);
  // èŽ·å–å·¥è‰ºè·¯çº¿åˆ—è¡¨
  const getRouteList = () => {
    // å¯¼å…¥ listPage æ–¹æ³•
    import("@/api/productionManagement/processRoute.js").then(({ listPage }) => {
      listPage({ pageNum: routePage.current, pageSize: routePage.size })
        .then(res => {
          // å¤„理返回的数据,映射到页面需要的格式
          routeList.value = (res.data?.records || []).map(item => ({
            id: item.id,
            productModelId: item.productModelId,
            productName: item.productName,
            productModelName: item.model || item.productModelName,
            bomId: item.bomId,
            bomNo: item.bomNo,
            routeCode: item.processRouteCode || item.routeCode,
            description: item.description || item.description,
            status: item.status,
            expanded: false,
            processList: (item.processList || []).map(process => ({
              ...process,
              processId: process.processId || process.id,
              expanded: false,
            })),
          }));
          // æ›´æ–°åˆ†é¡µæ€»æ•°
          routePage.total = res.data?.total || 0;
        })
        .catch(err => {
          console.error("获取工艺路线列表失败:", err);
          routeList.value = [];
          routePage.total = 0;
        });
    });
  };
  // å±•å¼€/收起工艺路线
  const toggleExpand = route => {
    route.expanded = !route.expanded;
    if (route.expanded) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å·¥åºåˆ—è¡¨
      findProcessRouteItemList({ routeId: route.id })
        .then(res => {
          route.processList = (res.data || []).map(process => ({
            ...process,
            processId: process.processId || process.id,
            expanded: false,
          }));
        })
        .catch(err => {
          console.error("获取工序列表失败:", err);
          route.processList = [];
        });
    }
  };
  // å±•å¼€/收起工序参数
  const toggleProcessParams = process => {
    process.expanded = !process.expanded;
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
  const getList = () => {
    tableLoading.value = true;
    const params = { ...searchForm.value, ...page };
    params.entryDate = undefined;
    listPage(params)
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records.map(item => ({
          ...item,
        }));
        page.total = res.data.total;
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
      .catch(err => {
        tableLoading.value = false;
      });
  };
  const toggleProcessParams2 = process => {
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // æ‰“开新增弹框
  const showNewModal = () => {
    isShowNewModal.value = true;
  };
  const showEditModal = row => {
    isShowEditModal.value = true;
    record.value = row;
  };
  const showItemModal = row => {
    router.push({
      path: "/productionManagement/processRouteItem",
      query: {
        id: row.id,
        processRouteCode: row.processRouteCode || "",
        productName: row.productName || "",
        model: row.model || "",
        bomNo: row.bomNo || "",
        bomId: row.bomId || null,
        description: row.description || "",
        type: "route",
      },
    });
  };
  // åˆ é™¤
  function handleDelete() {
    const ids = selectedRows.value.map(item => item.id);
    proxy.$modal
      .confirm("是否确认删除已勾选的数据项?")
      .then(function () {
        return del(ids);
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
  };
  // å·¥è‰ºè·¯çº¿æ“ä½œ
  const handleAddRoute = () => {
    isRouteEdit.value = false;
    routeForm.id = null;
    routeForm.productModelId = null;
    routeForm.productName = "";
    routeForm.productModelName = "";
    routeForm.bomId = null;
    routeForm.routeCode = "";
    routeForm.description = "";
    routeForm.status = false;
    bomOptions.value = [];
    routeDialogVisible.value = true;
  };
      .then(() => {
        getList();
        proxy.$modal.msgSuccess("删除成功");
      })
      .catch(() => {});
  }
  const handleEditRoute = route => {
    isRouteEdit.value = true;
    routeForm.id = route.id;
    routeForm.productModelId = route.productModelId;
    routeForm.productName = route.productName;
    routeForm.productModelName = route.productModelName;
    routeForm.bomId = route.bomId;
    routeForm.routeCode = route.routeCode;
    routeForm.description = route.description;
    routeForm.status = route.status;
    routeDialogVisible.value = true;
  };
  const handleDeleteRoute = route => {
    ElMessageBox.confirm("确定要删除该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      del(route.id)
        .then(res => {
          ElMessage.success("删除成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("删除失败");
        });
    });
  };
  const handleRouteSubmit = () => {
    routeFormRef.value.validate(valid => {
      if (valid) {
        // æž„建提交数据
        const submitData = {
          ...routeForm,
          // æ³¨æ„ï¼šAPI æœŸæœ›çš„字段名可能与表单字段名不同
          productId: routeForm.productModelId,
          productModelId: routeForm.productModelId,
          description: routeForm.description,
        };
        if (isRouteEdit.value) {
          // ç¼–辑操作
          update(submitData)
            .then(res => {
              ElMessage.success("编辑成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("编辑失败");
            });
        } else {
          // æ–°å¢žæ“ä½œ
          add(submitData)
            .then(res => {
              ElMessage.success("新增成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("新增失败");
            });
        }
      }
    });
  };
  const isform2 = ref(null);
  const handleProcessProductSelectClick = () => {
    isform2.value = true;
    showProductSelectDialog.value = true;
  };
  const handleProcessProductSelectClick2 = () => {
    isform2.value = false;
    showProductSelectDialog.value = true;
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (isform2.value) {
      // å¸®æˆ‘写工序中的选择产品的回调,并且把字段加进processForm
      if (products && products.length > 0) {
        const product = products[0];
        console.log("product:", product);
        // æŠŠproduct中的字段添加到processForm中
        // Object.assign(processForm, product);
        processForm.productModelId = product.id;
        processForm.productName = product.productName;
        processForm.model = product.model;
        processForm.unit = product.unit || "";
        console.log("processForm:", processForm);
        // è§¦å‘表单验证更新
        proxy.$refs["processFormRef"]?.validateField("productModelId");
      }
    } else {
      if (products && products.length > 0) {
        const product = products[0];
        // å…ˆæŸ¥è¯¢BOM列表(必选)
        try {
          const res = await getByModel(product.id);
          // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
          let bomList = [];
          if (Array.isArray(res)) {
            bomList = res;
          } else if (res && res.data) {
            bomList = Array.isArray(res.data) ? res.data : [res.data];
          } else if (res && typeof res === "object") {
            bomList = [res];
          }
          console.log("bomList:", bomList);
          if (bomList.length > 0) {
            routeForm.productModelId = product.id;
            routeForm.productName = product.productName;
            routeForm.productModelName = product.model;
            routeForm.bomId = undefined; // é‡ç½®BOM选择
            bomOptions.value = bomList;
            showProductSelectDialog.value = false;
            // è§¦å‘表单验证更新
            proxy.$refs["routeFormRef"]?.validateField("productModelId");
          } else {
            proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
          }
        } catch (error) {
          // å¦‚果接口返回404或其他错误,说明没有BOM
          proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
        }
      }
    }
  };
  // æ‰¹å‡†å·¥è‰ºè·¯çº¿
  const handleApproveRoute = route => {
    ElMessageBox.confirm("确定要批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
@@ -1206,7 +237,7 @@
      update({ id: route.id, status: true })
        .then(res => {
          ElMessage.success("批准成功");
          getRouteList();
          getList();
        })
        .catch(err => {
          ElMessage.error("批准失败");
@@ -1214,6 +245,7 @@
    });
  };
  // å–消批准工艺路线
  const handleRevokeApproveRoute = route => {
    ElMessageBox.confirm("确定要撤销批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
@@ -1224,1194 +256,17 @@
      update({ id: route.id, status: false })
        .then(res => {
          ElMessage.success("撤销批准成功");
          getRouteList();
          getList();
        })
        .catch(err => {
          ElMessage.error("撤销批准失败");
        });
    });
  };
  // å·¥åºæ“ä½œ
  const handleSelectProcess = (route, index) => {
    console.log("route:", route);
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    selectedProcessItem.value = null;
    selectProcessDialogVisible.value = true;
  };
  const dragSort = ref(0);
  const currentId = ref(null);
  // ä¿®æ”¹å·¥åº
  const handleEditProcessSelect = (route, index, process) => {
    console.log("route:", route);
    console.log("process:", process);
    currentId.value = process.id;
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    // è®¾ç½®é€‰ä¸­çš„工序
    filteredProcessList.value.map(item => {
      if (item.id === process.processId) {
        selectedProcessItem.value = item;
      }
    });
    dragSort.value = process.dragSort;
    // selectedProcessItem.value = process;
    // å¡«å……产品选择表单
    processForm.productModelId = process.productModelId;
    processForm.productName = process.productName;
    processForm.model = process.model;
    processForm.processId = process.no;
    // processForm.name = process.name;
    processForm.unit = process.unit || "";
    processForm.isQuality = process.isQuality || false;
    selectProcessDialogVisible.value = true;
  };
  const handleEditProcess = (routeId, process) => {
    currentRouteId.value = routeId;
    isProcessEdit.value = true;
    processForm.id = process.id;
    processForm.no = process.no;
    processForm.name = process.name;
    processForm.remark = process.remark;
    processForm.status = process.status;
    processDialogVisible.value = true;
  };
  const handleDeleteProcess = (routeId, process) => {
    ElMessageBox.confirm("确定要删除该工序吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除工序
      batchDeleteProcessRouteItem([process.id])
        .then(res => {
          ElMessage.success("删除成功");
          // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              const route = routeList.value.find(r => r.id === routeId);
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        })
        .catch(err => {
          ElMessage.error("删除失败");
          console.error("删除工序失败:", err);
        });
    });
  };
  const handleProcessSubmit = () => {
    processFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isProcessEdit.value ? "编辑成功" : "新增成功");
        processDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        if (currentRouteId.value) {
          findProcessRouteItemList({ routeId: currentRouteId.value })
            .then(res => {
              const route = routeList.value.find(
                r => r.id === currentRouteId.value
              );
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        }
      }
    });
  };
  // é€‰æ‹©å·¥åºç›¸å…³æ–¹æ³•
  const handleProcessSearch = () => {
    const keyword = processSearchKeyword.value.trim().toLowerCase();
    if (!keyword) {
      filteredProcessList.value = availableProcessList.value;
    } else {
      filteredProcessList.value = availableProcessList.value.filter(
        item =>
          (item.name && item.name.toLowerCase().includes(keyword)) ||
          (item.no && item.no.toLowerCase().includes(keyword))
      );
    }
  };
  const handleProcessSelect = row => {
    selectedProcessItem.value = row;
    // é‡ç½®äº§å“é€‰æ‹©è¡¨å•
    processForm.productModelId = undefined;
    processForm.productName = "";
    processForm.productModelName = "";
    processForm.unit = "";
    processForm.isQuality = row.isQuality || false;
  };
  // å¤„理工序选择时的产品选择
  const handleProcessProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      processForm.productModelId = product.id;
      processForm.productName = product.productName;
      processForm.productModelName = product.model;
      processForm.unit = product.unit || "";
      showProductSelectDialog.value = false;
    }
  };
  const handleProcessSelectSubmit = () => {
    if (!selectedProcessItem.value) {
      ElMessage.warning("请先选择一个工序");
      return;
    }
    if (!processForm.productModelId) {
      ElMessage.warning("请选择产品");
      return;
    }
    // æž„建请求参数
    const params = {
      routeId: currentRouteId.value,
      processId: selectedProcessItem.value.id,
      dragSort: routePage.total + 1,
      ...processForm,
    };
    // å¦‚果是修改操作,添加id参数
    if (selectedProcessItem.value.id) {
      params.id = currentId.value;
      params.dragSort = dragSort.value;
    }
    // è°ƒç”¨API添加工序或修改工序
    addOrUpdateProcessRouteItem(params)
      .then(res => {
        ElMessage.success(
          selectedProcessItem.value.id ? "修改工序成功" : "添加工序成功"
        );
        selectProcessDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        findProcessRouteItemList({ routeId: currentRouteId.value })
          .then(res => {
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            if (route) {
              route.processList = (res.data || []).map(process => ({
                ...process,
                processId: process.processId || process.id,
                expanded: false,
              }));
            }
          })
          .catch(err => {
            console.error("获取工序列表失败:", err);
          });
      })
      .catch(err => {
        ElMessage.error(
          selectedProcessItem.value.id ? "修改工序失败" : "添加工序失败"
        );
        console.error(
          selectedProcessItem.value.id ? "修改工序失败:" : "添加工序失败:",
          err
        );
      });
  };
  // å‚数操作
  const handleAddParam = (routeId, process) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    selectedParam.value = null;
    paramSearchKeyword.value = "";
    paramPage.current = 1;
    // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
    selectParamDialogVisible.value = true;
  };
  const handleEditParam = (routeId, process, param) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    editParamForm.id = param.id;
    editParamForm.processId = process.id;
    editParamForm.paramId = param.paramId;
    editParamForm.paramName = param.parameterName || param.paramName;
    editParamForm.valueMode = param.parameterType2 || param.valueMode || "1";
    editParamForm.standardValue = param.standardValue;
    editParamForm.minValue = param.minValue;
    editParamForm.maxValue = param.maxValue;
    editParamForm.sort = param.sort || 1;
    editParamForm.isRequired = param.isRequired || 0;
    editParamForm.paramType = param.parameterType || param.paramType;
    editParamForm.paramFormat = param.parameterFormat || param.paramFormat;
    editParamForm.unit = param.unit || param.unit;
    editParamDialogVisible.value = true;
  };
  const handleDeleteParam = (routeId, process, param) => {
    ElMessageBox.confirm("确定要删除该参数吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除参数
      delProcessRouteItemParam(param.id)
        .then(res => {
          ElMessage.success("删除成功");
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("删除参数失败");
          console.error("删除参数失败:", err);
        });
    });
  };
  const handleParamSubmit = () => {
    paramFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isParamEdit.value ? "编辑成功" : "新增成功");
        paramDialogVisible.value = false;
        getRouteList();
      }
    });
  };
  const handleParamTypeChange = () => {
    if (paramForm.parameterType === "数值格式") {
      paramForm.parameterFormat = "#.0000";
    } else if (paramForm.parameterType === "时间格式") {
      paramForm.parameterFormat = "YYYY-MM-DD HH:mm:ss";
    } else {
      paramForm.parameterFormat = "";
    }
  };
  const getParamTypeTag = type => {
    const typeMap = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return typeMap[type] || "default";
  };
  const getParamTypeText = type => {
    const typeMap = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return typeMap[type] || "未知参数类型";
  };
  // é€‰æ‹©å‚数相关方法
  const handleParamSearch = () => {
    // é‡ç½®åˆ†é¡µ
    paramPage.current = 1;
    // é‡æ–°åŠ è½½æ•°æ®
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  const handleParamSelect = row => {
    selectedParam.value = row;
  };
  // å¤„理分页大小变化
  const handleParamSizeChange = size => {
    paramPage.size = size;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å¤„理当前页码变化
  const handleParamCurrentChange = current => {
    paramPage.current = current;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å·¥è‰ºè·¯çº¿åˆ†é¡µå¤„理
  const handleRouteSizeChange = size => {
    routePage.size = size;
    getRouteList();
  };
  const handleRouteCurrentChange = current => {
    routePage.current = current;
    getRouteList();
  };
  const handleParamSelectSubmit = () => {
    if (!selectedParam.value) {
      ElMessage.warning("请先选择一个参数");
      return;
    }
    // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
    const route = routeList.value.find(r => r.id === currentRouteId.value);
    const process = route?.processList.find(p => p.id === currentProcessId.value);
    if (route && process) {
      // æ£€æŸ¥å‚数是否已存在
      // const exists = process.paramList?.some(
      //   p =>
      //     p.paramId === selectedParam.value.id ||
      //     p.parameterCode === selectedParam.value.paramCode
      // );
      // if (exists) {
      //   ElMessage.warning("该参数已存在于工序中");
      //   return;
      // }
      // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
      const isNumericMode = selectedParam.value.valueMode === 1;
      // è°ƒç”¨API新增参数
      addProcessRouteItemParam({
        routeItemId: process.id,
        paramId: selectedParam.value.id,
        standardValue: isNumericMode
          ? selectedParam.value.standardValue || ""
          : "",
        minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
        maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
        isRequired: selectedParam.value.isRequired || 0,
      })
        .then(res => {
          ElMessage.success("添加参数成功");
          selectParamDialogVisible.value = false;
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    }
  };
  const handleEditParamSubmit = () => {
    editParamFormRef.value.validate(valid => {
      if (valid) {
        // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
        const isNumericMode = editParamForm.valueMode == 1;
        // è°ƒç”¨API修改参数
        editProcessRouteItemParam({
          id: editParamForm.id,
          routeItemId: currentProcessId.value,
          paramId: editParamForm.paramId,
          standardValue: isNumericMode ? editParamForm.standardValue || "" : "",
          minValue: isNumericMode ? editParamForm.minValue || 0 : null,
          maxValue: isNumericMode ? editParamForm.maxValue || 0 : null,
          isRequired: editParamForm.isRequired || 0,
        })
          .then(res => {
            ElMessage.success("编辑成功");
            editParamDialogVisible.value = false;
            // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            const process = route?.processList.find(
              p => p.id === currentProcessId.value
            );
            // åˆ·æ–°å‚数列表
            if (process) {
              toggleProcessParams2(process);
            }
          })
          .catch(err => {
            ElMessage.error("编辑参数失败");
            console.error("编辑参数失败:", err);
          });
      }
    });
  };
  // æ‹–拽排序
  const handleDragStart = (event, index, routeId) => {
    draggedItem.value = index;
    draggedRouteId.value = routeId;
    event.dataTransfer.effectAllowed = "move";
  };
  const handleDragOver = event => {
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  };
  const handleDrop = (event, dropIndex, routeId) => {
    event.preventDefault();
    if (draggedItem.value === null || draggedItem.value === dropIndex) return;
    const route = routeList.value.find(r => r.id === routeId);
    if (route && route.processList) {
      const draggedProcess = route.processList[draggedItem.value];
      // è®¡ç®—新的排序值
      const newDragSort = dropIndex + 1;
      // è°ƒç”¨API排序工序
      sortProcessRouteItem({
        id: draggedProcess.id,
        dragSort: newDragSort,
      })
        .then(res => {
          // è°ƒç”¨æŽ¥å£èŽ·å–æœ€æ–°çš„å·¥åºåˆ—è¡¨
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
              ElMessage.success("排序成功");
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
              ElMessage.success("排序成功");
            });
        })
        .catch(err => {
          ElMessage.error("排序失败");
          console.error("排序工序失败:", err);
        });
    }
  };
  const handleDragEnd = () => {
    draggedItem.value = null;
    draggedRouteId.value = null;
  };
  // èŽ·å–æ•°æ®å­—å…¸
  const getDictTypes = () => {
    listType({ pageNum: 1, pageSize: 1000 }).then(res => {
      dictTypes.value = res.rows || [];
    });
  };
  getRouteList();
  getDictTypes();
  // é¡µé¢åŠ è½½æ—¶èŽ·å–å·¥åºåˆ—è¡¨
  onMounted(() => {
    getProcessListApi()
      .then(res => {
        // å¤„理返回的数据,映射到页面需要的格式
        availableProcessList.value = (res.data || []).map(item => ({
          id: item.id,
          no: item.no || item.no,
          name: item.name || item.name,
          remark: item.remark || item.remark,
          status: item.status,
          isQuality: item.isQuality,
        }));
        filteredProcessList.value = availableProcessList.value;
      })
      .catch(() => {
        ElMessage.error("获取工序列表失败");
      });
    getList();
  });
</script>
<style scoped lang="scss">
  .app-container {
    padding: 20px;
    padding-bottom: 80px;
    background-color: #f0f2f5;
    min-height: calc(100vh - 84px);
    overflow: hidden;
  }
  .route-header {
    margin-bottom: 20px;
    .add-route-btn {
      width: 100%;
      display: inline-flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-width: 120px;
      height: 100px;
      border: 2px dashed #dcdfe6;
      border-radius: 12px;
      background: #fafafa;
      cursor: pointer;
      transition: all 0.3s ease;
      color: #909399;
      padding: 0 20px;
      .el-icon {
        font-size: 24px;
        margin-bottom: 8px;
      }
      span {
        font-size: 13px;
      }
      &:hover {
        border-color: #409eff;
        background: #ecf5ff;
        color: #409eff;
      }
    }
  }
  .route-card-list {
    display: grid;
    grid-template-columns: repeat(1, 1fr);
    gap: 20px;
    max-height: calc(100vh - 240px);
    overflow-y: auto;
    padding-right: 10px;
  }
  /* è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼ */
  .route-card-list::-webkit-scrollbar {
    width: 8px;
  }
  .route-card-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  .route-card {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    overflow: hidden;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20px 40px;
      border-bottom: 1px solid #ebeef5;
      background: #f8f9fa;
      .route-info {
        display: flex;
        // flex-direction: column;
        // justify-content: center;
        // items-align: center;
        gap: 4px;
        .route-code {
          font-size: 12px;
          color: #909399;
          font-family: "Courier New", monospace;
          line-height: 30px;
        }
        .route-name {
          font-size: 18px;
          font-weight: 600;
          color: #303133;
          display: flex;
          align-items: center;
        }
      }
      .route-actions {
        display: flex;
        gap: 8px;
        // .el-button {
        //   color: #409eff;
        // }
      }
    }
    .card-body {
      padding: 16px 40px;
      .route-desc {
        font-size: 14px;
        color: #606266;
        margin-bottom: 12px;
      }
      .route-meta {
        display: flex;
        gap: 24px;
        margin-bottom: 12px;
        padding: 10px 14px;
        background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
        border-radius: 8px;
        border-left: 3px solid #409eff;
        .meta-item {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 13px;
          margin-right: 40px;
          .el-icon {
            font-size: 14px;
            color: #409eff;
          }
          .meta-label {
            color: #909399;
            font-weight: 500;
          }
          .meta-value {
            color: #303133;
            font-weight: 600;
          }
        }
      }
      .expand-btn-wrapper {
        display: flex;
        justify-content: center;
        margin-top: 8px;
        .expand-btn {
          padding: 8px 20px;
          border-radius: 20px;
          background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
          border: 1px solid #b3d8ff;
          transition: all 0.3s ease;
          .btn-text {
            font-size: 13px;
            font-weight: 500;
            color: #409eff;
            margin-right: 6px;
          }
          .expand-icon {
            font-size: 14px;
            color: #409eff;
            transition: transform 0.3s ease;
          }
          &:hover {
            background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
            border-color: #409eff;
            box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
            .btn-text,
            .expand-icon {
              color: #fff;
            }
          }
          &.expanded {
            background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%);
            border-color: #a5d69a;
            .btn-text,
            .expand-icon {
              color: #67c23a;
            }
            &:hover {
              background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
              border-color: #67c23a;
              box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
              .btn-text,
              .expand-icon {
                color: #fff;
              }
            }
          }
        }
      }
    }
    .process-route {
      padding: 0 20px 20px;
      background: #f5f7fa;
      border-top: 1px solid #ebeef5;
      .process-flow {
        display: flex;
        align-items: flex-start;
        gap: 8px;
        padding: 20px 0;
        overflow-x: auto;
        overflow-y: hidden;
        .process-flow-item {
          display: flex;
          align-items: center;
          gap: 8px;
          .process-node {
            background: #fff;
            border-radius: 12px;
            padding: 16px;
            border: 2px solid #ebeef5;
            cursor: move;
            transition: all 0.3s ease;
            // min-width: 180px;
            // max-width: 220px;
            width: 300px;
            &.expanded {
              width: 400px;
            }
            &:hover {
              box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
              transform: translateY(-2px);
              border-color: #409eff;
            }
            &:active {
              cursor: grabbing;
            }
            .process-node-header {
              display: flex;
              justify-content: space-between;
              align-items: center;
              margin-bottom: 12px;
              .process-number {
                width: 28px;
                height: 28px;
                border-radius: 50%;
                background: #409eff;
                color: #ffffff;
                font-size: 12px;
                font-weight: 600;
                display: flex;
                align-items: center;
                justify-content: center;
              }
              .process-actions {
                display: flex;
                gap: 4px;
              }
            }
            .process-node-body {
              text-align: center;
              margin-bottom: 12px;
              .process-code {
                font-size: 11px;
                color: #909399;
                font-family: "Courier New", monospace;
                margin-bottom: 4px;
              }
              .process-name {
                font-size: 15px;
                font-weight: 600;
                color: #303133;
                margin-bottom: 6px;
              }
              .process-desc {
                font-size: 12px;
                color: #606266;
                overflow: hidden;
                text-overflow: ellipsis;
                display: -webkit-box;
                -webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
              }
            }
            .process-node-footer {
              display: flex;
              justify-content: flex-end;
              align-items: center;
              padding-top: 10px;
              border-top: 1px solid #ebeef5;
            }
            .process-params-section {
              margin-top: 12px;
              padding-top: 12px;
              border-top: 1px solid #ebeef5;
              .params-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 8px;
                font-size: 13px;
                font-weight: 600;
                color: #303133;
              }
              .params-list {
                display: flex;
                flex-direction: column;
                gap: 6px;
                max-height: 200px;
                overflow-y: auto;
                .param-item {
                  display: flex;
                  justify-content: space-between;
                  align-items: center;
                  padding: 6px 8px;
                  background: #fafafa;
                  border-radius: 4px;
                  border-left: 2px solid #409eff;
                  font-size: 12px;
                  .param-info {
                    display: flex;
                    flex-direction: row;
                    align-items: center;
                    gap: 6px;
                    flex: 1;
                    min-width: 0;
                    .param-code {
                      font-size: 11px;
                      color: #e6a23c;
                      font-family: "Courier New", monospace;
                      margin-right: 20px;
                    }
                    .param-name {
                      font-size: 12px;
                      color: #303133;
                      font-weight: 500;
                      margin-right: 20px;
                    }
                    .param-value {
                      font-size: 11px;
                      color: #606266;
                    }
                  }
                  .param-actions {
                    display: flex;
                    gap: 4px;
                    flex-shrink: 0;
                  }
                }
              }
            }
          }
          .flow-arrow {
            display: flex;
            align-items: center;
            color: #c0c4cc;
            font-size: 24px;
            padding: 0 4px;
            .el-icon {
              font-size: 20px;
            }
          }
        }
        .add-process-node {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          min-width: 100px;
          height: 137px;
          border: 2px dashed #dcdfe6;
          border-radius: 12px;
          background: #fafafa;
          cursor: pointer;
          transition: all 0.3s ease;
          color: #909399;
          // margin-left: 10px;
          .el-icon {
            font-size: 24px;
            margin-bottom: 8px;
          }
          span {
            font-size: 13px;
          }
          &:hover {
            border-color: #409eff;
            background: #ecf5ff;
            color: #409eff;
          }
        }
      }
    }
  }
  // æ‹–拽时的样式
  .process-flow-item.dragging {
    opacity: 0.5;
    transform: scale(0.98);
  }
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†æ ·å¼
  .process-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .process-list-area {
      flex: 1;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .process-detail-area {
      width: 380px;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .process-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // é€‰æ‹©å‚数对话框样式
  .param-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .param-list-area {
      // flex: 1;
      width: 380px;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .param-detail-area {
      // width: 380px;
      flex: 1;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .param-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // åˆ†é¡µæŽ§ä»¶æ ·å¼
  .pagination-container {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: flex-end;
    padding: 16px 20px;
    background-color: #fff !important;
    border-top: 1px solid #ebeef5;
    box-shadow: 0 -2px 12px 0 rgba(0, 0, 0, 0.1);
    z-index: 100;
    .el-pagination {
      .el-pagination__sizes {
        margin-right: 16px;
      }
      .el-pagination__jump {
        margin-left: 16px;
      }
      .el-pagination__total {
        color: #606266;
        font-size: 14px;
      }
      .el-pagination__button {
        border-radius: 4px;
        transition: all 0.3s ease;
        &:hover:not(:disabled) {
          color: #409eff;
          border-color: #409eff;
        }
      }
      .el-pagination__button--active {
        background-color: #409eff;
        border-color: #409eff;
        color: #fff;
      }
    }
  }
</style>
<style scoped></style>
src/views/productionManagement/processRoute/index2.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2417 @@
<template>
  <div class="app-container">
    <div class="route-header">
      <div class="add-route-btn"
           @click="handleAddRoute">
        <el-icon>
          <Plus />
        </el-icon>
        <span>新增工艺路线</span>
      </div>
    </div>
    <div class="route-card-list">
      <div v-for="route in routeList"
           :key="route.id"
           class="route-card">
        <div class="card-header">
          <div class="route-info">
            <span class="route-name"><el-icon style="margin-right: 8px;line-height: 30px;">
                <ScaleToOriginal />
              </el-icon>{{route.routeCode }}<el-tag style="margin-left: 8px"
                      :type="!route.status ? 'warning' : 'success'">{{ !route.status ? '草稿' : '批准' }}</el-tag></span>
            <!-- <span class="route-code">{{ route.routeCode }}</span> -->
          </div>
          <div class="route-actions">
            <el-button v-if="!route.status"
                       link
                       type="success"
                       @click="handleApproveRoute(route)">
              <el-icon>
                <Check />
              </el-icon>
              æ‰¹å‡†
            </el-button>
            <el-button v-if="route.status"
                       link
                       type="warning"
                       @click="handleRevokeApproveRoute(route)">
              <el-icon>
                <Close />
              </el-icon>
              æ’¤é”€æ‰¹å‡†
            </el-button>
            <el-button link
                       type="primary"
                       @click="handleEditRoute(route)">
              <el-icon>
                <Edit />
              </el-icon>
              ç¼–辑
            </el-button>
            <el-button link
                       type="danger"
                       @click="handleDeleteRoute(route)">
              <el-icon>
                <Delete />
              </el-icon>
              åˆ é™¤
            </el-button>
          </div>
        </div>
        <div class="card-body">
          <div class="route-meta">
            <span class="meta-item">
              <el-icon>
                <Box />
              </el-icon>
              <span class="meta-label">产品:</span>
              <span class="meta-value">{{ route.productName }} - {{ route.productModelName }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">BOM:</span>
              <span class="meta-value">{{ route.bomNo || '-' }}</span>
            </span>
            <span class="meta-item">
              <el-icon>
                <Document />
              </el-icon>
              <span class="meta-label">备注:</span>
              <span class="meta-value">{{ route.description || '暂无描述' }}</span>
            </span>
          </div>
          <div class="expand-btn-wrapper">
            <el-button class="expand-btn"
                       :class="{ expanded: route.expanded }"
                       type="primary"
                       text
                       @click="toggleExpand(route)">
              <span class="btn-text">{{ route.expanded ? '收起工序路线' : '展开工序路线' }}</span>
              <el-icon class="expand-icon">
                <component :is="route.expanded ? 'ArrowUp' : 'ArrowDown'" />
              </el-icon>
            </el-button>
          </div>
        </div>
        <div v-if="route.expanded"
             class="process-route">
          <div class="process-flow">
            <div v-for="(process, index) in route.processList"
                 :key="process.id"
                 class="process-flow-item"
                 draggable="true"
                 @dragstart="handleDragStart($event, index, route.id)"
                 @dragover="handleDragOver($event)"
                 @drop="handleDrop($event, index, route.id)"
                 @dragend="handleDragEnd">
              <div class="process-node"
                   :class="{ expanded: process.expanded }">
                <div class="process-node-header">
                  <div class="process-number">{{ index + 1 }}</div>
                  <div class="process-actions">
                    <el-button link
                               type="primary"
                               @click="handleEditProcessSelect(route, index, process)">
                      <el-icon>
                        <Edit />
                      </el-icon>
                    </el-button>
                    <el-button link
                               type="danger"
                               @click="handleDeleteProcess(route.id, process)">
                      <el-icon>
                        <Delete />
                      </el-icon>
                    </el-button>
                  </div>
                </div>
                <div class="process-node-body">
                  <!-- <div class="process-code">{{ process.processId }}</div> -->
                  <div class="process-name">{{ process.processName }}</div>
                  <!-- <div class="process-desc">{{ process.remark || '暂无描述' }}</div> -->
                </div>
                <div class="process-node-footer">
                  <!-- <el-tag size="small"
                          :type="process.status === '1' ? 'success' : 'info'">
                    {{ process.status === '1' ? '启用' : '停用' }}
                  </el-tag> -->
                  <el-button type="primary"
                             link
                             size="small"
                             @click="toggleProcessParams(process)">
                    {{ process.expanded ? '收起参数' : '展开参数' }}
                    ({{ process.paramCount }})
                  </el-button>
                </div>
                <div v-if="process.expanded"
                     class="process-params-section">
                  <div class="params-header">
                    <span>参数列表</span>
                    <el-button type="primary"
                               link
                               size="small"
                               @click="handleAddParam(route.id, process)">
                      <el-icon>
                        <Plus />
                      </el-icon>新增
                    </el-button>
                  </div>
                  <div class="params-list">
                    <div v-for="param in process.paramList"
                         :key="param.id"
                         class="param-item">
                      <div class="param-info">
                        <span class="param-code">{{ param.paramName }}</span>
                        <!-- <span class="param-name">{{ param.paramName }}</span> -->
                        <!-- <el-tag size="small"
                                style="margin-right: 20px;"
                                :type="getParamTypeTag(param.parameterType)">
                          {{ param.parameterType }}
                        </el-tag> -->
                        <span v-if="param.valueMode==1"
                              class="param-value">标准值:{{ param.standardValue || "-" }} {{ param.unit }}</span>
                        <span v-else
                              class="param-value">标准值:{{ param.minValue || "-" }}-{{ param.maxValue || "-" }} {{ param.unit }}</span>
                      </div>
                      <div class="param-actions">
                        <el-button link
                                   type="primary"
                                   size="small"
                                   @click="handleEditParam(route.id, process, param)">
                          ç¼–辑
                        </el-button>
                        <el-button link
                                   type="danger"
                                   size="small"
                                   @click="handleDeleteParam(route.id, process, param)">
                          åˆ é™¤
                        </el-button>
                      </div>
                    </div>
                    <el-empty v-if="!process.paramList || process.paramList.length === 0"
                              description="暂无参数"
                              :image-size="50" />
                  </div>
                </div>
              </div>
              <div v-if="index < route.processList.length - 1"
                   class="flow-arrow">
                <el-icon>
                  <Right />
                </el-icon>
              </div>
            </div>
            <div class="add-process-node"
                 @click="handleSelectProcess(route, index)">
              <el-icon>
                <Plus />
              </el-icon>
              <span>新增工序</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- åˆ†é¡µæŽ§ä»¶ -->
    <div class="pagination-container">
      <el-pagination v-model:current-page="routePage.current"
                     v-model:page-size="routePage.size"
                     :page-sizes="[10, 20, 50, 100]"
                     layout="total, sizes, prev, pager, next, jumper"
                     :total="routePage.total"
                     @size-change="handleRouteSizeChange"
                     @current-change="handleRouteCurrentChange" />
    </div>
    <!-- å·¥è‰ºè·¯çº¿æ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="routeDialogVisible"
               :title="isRouteEdit ? '编辑工艺路线' : '新增工艺路线'"
               width="500px">
      <el-form :model="routeForm"
               :rules="routeRules"
               ref="routeFormRef"
               label-width="120px">
        <el-form-item label="产品名称"
                      prop="productModelId">
          <el-button type="primary"
                     @click="handleProcessProductSelectClick2">
            {{ routeForm.productName && routeForm.productModelName
              ? `${routeForm.productName} - ${routeForm.productModelName}`
              : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item label="BOM"
                      prop="bomId">
          <el-select v-model="routeForm.bomId"
                     placeholder="请选择BOM"
                     clearable
                     :disabled="!routeForm.productModelId || bomOptions.length === 0"
                     style="width: 100%">
            <el-option v-for="item in bomOptions"
                       :key="item.id"
                       :label="item.bomNo || `BOM-${item.id}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="路线编码"
                      prop="routeCode">
          <el-input v-model="routeForm.routeCode"
                    disabled
                    placeholder="自动生成" />
        </el-form-item>
        <el-form-item label="备注"
                      prop="description">
          <el-input v-model="routeForm.description"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入路线描述" />
        </el-form-item>
        <!-- <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="routeForm.status">
            <el-radio label="1">启用</el-radio>
            <el-radio label="0">停用</el-radio>
          </el-radio-group>
        </el-form-item> -->
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="routeDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleRouteSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¼¹çª— -->
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
    <!-- å·¥åºæ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="processDialogVisible"
               :title="isProcessEdit ? '编辑工序' : '新增工序'"
               width="500px">
      <el-form :model="processForm"
               :rules="processRules"
               ref="processFormRef"
               label-width="120px">
        <el-form-item label="工序编码"
                      prop="no">
          <el-input v-model="processForm.no"
                    placeholder="请输入工序编码" />
        </el-form-item>
        <el-form-item label="工序名称"
                      prop="name">
          <el-input v-model="processForm.name"
                    placeholder="请输入工序名称" />
        </el-form-item>
        <el-form-item label="工序描述"
                      prop="remark">
          <el-input v-model="processForm.remark"
                    type="textarea"
                    :rows="3"
                    placeholder="请输入工序描述" />
        </el-form-item>
        <el-form-item label="状态"
                      prop="status">
          <el-radio-group v-model="processForm.status">
            <el-radio :label="true">启用</el-radio>
            <el-radio :label="false">停用</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="processDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleProcessSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å·¥åºå¯¹è¯æ¡† -->
    <el-dialog v-model="selectProcessDialogVisible"
               title="选择工序"
               width="1000px">
      <div class="process-select-container">
        <!-- å·¦ä¾§å·¥åºåˆ—表 -->
        <div class="process-list-area">
          <div class="area-title">可选工序</div>
          <div class="search-box">
            <el-input v-model="processSearchKeyword"
                      placeholder="请输入工序名称搜索"
                      clearable
                      size="small"
                      @input="handleProcessSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredProcessList"
                    height="360"
                    border
                    highlight-current-row
                    @current-change="handleProcessSelect">
            <el-table-column prop="no"
                             label="工序编号"
                             width="100" />
            <el-table-column prop="name"
                             label="工序名称" />
            <el-table-column prop="remark"
                             label="工序描述" />
            <el-table-column prop="status"
                             label="状态"
                             width="80">
              <template #default="scope">
                <el-tag size="small"
                        :type="scope.row.status ? 'success' : 'info'">
                  {{ scope.row.status ? '启用' : '停用' }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <!-- å³ä¾§å·¥åºè¯¦æƒ… -->
        <div class="process-detail-area">
          <div class="area-title">工序详情</div>
          <el-form v-if="selectedProcessItem"
                   :model="processForm"
                   label-width="100px"
                   class="process-detail-form">
            <el-form-item label="工序编号">
              <span class="detail-text">{{ selectedProcessItem.no }}</span>
            </el-form-item>
            <el-form-item label="工序名称">
              <span class="detail-text">{{ selectedProcessItem.name }}</span>
            </el-form-item>
            <el-form-item label="工序描述">
              <span class="detail-text">{{ selectedProcessItem.remark || '-' }}</span>
            </el-form-item>
            <el-form-item label="状态">
              <el-tag size="small"
                      :type="selectedProcessItem.status ? 'success' : 'info'">
                {{ selectedProcessItem.status ? '启用' : '停用' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="是否质检">
              <el-tag size="small"
                      :type="selectedProcessItem.isQuality ? 'success' : 'info'">
                {{ selectedProcessItem.isQuality ? '质检' : '非质检' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="产品名称"
                          prop="productModelId">
              <el-button type="primary"
                         @click="handleProcessProductSelectClick">
                {{ processForm.productName && processForm.model
                  ? `${processForm.productName} - ${processForm.model}`
                  : '选择产品' }}
              </el-button>
            </el-form-item>
            <el-form-item label="单位"
                          prop="unit">
              <el-input v-model="processForm.unit"
                        :placeholder="processForm.productModelId ? '根据选择的产品自动带出' : '请先选择产品' "
                        clearable
                        :disabled="true" />
            </el-form-item>
            <el-form-item label="是否质检"
                          prop="isQuality">
              <el-switch v-model="processForm.isQuality"
                         :active-value="true"
                         inactive-value="false" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择工序" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectProcessDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedProcessItem || !processForm.productModelId"
                     @click="handleProcessSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- å‚数新增/编辑对话框 -->
    <el-dialog v-model="paramDialogVisible"
               :title="isParamEdit ? '编辑参数' : '新增参数'"
               width="500px">
      <el-form :model="paramForm"
               :rules="paramRules"
               ref="paramFormRef"
               label-width="120px">
        <el-form-item label="参数编号"
                      prop="parameterCode">
          <el-input v-model="paramForm.parameterCode"
                    placeholder="请输入参数编号" />
        </el-form-item>
        <el-form-item label="参数名称"
                      prop="parameterName">
          <el-input v-model="paramForm.parameterName"
                    placeholder="请输入参数名称" />
        </el-form-item>
        <el-form-item label="参数模式"
                      prop="parameterType2">
          <el-select v-model="paramForm.parameterType2"
                     placeholder="请选择参数模式">
            <el-option label="单值"
                       value="1" />
            <el-option label="区间"
                       value="2" />
          </el-select>
        </el-form-item>
        <el-form-item label="参数类型"
                      prop="parameterType">
          <el-select v-model="paramForm.parameterType"
                     @change="handleParamTypeChange"
                     placeholder="请选择参数类型">
            <el-option label="数值格式"
                       value="数值格式" />
            <el-option label="文本格式"
                       value="文本格式" />
            <el-option label="下拉选项"
                       value="下拉选项" />
            <el-option label="时间格式"
                       value="时间格式" />
          </el-select>
        </el-form-item>
        <el-form-item v-if="paramForm.parameterType === '下拉选项'"
                      label="数据字典"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择数据字典">
            <el-option v-for="item in dictTypes"
                       :key="item.dictType"
                       :label="item.dictName"
                       :value="item.dictType" />
          </el-select>
        </el-form-item>
        <el-form-item v-else-if="paramForm.parameterType === '时间格式'"
                      label="时间格式"
                      prop="parameterFormat">
          <el-select v-model="paramForm.parameterFormat"
                     placeholder="请选择时间格式">
            <el-option label="YYYY-MM-DD HH:mm:ss"
                       value="YYYY-MM-DD HH:mm:ss" />
            <el-option label="YYYY-MM-DD"
                       value="YYYY-MM-DD" />
          </el-select>
        </el-form-item>
        <el-form-item v-else
                      label="参数格式"
                      prop="parameterFormat">
          <el-input v-model="paramForm.parameterFormat"
                    placeholder="请输入参数格式" />
        </el-form-item>
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="paramForm.standardValue"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="单位"
                      prop="unit">
          <el-input v-model="paramForm.unit"
                    placeholder="请输入单位" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="paramDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="selectParamDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
        <!-- å·¦ä¾§å‚数列表 -->
        <div class="param-list-area">
          <div class="area-title">可选参数</div>
          <div class="search-box">
            <el-input v-model="paramSearchKeyword"
                      placeholder="请输入参数名称搜索"
                      clearable
                      size="small"
                      @input="handleParamSearch">
              <template #prefix>
                <el-icon>
                  <Search />
                </el-icon>
              </template>
            </el-input>
          </div>
          <el-table :data="filteredParamList"
                    height="300"
                    border
                    highlight-current-row
                    @current-change="handleParamSelect">
            <el-table-column prop="paramName"
                             label="参数名称" />
            <el-table-column prop="paramType"
                             label="参数类型">
              <template #default="scope">
                <el-tag size="small"
                        :type="getParamTypeTag(scope.row.paramType)">
                  {{ getParamTypeText(scope.row.paramType) }}
                </el-tag>
              </template>
            </el-table-column>
          </el-table>
          <!-- åˆ†é¡µæŽ§ä»¶ -->
          <div class="pagination-container"
               style="margin-top: 10px;">
            <el-pagination v-model:current-page="paramPage.current"
                           v-model:page-size="paramPage.size"
                           :page-sizes="[10, 20, 50, 100]"
                           layout="total, sizes, prev, pager, next, jumper"
                           :total="paramPage.total"
                           @size-change="handleParamSizeChange"
                           @current-change="handleParamCurrentChange"
                           size="small" />
          </div>
        </div>
        <!-- å³ä¾§å‚数详情 -->
        <div class="param-detail-area">
          <div class="area-title">参数详情</div>
          <el-form v-if="selectedParam"
                   :model="selectedParam"
                   label-width="100px"
                   class="param-detail-form">
            <el-form-item label="参数名称">
              <span class="detail-text">{{ selectedParam.paramName }}</span>
            </el-form-item>
            <el-form-item label="参数模式">
              <el-tag size="small"
                      :type="selectedParam.valueMode == '1' ? 'success' : 'warning'">
                {{ selectedParam.valueMode == '1' ? '单值' : '区间' }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数类型">
              <el-tag size="small"
                      :type="getParamTypeTag(selectedParam.paramType)">
                {{ getParamTypeText(selectedParam.paramType) }}
              </el-tag>
            </el-form-item>
            <el-form-item label="参数格式">
              <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
            </el-form-item>
            <el-form-item label="单位">
              <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
            </el-form-item>
            <el-form-item label="标准值"
                          v-if="selectedParam.valueMode == '1' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.standardValue"
                        type="number"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="最小值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.minValue"
                        type="number"
                        placeholder="请输入最小值" />
            </el-form-item>
            <el-form-item label="最大值"
                          v-if="selectedParam.valueMode == '2' && selectedParam.paramType == '1'">
              <el-input v-model="selectedParam.maxValue"
                        type="number"
                        placeholder="请输入最大值" />
            </el-form-item>
            <el-form-item label="排序">
              <el-input v-model="selectedParam.sort"
                        type="number"
                        placeholder="请输入排序" />
            </el-form-item>
            <el-form-item label="是否必填">
              <el-switch v-model="selectedParam.isRequired"
                         :active-value="1"
                         :inactive-value="0" />
            </el-form-item>
          </el-form>
          <el-empty v-else
                    description="请从左侧选择参数" />
        </div>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="selectParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     :disabled="!selectedParam"
                     @click="handleParamSelectSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
               width="600px">
      <el-form :model="editParamForm"
               :rules="editParamRules"
               ref="editParamFormRef"
               label-width="120px">
        <el-form-item label="参数名称">
          <span class="detail-text">{{ editParamForm.paramName }}</span>
        </el-form-item>
        <el-form-item label="参数模式">
          <el-tag size="small"
                  :type="editParamForm.valueMode == '1' ? 'success' : 'warning'">
            {{ editParamForm.valueMode == '1' ? '单值' : '区间' }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数类型">
          <el-tag size="small"
                  :type="getParamTypeTag(editParamForm.paramType)">
            {{ getParamTypeText(editParamForm.paramType) }}
          </el-tag>
        </el-form-item>
        <el-form-item label="参数格式">
          <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
        </el-form-item>
        <el-form-item label="单位">
          <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
        </el-form-item>
        <el-form-item label="标准值"
                      v-if="editParamForm.valueMode == '1' && editParamForm.paramType == '1'"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    type="number"
                    placeholder="请输入标准值" />
        </el-form-item>
        <el-form-item label="最小值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="minValue">
          <el-input v-model="editParamForm.minValue"
                    type="number"
                    placeholder="请输入最小值" />
        </el-form-item>
        <el-form-item label="最大值"
                      v-if="editParamForm.valueMode == '2' && editParamForm.paramType == '1'"
                      prop="maxValue">
          <el-input v-model="editParamForm.maxValue"
                    type="number"
                    placeholder="请输入最大值" />
        </el-form-item>
        <el-form-item label="排序"
                      prop="sort">
          <el-input v-model="editParamForm.sort"
                    type="number"
                    placeholder="请输入排序" />
        </el-form-item>
        <el-form-item label="是否必填"
                      prop="isRequired">
          <el-switch v-model="editParamForm.isRequired"
                     :active-value="1"
                     :inactive-value="0" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="editParamDialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="handleEditParamSubmit">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
  import { ref, reactive, getCurrentInstance, onMounted } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import {
    Plus,
    Edit,
    Delete,
    ArrowUp,
    ArrowDown,
    Right,
    Search,
    Check,
    Close,
    Box,
    Document,
  } from "@element-plus/icons-vue";
  import { listType } from "@/api/system/dict/type";
  import { getByModel } from "@/api/productionManagement/productBom.js";
  import { add, update, del } from "@/api/productionManagement/processRoute.js";
  import {
    addOrUpdateProcessRouteItem,
    batchDeleteProcessRouteItem,
    sortProcessRouteItem,
    findProcessRouteItemList,
    getProcessParamList,
    addProcessRouteItemParam,
    editProcessRouteItemParam,
    delProcessRouteItemParam,
  } from "@/api/productionManagement/processRouteItem.js";
  import { list as getProcessListApi } from "@/api/productionManagement/productionProcess.js";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  // å·¥è‰ºè·¯çº¿åˆ—表
  const routeList = ref([]);
  const dictTypes = ref([]);
  // å·¥è‰ºè·¯çº¿åˆ†é¡µ
  const routePage = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // èŽ·å–å…¨å±€å®žä¾‹
  const { proxy } = getCurrentInstance();
  // äº§å“é€‰æ‹©å’ŒBOM相关
  const showProductSelectDialog = ref(false);
  const bomOptions = ref([]);
  // å·¥è‰ºè·¯çº¿å¯¹è¯æ¡†
  const routeDialogVisible = ref(false);
  const isRouteEdit = ref(false);
  const routeFormRef = ref(null);
  const routeForm = reactive({
    id: null,
    productModelId: null,
    productName: "",
    productModelName: "",
    bomId: null,
    routeCode: "",
    description: "",
    status: true,
  });
  const routeRules = {
    productModelId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
    bomId: [{ required: true, message: "请选择BOM", trigger: "change" }],
  };
  // å·¥åºå¯¹è¯æ¡†
  const processDialogVisible = ref(false);
  const isProcessEdit = ref(false);
  const processFormRef = ref(null);
  const currentRouteId = ref(null);
  const processForm = reactive({
    id: null,
    no: "",
    name: "",
    remark: "",
    status: true,
  });
  const processRules = {
    no: [{ required: true, message: "请输入工序编码", trigger: "blur" }],
    name: [{ required: true, message: "请输入工序名称", trigger: "blur" }],
  };
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†
  const selectProcessDialogVisible = ref(false);
  const availableProcessList = ref([]);
  const filteredProcessList = ref([]);
  const selectedProcessItem = ref(null);
  const processSearchKeyword = ref("");
  const currentRouteIndex = ref(null);
  // å‚数对话框
  const paramDialogVisible = ref(false);
  const isParamEdit = ref(false);
  const paramFormRef = ref(null);
  const currentProcessId = ref(null);
  const paramForm = reactive({
    id: null,
    parameterCode: "",
    parameterName: "",
    parameterType2: "1",
    parameterType: "",
    parameterFormat: "",
    standardValue: "",
    unit: "",
  });
  const paramRules = {
    parameterCode: [
      { required: true, message: "请输入参数编号", trigger: "blur" },
    ],
    parameterName: [
      { required: true, message: "请输入参数名称", trigger: "blur" },
    ],
    parameterType: [
      { required: true, message: "请选择参数类型", trigger: "change" },
    ],
  };
  // é€‰æ‹©å‚数对话框
  const selectParamDialogVisible = ref(false);
  const availableParamList = ref([]);
  const filteredParamList = ref([]);
  const selectedParam = ref(null);
  const paramSearchKeyword = ref("");
  // å¯é€‰å‚数分页
  const paramPage = reactive({
    current: 1,
    size: 10,
    total: 0,
  });
  // ç¼–辑参数对话框
  const editParamDialogVisible = ref(false);
  const editParamFormRef = ref(null);
  const editParamForm = reactive({
    id: null,
    processId: null,
    paramId: null,
    paramName: "",
    valueMode: "1",
    standardValue: null,
    minValue: null,
    maxValue: null,
    sort: 1,
    isRequired: 0,
  });
  const editParamRules = reactive({
    standardValue: [
      {
        required: true,
        message: "请输入标准值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入标准值"));
          } else {
            callback();
          }
        },
      },
    ],
    minValue: [
      {
        required: true,
        message: "请输入最小值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最小值"));
          } else {
            callback();
          }
        },
      },
    ],
    maxValue: [
      {
        required: true,
        message: "请输入最大值",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入最大值"));
          } else {
            callback();
          }
        },
      },
    ],
    sort: [
      {
        required: true,
        message: "请输入排序",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入排序"));
          } else if (isNaN(value) || value < 1) {
            callback(new Error("排序必须是大于0的整数"));
          } else {
            callback();
          }
        },
      },
    ],
  });
  // æ‹–拽相关
  const draggedItem = ref(null);
  const draggedRouteId = ref(null);
  // èŽ·å–å·¥è‰ºè·¯çº¿åˆ—è¡¨
  const getRouteList = () => {
    // å¯¼å…¥ listPage æ–¹æ³•
    import("@/api/productionManagement/processRoute.js").then(({ listPage }) => {
      listPage({ pageNum: routePage.current, pageSize: routePage.size })
        .then(res => {
          // å¤„理返回的数据,映射到页面需要的格式
          routeList.value = (res.data?.records || []).map(item => ({
            id: item.id,
            productModelId: item.productModelId,
            productName: item.productName,
            productModelName: item.model || item.productModelName,
            bomId: item.bomId,
            bomNo: item.bomNo,
            routeCode: item.processRouteCode || item.routeCode,
            description: item.description || item.description,
            status: item.status,
            expanded: false,
            processList: (item.processList || []).map(process => ({
              ...process,
              processId: process.processId || process.id,
              expanded: false,
            })),
          }));
          // æ›´æ–°åˆ†é¡µæ€»æ•°
          routePage.total = res.data?.total || 0;
        })
        .catch(err => {
          console.error("获取工艺路线列表失败:", err);
          routeList.value = [];
          routePage.total = 0;
        });
    });
  };
  // å±•å¼€/收起工艺路线
  const toggleExpand = route => {
    route.expanded = !route.expanded;
    if (route.expanded) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å·¥åºåˆ—è¡¨
      findProcessRouteItemList({ routeId: route.id })
        .then(res => {
          route.processList = (res.data || []).map(process => ({
            ...process,
            processId: process.processId || process.id,
            expanded: false,
          }));
        })
        .catch(err => {
          console.error("获取工序列表失败:", err);
          route.processList = [];
        });
    }
  };
  // å±•å¼€/收起工序参数
  const toggleProcessParams = process => {
    process.expanded = !process.expanded;
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
  };
  const toggleProcessParams2 = process => {
    if (process.expanded && process.id) {
      // è°ƒç”¨æŽ¥å£èŽ·å–å‚æ•°åˆ—è¡¨
      getProcessParamList({
        routeItemId: process.id,
        pageNum: 1,
        pageSize: 1000,
      })
        .then(res => {
          if (res.code === 200) {
            process.paramList = res.data?.records || [];
            process.paramCount = process.paramList.length;
          } else {
            ElMessage.error(res.msg || "获取参数列表失败");
            process.paramList = [];
            process.paramCount = 0;
          }
        })
        .catch(err => {
          console.error("获取参数列表失败:", err);
          ElMessage.error("获取参数列表失败");
          process.paramList = [];
          process.paramCount = 0;
        });
    }
  };
  // å·¥è‰ºè·¯çº¿æ“ä½œ
  const handleAddRoute = () => {
    isRouteEdit.value = false;
    routeForm.id = null;
    routeForm.productModelId = null;
    routeForm.productName = "";
    routeForm.productModelName = "";
    routeForm.bomId = null;
    routeForm.routeCode = "";
    routeForm.description = "";
    routeForm.status = false;
    bomOptions.value = [];
    routeDialogVisible.value = true;
  };
  const handleEditRoute = route => {
    isRouteEdit.value = true;
    routeForm.id = route.id;
    routeForm.productModelId = route.productModelId;
    routeForm.productName = route.productName;
    routeForm.productModelName = route.productModelName;
    routeForm.bomId = route.bomId;
    routeForm.routeCode = route.routeCode;
    routeForm.description = route.description;
    routeForm.status = route.status;
    routeDialogVisible.value = true;
  };
  const handleDeleteRoute = route => {
    ElMessageBox.confirm("确定要删除该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      del(route.id)
        .then(res => {
          ElMessage.success("删除成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("删除失败");
        });
    });
  };
  const handleRouteSubmit = () => {
    routeFormRef.value.validate(valid => {
      if (valid) {
        // æž„建提交数据
        const submitData = {
          ...routeForm,
          // æ³¨æ„ï¼šAPI æœŸæœ›çš„字段名可能与表单字段名不同
          productId: routeForm.productModelId,
          productModelId: routeForm.productModelId,
          description: routeForm.description,
        };
        if (isRouteEdit.value) {
          // ç¼–辑操作
          update(submitData)
            .then(res => {
              ElMessage.success("编辑成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("编辑失败");
            });
        } else {
          // æ–°å¢žæ“ä½œ
          add(submitData)
            .then(res => {
              ElMessage.success("新增成功");
              routeDialogVisible.value = false;
              getRouteList();
            })
            .catch(err => {
              ElMessage.error("新增失败");
            });
        }
      }
    });
  };
  const isform2 = ref(null);
  const handleProcessProductSelectClick = () => {
    isform2.value = true;
    showProductSelectDialog.value = true;
  };
  const handleProcessProductSelectClick2 = () => {
    isform2.value = false;
    showProductSelectDialog.value = true;
  };
  // äº§å“é€‰æ‹©å¤„理
  const handleProductSelect = async products => {
    if (isform2.value) {
      // å¸®æˆ‘写工序中的选择产品的回调,并且把字段加进processForm
      if (products && products.length > 0) {
        const product = products[0];
        console.log("product:", product);
        // æŠŠproduct中的字段添加到processForm中
        // Object.assign(processForm, product);
        processForm.productModelId = product.id;
        processForm.productName = product.productName;
        processForm.model = product.model;
        processForm.unit = product.unit || "";
        console.log("processForm:", processForm);
        // è§¦å‘表单验证更新
        proxy.$refs["processFormRef"]?.validateField("productModelId");
      }
    } else {
      if (products && products.length > 0) {
        const product = products[0];
        // å…ˆæŸ¥è¯¢BOM列表(必选)
        try {
          const res = await getByModel(product.id);
          // å¤„理返回的BOM数据:可能是数组、对象或包含data字段
          let bomList = [];
          if (Array.isArray(res)) {
            bomList = res;
          } else if (res && res.data) {
            bomList = Array.isArray(res.data) ? res.data : [res.data];
          } else if (res && typeof res === "object") {
            bomList = [res];
          }
          console.log("bomList:", bomList);
          if (bomList.length > 0) {
            routeForm.productModelId = product.id;
            routeForm.productName = product.productName;
            routeForm.productModelName = product.model;
            routeForm.bomId = undefined; // é‡ç½®BOM选择
            bomOptions.value = bomList;
            showProductSelectDialog.value = false;
            // è§¦å‘表单验证更新
            proxy.$refs["routeFormRef"]?.validateField("productModelId");
          } else {
            proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
          }
        } catch (error) {
          // å¦‚果接口返回404或其他错误,说明没有BOM
          proxy.$modal.msgError("该产品没有BOM,请先创建BOM");
        }
      }
    }
  };
  const handleApproveRoute = route => {
    ElMessageBox.confirm("确定要批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "info",
    }).then(() => {
      // è°ƒç”¨ä¿®æ”¹æŽ¥å£ï¼Œåªä¿®æ”¹status字段为批准状态
      update({ id: route.id, status: true })
        .then(res => {
          ElMessage.success("批准成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("批准失败");
        });
    });
  };
  const handleRevokeApproveRoute = route => {
    ElMessageBox.confirm("确定要撤销批准该工艺路线吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨ä¿®æ”¹æŽ¥å£ï¼Œåªä¿®æ”¹status字段为草稿状态
      update({ id: route.id, status: false })
        .then(res => {
          ElMessage.success("撤销批准成功");
          getRouteList();
        })
        .catch(err => {
          ElMessage.error("撤销批准失败");
        });
    });
  };
  // å·¥åºæ“ä½œ
  const handleSelectProcess = (route, index) => {
    console.log("route:", route);
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    selectedProcessItem.value = null;
    selectProcessDialogVisible.value = true;
  };
  const dragSort = ref(0);
  const currentId = ref(null);
  // ä¿®æ”¹å·¥åº
  const handleEditProcessSelect = (route, index, process) => {
    console.log("route:", route);
    console.log("process:", process);
    currentId.value = process.id;
    currentRouteId.value = route.id;
    currentRouteIndex.value = index;
    // é‡ç½®æœç´¢å’Œé€‰æ‹©çŠ¶æ€
    filteredProcessList.value = availableProcessList.value;
    processSearchKeyword.value = "";
    // è®¾ç½®é€‰ä¸­çš„工序
    filteredProcessList.value.map(item => {
      if (item.id === process.processId) {
        selectedProcessItem.value = item;
      }
    });
    dragSort.value = process.dragSort;
    // selectedProcessItem.value = process;
    // å¡«å……产品选择表单
    processForm.productModelId = process.productModelId;
    processForm.productName = process.productName;
    processForm.model = process.model;
    processForm.processId = process.no;
    // processForm.name = process.name;
    processForm.unit = process.unit || "";
    processForm.isQuality = process.isQuality || false;
    selectProcessDialogVisible.value = true;
  };
  const handleEditProcess = (routeId, process) => {
    currentRouteId.value = routeId;
    isProcessEdit.value = true;
    processForm.id = process.id;
    processForm.no = process.no;
    processForm.name = process.name;
    processForm.remark = process.remark;
    processForm.status = process.status;
    processDialogVisible.value = true;
  };
  const handleDeleteProcess = (routeId, process) => {
    ElMessageBox.confirm("确定要删除该工序吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除工序
      batchDeleteProcessRouteItem([process.id])
        .then(res => {
          ElMessage.success("删除成功");
          // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              const route = routeList.value.find(r => r.id === routeId);
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        })
        .catch(err => {
          ElMessage.error("删除失败");
          console.error("删除工序失败:", err);
        });
    });
  };
  const handleProcessSubmit = () => {
    processFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isProcessEdit.value ? "编辑成功" : "新增成功");
        processDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        if (currentRouteId.value) {
          findProcessRouteItemList({ routeId: currentRouteId.value })
            .then(res => {
              const route = routeList.value.find(
                r => r.id === currentRouteId.value
              );
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
            });
        }
      }
    });
  };
  // é€‰æ‹©å·¥åºç›¸å…³æ–¹æ³•
  const handleProcessSearch = () => {
    const keyword = processSearchKeyword.value.trim().toLowerCase();
    if (!keyword) {
      filteredProcessList.value = availableProcessList.value;
    } else {
      filteredProcessList.value = availableProcessList.value.filter(
        item =>
          (item.name && item.name.toLowerCase().includes(keyword)) ||
          (item.no && item.no.toLowerCase().includes(keyword))
      );
    }
  };
  const handleProcessSelect = row => {
    selectedProcessItem.value = row;
    // é‡ç½®äº§å“é€‰æ‹©è¡¨å•
    processForm.productModelId = undefined;
    processForm.productName = "";
    processForm.productModelName = "";
    processForm.unit = "";
    processForm.isQuality = row.isQuality || false;
  };
  // å¤„理工序选择时的产品选择
  const handleProcessProductSelect = async products => {
    if (products && products.length > 0) {
      const product = products[0];
      processForm.productModelId = product.id;
      processForm.productName = product.productName;
      processForm.productModelName = product.model;
      processForm.unit = product.unit || "";
      showProductSelectDialog.value = false;
    }
  };
  const handleProcessSelectSubmit = () => {
    if (!selectedProcessItem.value) {
      ElMessage.warning("请先选择一个工序");
      return;
    }
    if (!processForm.productModelId) {
      ElMessage.warning("请选择产品");
      return;
    }
    // æž„建请求参数
    const params = {
      routeId: currentRouteId.value,
      processId: selectedProcessItem.value.id,
      dragSort: routePage.total + 1,
      ...processForm,
    };
    // å¦‚果是修改操作,添加id参数
    if (selectedProcessItem.value.id) {
      params.id = currentId.value;
      params.dragSort = dragSort.value;
    }
    // è°ƒç”¨API添加工序或修改工序
    addOrUpdateProcessRouteItem(params)
      .then(res => {
        ElMessage.success(
          selectedProcessItem.value.id ? "修改工序成功" : "添加工序成功"
        );
        selectProcessDialogVisible.value = false;
        // è°ƒç”¨æŽ¥å£æ›´æ–°å·¥åºåˆ—表
        findProcessRouteItemList({ routeId: currentRouteId.value })
          .then(res => {
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            if (route) {
              route.processList = (res.data || []).map(process => ({
                ...process,
                processId: process.processId || process.id,
                expanded: false,
              }));
            }
          })
          .catch(err => {
            console.error("获取工序列表失败:", err);
          });
      })
      .catch(err => {
        ElMessage.error(
          selectedProcessItem.value.id ? "修改工序失败" : "添加工序失败"
        );
        console.error(
          selectedProcessItem.value.id ? "修改工序失败:" : "添加工序失败:",
          err
        );
      });
  };
  // å‚数操作
  const handleAddParam = (routeId, process) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    selectedParam.value = null;
    paramSearchKeyword.value = "";
    paramPage.current = 1;
    // èŽ·å–å¯é€‰å‚æ•°åˆ—è¡¨
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
    selectParamDialogVisible.value = true;
  };
  const handleEditParam = (routeId, process, param) => {
    currentRouteId.value = routeId;
    currentProcessId.value = process.id;
    editParamForm.id = param.id;
    editParamForm.processId = process.id;
    editParamForm.paramId = param.paramId;
    editParamForm.paramName = param.parameterName || param.paramName;
    editParamForm.valueMode = param.parameterType2 || param.valueMode || "1";
    editParamForm.standardValue = param.standardValue;
    editParamForm.minValue = param.minValue;
    editParamForm.maxValue = param.maxValue;
    editParamForm.sort = param.sort || 1;
    editParamForm.isRequired = param.isRequired || 0;
    editParamForm.paramType = param.parameterType || param.paramType;
    editParamForm.paramFormat = param.parameterFormat || param.paramFormat;
    editParamForm.unit = param.unit || param.unit;
    editParamDialogVisible.value = true;
  };
  const handleDeleteParam = (routeId, process, param) => {
    ElMessageBox.confirm("确定要删除该参数吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      // è°ƒç”¨API删除参数
      delProcessRouteItemParam(param.id)
        .then(res => {
          ElMessage.success("删除成功");
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("删除参数失败");
          console.error("删除参数失败:", err);
        });
    });
  };
  const handleParamSubmit = () => {
    paramFormRef.value.validate(valid => {
      if (valid) {
        ElMessage.success(isParamEdit.value ? "编辑成功" : "新增成功");
        paramDialogVisible.value = false;
        getRouteList();
      }
    });
  };
  const handleParamTypeChange = () => {
    if (paramForm.parameterType === "数值格式") {
      paramForm.parameterFormat = "#.0000";
    } else if (paramForm.parameterType === "时间格式") {
      paramForm.parameterFormat = "YYYY-MM-DD HH:mm:ss";
    } else {
      paramForm.parameterFormat = "";
    }
  };
  const getParamTypeTag = type => {
    const typeMap = {
      1: "primary",
      2: "info",
      3: "warning",
      4: "success",
    };
    return typeMap[type] || "default";
  };
  const getParamTypeText = type => {
    const typeMap = {
      1: "数值格式",
      2: "文本格式",
      3: "下拉选项",
      4: "时间格式",
    };
    return typeMap[type] || "未知参数类型";
  };
  // é€‰æ‹©å‚数相关方法
  const handleParamSearch = () => {
    // é‡ç½®åˆ†é¡µ
    paramPage.current = 1;
    // é‡æ–°åŠ è½½æ•°æ®
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  const handleParamSelect = row => {
    selectedParam.value = row;
  };
  // å¤„理分页大小变化
  const handleParamSizeChange = size => {
    paramPage.size = size;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å¤„理当前页码变化
  const handleParamCurrentChange = current => {
    paramPage.current = current;
    getBaseParamList({
      paramName: paramSearchKeyword.value,
      current: paramPage.current,
      size: paramPage.size,
    }).then(res => {
      if (res.code === 200) {
        filteredParamList.value = res.data?.records || [];
        paramPage.total = res.data?.total || 0;
      } else {
        ElMessage.error(res.msg || "查询失败");
      }
    });
  };
  // å·¥è‰ºè·¯çº¿åˆ†é¡µå¤„理
  const handleRouteSizeChange = size => {
    routePage.size = size;
    getRouteList();
  };
  const handleRouteCurrentChange = current => {
    routePage.current = current;
    getRouteList();
  };
  const handleParamSelectSubmit = () => {
    if (!selectedParam.value) {
      ElMessage.warning("请先选择一个参数");
      return;
    }
    // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
    const route = routeList.value.find(r => r.id === currentRouteId.value);
    const process = route?.processList.find(p => p.id === currentProcessId.value);
    if (route && process) {
      // æ£€æŸ¥å‚数是否已存在
      // const exists = process.paramList?.some(
      //   p =>
      //     p.paramId === selectedParam.value.id ||
      //     p.parameterCode === selectedParam.value.paramCode
      // );
      // if (exists) {
      //   ElMessage.warning("该参数已存在于工序中");
      //   return;
      // }
      // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
      const isNumericMode = selectedParam.value.valueMode === 1;
      // è°ƒç”¨API新增参数
      addProcessRouteItemParam({
        routeItemId: process.id,
        paramId: selectedParam.value.id,
        standardValue: isNumericMode
          ? selectedParam.value.standardValue || ""
          : "",
        minValue: isNumericMode ? selectedParam.value.minValue || 0 : null,
        maxValue: isNumericMode ? selectedParam.value.maxValue || 0 : null,
        isRequired: selectedParam.value.isRequired || 0,
      })
        .then(res => {
          ElMessage.success("添加参数成功");
          selectParamDialogVisible.value = false;
          // åˆ·æ–°å‚数列表
          toggleProcessParams2(process);
        })
        .catch(err => {
          ElMessage.error("添加参数失败");
          console.error("添加参数失败:", err);
        });
    }
  };
  const handleEditParamSubmit = () => {
    editParamFormRef.value.validate(valid => {
      if (valid) {
        // åˆ¤æ–­å‚数类型,只有数值类型才传标准值、最大值和最小值
        const isNumericMode = editParamForm.valueMode == 1;
        // è°ƒç”¨API修改参数
        editProcessRouteItemParam({
          id: editParamForm.id,
          routeItemId: currentProcessId.value,
          paramId: editParamForm.paramId,
          standardValue: isNumericMode ? editParamForm.standardValue || "" : "",
          minValue: isNumericMode ? editParamForm.minValue || 0 : null,
          maxValue: isNumericMode ? editParamForm.maxValue || 0 : null,
          isRequired: editParamForm.isRequired || 0,
        })
          .then(res => {
            ElMessage.success("编辑成功");
            editParamDialogVisible.value = false;
            // æ‰¾åˆ°å¯¹åº”的工艺路线和工序
            const route = routeList.value.find(
              r => r.id === currentRouteId.value
            );
            const process = route?.processList.find(
              p => p.id === currentProcessId.value
            );
            // åˆ·æ–°å‚数列表
            if (process) {
              toggleProcessParams2(process);
            }
          })
          .catch(err => {
            ElMessage.error("编辑参数失败");
            console.error("编辑参数失败:", err);
          });
      }
    });
  };
  // æ‹–拽排序
  const handleDragStart = (event, index, routeId) => {
    draggedItem.value = index;
    draggedRouteId.value = routeId;
    event.dataTransfer.effectAllowed = "move";
  };
  const handleDragOver = event => {
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  };
  const handleDrop = (event, dropIndex, routeId) => {
    event.preventDefault();
    if (draggedItem.value === null || draggedItem.value === dropIndex) return;
    const route = routeList.value.find(r => r.id === routeId);
    if (route && route.processList) {
      const draggedProcess = route.processList[draggedItem.value];
      // è®¡ç®—新的排序值
      const newDragSort = dropIndex + 1;
      // è°ƒç”¨API排序工序
      sortProcessRouteItem({
        id: draggedProcess.id,
        dragSort: newDragSort,
      })
        .then(res => {
          // è°ƒç”¨æŽ¥å£èŽ·å–æœ€æ–°çš„å·¥åºåˆ—è¡¨
          findProcessRouteItemList({ routeId: routeId })
            .then(res => {
              if (route) {
                route.processList = (res.data || []).map(process => ({
                  ...process,
                  processId: process.processId || process.id,
                  expanded: false,
                }));
              }
              ElMessage.success("排序成功");
            })
            .catch(err => {
              console.error("获取工序列表失败:", err);
              ElMessage.success("排序成功");
            });
        })
        .catch(err => {
          ElMessage.error("排序失败");
          console.error("排序工序失败:", err);
        });
    }
  };
  const handleDragEnd = () => {
    draggedItem.value = null;
    draggedRouteId.value = null;
  };
  // èŽ·å–æ•°æ®å­—å…¸
  const getDictTypes = () => {
    listType({ pageNum: 1, pageSize: 1000 }).then(res => {
      dictTypes.value = res.rows || [];
    });
  };
  getRouteList();
  getDictTypes();
  // é¡µé¢åŠ è½½æ—¶èŽ·å–å·¥åºåˆ—è¡¨
  onMounted(() => {
    getProcessListApi()
      .then(res => {
        // å¤„理返回的数据,映射到页面需要的格式
        availableProcessList.value = (res.data || []).map(item => ({
          id: item.id,
          no: item.no || item.no,
          name: item.name || item.name,
          remark: item.remark || item.remark,
          status: item.status,
          isQuality: item.isQuality,
        }));
        filteredProcessList.value = availableProcessList.value;
      })
      .catch(() => {
        ElMessage.error("获取工序列表失败");
      });
  });
</script>
<style scoped lang="scss">
  .app-container {
    padding: 20px;
    padding-bottom: 80px;
    background-color: #f0f2f5;
    min-height: calc(100vh - 84px);
    overflow: hidden;
  }
  .route-header {
    margin-bottom: 20px;
    .add-route-btn {
      width: 100%;
      display: inline-flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-width: 120px;
      height: 100px;
      border: 2px dashed #dcdfe6;
      border-radius: 12px;
      background: #fafafa;
      cursor: pointer;
      transition: all 0.3s ease;
      color: #909399;
      padding: 0 20px;
      .el-icon {
        font-size: 24px;
        margin-bottom: 8px;
      }
      span {
        font-size: 13px;
      }
      &:hover {
        border-color: #409eff;
        background: #ecf5ff;
        color: #409eff;
      }
    }
  }
  .route-card-list {
    display: grid;
    grid-template-columns: repeat(1, 1fr);
    gap: 20px;
    max-height: calc(100vh - 240px);
    overflow-y: auto;
    padding-right: 10px;
  }
  /* è‡ªå®šä¹‰æ»šåŠ¨æ¡æ ·å¼ */
  .route-card-list::-webkit-scrollbar {
    width: 8px;
  }
  .route-card-list::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 4px;
  }
  .route-card-list::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  .route-card {
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    overflow: hidden;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 20px 40px;
      border-bottom: 1px solid #ebeef5;
      background: #f8f9fa;
      .route-info {
        display: flex;
        // flex-direction: column;
        // justify-content: center;
        // items-align: center;
        gap: 4px;
        .route-code {
          font-size: 12px;
          color: #909399;
          font-family: "Courier New", monospace;
          line-height: 30px;
        }
        .route-name {
          font-size: 18px;
          font-weight: 600;
          color: #303133;
          display: flex;
          align-items: center;
        }
      }
      .route-actions {
        display: flex;
        gap: 8px;
        // .el-button {
        //   color: #409eff;
        // }
      }
    }
    .card-body {
      padding: 16px 40px;
      .route-desc {
        font-size: 14px;
        color: #606266;
        margin-bottom: 12px;
      }
      .route-meta {
        display: flex;
        gap: 24px;
        margin-bottom: 12px;
        padding: 10px 14px;
        background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
        border-radius: 8px;
        border-left: 3px solid #409eff;
        .meta-item {
          display: flex;
          align-items: center;
          gap: 6px;
          font-size: 13px;
          margin-right: 40px;
          .el-icon {
            font-size: 14px;
            color: #409eff;
          }
          .meta-label {
            color: #909399;
            font-weight: 500;
          }
          .meta-value {
            color: #303133;
            font-weight: 600;
          }
        }
      }
      .expand-btn-wrapper {
        display: flex;
        justify-content: center;
        margin-top: 8px;
        .expand-btn {
          padding: 8px 20px;
          border-radius: 20px;
          background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
          border: 1px solid #b3d8ff;
          transition: all 0.3s ease;
          .btn-text {
            font-size: 13px;
            font-weight: 500;
            color: #409eff;
            margin-right: 6px;
          }
          .expand-icon {
            font-size: 14px;
            color: #409eff;
            transition: transform 0.3s ease;
          }
          &:hover {
            background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
            border-color: #409eff;
            box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
            .btn-text,
            .expand-icon {
              color: #fff;
            }
          }
          &.expanded {
            background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%);
            border-color: #a5d69a;
            .btn-text,
            .expand-icon {
              color: #67c23a;
            }
            &:hover {
              background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
              border-color: #67c23a;
              box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
              .btn-text,
              .expand-icon {
                color: #fff;
              }
            }
          }
        }
      }
    }
    .process-route {
      padding: 0 20px 20px;
      background: #f5f7fa;
      border-top: 1px solid #ebeef5;
      .process-flow {
        display: flex;
        align-items: flex-start;
        gap: 8px;
        padding: 20px 0;
        overflow-x: auto;
        overflow-y: hidden;
        .process-flow-item {
          display: flex;
          align-items: center;
          gap: 8px;
          .process-node {
            background: #fff;
            border-radius: 12px;
            padding: 16px;
            border: 2px solid #ebeef5;
            cursor: move;
            transition: all 0.3s ease;
            // min-width: 180px;
            // max-width: 220px;
            width: 300px;
            &.expanded {
              width: 400px;
            }
            &:hover {
              box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
              transform: translateY(-2px);
              border-color: #409eff;
            }
            &:active {
              cursor: grabbing;
            }
            .process-node-header {
              display: flex;
              justify-content: space-between;
              align-items: center;
              margin-bottom: 12px;
              .process-number {
                width: 28px;
                height: 28px;
                border-radius: 50%;
                background: #409eff;
                color: #ffffff;
                font-size: 12px;
                font-weight: 600;
                display: flex;
                align-items: center;
                justify-content: center;
              }
              .process-actions {
                display: flex;
                gap: 4px;
              }
            }
            .process-node-body {
              text-align: center;
              margin-bottom: 12px;
              .process-code {
                font-size: 11px;
                color: #909399;
                font-family: "Courier New", monospace;
                margin-bottom: 4px;
              }
              .process-name {
                font-size: 15px;
                font-weight: 600;
                color: #303133;
                margin-bottom: 6px;
              }
              .process-desc {
                font-size: 12px;
                color: #606266;
                overflow: hidden;
                text-overflow: ellipsis;
                display: -webkit-box;
                -webkit-line-clamp: 2;
                -webkit-box-orient: vertical;
              }
            }
            .process-node-footer {
              display: flex;
              justify-content: flex-end;
              align-items: center;
              padding-top: 10px;
              border-top: 1px solid #ebeef5;
            }
            .process-params-section {
              margin-top: 12px;
              padding-top: 12px;
              border-top: 1px solid #ebeef5;
              .params-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 8px;
                font-size: 13px;
                font-weight: 600;
                color: #303133;
              }
              .params-list {
                display: flex;
                flex-direction: column;
                gap: 6px;
                max-height: 200px;
                overflow-y: auto;
                .param-item {
                  display: flex;
                  justify-content: space-between;
                  align-items: center;
                  padding: 6px 8px;
                  background: #fafafa;
                  border-radius: 4px;
                  border-left: 2px solid #409eff;
                  font-size: 12px;
                  .param-info {
                    display: flex;
                    flex-direction: row;
                    align-items: center;
                    gap: 6px;
                    flex: 1;
                    min-width: 0;
                    .param-code {
                      font-size: 11px;
                      color: #e6a23c;
                      font-family: "Courier New", monospace;
                      margin-right: 20px;
                    }
                    .param-name {
                      font-size: 12px;
                      color: #303133;
                      font-weight: 500;
                      margin-right: 20px;
                    }
                    .param-value {
                      font-size: 11px;
                      color: #606266;
                    }
                  }
                  .param-actions {
                    display: flex;
                    gap: 4px;
                    flex-shrink: 0;
                  }
                }
              }
            }
          }
          .flow-arrow {
            display: flex;
            align-items: center;
            color: #c0c4cc;
            font-size: 24px;
            padding: 0 4px;
            .el-icon {
              font-size: 20px;
            }
          }
        }
        .add-process-node {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          min-width: 100px;
          height: 137px;
          border: 2px dashed #dcdfe6;
          border-radius: 12px;
          background: #fafafa;
          cursor: pointer;
          transition: all 0.3s ease;
          color: #909399;
          // margin-left: 10px;
          .el-icon {
            font-size: 24px;
            margin-bottom: 8px;
          }
          span {
            font-size: 13px;
          }
          &:hover {
            border-color: #409eff;
            background: #ecf5ff;
            color: #409eff;
          }
        }
      }
    }
  }
  // æ‹–拽时的样式
  .process-flow-item.dragging {
    opacity: 0.5;
    transform: scale(0.98);
  }
  // é€‰æ‹©å·¥åºå¯¹è¯æ¡†æ ·å¼
  .process-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .process-list-area {
      flex: 1;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .process-detail-area {
      width: 380px;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .process-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // é€‰æ‹©å‚数对话框样式
  .param-select-container {
    display: flex;
    gap: 20px;
    height: 450px;
    .param-list-area {
      // flex: 1;
      width: 380px;
      display: flex;
      flex-direction: column;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 12px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .search-box {
        margin-bottom: 12px;
        .el-input {
          width: 100%;
        }
      }
    }
    .param-detail-area {
      // width: 380px;
      flex: 1;
      display: flex;
      flex-direction: column;
      background: #f5f7fa;
      border-radius: 8px;
      padding: 16px;
      .area-title {
        font-size: 14px;
        font-weight: 600;
        color: #303133;
        margin-bottom: 16px;
        padding-bottom: 8px;
        border-bottom: 1px solid #ebeef5;
      }
      .param-detail-form {
        .el-form-item {
          margin-bottom: 12px;
          .el-form-item__label {
            color: #606266;
            font-weight: 500;
          }
        }
        .detail-text {
          color: #303133;
          font-weight: 500;
        }
      }
    }
  }
  // åˆ†é¡µæŽ§ä»¶æ ·å¼
  .pagination-container {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    justify-content: flex-end;
    padding: 16px 20px;
    background-color: #fff !important;
    border-top: 1px solid #ebeef5;
    box-shadow: 0 -2px 12px 0 rgba(0, 0, 0, 0.1);
    z-index: 100;
    .el-pagination {
      .el-pagination__sizes {
        margin-right: 16px;
      }
      .el-pagination__jump {
        margin-left: 16px;
      }
      .el-pagination__total {
        color: #606266;
        font-size: 14px;
      }
      .el-pagination__button {
        border-radius: 4px;
        transition: all 0.3s ease;
        &:hover:not(:disabled) {
          color: #409eff;
          border-color: #409eff;
        }
      }
      .el-pagination__button--active {
        background-color: #409eff;
        border-color: #409eff;
        color: #fff;
      }
    }
  }
</style>
src/views/productionManagement/processRoute/processRouteItem/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1024 @@
<template>
  <div class="app-container">
    <PageHeader content="工艺路线项目" />
    <!-- å·¥è‰ºè·¯çº¿ä¿¡æ¯å±•示 -->
    <el-card v-if="routeInfo.processRouteCode"
             class="route-info-card"
             shadow="hover">
      <div class="route-info">
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">工艺路线编号</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.processRouteCode }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">产品名称</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.productName || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">规格名称</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.model || '-' }}</span>
          </div>
        </div>
        <div class="info-item">
          <div class="info-label-wrapper">
            <span class="info-label">BOM编号</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.bomNo || '-' }}</span>
          </div>
        </div>
        <div class="info-item full-width"
             v-if="routeInfo.description">
          <div class="info-label-wrapper">
            <span class="info-label">描述</span>
          </div>
          <div class="info-value-wrapper">
            <span class="info-value">{{ routeInfo.description }}</span>
          </div>
        </div>
      </div>
    </el-card>
    <!-- bom展示 -->
    <!-- è¡¨æ ¼è§†å›¾ -->
    <div v-if="viewMode === 'table'"
         class="section-header">
      <div class="section-title">工艺路线项目列表</div>
      <div class="section-actions">
        <el-button icon="Grid"
                   @click="toggleView"
                   style="margin-right: 10px;">
          å¡ç‰‡è§†å›¾
        </el-button>
        <el-button type="primary"
                   @click="handleAdd">新增</el-button>
      </div>
    </div>
    <el-table v-if="viewMode === 'table'"
              ref="tableRef"
              v-loading="tableLoading"
              border
              :data="tableData"
              :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
              row-key="id"
              tooltip-effect="dark"
              class="lims-table">
      <el-table-column align="center"
                       label="序号"
                       width="60"
                       type="index" />
      <el-table-column label="工序名称"
                       prop="processId"
                       width="200">
        <template #default="scope">
          {{ 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="参数列表"
                       min-width="160">
        <template #default="scope">
          <el-button type="primary"
                     link
                     size="small"
                     @click="handleViewParams(scope.row)">参数列表</el-button>
        </template>
      </el-table-column>
      <el-table-column label="单位"
                       prop="unit"
                       width="100" />
      <el-table-column label="是否质检"
                       prop="isQuality"
                       width="100">
        <template #default="scope">
          {{scope.row.isQuality ? "是" : "否"}}
        </template>
      </el-table-column>
      <el-table-column label="操作"
                       align="center"
                       fixed="right"
                       width="150">
        <template #default="scope">
          <el-button type="primary"
                     link
                     size="small"
                     @click="handleEdit(scope.row)"
                     :disabled="scope.row.isComplete">编辑</el-button>
          <!-- <el-button type="info"
                     link
                     size="small"
                     @click="handleViewParams(scope.row)">参数列表</el-button> -->
          <el-button type="danger"
                     link
                     size="small"
                     @click="handleDelete(scope.row)"
                     :disabled="scope.row.isComplete">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- å¡ç‰‡è§†å›¾ -->
    <template v-else>
      <div class="section-header">
        <div class="section-title">工艺路线项目列表</div>
        <div class="section-actions">
          <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 ref="cardsContainer"
             class="cards-wrapper">
          <div v-for="(item, index) in tableData"
               :key="item.id || index"
               class="process-card"
               :data-index="index">
            <!-- åºå·åœ†åœˆ -->
            <div class="card-header">
              <div class="card-number">{{ index + 1 }}</div>
              <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 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>
            </div>
            <!-- æ“ä½œæŒ‰é’® -->
            <div class="card-footer">
              <el-button type="primary"
                         link
                         size="small"
                         @click="handleEdit(item)"
                         :disabled="item.isComplete">编辑</el-button>
              <el-button type="info"
                         link
                         size="small"
                         @click="handleViewParams(item)">参数列表</el-button>
              <el-button type="danger"
                         link
                         size="small"
                         @click="handleDelete(item)"
                         :disabled="item.isComplete">删除</el-button>
            </div>
          </div>
        </div>
      </div>
    </template>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog v-model="dialogVisible"
               :title="operationType === 'add' ? '新增工艺路线项目' : '编辑工艺路线项目'"
               width="500px"
               @close="closeDialog">
      <el-form ref="formRef"
               :model="form"
               :rules="rules"
               label-width="120px">
        <el-form-item label="工序"
                      prop="processId">
          <el-select v-model="form.processId"
                     placeholder="请选择工序"
                     clearable
                     style="width: 100%">
            <el-option v-for="process in processOptions"
                       :key="process.id"
                       :label="process.name"
                       :value="process.id" />
          </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>
      </el-form>
      <template #footer>
        <el-button @click="closeDialog">取消</el-button>
        <el-button type="primary"
                   @click="handleSubmit"
                   :loading="submitLoading">确定</el-button>
      </template>
    </el-dialog>
    <!-- äº§å“é€‰æ‹©å¯¹è¯æ¡† -->
    <ProductSelectDialog v-model="showProductSelectDialog"
                         @confirm="handleProductSelect"
                         single />
    <!-- å‚数列表对话框 -->
    <ProcessParamListDialog v-model="showParamListDialog"
                            :title="`${currentProcess ? getProcessName(currentProcess.processId) : ''} - å‚数列表`"
                            :route-id="routeId"
                            :editable="false"
                            :process="currentProcess"
                            :param-list="paramList"
                            @refresh="refreshParamList" />
  </div>
</template>
<script setup>
  import {
    ref,
    computed,
    getCurrentInstance,
    onMounted,
    onUnmounted,
    nextTick,
  } from "vue";
  import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
  import ProcessParamListDialog from "@/components/ProcessParamListDialog.vue";
  import {
    findProcessRouteItemList,
    addOrUpdateProcessRouteItem,
    sortProcessRouteItem,
    batchDeleteProcessRouteItem,
    getProcessParamList,
  } from "@/api/productionManagement/processRouteItem.js";
  import {
    findProductProcessRouteItemList,
    deleteRouteItem,
    addRouteItem,
    addOrUpdateProductProcessRouteItem,
    sortRouteItem,
  } from "@/api/productionManagement/productProcessRoute.js";
  import { processList } from "@/api/productionManagement/productionProcess.js";
  import { useRoute } from "vue-router";
  import { ElMessageBox, ElMessage } from "element-plus";
  import Sortable from "sortablejs";
  const route = useRoute();
  const { proxy } = getCurrentInstance() || {};
  const routeId = computed(() => route.query.id);
  const orderId = computed(() => route.query.orderId);
  const pageType = computed(() => route.query.type);
  const tableLoading = ref(false);
  const tableData = ref([]);
  const dialogVisible = ref(false);
  const operationType = ref("add"); // add | edit
  const formRef = ref(null);
  const submitLoading = ref(false);
  const cardsContainer = ref(null);
  const tableRef = ref(null);
  const viewMode = ref("table"); // table | card
  const routeInfo = ref({
    processRouteCode: "",
    productName: "",
    model: "",
    bomNo: "",
    bomId: null,
    description: "",
  });
  const processOptions = ref([]);
  const showProductSelectDialog = ref(false);
  const showParamListDialog = ref(false);
  const currentProcess = ref(null);
  const paramList = ref([]);
  let tableSortable = null;
  let cardSortable = null;
  // åˆ‡æ¢è§†å›¾
  const toggleView = () => {
    viewMode.value = viewMode.value === "table" ? "card" : "table";
    // åˆ‡æ¢è§†å›¾åŽé‡æ–°åˆå§‹åŒ–拖拽排序
    nextTick(() => {
      initSortable();
    });
  };
  const form = ref({
    id: undefined,
    routeId: routeId.value,
    processId: undefined,
    productModelId: undefined,
    productName: "",
    model: "",
    unit: "",
    isQuality: false,
  });
  const rules = {
    processId: [{ required: true, message: "请选择工序", trigger: "change" }],
    productModelId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
  };
  // æ ¹æ®å·¥åºID获取工序名称
  const getProcessName = processId => {
    if (!processId) return "";
    const process = processOptions.value.find(p => p.id === processId);
    return process ? process.name : "";
  };
  // èŽ·å–åˆ—è¡¨
  const getList = () => {
    tableLoading.value = true;
    const listPromise =
      pageType.value === "order"
        ? findProductProcessRouteItemList({ orderId: orderId.value })
        : findProcessRouteItemList({ routeId: routeId.value });
    listPromise
      .then(res => {
        tableData.value = res.data || [];
        tableLoading.value = false;
        // åˆ—表加载完成后初始化拖拽排序
        nextTick(() => {
          initSortable();
        });
      })
      .catch(err => {
        tableLoading.value = false;
        console.error("获取列表失败:", err);
        proxy?.$modal?.msgError("获取列表失败");
      });
  };
  // èŽ·å–å·¥åºåˆ—è¡¨
  const getProcessList = () => {
    processList({})
      .then(res => {
        processOptions.value = res.data || [];
      })
      .catch(err => {
        console.error("获取工序失败:", err);
      });
  };
  // èŽ·å–å·¥è‰ºè·¯çº¿è¯¦æƒ…ï¼ˆä»Žè·¯ç”±å‚æ•°èŽ·å–ï¼‰
  const getRouteInfo = () => {
    routeInfo.value = {
      processRouteCode: route.query.processRouteCode || "",
      productName: route.query.productName || "",
      model: route.query.model || "",
      bomNo: route.query.bomNo || "",
      bomId: route.query.bomId || null,
      description: route.query.description || "",
    };
  };
  // æ–°å¢ž
  const handleAdd = () => {
    operationType.value = "add";
    resetForm();
    dialogVisible.value = true;
  };
  // ç¼–辑
  const handleEdit = row => {
    operationType.value = "edit";
    form.value = {
      id: row.id,
      routeId: routeId.value,
      processId: row.processId,
      productModelId: row.productModelId,
      productName: row.productName || "",
      model: row.model || "",
      unit: row.unit || "",
      isQuality: row.isQuality,
    };
    dialogVisible.value = true;
  };
  // åˆ é™¤
  const handleDelete = row => {
    ElMessageBox.confirm("确认删除该工艺路线项目?", "提示", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // ç”Ÿäº§è®¢å•下使用 productProcessRoute çš„删除接口(路由后拼接 id),其它情况使用工艺路线项目批量删除接口
        const deletePromise =
          pageType.value === "order"
            ? deleteRouteItem(row.id)
            : batchDeleteProcessRouteItem([row.id]);
        deletePromise
          .then(() => {
            proxy?.$modal?.msgSuccess("删除成功");
            getList();
          })
          .catch(() => {
            proxy?.$modal?.msgError("删除失败");
          });
      })
      .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 => {
      if (valid) {
        submitLoading.value = true;
        if (operationType.value === "add") {
          // æ–°å¢žï¼šä¼ å•个对象,包含dragSort字段
          // dragSort = å½“前列表长度 + 1,表示新增记录排在最后
          const dragSort = tableData.value.length + 1;
          const isOrderPage = pageType.value === "order";
          const addPromise = isOrderPage
            ? addRouteItem({
                productOrderId: orderId.value,
                productRouteId: routeId.value,
                processId: form.value.processId,
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
                dragSort,
              })
            : addOrUpdateProcessRouteItem({
                routeId: routeId.value,
                processId: form.value.processId,
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
                dragSort,
              });
          addPromise
            .then(() => {
              proxy?.$modal?.msgSuccess("新增成功");
              closeDialog();
              getList();
            })
            .catch(() => {
              proxy?.$modal?.msgError("新增失败");
            })
            .finally(() => {
              submitLoading.value = false;
            });
        } else {
          // ç¼–辑:生产订单下使用 productProcessRoute/updateRouteItem,其它情况使用工艺路线项目更新接口
          const isOrderPage = pageType.value === "order";
          const updatePromise = isOrderPage
            ? addOrUpdateProductProcessRouteItem({
                id: form.value.id,
                processId: form.value.processId,
                productModelId: form.value.productModelId,
                isQuality: form.value.isQuality,
              })
            : addOrUpdateProcessRouteItem({
                routeId: routeId.value,
                processId: form.value.processId,
                productModelId: form.value.productModelId,
                id: form.value.id,
                isQuality: form.value.isQuality,
              });
          updatePromise
            .then(() => {
              proxy?.$modal?.msgSuccess("修改成功");
              closeDialog();
              getList();
            })
            .catch(() => {
              proxy?.$modal?.msgError("修改失败");
            })
            .finally(() => {
              submitLoading.value = false;
            });
        }
      }
    });
  };
  // é‡ç½®è¡¨å•
  const resetForm = () => {
    form.value = {
      id: undefined,
      routeId: routeId.value,
      processId: undefined,
      productModelId: undefined,
      productName: "",
      model: "",
      unit: "",
    };
    formRef.value?.resetFields();
  };
  // å…³é—­å¼¹çª—
  const closeDialog = () => {
    dialogVisible.value = false;
    resetForm();
  };
  // åˆå§‹åŒ–拖拽排序
  const initSortable = () => {
    destroySortable();
    if (viewMode.value === "table") {
      // è¡¨æ ¼è§†å›¾çš„æ‹–拽排序
      if (!tableRef.value) return;
      const tbody =
        tableRef.value.$el.querySelector(".el-table__body tbody") ||
        tableRef.value.$el.querySelector(
          ".el-table__body-wrapper > table > tbody"
        );
      if (!tbody) return;
      tableSortable = new Sortable(tbody, {
        animation: 150,
        ghostClass: "sortable-ghost",
        handle: ".el-table__row",
        filter: ".el-button, .el-select",
        onEnd: evt => {
          if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex])
            return;
          // é‡æ–°æŽ’序数组
          const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
          tableData.value.splice(evt.newIndex, 0, moveItem);
          // è®¡ç®—新的序号(dragSort从1开始)
          const newIndex = evt.newIndex;
          const dragSort = newIndex + 1;
          // è°ƒç”¨æŽ’序接口
          if (moveItem.id) {
            const isOrderPage = pageType.value === "order";
            const sortPromise = isOrderPage
              ? sortRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                })
              : sortProcessRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                });
            sortPromise
              .then(() => {
                // æ›´æ–°æ‰€æœ‰è¡Œçš„dragSort
                tableData.value.forEach((item, index) => {
                  if (item.id) {
                    item.dragSort = index + 1;
                  }
                });
                proxy?.$modal?.msgSuccess("排序成功");
              })
              .catch(err => {
                // æŽ’序失败,恢复原数组
                tableData.value.splice(newIndex, 1);
                tableData.value.splice(evt.oldIndex, 0, moveItem);
                proxy?.$modal?.msgError("排序失败");
                console.error("排序失败:", err);
              });
          }
        },
      });
    } else {
      // å¡ç‰‡è§†å›¾çš„æ‹–拽排序
      if (!cardsContainer.value) return;
      cardSortable = new Sortable(cardsContainer.value, {
        animation: 150,
        ghostClass: "sortable-ghost",
        handle: ".process-card",
        filter: ".el-button",
        onEnd: evt => {
          if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex])
            return;
          // é‡æ–°æŽ’序数组
          const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
          tableData.value.splice(evt.newIndex, 0, moveItem);
          // è®¡ç®—新的序号(dragSort从1开始)
          const newIndex = evt.newIndex;
          const dragSort = newIndex + 1;
          // è°ƒç”¨æŽ’序接口
          if (moveItem.id) {
            const isOrderPage = pageType.value === "order";
            const sortPromise = isOrderPage
              ? sortRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                })
              : sortProcessRouteItem({
                  id: moveItem.id,
                  dragSort: dragSort,
                });
            sortPromise
              .then(() => {
                // æ›´æ–°æ‰€æœ‰è¡Œçš„dragSort
                tableData.value.forEach((item, index) => {
                  if (item.id) {
                    item.dragSort = index + 1;
                  }
                });
                proxy?.$modal?.msgSuccess("排序成功");
              })
              .catch(err => {
                // æŽ’序失败,恢复原数组
                tableData.value.splice(newIndex, 1);
                tableData.value.splice(evt.oldIndex, 0, moveItem);
                proxy?.$modal?.msgError("排序失败");
                console.error("排序失败:", err);
              });
          }
        },
      });
    }
  };
  // é”€æ¯æ‹–拽排序
  const destroySortable = () => {
    if (tableSortable) {
      tableSortable.destroy();
      tableSortable = null;
    }
    if (cardSortable) {
      cardSortable.destroy();
      cardSortable = null;
    }
  };
  onMounted(() => {
    getRouteInfo();
    getList();
    getProcessList();
  });
  // æŸ¥çœ‹å‚数列表
  const handleViewParams = process => {
    currentProcess.value = process;
    // è°ƒç”¨API获取参数列表
    getProcessParamList({
      routeItemId: process.id,
      pageNum: 1,
      pageSize: 1000,
    })
      .then(res => {
        if (res.code === 200) {
          paramList.value = res.data?.records || [];
        } else {
          ElMessage.error(res.msg || "获取参数列表失败");
          paramList.value = [];
        }
        showParamListDialog.value = true;
      })
      .catch(err => {
        console.error("获取参数列表失败:", err);
        ElMessage.error("获取参数列表失败");
        paramList.value = [];
        showParamListDialog.value = true;
      });
  };
  // åˆ·æ–°å‚数列表
  const refreshParamList = () => {
    if (!currentProcess.value) return;
    // é‡æ–°è°ƒç”¨API获取参数列表
    getProcessParamList({
      routeItemId: currentProcess.value.id,
      pageNum: 1,
      pageSize: 1000,
    })
      .then(res => {
        if (res.code === 200) {
          paramList.value = res.data?.records || [];
        } else {
          ElMessage.error(res.msg || "获取参数列表失败");
          paramList.value = [];
        }
      })
      .catch(err => {
        console.error("获取参数列表失败:", err);
        ElMessage.error("获取参数列表失败");
        paramList.value = [];
      });
  };
  onUnmounted(() => {
    destroySortable();
  });
</script>
<style scoped>
  .card-container {
    padding: 20px 0;
  }
  .cards-wrapper {
    display: flex;
    gap: 16px;
    overflow-x: auto;
    padding: 10px 0;
    min-height: 200px;
  }
  .cards-wrapper::-webkit-scrollbar {
    height: 8px;
  }
  .cards-wrapper::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
  }
  .cards-wrapper::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 4px;
  }
  .cards-wrapper::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
  }
  .process-card {
    flex-shrink: 0;
    width: 220px;
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    padding: 16px;
    display: flex;
    flex-direction: column;
    cursor: move;
    transition: all 0.3s;
  }
  .process-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    transform: translateY(-2px);
  }
  .card-header {
    text-align: center;
    margin-bottom: 12px;
  }
  .card-number {
    width: 36px;
    height: 36px;
    line-height: 36px;
    border-radius: 50%;
    background: #409eff;
    color: #fff;
    font-weight: bold;
    font-size: 16px;
    margin: 0 auto 8px;
  }
  .card-process-name {
    font-size: 14px;
    color: #333;
    font-weight: 500;
    word-break: break-all;
  }
  .card-content {
    flex: 1;
    margin-bottom: 12px;
    min-height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .product-info {
    font-size: 13px;
    color: #666;
    text-align: center;
    width: 100%;
  }
  .product-info.empty {
    color: #999;
    text-align: center;
    padding: 20px 0;
  }
  .product-name {
    margin-bottom: 6px;
    word-break: break-all;
    line-height: 1.5;
    text-align: center;
  }
  .product-model {
    color: #909399;
    font-size: 12px;
    word-break: break-all;
    line-height: 1.5;
    text-align: center;
  }
  .product-unit {
    margin-left: 4px;
    color: #409eff;
  }
  .product-tag {
    margin: 10px 0;
  }
  .card-footer {
    display: flex;
    justify-content: space-around;
    padding-top: 12px;
    border-top: 1px solid #f0f0f0;
  }
  .card-footer .el-button {
    padding: 0;
    font-size: 12px;
  }
  :deep(.sortable-ghost) {
    opacity: 0.5;
    background-color: #f5f7fa !important;
  }
  :deep(.sortable-drag) {
    opacity: 0.8;
  }
  /* è¡¨æ ¼è§†å›¾æ ·å¼ */
  :deep(.el-table__row) {
    transition: background-color 0.2s;
    cursor: move;
  }
  :deep(.el-table__row:hover) {
    background-color: #f9fafc !important;
  }
  /* åŒºåŸŸæ ‡é¢˜æ ·å¼ */
  .section-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 12px;
  }
  .section-title {
    font-size: 16px;
    font-weight: 600;
    color: #303133;
    padding-left: 12px;
    position: relative;
    margin-bottom: 0;
  }
  .section-title::before {
    content: "";
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 16px;
    background: #409eff;
    border-radius: 2px;
  }
  .section-actions {
    display: flex;
    align-items: center;
  }
  /* å·¥è‰ºè·¯çº¿ä¿¡æ¯å¡ç‰‡æ ·å¼ */
  .route-info-card {
    margin-bottom: 20px;
    border: 1px solid #e4e7ed;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    border-radius: 8px;
    overflow: hidden;
  }
  .route-info {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 16px;
    padding: 4px;
  }
  .info-item {
    display: flex;
    flex-direction: column;
    background: #ffffff;
    border-radius: 6px;
    padding: 14px 16px;
    border: 1px solid #f0f2f5;
    transition: all 0.3s ease;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  }
  .info-item:hover {
    border-color: #409eff;
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
    transform: translateY(-1px);
  }
  .info-item.full-width {
    grid-column: 1 / -1;
  }
  .info-label-wrapper {
    margin-bottom: 8px;
  }
  .info-label {
    display: inline-block;
    color: #909399;
    font-size: 12px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 2px 0;
    position: relative;
  }
  .info-label::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 20px;
    height: 2px;
    background: linear-gradient(90deg, #409eff, transparent);
    border-radius: 1px;
  }
  .info-value-wrapper {
    flex: 1;
  }
  .info-value {
    display: block;
    color: #303133;
    font-size: 15px;
    font-weight: 500;
    line-height: 1.5;
    word-break: break-all;
  }
</style>
src/views/productionPlan/productionPlan/index.vue
@@ -117,6 +117,21 @@
                          value-format="YYYY-MM-DD"
                          style="width: 100%" />
        </el-form-item>
        <el-form-item label="强度"
                      v-if="mergeForm.productName === '砌块'">
          <div v-if="strengthError"
               class="strength-error"
               style="color: red; margin-bottom: 8px;">{{ strengthError }}</div>
          <el-select v-model="mergeForm.strength"
                     placeholder="请选择强度"
                     style="width: 100%"
                     required>
            <el-option v-for="item in block_strength"
                       :key="item.id"
                       :label="item.label"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="生产方数">
          <el-input-number v-model="mergeForm.totalAssignedQuantity"
                           :min="0"
@@ -301,14 +316,15 @@
                          placeholder="请选择计划结束日期" />
        </el-form-item>
        <el-form-item label="强度"
                      prop="strength">
                      prop="strength"
                      v-if="form.productName === '砌块'">
          <el-select v-model="form.strength"
                     placeholder="请选择强度"
                     style="width: 100%">
            <el-option label="A3.5"
                       value="A3.5" />
            <el-option label="A5.0"
                       value="A5.0" />
            <el-option v-for="item in block_strength"
                       :key="item.id"
                       :label="item.label"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="备注 1"
@@ -334,11 +350,12 @@
</template>
<script setup>
  import { onMounted, ref, reactive, getCurrentInstance } from "vue";
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { ElMessage } from "element-plus";
  import dayjs from "dayjs";
  import ImportDialog from "@/components/Dialog/ImportDialog.vue";
  import { getToken } from "@/utils/auth";
  import { useDict } from "@/utils/dict";
  import {
    productionPlanListPage,
    loadProdData,
@@ -490,6 +507,11 @@
    {
      label: "强度",
      prop: "strength",
      formatData: cell => {
        if (!cell) return "";
        const strengthItem = block_strength.value.find(item => item.id === cell);
        return strengthItem ? strengthItem.label : cell;
      },
    },
    {
@@ -546,6 +568,7 @@
          clickFun: row => {
            // å•独下发操作
            // è®¾ç½®è¡¨å•数据
            strengthError.value = "";
            mergeForm.ids = [row.id];
            mergeForm.materialCode = row.materialCode;
            mergeForm.productName = row.productName || "";
@@ -556,6 +579,7 @@
            mergeForm.totalAssignedQuantity =
              (Number(row.volume) - Number(row.assignedQuantity)).toFixed(4) || 0;
            mergeForm.planCompleteTime = row.planCompleteTime || "";
            mergeForm.strength = row.strength || "";
            sumAssignedQuantity.value = mergeForm.totalAssignedQuantity;
            // æ‰“开弹窗
            isShowNewModal.value = true;
@@ -597,6 +621,7 @@
    height: 0,
    totalAssignedQuantity: 0,
    planCompleteTime: "",
    strength: "",
  });
  // è¿½è¸ªè¿›åº¦å¼¹çª—控制
@@ -625,6 +650,8 @@
  const productOptions = ref([]);
  const specificationOptions = ref([]);
  const formRef = ref(null);
  // èŽ·å–å¼ºåº¦å­—å…¸
  const { block_strength } = useDict("block_strength");
  const form = reactive({
    id: undefined,
    applyNo: "",
@@ -656,6 +683,19 @@
    volume: [{ required: true, message: "请输入方数", trigger: "blur" }],
    productMaterialId: [
      { required: true, message: "请选择产品", trigger: "change" },
    ],
    strength: [
      {
        validator: (rule, value, callback) => {
          if (form.productName === "砌块" && !value) {
            callback(new Error("砌块产品的强度为必填项"));
          } else {
            callback();
          }
        },
        trigger: ["blur", "change"],
        required: false,
      },
    ],
  });
@@ -709,6 +749,26 @@
  const handleProductChange = value => {
    form.productMaterialSkuId = undefined;
    // æŸ¥æ‰¾é€‰ä¸­çš„产品名称
    const findProductName = (options, value) => {
      for (const option of options) {
        if (option.value === value) {
          return option.label;
        }
        if (option.children) {
          const found = findProductName(option.children, value);
          if (found) {
            return found;
          }
        }
      }
      return "";
    };
    form.productName = findProductName(productOptions.value, value);
    // è§¦å‘强度字段验证
    if (formRef.value) {
      formRef.value.validateField("strength");
    }
    fetchSpecificationOptions(value);
  };
@@ -931,6 +991,9 @@
      .catch(() => {});
  };
  const sumAssignedQuantity = ref(0);
  // å“åº”式数据
  const strengthError = ref("");
  // å¤„理合并下发按钮点击
  const handleMerge = () => {
    if (selectedRows.value.length === 0) {
@@ -938,6 +1001,26 @@
      return;
    }
    console.log(selectedRows.value);
    // æ£€æŸ¥å¼ºåº¦ä¸€è‡´æ€§
    const firstRow = selectedRows.value[0];
    const productName = firstRow.productName || "";
    let strengthConsistent = true;
    let firstStrength = firstRow.strength || "";
    strengthError.value = "";
    if (productName === "砌块") {
      for (const row of selectedRows.value) {
        if (row.strength !== firstStrength) {
          strengthConsistent = false;
          break;
        }
      }
      if (!strengthConsistent) {
        strengthError.value = "选择的砌块强度不一致,请重新选择";
      }
    }
    // è®¡ç®—总制造数量
    const totalAssignedQuantity = selectedRows.value.reduce((sum, row) => {
      return (
@@ -950,15 +1033,15 @@
    sumAssignedQuantity.value = totalAssignedQuantity;
    console.log(totalAssignedQuantity);
    // è®¾ç½®è¡¨å•数据
    const firstRow = selectedRows.value[0];
    mergeForm.materialCode = selectedserialNo.value;
    mergeForm.productName = firstRow.productName || "";
    mergeForm.productName = productName;
    mergeForm.model = firstRow.model || "";
    mergeForm.length = firstRow.length || 0;
    mergeForm.width = firstRow.width || 0;
    mergeForm.height = firstRow.height || 0;
    mergeForm.totalAssignedQuantity = totalAssignedQuantity;
    mergeForm.planCompleteTime = firstRow.planCompleteTime || "";
    mergeForm.strength = firstStrength;
    mergeForm.ids = selectedRows.value.map(row => row.id);
    // æ‰“开弹窗
@@ -971,6 +1054,11 @@
      ElMessage.warning("请输入生产方数");
      return;
    }
    // éªŒè¯ç Œå—产品的强度
    if (mergeForm.productName === "砌块" && !mergeForm.strength) {
      ElMessage.error("砌块产品的强度为必填项");
      return;
    }
    console.log(sumAssignedQuantity.value, "sumAssignedQuantity");
    // è®¡ç®—当前选中行的总方数
    const totalVolume = selectedRows.value.reduce((sum, row) => {