已添加1个文件
已修改3个文件
462 ■■■■■ 文件已修改
src/api/productionManagement/productionProcess.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/New.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/ProcessSelectDialog.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProcess.js
@@ -26,6 +26,14 @@
    params: query,
  });
}
export function listAll() {
  return request({
    url: "/technologyOperation/listPage",
    method: "get",
    params: { current: -1, size: -1 },
  });
}
export function add(data) {
  return request({
    url: "/technologyOperation/add",
src/views/inventoryManagement/stockManagement/New.vue
@@ -52,8 +52,9 @@
        <el-form-item label="库存数量"
                      prop="qualitity">
          <el-input-number v-model="formState.qualitity"
                           :step="1"
                           :min="1"
                           :step="0.00000001"
                           :min="0"
                           :precision="8"
                           style="width: 100%" />
        </el-form-item>
        <el-form-item label="批号"
@@ -65,8 +66,9 @@
                      label="库存预警数量"
                      prop="warnNum">
          <el-input-number v-model="formState.warnNum"
                           :step="1"
                           :step="0.00000001"
                           :min="0"
                           :precision="8"
                           :max="formState.qualitity"
                           style="width: 100%" />
        </el-form-item>
@@ -119,7 +121,7 @@
    productName: "",
    productModelName: "",
    unit: "",
    type: undefined,
    type: "qualified",
    qualitity: 0,
    batchNo: null,
    warnNum: 0,
@@ -157,7 +159,7 @@
      productName: "",
      productModelName: "",
      unit: "",
      type: undefined,
      type: "qualified",
      qualitity: 0,
      batchNo: null,
      warnNum: 0,
src/views/productionManagement/productStructure/Detail/index.vue
@@ -22,16 +22,34 @@
              border
              :preserve-expanded-content="false"
              :default-expand-all="true"
              style="width: 100%">
              style="width: 100%"
              @expand-change="handleOuterExpandChange">
      <el-table-column type="expand">
        <template #default="props">
          <el-form ref="form"
                   :model="dataValue">
            <el-table :data="dataValue.dataList"
            <div v-if="dataValue.isEdit"
                 class="drag-tip">
              <el-icon><Rank /></el-icon>
              <span>拖动手柄调整结构:同层级直接拖动排序;拖到某行正下方成为其子级;拖到列表最前成为一级</span>
            </div>
            <el-table ref="tableRef"
                      :data="dataValue.dataList"
                      row-key="tempId"
                      default-expand-all
                      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
                      @expand-change="handleTreeExpandChange"
                      style="width: 100%">
              <el-table-column v-if="dataValue.isEdit"
                               width="44"
                               align="center">
                <template #default="{ row }">
                  <div class="drag-handle"
                       title="拖动调整顺序:拖到某行正下方成为其子级,拖到列表最前成为一级">
                    <el-icon><Rank /></el-icon>
                  </div>
                </template>
              </el-table-column>
              <el-table-column prop="productName"
                               label="产品" />
              <el-table-column prop="model"
@@ -59,18 +77,20 @@
                  <el-form-item v-if="dataValue.isEdit"
                                :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '请选择消耗工序', trigger: 'change' }]"
                                style="margin: 0">
                    <el-select v-model="row.processId"
                               placeholder="请选择"
                               filterable
                               clearable
                               style="width: 100%"
                               @change="value => handleProcessChange(row, value)"
                               :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
                      <el-option v-for="item in dataValue.processOptions"
                                 :key="item.id"
                                 :label="item.name"
                                 :value="item.id" />
                    </el-select>
                    <el-input v-model="row.processName"
                              readonly
                              placeholder="请选择消耗工序"
                              clearable
                              style="width: 100%"
                              :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                              @click="openProcessDialog(row, $event)"
                              @clear="clearProcess(row)">
                      <template #append>
                        <el-button icon="Search"
                                   :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
                                   @click="openProcessDialog(row)" />
                      </template>
                    </el-input>
                  </el-form-item>
                </template>
              </el-table-column>
@@ -150,6 +170,9 @@
                           v-model:model-value="dataValue.showProductDialog"
                           :single="true"
                           @confirm="handleProduct" />
    <process-select-dialog v-if="dataValue.showProcessDialog"
                           v-model:model-value="dataValue.showProcessDialog"
                           @confirm="handleProcessConfirm" />
  </div>
</template>
@@ -158,18 +181,23 @@
    computed,
    defineAsyncComponent,
    defineComponent,
    nextTick,
    onBeforeUnmount,
    onMounted,
    reactive,
    ref,
    watch,
  } from "vue";
  import {
    queryList,
    addBomDetail,
  } from "@/api/productionManagement/productStructure.js";
  import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
  import { list } from "@/api/productionManagement/productionProcess";
  import { listAll } from "@/api/productionManagement/productionProcess";
  import { ElMessage } from "element-plus";
  import { useRoute, useRouter } from "vue-router";
  import Sortable from "sortablejs";
  import { Rank } from "@element-plus/icons-vue";
  defineComponent({
    name: "StructureEdit",
@@ -178,8 +206,16 @@
  const ProductSelectDialog = defineAsyncComponent(
    () => import("@/views/basicData/product/ProductSelectDialog.vue")
  );
  const ProcessSelectDialog = defineAsyncComponent(
    () =>
      import(
        "@/views/productionManagement/productionProcess/ProcessSelectDialog.vue"
      )
  );
  const emit = defineEmits(["update:router"]);
  const form = ref();
  const tableRef = ref();
  let tableSortable = null;
  const route = useRoute();
  const router = useRouter();
@@ -210,8 +246,10 @@
    productOptions: [],
    processOptions: [],
    showProductDialog: false,
    showProcessDialog: false,
    currentRowIndex: null,
    currentRowName: null,
    currentProcessRow: null,
    loading: false,
    isEdit: false,
  });
@@ -303,6 +341,155 @@
    syncDemandedQuantityTree(dataValue.dataList);
  };
  // ===== æ‹–拽排序 =====
  // æŒ‰å½“前 DOM è¡Œé¡ºåºå±•开成扁平列表(编辑模式下强制全部展开,保证行顺序与数据一致)
  const buildFlatTree = () => {
    const flat = [];
    const walk = (items, depth, parentNode) => {
      items.forEach(node => {
        flat.push({ node, depth, parentArray: items, parentNode });
        if (Array.isArray(node.children) && node.children.length > 0) {
          walk(node.children, depth + 1, node);
        }
      });
    };
    walk(dataValue.dataList, 0, null);
    return flat;
  };
  const removeNodeByTempId = (items, tempId) => {
    for (let i = 0; i < items.length; i++) {
      if (String(items[i].tempId) === String(tempId)) {
        items.splice(i, 1);
        return true;
      }
      if (
        Array.isArray(items[i].children) &&
        removeNodeByTempId(items[i].children, tempId)
      ) {
        return true;
      }
    }
    return false;
  };
  const updateParentRef = (node, parent) => {
    node.parentTempId = parent?.tempId ?? "";
    node.parentId = parent?.id ?? "";
  };
  const handleSortEnd = evt => {
    const { oldIndex, newIndex } = evt;
    if (
      typeof oldIndex !== "number" ||
      typeof newIndex !== "number" ||
      oldIndex === newIndex
    ) {
      return;
    }
    const flat = buildFlatTree();
    if (oldIndex < 0 || oldIndex >= flat.length) return;
    const dragged = flat[oldIndex];
    // æ‹–放后位于被拖行上方一行的数据(按最终 DOM é¡ºåºæ¢ç®—到原扁平列表)
    let aEntry = null;
    if (newIndex > oldIndex) {
      aEntry = flat[newIndex];
    } else if (newIndex > 0) {
      aEntry = flat[newIndex - 1];
    }
    if (!aEntry) {
      // æ‹–到列表最前 â†’ æˆä¸ºä¸€çº§
      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
      dataValue.dataList.unshift(dragged.node);
      updateParentRef(dragged.node, null);
    } else if (aEntry.depth < dragged.depth) {
      // æ‹–到某行正下方 â†’ æˆä¸ºè¯¥è¡Œçš„子级
      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
      if (!aEntry.node.children) {
        aEntry.node.children = [];
      }
      aEntry.node.children.push(dragged.node);
      updateParentRef(dragged.node, aEntry.node);
    } else {
      // åŒçº§æ’å…¥:向上找第一个深度不超过被拖行的节点
      let i = flat.indexOf(aEntry);
      while (i >= 0 && flat[i].depth > dragged.depth) {
        i--;
      }
      const eEntry = flat[i];
      // æ‹–回自己的子树内 â†’ ä¸å¤„理(防止形成循环)
      if (!eEntry || eEntry.node.tempId === dragged.node.tempId) {
        return;
      }
      removeNodeByTempId(dataValue.dataList, dragged.node.tempId);
      const siblings = eEntry.parentArray;
      const idx = siblings.indexOf(eEntry.node);
      siblings.splice(idx + 1, 0, dragged.node);
      updateParentRef(dragged.node, eEntry.parentNode);
    }
    recalculateDemandedQuantities();
  };
  const initSortable = () => {
    if (!tableRef.value) return;
    const tbody = tableRef.value.$el.querySelector(".el-table__body tbody");
    if (!tbody) return;
    tableSortable?.destroy();
    tableSortable = new Sortable(tbody, {
      animation: 150,
      handle: ".drag-handle",
      disabled: !dataValue.isEdit,
      ghostClass: "sortable-ghost",
      onEnd: handleSortEnd,
    });
  };
  const expandAllTree = () => {
    const table = tableRef.value;
    if (!table) return;
    const walk = items => {
      items.forEach(item => {
        if (Array.isArray(item.children) && item.children.length > 0) {
          table.toggleRowExpansion(item, true);
          walk(item.children);
        }
      });
    };
    walk(dataValue.dataList);
  };
  // ç¼–辑模式下不允许折叠,保证拖拽时 DOM è¡Œé¡ºåºä¸Ž dataList ä¸€è‡´
  const handleTreeExpandChange = (row, expanded) => {
    if (dataValue.isEdit && expanded === false) {
      nextTick(() => {
        tableRef.value?.toggleRowExpansion(row, true);
      });
    }
  };
  // å¤–层行折叠后内容会销毁,重新展开时重新初始化拖拽
  const handleOuterExpandChange = (row, expandedRows) => {
    if (Array.isArray(expandedRows) && expandedRows.some(r => r === row)) {
      nextTick(() => initSortable());
    }
  };
  watch(
    () => dataValue.isEdit,
    val => {
      tableSortable?.option("disabled", !val);
      if (val) {
        nextTick(() => {
          initSortable();
          expandAllTree();
        });
      }
    }
  );
  const buildSubmitTree = (items: any[]) => {
    return items.map((item: any) => {
      const current = { ...item };
@@ -346,6 +533,35 @@
    }
  };
  const openProcessDialog = (row: any, event?: Event) => {
    // ç‚¹å‡»æ¸…除按钮时不弹窗
    const target = event?.target as HTMLElement | undefined;
    if (target && target.closest(".el-input__clear")) {
      return;
    }
    dataValue.currentProcessRow = row;
    dataValue.showProcessDialog = true;
  };
  const clearProcess = (row: any) => {
    row.processId = "";
    row.operationId = "";
    row.processName = "";
    row.operationName = "";
  };
  const handleProcessConfirm = (processRow: any) => {
    const row = dataValue.currentProcessRow;
    if (!row) {
      return;
    }
    row.processId = processRow.id;
    row.operationId = processRow.id;
    row.processName = processRow.name;
    row.operationName = processRow.name;
    handleProcessChange(row, processRow.id);
  };
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
@@ -382,7 +598,7 @@
  };
  const fetchProcessOptions = async () => {
    const { data } = await list({});
    const { data } = await listAll();
    console.log(data, "dataValue.dataList");
    dataValue.processOptions = normalizeListData(data);
  };
@@ -693,5 +909,38 @@
    // å…ˆåŠ è½½å·¥åºé€‰é¡¹ï¼Œå†åŠ è½½æ•°æ®ï¼Œç¡®ä¿el-select能够正确回显
    await fetchProcessOptions();
    await fetchData();
    await nextTick();
    initSortable();
  });
</script>
  onBeforeUnmount(() => {
    tableSortable?.destroy();
    tableSortable = null;
  });
</script>
<style scoped>
  .drag-tip {
    display: flex;
    align-items: center;
    gap: 4px;
    margin-bottom: 8px;
    font-size: 12px;
    color: #909399;
  }
  .drag-handle {
    cursor: move;
    color: #909399;
    text-align: center;
  }
  .drag-handle:hover {
    color: var(--el-color-primary);
  }
  :deep(.sortable-ghost) {
    opacity: 0.6;
    background-color: #f5f7fa !important;
  }
</style>
src/views/productionManagement/productionProcess/ProcessSelectDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
<template>
  <el-dialog v-model="visible" title="选择工序" width="800px" destroy-on-close :close-on-click-modal="false">
    <el-form :inline="true" :model="query" class="mb-2">
      <el-form-item label="工序名称">
        <el-input v-model="query.name" placeholder="输入工序名称" clearable @keyup.enter="onSearch" style="width: 180px" />
      </el-form-item>
      <el-form-item label="工序编码">
        <el-input v-model="query.no" placeholder="输入工序编码" clearable @keyup.enter="onSearch" style="width: 180px" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSearch">搜索</el-button>
        <el-button @click="onReset">重置</el-button>
      </el-form-item>
    </el-form>
    <el-table ref="tableRef" v-loading="loading" :data="tableData" height="380" highlight-current-row row-key="id"
      @current-change="handleCurrentChange" @row-dblclick="onConfirm">
      <el-table-column type="index" label="序号" width="60" />
      <el-table-column prop="no" label="工序编码" min-width="130" />
      <el-table-column prop="name" label="工序名称" min-width="150" />
      <el-table-column prop="isQuality" label="是否质检" min-width="90">
        <template #default="{ row }">
          <el-tag size="small" :type="row.isQuality ? 'warning' : 'info'">{{ row.isQuality ? '是' : '否' }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="isProduction" label="是否生产" min-width="90">
        <template #default="{ row }">
          <el-tag size="small" :type="row.isProduction ? 'success' : 'info'">{{ row.isProduction ? '是' : '否' }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
    </el-table>
    <div class="mt-3 flex justify-end">
      <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
        v-model:page-size="page.pageSize" v-model:current-page="page.pageNum" :page-sizes="[10, 20, 50, 100]"
        @size-change="onPageChange" @current-change="onPageChange" />
    </div>
    <template #footer>
      <el-button type="primary" :disabled="!selectedRow" @click="onConfirm">确定</el-button>
      <el-button @click="close()">取消</el-button>
    </template>
  </el-dialog>
</template>
<script setup lang="ts">
  import { computed, reactive, ref, watch } from "vue";
  import { ElMessage } from "element-plus";
  import { listPage } from "@/api/productionManagement/productionProcess";
  export type ProcessRow = {
    id: number;
    no?: string;
    name: string;
    isQuality?: boolean;
    isProduction?: boolean;
    remark?: string;
  };
  const props = defineProps<{
    modelValue: boolean;
  }>();
  const emit = defineEmits(["update:modelValue", "confirm"]);
  const visible = computed({
    get: () => props.modelValue,
    set: v => emit("update:modelValue", v),
  });
  const query = reactive({
    name: "",
    no: "",
  });
  const page = reactive({
    pageNum: 1,
    pageSize: 10,
  });
  const loading = ref(false);
  const tableData = ref<ProcessRow[]>([]);
  const total = ref(0);
  const selectedRow = ref<ProcessRow | null>(null);
  const tableRef = ref();
  function close() {
    visible.value = false;
  }
  function handleCurrentChange(row: ProcessRow) {
    selectedRow.value = row;
  }
  function onSearch() {
    page.pageNum = 1;
    loadData();
  }
  function onReset() {
    query.name = "";
    query.no = "";
    onSearch();
  }
  function onPageChange() {
    loadData();
  }
  function onConfirm() {
    if (!selectedRow.value) {
      ElMessage.warning("请选择一个工序");
      return;
    }
    emit("confirm", selectedRow.value);
    close();
  }
  async function loadData() {
    loading.value = true;
    try {
      selectedRow.value = null;
      if (tableRef.value) {
        tableRef.value.setCurrentRow(null);
      }
      const { data } = await listPage({
        current: page.pageNum,
        size: page.pageSize,
        name: query.name.trim(),
        no: query.no.trim(),
      });
      const records = data?.records ?? (Array.isArray(data) ? data : []);
      tableData.value = Array.isArray(records) ? records : [];
      total.value = Number(data?.total ?? tableData.value.length);
    } catch (e) {
      console.error("加载工序列表失败", e);
      tableData.value = [];
      total.value = 0;
    } finally {
      loading.value = false;
    }
  }
  // ç›‘听弹窗打开,重置查询条件和页码(immediate:组件创建时 modelValue å·²ä¸º true,也立即加载)
  watch(
    () => props.modelValue,
    visible => {
      if (visible) {
        selectedRow.value = null;
        query.name = "";
        query.no = "";
        page.pageNum = 1;
        loadData();
      }
    },
    { immediate: true }
  );
</script>