From 6535d8f34b4b1ed0d9551608847b4041c2e9e701 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 03 九月 2026 17:27:42 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0

---
 src/views/mes/process-design/route/modules/workbench.vue |  545 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 545 insertions(+), 0 deletions(-)

diff --git a/src/views/mes/process-design/route/modules/workbench.vue b/src/views/mes/process-design/route/modules/workbench.vue
new file mode 100644
index 0000000..7959f3b
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/workbench.vue
@@ -0,0 +1,545 @@
+<script lang="ts" setup>
+import type { MesMdItemApi } from '#/api/mes/md/item';
+import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
+import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
+
+import { computed, ref, watch } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+
+import {
+  Button,
+  Card,
+  Empty,
+  Input,
+  InputNumber,
+  message,
+  Popconfirm,
+  Table,
+} from 'ant-design-vue';
+
+import {
+  deleteRouteProcess,
+  getRouteProcessListByRoute,
+  updateRouteProcess,
+} from '#/api/mes/pro/route/process';
+import {
+  createRouteProductBom,
+  deleteRouteProductBom,
+  getRouteProductBomList,
+  updateRouteProductBom,
+} from '#/api/mes/pro/route/productbom';
+import { $t } from '#/locales';
+import { MdItemSelect, MdProductBomSelect } from '#/views/mes/md/item/components';
+
+import type { FormType } from '../data';
+import ProcessForm from './process-form.vue';
+
+defineOptions({ name: 'MesRouteWorkbench' });
+
+const props = withDefaults(
+  defineProps<{
+    routeId: number;
+    formType: FormType;
+    product?: MesProRouteProductApi.RouteProduct;
+  }>(),
+  { product: undefined },
+);
+
+const editable = computed(() => props.formType !== 'detail');
+
+const processes = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ュ簭鍒楄〃
+const selectedProcessId = ref<number>(); // 褰撳墠閫変腑宸ュ簭
+const outputItemIds = ref<number[]>([]); // 閫変腑宸ュ簭鐨勪骇鍑哄崐鎴愬搧
+const bomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); // 閫変腑宸ュ簭鍦ㄥ綋鍓嶄骇鍝佷笅鐨勬姇鍏ョ墿鏂�
+const adding = ref(false); // 鏄惁鏄剧ず鏂板鎶曞叆鐗╂枡琛�
+
+const selectedProcess = computed(() =>
+  processes.value.find((item) => item.processId === selectedProcessId.value),
+);
+const productItemId = computed(() => props.product?.itemId);
+const linkTypeLabel = computed(() =>
+  selectedProcess.value?.linkType != null
+    ? getDictLabel(DICT_TYPE.MES_PRO_LINK_TYPE, selectedProcess.value.linkType)
+    : '-',
+);
+
+const [ProcessFormModal, processFormModalApi] = useVbenModal({
+  connectedComponent: ProcessForm,
+  destroyOnClose: true,
+});
+
+/** 鍔犺浇宸ュ簭鍒楄〃锛堜繚鎸侀�変腑锛屽け鏁堟椂鍥為��绗竴涓級 */
+async function loadProcesses() {
+  const list = (await getRouteProcessListByRoute(props.routeId)) || [];
+  processes.value = list;
+  if (list.length === 0) {
+    selectedProcessId.value = undefined;
+    return;
+  }
+  if (!list.some((item) => item.processId === selectedProcessId.value)) {
+    selectedProcessId.value = list[0]!.processId;
+  }
+}
+
+/** 鏍规嵁閫変腑宸ュ簭鍚屾寰呯紪杈戠殑浜у嚭鍗婃垚鍝� */
+function syncOutputItemIds() {
+  outputItemIds.value = selectedProcess.value?.outputItemIds ?? [];
+}
+
+/** 鍔犺浇閫変腑宸ュ簭鍦ㄥ綋鍓嶄骇鍝佷笅鐨勬姇鍏ョ墿鏂� */
+async function loadBomList() {
+  const processId = selectedProcess.value?.processId;
+  if (!processId || !productItemId.value) {
+    bomList.value = [];
+    return;
+  }
+  bomList.value =
+    (await getRouteProductBomList({
+      routeId: props.routeId,
+      processId,
+      productId: productItemId.value,
+    })) || [];
+  // 娓呯悊宸蹭笉鍦ㄥ綋鍓嶆姇鍏ユ竻鍗曚腑鐨勮鍐呭娉ㄨ崏绋匡紝閬垮厤鍒囨崲鍚庢畫鐣欐棫杈撳叆
+  const draftIds = new Set(
+    bomList.value
+      .map((item) => item.id)
+      .filter((id): id is number => id != null),
+  );
+  Object.keys(remarkEditValues.value).forEach((key) => {
+    if (!draftIds.has(Number(key))) {
+      delete remarkEditValues.value[Number(key)];
+    }
+  });
+}
+
+watch(
+  () => props.routeId,
+  () => loadProcesses(),
+  { immediate: true },
+);
+
+watch(
+  () => selectedProcessId.value,
+  () => {
+    syncOutputItemIds();
+    loadBomList();
+  },
+);
+
+watch(
+  () => productItemId.value,
+  () => {
+    outputItemIds.value = [];
+    loadBomList();
+  },
+);
+
+/** 鏂板宸ュ簭 */
+function openProcessCreate() {
+  const maxSort = processes.value.length
+    ? Math.max(...processes.value.map((item) => item.sort || 0))
+    : 0;
+  processFormModalApi
+    .setData({ maxSort, routeId: props.routeId, formType: props.formType })
+    .open();
+}
+
+/** 缂栬緫宸ュ簭淇℃伅 */
+function openProcessEdit(row: MesProRouteProcessApi.RouteProcess) {
+  processFormModalApi
+    .setData({ id: row.id, routeId: props.routeId, formType: props.formType })
+    .open();
+}
+
+/** 鍒犻櫎宸ュ簭 */
+async function handleProcessDelete(row: MesProRouteProcessApi.RouteProcess) {
+  await deleteRouteProcess(row.id!);
+  message.success($t('ui.actionMessage.operationSuccess'));
+  await loadProcesses();
+}
+
+/** 浜у嚭鍗婃垚鍝侊細閫夋嫨鍙樺寲鍗充繚瀛� */
+async function handleOutputChange(
+  items: MesMdItemApi.Item | MesMdItemApi.Item[] | undefined,
+) {
+  if (!selectedProcess.value) {
+    return;
+  }
+  const ids = (
+    Array.isArray(items) ? items : items ? [items] : []
+  )
+    .map((item) => item.id)
+    .filter((id): id is number => id != null);
+  outputItemIds.value = ids;
+  try {
+    await updateRouteProcess({
+      id: selectedProcess.value.id,
+      routeId: selectedProcess.value.routeId,
+      outputItemIds: ids,
+    });
+    message.success($t('ui.actionMessage.operationSuccess'));
+  } catch {
+    // 淇濆瓨澶辫触锛氶噸杞藉伐搴忓垪琛ㄥ洖閫�鏈湴鍊�
+    await loadProcesses();
+    syncOutputItemIds();
+  }
+}
+
+/** 鐢ㄩ噺淇敼锛氬彉鍖栧嵆淇濆瓨锛堟惡甯﹀畬鏁村叧鑱斿瓧娈碉紝閫氳繃鍚庣蹇呭~鏍¢獙锛� */
+async function handleQuantityChange(
+  record: MesProRouteProductBomApi.RouteProductBom,
+  value: number | string | null,
+) {
+  if (value == null || record.id == null) {
+    return;
+  }
+  const quantity = Number(value);
+  if (Number.isNaN(quantity)) {
+    return;
+  }
+  record.quantity = quantity;
+  try {
+    await updateRouteProductBom(toUpdatePayload(record));
+  } catch {
+    await loadBomList();
+  }
+}
+
+const remarkEditValues = ref<Record<number, string>>({}); // 琛屽唴澶囨敞缂栬緫鐨勬殏瀛樺��
+
+/** 澶囨敞杈撳叆锛氭殏瀛樺�硷紝涓嶇珛鍗虫彁浜� */
+function handleRemarkInput(
+  record: MesProRouteProductBomApi.RouteProductBom,
+  value: string,
+) {
+  if (record.id != null) {
+    remarkEditValues.value[record.id] = value;
+  }
+}
+
+/** 璇诲彇琛屽唴澶囨敞鑽夌锛屾棤缂栬緫鏃跺洖閫�鏁版嵁搴撳�� */
+function getRemarkDraft(record: MesProRouteProductBomApi.RouteProductBom) {
+  const draft =
+    record.id != null ? remarkEditValues.value[record.id] : undefined;
+  return draft ?? record.remark ?? '';
+}
+
+/** 澶囨敞淇敼锛氬け鐒�/鍥炶溅鍗充繚瀛� */
+async function handleRemarkChange(
+  record: MesProRouteProductBomApi.RouteProductBom,
+  value: string,
+) {
+  if (record.id == null) {
+    return;
+  }
+  const remark = typeof value === 'string' ? value.trim() : '';
+  // 鍚屾鏆傚瓨鍊硷紝淇濊瘉鍥炶溅鍚庡啀瑙﹀彂鐨� blur 璇诲埌鐨勬槸鎻愪氦鍚庣殑鍊硷紝涓嶄細鐢ㄦ棫鍊煎洖鍐�
+  remarkEditValues.value[record.id] = remark;
+  if (record.remark === remark) {
+    return;
+  }
+  record.remark = remark;
+  try {
+    await updateRouteProductBom(toUpdatePayload(record));
+  } catch {
+    await loadBomList();
+  }
+}
+
+/** 琛屽唴缂栬緫鎻愪氦锛氫粎鎼哄甫鍚庣鏍¢獙鎵�闇�鐨勫叧鑱斿瓧娈� */
+function toUpdatePayload(record: MesProRouteProductBomApi.RouteProductBom) {
+  return {
+    id: record.id,
+    routeId: record.routeId,
+    processId: record.processId,
+    productId: record.productId,
+    itemId: record.itemId,
+    quantity: record.quantity,
+    remark: record.remark,
+  };
+}
+
+/** 閫変腑 BOM 鍚庢壒閲忓叆搴擄紙鏀寔澶氶�夛紝宸插瓨鍦ㄧ殑鐗╂枡鑷姩璺宠繃锛� */
+async function handleBomSelected(
+  boms?: MesMdProductBomApi.ProductBom | MesMdProductBomApi.ProductBom[],
+) {
+  if (!selectedProcess.value || !productItemId.value) {
+    return;
+  }
+  const selectedList = Array.isArray(boms) ? boms : boms ? [boms] : [];
+  if (selectedList.length === 0) {
+    return;
+  }
+  // 璺宠繃宸插湪鎶曞叆娓呭崟涓殑鐗╂枡锛岄伩鍏嶉噸澶嶅叆搴�
+  const existingIds = new Set(
+    bomList.value
+      .map((item) => item.itemId)
+      .filter((id): id is number => id != null),
+  );
+  let created = 0;
+  for (const bom of selectedList) {
+    if (!bom.bomItemId || existingIds.has(bom.bomItemId)) {
+      continue;
+    }
+    await createRouteProductBom({
+      routeId: props.routeId,
+      processId: selectedProcess.value.processId,
+      productId: productItemId.value,
+      itemId: bom.bomItemId,
+      quantity: bom.quantity ?? 1,
+    });
+    created++;
+  }
+  if (created > 0) {
+    message.success($t('ui.actionMessage.operationSuccess'));
+  }
+  adding.value = false;
+  await loadBomList();
+}
+
+/** 鍒犻櫎鎶曞叆鐗╂枡 */
+async function handleBomDelete(record: MesProRouteProductBomApi.RouteProductBom) {
+  await deleteRouteProductBom(record.id!);
+  message.success($t('ui.actionMessage.operationSuccess'));
+  await loadBomList();
+}
+
+/** 鎶曞叆鐗╂枡琛ㄦ牸鍒� */
+const columns = [
+  { title: '鐗╂枡', key: 'item' as const, width: 220 },
+  { title: '鐢ㄩ噺', key: 'quantity' as const, width: 110 },
+  { title: '鍗曚綅', key: 'unit' as const, dataIndex: 'unitName', width: 70 },
+  { title: '澶囨敞', key: 'remark' as const, dataIndex: 'remark', minWidth: 160 },
+  { title: '鎿嶄綔', key: 'action' as const, width: 70 },
+];
+</script>
+
+<template>
+  <ProcessFormModal @success="loadProcesses" />
+  <div class="flex gap-4">
+    <!-- 宸︿晶锛氬伐搴忓垪琛� -->
+    <div
+      class="w-72 shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-white"
+    >
+      <div
+        class="flex h-10 items-center justify-between border-b border-gray-100 bg-gray-50 px-3"
+      >
+        <span class="text-sm font-medium">宸ュ簭鍒楄〃</span>
+        <Button
+          v-if="editable"
+          size="small"
+          type="link"
+          @click="openProcessCreate"
+        >
+          鏂板宸ュ簭
+        </Button>
+      </div>
+      <ul class="max-h-[560px] overflow-auto">
+        <li
+          v-for="item in processes"
+          :key="item.processId"
+          class="group flex cursor-pointer items-center gap-2 px-3 py-2.5 text-sm transition-colors"
+          :class="
+            selectedProcess?.processId === item.processId
+              ? 'bg-blue-50 text-blue-600'
+              : 'hover:bg-gray-50'
+          "
+          @click="selectedProcessId = item.processId"
+        >
+          <span class="w-6 shrink-0 text-right text-xs text-gray-400">
+            {{ item.sort }}
+          </span>
+          <span
+            class="h-3 w-3 shrink-0 rounded-full border border-gray-200"
+            :style="{ backgroundColor: item.colorCode }"
+          ></span>
+          <span class="min-w-0 flex-1 truncate">{{ item.processName }}</span>
+          <span v-if="item.keyFlag" class="shrink-0 text-amber-500" title="鍏抽敭宸ュ簭">鈽�</span>
+          <span v-if="item.checkFlag" class="shrink-0 text-green-600" title="璐ㄦ纭">鈼�</span>
+          <span v-if="item.backflushFlag" class="shrink-0 text-cyan-600" title="鍊掑啿">鈫�</span>
+          <Popconfirm
+            v-if="editable"
+            title="纭鍒犻櫎璇ュ伐搴忥紵"
+            ok-text="纭畾"
+            cancel-text="鍙栨秷"
+            @confirm="handleProcessDelete(item)"
+            @click.stop
+          >
+            <Button
+              class="opacity-0 transition-opacity group-hover:opacity-100"
+              size="small"
+              type="text"
+              danger
+            >
+              鍒�
+            </Button>
+          </Popconfirm>
+        </li>
+        <li v-if="processes.length === 0" class="px-3 py-10 text-center text-xs text-gray-400">
+          灏氭湭閰嶇疆宸ュ簭锛岀偣鍑诲彸涓婅銆屾柊澧炲伐搴忋�嶅紑濮�
+        </li>
+      </ul>
+    </div>
+
+    <!-- 鍙充晶锛氶�変腑宸ュ簭鐨勯厤缃潰鏉� -->
+    <div class="min-w-0 flex-1 space-y-3">
+      <template v-if="selectedProcess">
+        <!-- 宸ュ簭淇℃伅 -->
+        <Card :bordered="false" class="!border border-gray-200">
+          <div class="flex items-start justify-between gap-3">
+            <div class="min-w-0">
+              <div class="flex items-center gap-2">
+                <span class="text-base font-medium">
+                  {{ selectedProcess.processName }}
+                </span>
+                <span class="text-xs text-gray-400">
+                  {{ selectedProcess.processCode }}
+                </span>
+                <span v-if="selectedProcess.keyFlag" class="text-amber-500" title="鍏抽敭宸ュ簭">鈽�</span>
+                <span v-if="selectedProcess.checkFlag" class="text-green-600" title="璐ㄦ纭">鈼�</span>
+                <span v-if="selectedProcess.backflushFlag" class="text-cyan-600" title="鍊掑啿">鈫�</span>
+              </div>
+              <div class="mt-1 flex flex-wrap gap-x-5 gap-y-1 text-sm text-gray-500">
+                <span>搴忓彿锛歿{ selectedProcess.sort }}</span>
+                <span>鍑嗗鏃堕棿锛歿{ selectedProcess.prepareTime ?? 0 }} 鍒�</span>
+                <span>绛夊緟鏃堕棿锛歿{ selectedProcess.waitTime ?? 0 }} 鍒�</span>
+                <span v-if="selectedProcess.linkType != null">
+                  涓庝笅閬撳伐搴忥細{{ linkTypeLabel }}
+                </span>
+              </div>
+              <div v-if="selectedProcess.remark" class="mt-1 text-xs text-gray-400">
+                澶囨敞锛歿{ selectedProcess.remark }}
+              </div>
+            </div>
+            <Button
+              v-if="editable"
+              type="link"
+              @click="openProcessEdit(selectedProcess)"
+            >
+              缂栬緫淇℃伅
+            </Button>
+          </div>
+        </Card>
+
+        <!-- 浜у嚭 -->
+        <Card :bordered="false" class="!border border-gray-200">
+          <template #title>
+            <span class="text-sm font-medium">浜у嚭</span>
+          </template>
+          <MdItemSelect
+            :model-value="outputItemIds"
+            multiple
+            :disabled="!editable"
+            placeholder="鐣欑┖鍒欓粯璁や负宸ュ崟鎴愬搧"
+            @change="handleOutputChange"
+          />
+          <p class="mt-1 text-xs text-gray-400">
+            鐣欑┖琛ㄧず璇ュ伐搴忎骇鍑哄伐鍗曟垚鍝侊紱閫夋嫨澶氫釜琛ㄧず涓�涓伐搴忎骇鍑哄绉嶅崐鎴愬搧銆�
+          </p>
+        </Card>
+
+        <!-- 鎶曞叆鐗╂枡 -->
+        <Card :bordered="false" class="!border border-gray-200">
+          <template #title>
+            <span class="text-sm font-medium">
+              鎶曞叆鐗╂枡{{ productItemId ? `锛�${props.product?.itemName}锛塦 : '' }}
+            </span>
+          </template>
+          <Empty
+            v-if="!productItemId"
+            :image="Empty.PRESENTED_IMAGE_SIMPLE"
+            description="璇峰厛鍦ㄩ《閮ㄧ粦瀹氱敓浜т骇鍝�"
+          />
+          <template v-else>
+            <Table
+              :data-source="bomList"
+              :columns="columns"
+              :pagination="false"
+              size="small"
+              row-key="id"
+            >
+              <template #bodyCell="{ column, record }">
+                <template v-if="column.key === 'item'">
+                  <div class="leading-4">
+                    <div class="text-sm">{{ record.itemName }}</div>
+                    <div class="text-xs text-gray-400">{{ record.itemCode }}</div>
+                  </div>
+                </template>
+                <template v-else-if="column.key === 'quantity'">
+                  <InputNumber
+                    v-if="editable"
+                    :value="record.quantity"
+                    :min="0"
+                    :precision="2"
+                    size="small"
+                    class="!w-24"
+                    @change="(value) => handleQuantityChange(record, value)"
+                  />
+                  <span v-else>{{ record.quantity }}</span>
+                </template>
+                <template v-else-if="column.key === 'remark'">
+                  <span v-if="!editable">{{ record.remark }}</span>
+                  <Input
+                    v-else
+                    :value="getRemarkDraft(record)"
+                    :maxlength="255"
+                    size="small"
+                    placeholder="澶囨敞"
+                    @update:value="(value: string) => handleRemarkInput(record, value)"
+                    @blur="() => handleRemarkChange(record, getRemarkDraft(record))"
+                    @pressEnter="() => handleRemarkChange(record, getRemarkDraft(record))"
+                  />
+                </template>
+                <template v-else-if="column.key === 'action'">
+                  <Popconfirm
+                    v-if="editable"
+                    title="纭鍒犻櫎璇ユ姇鍏ョ墿鏂欙紵"
+                    ok-text="纭畾"
+                    cancel-text="鍙栨秷"
+                    @confirm="handleBomDelete(record)"
+                  >
+                    <Button type="link" size="small" danger> 鍒犻櫎 </Button>
+                  </Popconfirm>
+                </template>
+              </template>
+            </Table>
+            <div class="mt-2">
+              <template v-if="editable && adding">
+                <div class="flex items-center gap-2">
+                  <MdProductBomSelect
+                    :item-id="productItemId"
+                    multiple
+                    placeholder="閫夋嫨璇ヤ骇鍝佺殑 BOM 鐗╂枡锛堝彲澶氶�夛級"
+                    @change="handleBomSelected"
+                  />
+                  <Button size="small" @click="adding = false">鍙栨秷</Button>
+                </div>
+              </template>
+              <div v-else-if="editable" class="flex items-center gap-3">
+                <Button size="small" type="dashed" block @click="adding = true">
+                  锛� 鏂板鎶曞叆鐗╂枡
+                </Button>
+                <span v-if="bomList.length === 0" class="text-xs text-orange-500">
+                  璇ュ伐搴忓皻鏈厤缃姇鍏ョ墿鏂欙紱鏍囦负銆屽�掑啿銆嶇殑宸ュ簭鎶ュ伐鏃朵細鎸夋澶勭敤閲忚嚜鍔ㄦ墸鏂�
+                </span>
+              </div>
+            </div>
+          </template>
+        </Card>
+      </template>
+
+      <!-- 鏃犲伐搴忕┖鎬� -->
+      <div
+        v-else
+        class="flex h-56 items-center justify-center rounded-lg border border-dashed border-gray-300 text-sm text-gray-400"
+      >
+        璇峰厛鍦ㄥ乏渚ф柊澧炲苟閫変腑涓�閬撳伐搴�
+      </div>
+    </div>
+  </div>
+</template>
\ No newline at end of file

--
Gitblit v1.9.3