From 6738e8f5a78431e41fed28e6b795e70c2396070a Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 03 九月 2026 17:27:23 +0800
Subject: [PATCH] refactor(mes): 优化工艺路线配置与工单表单功能

---
 src/views/dashboard/analytics/modules/quick-actions.vue        |    3 
 src/views/mes/md/item/modules/form.vue                         |    6 
 src/views/mes/pro/workorder/data.ts                            |   16 
 src/views/mes/md/item/components/product-bom-select-dialog.vue |  182 ++++++-
 src/views/dashboard/analytics/modules/alert-panel.vue          |   20 
 src/views/mes/md/item/components/select-dialog.vue             |   61 ++
 src/views/mes/pro/workorder/modules/form.vue                   |   20 
 src/views/mes/process-design/route/modules/workbench.vue       |  545 ++++++++++++++++++++++++
 src/views/dashboard/analytics/index.vue                        |    1 
 src/views/mes/pro/workorder/modules/process-list.vue           |    3 
 src/views/mes/process-design/route/modules/product-form.vue    |  107 +++-
 src/views/mes/process-design/route/modules/form.vue            |  146 +++++-
 src/views/mes/md/item/components/product-bom-select.vue        |  122 +++-
 src/views/mes/process-design/route/data.ts                     |   41 +
 14 files changed, 1,100 insertions(+), 173 deletions(-)

diff --git a/src/views/dashboard/analytics/index.vue b/src/views/dashboard/analytics/index.vue
index 3714991..13794de 100644
--- a/src/views/dashboard/analytics/index.vue
+++ b/src/views/dashboard/analytics/index.vue
@@ -95,7 +95,6 @@
       </Col>
       <Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4">
         <AlertPanel
-          :andon-active-count="summary.andonActiveCount"
           :repair-active-count="summary.repairActiveCount"
           @navigate="handleNavigate"
         />
diff --git a/src/views/dashboard/analytics/modules/alert-panel.vue b/src/views/dashboard/analytics/modules/alert-panel.vue
index 4be1544..8ac16f2 100644
--- a/src/views/dashboard/analytics/modules/alert-panel.vue
+++ b/src/views/dashboard/analytics/modules/alert-panel.vue
@@ -15,7 +15,6 @@
 defineOptions({ name: 'DashboardAlertPanel' });
 
 const props = defineProps<{
-  andonActiveCount: number;
   repairActiveCount: number;
 }>();
 
@@ -67,24 +66,7 @@
 <template>
   <div class="space-y-4">
     <!-- 棰勮鍗$墖琛� -->
-    <div class="grid grid-cols-2 gap-3">
-      <div
-        class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4"
-        @click="emit('navigate', 'MesProAndon')"
-      >
-        <div class="relative z-10 flex items-center gap-3">
-          <div class="flex size-10 items-center justify-center rounded-lg bg-red-50 text-red-600 shadow-sm dark:bg-red-500/15 dark:text-red-400">
-            <IconifyIcon class="size-5" icon="lucide:alert-triangle" />
-          </div>
-          <div>
-            <div class="text-xs text-gray-500 dark:text-gray-400">瀹夌伅鍛煎彨</div>
-            <div class="flex items-baseline gap-1">
-              <CountTo class="text-xl font-bold text-red-600 dark:text-red-400" :end-val="andonActiveCount" :duration="1000" />
-              <span class="text-xs text-gray-400">鏈缃�</span>
-            </div>
-          </div>
-        </div>
-      </div>
+    <div class="grid grid-cols-1 gap-3">
       <div
         class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4"
         @click="emit('navigate', 'MesProRepair')"
diff --git a/src/views/dashboard/analytics/modules/quick-actions.vue b/src/views/dashboard/analytics/modules/quick-actions.vue
index f84affb..9a825b5 100644
--- a/src/views/dashboard/analytics/modules/quick-actions.vue
+++ b/src/views/dashboard/analytics/modules/quick-actions.vue
@@ -21,7 +21,6 @@
   { title: '璐ㄦ绠$悊', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' },
   { title: '璁惧鐐规', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' },
   { title: '鐢熶骇鎺掔▼', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' },
-  { title: '瀹夌伅鍛煎彨', icon: 'lucide:bell-ring', route: 'MesProAndon', gradient: 'from-red-500 to-rose-500', shadow: 'shadow-red-500/15' },
   { title: '鐗╂枡搴撳瓨', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' },
   { title: '宸ヨ壓璺嚎', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' },
   { title: '鏁版嵁鎶ヨ〃', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' },
@@ -31,7 +30,7 @@
 <template>
   <div class="glass-card rounded-2xl p-5">
     <h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">蹇嵎鎿嶄綔</h3>
-    <div class="grid grid-cols-3 gap-3 sm:grid-cols-9">
+    <div class="grid grid-cols-4 gap-3 sm:grid-cols-8">
       <div
         v-for="action in actions"
         :key="action.route"
diff --git a/src/views/mes/md/item/components/product-bom-select-dialog.vue b/src/views/mes/md/item/components/product-bom-select-dialog.vue
index 9718d32..64eeccb 100644
--- a/src/views/mes/md/item/components/product-bom-select-dialog.vue
+++ b/src/views/mes/md/item/components/product-bom-select-dialog.vue
@@ -13,26 +13,115 @@
 
 defineOptions({ name: 'MdProductBomSelectDialog' });
 
+const props = withDefaults(
+  defineProps<{
+    multiple?: boolean;
+  }>(),
+  {
+    multiple: false,
+  },
+);
+
 const emit = defineEmits<{
-  selected: [row: MesMdProductBomApi.ProductBom];
+  selected: [rows: MesMdProductBomApi.ProductBom[]];
 }>();
 
 const open = ref(false); // 寮圭獥鏄惁鎵撳紑
 const list = ref<MesMdProductBomApi.ProductBom[]>([]); // BOM 瀛愮墿鏂欏垪琛�
-const selectedRow = ref<MesMdProductBomApi.ProductBom>(); // 褰撳墠閫変腑鐨� BOM 琛�
+const selectedRows = ref<MesMdProductBomApi.ProductBom[]>([]); // 宸查�� BOM 琛岋紙鍗曢�夋椂鏈�澶氫竴琛岋級
+const preSelectedIds = ref<number[]>([]); // 棰勯�� BOM 鐗╂枡缂栧彿
+
+/** 鑾峰彇澶氶�夎褰曪紝鍖呭惈 VXE reserve 璁板綍 */
+function getMultipleSelectedRows() {
+  const selectedMap = new Map<number, MesMdProductBomApi.ProductBom>();
+  const records = [
+    ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
+    ...(gridApi.grid.getCheckboxRecords?.() ?? []),
+  ] as MesMdProductBomApi.ProductBom[];
+  records.forEach((row) => {
+    if (row.bomItemId != null) {
+      selectedMap.set(row.bomItemId, row);
+    }
+  });
+  return [...selectedMap.values()];
+}
+
+/** 澶勭悊鍕鹃�夊彉鍖� */
+function handleCheckboxSelectChange() {
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 涓婂懆鍗曞嚮琛岀棔杩癸紝鐢ㄤ簬鍚炴帀鍙屽嚮瑙﹀彂鐨勭浜屾 click */
+let lastClick = { bomItemId: undefined as number | undefined, time: 0 };
+
+/** 鍗曞嚮琛岋細澶氶�夊垏鎹㈠嬀閫夛紱鍗曢�夌敱 radioConfig.trigger='row' 澶勭悊 */
+async function handleCellClick({
+  row,
+  column,
+}: {
+  row: MesMdProductBomApi.ProductBom;
+  column?: { type?: string };
+}) {
+  // 鐐瑰嚮鍕鹃��/鍗曢�夊垪鑷韩鐢辨帶浠跺鐞嗭紝閬垮厤浜屾鍒囨崲
+  if (column?.type === 'checkbox' || column?.type === 'radio') {
+    return;
+  }
+  if (!props.multiple) {
+    return;
+  }
+  const now = Date.now();
+  if (lastClick.bomItemId === row.bomItemId && now - lastClick.time < 300) {
+    lastClick.time = 0;
+    return;
+  }
+  lastClick = { bomItemId: row.bomItemId, time: now };
+  const selected = gridApi.grid.isCheckedByCheckboxRow(row);
+  await gridApi.grid.setCheckboxRow(row, !selected);
+  selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊琛屽弻鍑� */
+async function handleCellDblclick({
+  row,
+}: {
+  row: MesMdProductBomApi.ProductBom;
+}) {
+  if (props.multiple) {
+    return; // 澶氶�夌敱鍗曞嚮 cellClick 鍒囨崲锛屽弻鍑讳笉鍐嶉噸澶嶅鐞�
+  }
+  selectedRows.value = [row];
+  gridApi.grid.setRadioRow(row);
+  handleConfirm();
+}
+
+/** 澶勭悊鍗曢�夊彉鍖� */
+function handleRadioChange(row: MesMdProductBomApi.ProductBom) {
+  selectedRows.value = [row];
+}
+
+/** 褰撳墠閫夋嫨鍒楃被鍨� */
+function getSelectColumn() {
+  return {
+    type: props.multiple ? 'checkbox' : 'radio',
+    width: 55,
+    align: 'center',
+  } as const;
+}
 
 const [Grid, gridApi] = useVbenVxeGrid({
   gridOptions: {
     border: true,
-    columns: [
-      { type: 'radio', width: 55, align: 'center' },
-      ...(useProductBomGridColumns(true) || []),
-    ],
+    columns: [getSelectColumn(), ...(useProductBomGridColumns(true) || [])],
     data: list.value,
     height: 500,
     keepSource: true,
     pagerConfig: {
       enabled: false,
+    },
+    checkboxConfig: {
+      highlight: true,
+      range: true,
+      reserve: true,
     },
     radioConfig: {
       highlight: true,
@@ -48,35 +137,62 @@
     },
   } as VxeTableGridOptions<MesMdProductBomApi.ProductBom>,
   gridEvents: {
-    cellDblclick: ({ row }: { row: MesMdProductBomApi.ProductBom }) => {
-      selectedRow.value = row;
-      gridApi.grid.setRadioRow(row);
-      handleConfirm();
-    },
+    cellClick: handleCellClick,
+    cellDblclick: handleCellDblclick,
+    checkboxChange: handleCheckboxSelectChange,
+    checkboxAll: handleCheckboxSelectChange,
     radioChange: ({ row }: { row: MesMdProductBomApi.ProductBom }) => {
-      selectedRow.value = row;
+      handleRadioChange(row);
     },
   },
 });
 
-/** 鎵撳紑 BOM 鐗╂枡閫夋嫨寮圭獥 */
-async function openModal(itemId: number, selectedBomItemId?: number) {
+/** 鍥炴樉棰勯�� BOM 鐗╂枡锛堟暟鎹竴娆℃�у姞杞斤紝鏃犲垎椤碉紝鐩存帴鍕鹃�夊嵆鍙級 */
+async function applyPreSelection() {
+  if (preSelectedIds.value.length === 0) {
+    return;
+  }
+  for (const row of list.value) {
+    if (
+      row.bomItemId === undefined ||
+      row.bomItemId === null ||
+      !preSelectedIds.value.includes(row.bomItemId)
+    ) {
+      continue;
+    }
+    if (props.multiple) {
+      await gridApi.grid.setCheckboxRow(row, true);
+    } else {
+      await gridApi.grid.setRadioRow(row);
+      selectedRows.value = [row];
+      return;
+    }
+  }
+  if (props.multiple) {
+    selectedRows.value = getMultipleSelectedRows();
+  }
+}
+
+/** 鎵撳紑 BOM 鐗╂枡閫夋嫨寮圭獥锛坰electedBomItemIds 鍗曢�変紶鍗曚釜缂栧彿锛屽閫変紶缂栧彿鏁扮粍锛� */
+async function openModal(itemId: number, selectedBomItemIds?: number | number[]) {
   open.value = true;
-  selectedRow.value = undefined;
+  preSelectedIds.value = (
+    Array.isArray(selectedBomItemIds)
+      ? selectedBomItemIds
+      : selectedBomItemIds === undefined || selectedBomItemIds === null
+        ? []
+        : [selectedBomItemIds]
+  ).filter((id): id is number => id != null);
+  selectedRows.value = [];
   gridApi.setLoading(true);
   try {
     list.value = await getProductBomListByItemId(itemId);
-    gridApi.setGridOptions({ data: list.value });
+    gridApi.setGridOptions({
+      data: list.value,
+      columns: [getSelectColumn(), ...(useProductBomGridColumns(true) || [])],
+    });
     await nextTick();
-    if (selectedBomItemId !== null) {
-      const match = list.value.find(
-        (row) => row.bomItemId === selectedBomItemId,
-      );
-      if (match) {
-        selectedRow.value = match;
-        gridApi.grid.setRadioRow(match);
-      }
-    }
+    await applyPreSelection();
   } finally {
     gridApi.setLoading(false);
   }
@@ -85,17 +201,23 @@
 /** 鍏抽棴 BOM 鐗╂枡閫夋嫨寮圭獥 */
 async function closeModal() {
   open.value = false;
-  selectedRow.value = undefined;
+  selectedRows.value = [];
+  preSelectedIds.value = [];
+  await gridApi.grid.clearCheckboxRow();
+  await gridApi.grid.clearCheckboxReserve();
   await gridApi.grid.clearRadioRow();
 }
 
 /** 纭閫夋嫨 BOM 鐗╂枡 */
 function handleConfirm() {
-  if (!selectedRow.value) {
-    message.warning('璇烽�夋嫨涓�鏉℃暟鎹�');
+  const rows = props.multiple
+    ? getMultipleSelectedRows()
+    : selectedRows.value.slice(0, 1);
+  if (rows.length === 0) {
+    message.warning(props.multiple ? '璇疯嚦灏戦�夋嫨涓�鏉℃暟鎹�' : '璇烽�夋嫨涓�鏉℃暟鎹�');
     return;
   }
-  emit('selected', selectedRow.value);
+  emit('selected', rows);
   open.value = false;
 }
 
@@ -113,4 +235,4 @@
   >
     <Grid table-title="BOM 瀛愮墿鏂欏垪琛�" />
   </Modal>
-</template>
+</template>
\ No newline at end of file
diff --git a/src/views/mes/md/item/components/product-bom-select.vue b/src/views/mes/md/item/components/product-bom-select.vue
index fb056f2..2637585 100644
--- a/src/views/mes/md/item/components/product-bom-select.vue
+++ b/src/views/mes/md/item/components/product-bom-select.vue
@@ -18,7 +18,8 @@
     allowClear?: boolean;
     disabled?: boolean;
     itemId?: number;
-    modelValue?: number;
+    modelValue?: number | number[];
+    multiple?: boolean;
     placeholder?: string;
   }>(),
   {
@@ -26,50 +27,68 @@
     disabled: false,
     itemId: undefined,
     modelValue: undefined,
+    multiple: false,
     placeholder: '璇烽�夋嫨 BOM 鐗╂枡',
   },
 );
 const emit = defineEmits<{
-  change: [bom: MesMdProductBomApi.ProductBom | undefined];
-  'update:modelValue': [value: number | undefined];
+  change: [
+    bom: MesMdProductBomApi.ProductBom[] | MesMdProductBomApi.ProductBom | undefined,
+  ];
+  'update:modelValue': [value: number[] | number | undefined];
 }>();
 const attrs = useAttrs(); // 閫忎紶灞炴��
 const dialogRef = ref<InstanceType<typeof MdProductBomSelectDialog>>(); // BOM 鐗╂枡閫夋嫨寮圭獥
 const hovering = ref(false); // 鏄惁鎮仠
-const selectedBom = ref<MesMdProductBomApi.ProductBom>(); // 褰撳墠閫変腑鐨� BOM 鐗╂枡
+const selectedBoms = ref<MesMdProductBomApi.ProductBom[]>([]); // 宸查�� BOM 鐗╂枡锛堝崟閫夋椂鏈�澶氫竴椤癸級
 
-const displayLabel = computed(() => selectedBom.value?.bomItemName ?? ''); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
-const showClear = computed(
-  () =>
-    props.allowClear &&
-    !props.disabled &&
-    hovering.value &&
-    props.modelValue !== undefined &&
-    props.modelValue !== null,
-);
-
-/** 鏍规嵁 BOM 瀛愮墿鏂欑紪鍙峰洖鏄鹃�夋嫨鍣� */
-async function resolveBomById(bomItemId: number | undefined) {
-  if (
-    bomItemId === undefined ||
-    bomItemId === null ||
-    props.itemId === undefined ||
-    props.itemId === null
-  ) {
-    selectedBom.value = undefined;
-    return;
+const selectedBom = computed(() => selectedBoms.value[0]); // 鍗曢�夋椂鐨勫綋鍓嶉」
+const displayLabel = computed(() => {
+  // 澶氶�夊睍绀恒�屽悕绉般�佸悕绉般�嶏紝鍗曢�夊睍绀哄崟涓悕绉�
+  if (props.multiple) {
+    return selectedBoms.value
+      .map((bom) => bom.bomItemName)
+      .filter(Boolean)
+      .join('銆�');
   }
-  if (selectedBom.value?.bomItemId === bomItemId) {
+  return selectedBom.value?.bomItemName ?? '';
+}); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
+const showClear = computed(() => {
+  if (!props.allowClear || props.disabled || !hovering.value) {
+    return false;
+  }
+  if (props.multiple) {
+    return Array.isArray(props.modelValue) && props.modelValue.length > 0;
+  }
+  return props.modelValue !== undefined && props.modelValue !== null;
+});
+
+/** 浠� modelValue 瑙f瀽鍑� BOM 鐗╂枡缂栧彿鍒楄〃 */
+function getModelIds() {
+  if (props.multiple) {
+    return Array.isArray(props.modelValue)
+      ? props.modelValue.filter((id): id is number => id != null)
+      : [];
+  }
+  return typeof props.modelValue === 'number' ? [props.modelValue] : [];
+}
+
+/** 鏍规嵁 BOM 鐗╂枡缂栧彿鍥炴樉閫夋嫨鍣� */
+async function resolveBomsById(ids: number[]) {
+  if (ids.length === 0 || props.itemId === undefined || props.itemId === null) {
+    selectedBoms.value = [];
     return;
   }
   const list = await getProductBomListByItemId(props.itemId as number);
-  selectedBom.value = list.find((item) => item.bomItemId === bomItemId);
+  selectedBoms.value = list.filter(
+    (item) => item.bomItemId != null && ids.includes(item.bomItemId),
+  );
 }
 
 watch(
   () => props.modelValue,
-  (value) => {
-    resolveBomById(value);
+  () => {
+    resolveBomsById(getModelIds());
   },
   { immediate: true },
 );
@@ -77,17 +96,17 @@
 watch(
   () => props.itemId,
   () => {
-    selectedBom.value = undefined;
-    emit('update:modelValue', undefined);
-    emit('change', undefined);
+    selectedBoms.value = [];
+    emit('update:modelValue', props.multiple ? [] : undefined);
+    emit('change', props.multiple ? [] : undefined);
   },
 );
 
 /** 娓呯┖宸查�� BOM 鐗╂枡 */
 function clearSelected() {
-  selectedBom.value = undefined;
-  emit('update:modelValue', undefined);
-  emit('change', undefined);
+  selectedBoms.value = [];
+  emit('update:modelValue', props.multiple ? [] : undefined);
+  emit('change', props.multiple ? [] : undefined);
 }
 
 /** 鎵撳紑 BOM 鐗╂枡閫夋嫨寮圭獥 */
@@ -101,14 +120,22 @@
     clearSelected();
     return;
   }
-  dialogRef.value?.open(props.itemId as number, props.modelValue);
+  const ids = getModelIds();
+  dialogRef.value?.open(props.itemId as number, props.multiple ? ids : ids[0]);
 }
 
 /** 鍥炲~閫変腑鐨� BOM 鐗╂枡 */
-function handleSelected(row: MesMdProductBomApi.ProductBom) {
-  selectedBom.value = row;
-  emit('update:modelValue', row.bomItemId);
-  emit('change', row);
+function handleSelected(rows: MesMdProductBomApi.ProductBom[]) {
+  selectedBoms.value = rows;
+  emit(
+    'update:modelValue',
+    props.multiple
+      ? rows
+          .map((row) => row.bomItemId)
+          .filter((id): id is number => id != null)
+      : rows[0]?.bomItemId,
+  );
+  emit('change', props.multiple ? rows : rows[0]);
 }
 </script>
 
@@ -121,9 +148,14 @@
     @mouseenter="hovering = true"
     @mouseleave="hovering = false"
   >
-    <Tooltip :mouse-enter-delay="0.5" :open="selectedBom ? undefined : false">
+    <Tooltip :mouse-enter-delay="0.5" :open="selectedBoms.length ? undefined : false">
       <template #title>
-        <div v-if="selectedBom" class="leading-6">
+        <div v-if="multiple" class="leading-6">
+          <div v-for="bom in selectedBoms" :key="bom.bomItemId">
+            {{ bom.bomItemName || '-' }}{{ bom.bomItemCode ? `锛�${bom.bomItemCode}锛塦 : '' }}
+          </div>
+        </div>
+        <div v-else-if="selectedBom" class="leading-6">
           <div>缂栫爜锛歿{ selectedBom.bomItemCode || '-' }}</div>
           <div>鍚嶇О锛歿{ selectedBom.bomItemName || '-' }}</div>
           <div>瑙勬牸锛歿{ selectedBom.bomItemSpecification || '-' }}</div>
@@ -146,5 +178,9 @@
       </Input>
     </Tooltip>
   </div>
-  <MdProductBomSelectDialog ref="dialogRef" @selected="handleSelected" />
-</template>
+  <MdProductBomSelectDialog
+    ref="dialogRef"
+    :multiple="multiple"
+    @selected="handleSelected"
+  />
+</template>
\ No newline at end of file
diff --git a/src/views/mes/md/item/components/select-dialog.vue b/src/views/mes/md/item/components/select-dialog.vue
index fe17e92..61f3fd1 100644
--- a/src/views/mes/md/item/components/select-dialog.vue
+++ b/src/views/mes/md/item/components/select-dialog.vue
@@ -67,11 +67,38 @@
     selectedRows.value = getMultipleSelectedRows();
   }
 
+  /** 涓婃鍗曞嚮琛岀棔杩癸紝鐢ㄤ簬鍚炴帀鍙屽嚮瑙﹀彂鐨勭浜屾 click锛岄伩鍏�"閫変簡鍙堝彇娑�" */
+  let lastClick = { rowId: undefined as number | undefined, time: 0 };
+
+  /** 鍗曞嚮琛岋細澶氶�夌洿鎺ュ垏鎹㈠嬀閫夛紱鍗曢�夌敱 radioConfig.trigger='row' 澶勭悊 */
+  async function handleCellClick({
+    row,
+    column,
+  }: {
+    row: MesMdItemApi.Item;
+    column?: { type?: string };
+  }) {
+    // 鐐瑰嚮鍕鹃�夊垪鑷韩鐢� checkbox 鎺т欢澶勭悊锛岄伩鍏嶄簩娆″垏鎹�
+    if (column?.type === "checkbox") {
+      return;
+    }
+    if (!multiple.value) {
+      return;
+    }
+    const rowId = row.id;
+    const now = Date.now();
+    if (lastClick.rowId === rowId && now - lastClick.time < 300) {
+      lastClick.time = 0;
+      return;
+    }
+    lastClick = { rowId, time: now };
+    await toggleMultipleRow(row);
+  }
+
   /** 澶勭悊琛屽弻鍑� */
   async function handleCellDblclick({ row }: { row: MesMdItemApi.Item }) {
     if (multiple.value) {
-      await toggleMultipleRow(row);
-      return;
+      return; // 澶氶�夌敱鍗曞嚮 cellClick 鍒囨崲锛屽弻鍑讳笉鍐嶉噸澶嶅鐞�
     }
     selectedRows.value = [row];
     await gridApi.grid.setRadioRow(row);
@@ -84,18 +111,20 @@
     gridApi.query();
   }
 
-  /** 鍥炴樉棰勯�夌墿鏂� */
+  /** 鍥炴樉棰勯�夌墿鏂欙細褰撳墠椤电洿鎺ュ嬀閫夛紝璺ㄩ〉棰勯�夋敞鍐屼负 reserve锛岀炕椤靛悗鑷姩鍥炴樉 */
   async function applyPreSelection() {
     if (preSelectedIds.value.length === 0) {
       return;
     }
     const rows = getTableRows();
+    const currentIds = new Set(
+      rows.filter((row) => row.id != null).map((row) => row.id),
+    );
     for (const row of rows) {
-      if (
-        row.id === undefined ||
-        row.id === null ||
-        !preSelectedIds.value.includes(row.id)
-      ) {
+      if (row.id === undefined || row.id === null) {
+        continue;
+      }
+      if (!preSelectedIds.value.includes(row.id)) {
         continue;
       }
       if (multiple.value) {
@@ -107,6 +136,21 @@
       }
     }
     if (multiple.value) {
+      // 鎷夊彇涓嶅湪褰撳墠椤电殑棰勯�夌墿鏂欏畬鏁存暟鎹紝娉ㄥ唽涓� checkbox reserve锛堢炕椤靛悗鑷姩鍕鹃�夛級
+      const missingIds = preSelectedIds.value.filter(
+        (id) => !currentIds.has(id),
+      );
+      if (missingIds.length > 0) {
+        const result = await getItemPage({
+          pageNo: 1,
+          pageSize: 200,
+          ids: missingIds.join(","),
+        } as never);
+        const fullRows = result?.list as MesMdItemApi.Item[] | undefined;
+        if (fullRows && fullRows.length > 0) {
+          await gridApi.grid.setCheckboxReserve(fullRows, true);
+        }
+      }
       selectedRows.value = getMultipleSelectedRows();
     }
   }
@@ -159,6 +203,7 @@
       },
     } as VxeTableGridOptions<MesMdItemApi.Item>,
     gridEvents: {
+      cellClick: handleCellClick,
       cellDblclick: handleCellDblclick,
       checkboxAll: handleCheckboxSelectChange,
       checkboxChange: handleCheckboxSelectChange,
diff --git a/src/views/mes/md/item/modules/form.vue b/src/views/mes/md/item/modules/form.vue
index 5227e24..1639098 100644
--- a/src/views/mes/md/item/modules/form.vue
+++ b/src/views/mes/md/item/modules/form.vue
@@ -129,9 +129,15 @@
       class="mx-4 mt-4"
     >
       <Tabs.TabPane key="bom" tab="BOM 缁勬垚">
+        <p class="mb-3 text-xs text-gray-400">
+          姝ゅ缁存姢鐨勬槸浜у搧鑷韩鐨勭墿鏂欐瀯鎴愶紙閰嶆柟锛夛紝骞堕潪宸ヨ壓璺嚎鐨勬姇鏂� BOM锛涘湪銆屽伐鑹烘祦绋嬨�嶅伐浣滃彴閰嶇疆銆屾姇鍏ョ墿鏂欍�嶆椂锛屼細浠庤 BOM 涓�夋嫨鐗╂枡銆�
+        </p>
         <ProductBomForm :form-type="formType" :item-id="formData.id" />
       </Tabs.TabPane>
       <Tabs.TabPane v-if="formData.batchFlag" key="batch" tab="鎵规灞炴��">
+        <p class="mb-3 text-xs text-gray-400">
+          姝ゅ閰嶇疆姣忎釜鎵规闇�瑕佺櫥璁扮殑灞炴�х淮搴︼紝鍕鹃�夊悗鍏ュ簱銆佹姤宸ャ�佽川妫�绛夌幆鑺傚皢鎸夋墍閫夌淮搴︾敓鎴愭壒娆′俊鎭�傜墿鏂欎笌浜у搧鐨勫彲閫夊睘鎬т笉鍚岋細鐗╂枡渚ч噸渚涘簲鍟嗐�佹湁鏁堟湡绛夛紝浜у搧渚ч噸璁㈠崟銆佸伐搴忋�佸伐浣滅珯绛夈��
+        </p>
         <ItemBatchConfigForm
           :form-type="formType"
           :item-id="formData.id"
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index cf8cdd4..de3d920 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -127,6 +127,10 @@
           }
         },
       },
+      help: (values: { orderSourceType?: number }) =>
+        values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER
+          ? '宸查�夊鎴疯鍗曪細闇�琛ュ厖鏉ユ簮鍗曟嵁缂栧彿涓庡鎴凤紝渚夸簬杩芥函璁㈠崟鏉ユ簮銆�'
+          : '鐢ㄤ簬鍖哄垎璁㈠崟涓氬姟褰掑睘锛涢�夈�屽鎴疯鍗曘�嶆椂鎵嶉渶鍏宠仈鏉ユ簮鍗曟嵁涓庡鎴枫��',
       rules: 'selectRequired',
     },
     {
@@ -142,6 +146,7 @@
         show: (values) =>
           values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER,
       },
+      help: '濉啓涓婃父鍗曟嵁锛堝閿�鍞鍗曪級鐨勭紪鍙凤紝鐢ㄤ簬杩芥函璇ヨ鍗曠殑鏉ユ簮銆�',
     },
     {
       fieldName: 'type',
@@ -161,6 +166,11 @@
           }
         },
       },
+      help: (values: { type?: number }) =>
+        values.type === MesProWorkOrderTypeEnum.OUTSOURCE ||
+        values.type === MesProWorkOrderTypeEnum.PURCHASE
+          ? '宸查�夊澶�/閲囪喘鐢熶骇锛氶渶鎸囧畾渚涘簲鍟嗭紝濮斿鎶ュ伐涓庡叆搴撳皢鎸夎渚涘簲鍟嗙粨绠椼��'
+          : '鑷惀鐢熶骇鎸夋湰鍗曞伐搴忔帓浜э紱閫夈�屽澶栧姞宸ャ�嶆垨銆岄噰璐敓浜с�嶆椂闇�棰濆鎸囧畾渚涘簲鍟嗐��',
       rules: 'selectRequired',
     },
     {
@@ -171,6 +181,7 @@
         disabled: headerReadonly,
         placeholder: '璇烽�夋嫨浜у搧',
       },
+      help: '閫夋嫨浜у搧鍚庯紝宸ュ簭灏嗚嚜鍔ㄤ粠鍏跺伐鑹鸿矾绾垮甫鍑哄苟鎸夐渶璋冩暣锛涗骇鍝侀渶鍏堝湪銆屽伐鑹烘祦绋嬨�嶄腑閰嶇疆璺嚎涓庢姇鏂� BOM銆�',
       rules: 'selectRequired',
     },
     {
@@ -199,6 +210,7 @@
         show: (values) =>
           values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER,
       },
+      help: '鍏宠仈瀹㈡埛鍚庯紝璁㈠崟鍙湪瀹㈡埛缁村害杩芥函骞跺鎺ュ悗缁粨绠椼��',
     },
     {
       fieldName: 'vendorId',
@@ -214,6 +226,7 @@
           values.type === MesProWorkOrderTypeEnum.OUTSOURCE ||
           values.type === MesProWorkOrderTypeEnum.PURCHASE,
       },
+      help: '濮斿/閲囪喘璁㈠崟闇�鎸囧畾渚涘簲鍟嗭紝濮斿鎶ュ伐涓庡叆搴撳潎浠ヨ渚涘簲鍟嗕负鍑嗐��',
     },
     {
       fieldName: 'batchCode',
@@ -235,6 +248,7 @@
         placeholder: '璇烽�夋嫨闇�姹傛棩鏈�',
         valueFormat: 'YYYY-MM-DD',
       },
+      help: '棰勮浜や粯/瀹屾垚鏃ユ湡锛岀敤浜庢帓浜с�佽繘搴﹁窡韪笌浜ゆ湡棰勮銆�',
       rules: 'required',
     },
     // {
@@ -262,6 +276,7 @@
         disabled: headerReadonly,
       },
       defaultValue: false,
+      help: '寮�鍚悗锛岀敓浜ф姤宸ユ彁浜ら渶缁忓鎵归�氳繃鎵嶈鍏ヤ骇閲忥紱鍏抽棴鍒欐姤宸ョ洿鎺ョ敓鏁堛��',
       rules: 'required',
     },
     {
@@ -285,6 +300,7 @@
           return undefined;
         },
       },
+      help: '閫夋嫨鎶ュ伐鎻愪氦鍚庤蛋鐨勫鎵规祦绋嬶紱娴佺▼闇�鍏堝湪绯荤粺銆屾祦绋嬬鐞嗐�嶄腑閮ㄧ讲锛屼粎銆屾姤宸ュ鎵广�嶇被娴佺▼鍙�夈��',
     },
 
     {
diff --git a/src/views/mes/pro/workorder/modules/form.vue b/src/views/mes/pro/workorder/modules/form.vue
index 5cca3a7..36a346e 100644
--- a/src/views/mes/pro/workorder/modules/form.vue
+++ b/src/views/mes/pro/workorder/modules/form.vue
@@ -353,6 +353,9 @@
         <Tabs.TabPane key="process"
                       tab="宸ュ簭"
                       forceRender>
+          <p class="mb-3 text-xs text-gray-400 leading-relaxed">
+            鏂板妯″紡浼氳嚜鍔ㄤ粠鎵�閫変骇鍝佺殑宸ヨ壓璺嚎甯﹀嚭宸ュ簭锛岀偣鍑昏棣栨墜鏌勫彲鎷栨嫿璋冩暣椤哄簭锛涖�屽叧閿伐搴忋�嶉渶閲嶇偣绠℃帶銆併�岃川妫�宸ュ簭銆嶇敱璐ㄦ浜哄憳妫�楠屻�佸嬀閫夈�屽�掑啿銆嶇殑宸ュ簭鍦ㄦ姤宸ュ悗鎸夋姇鏂欑敤閲忚嚜鍔ㄦ墸鍑忕墿鏂欏簱瀛樸�傛瘡閬撳伐搴忓彲鐐广�屾柊澧炵墿鏂欍�嶉厤缃姇鏂欐槑缁嗐��
+          </p>
           <ProcessList
             ref="processListRef"
             :form-type="formType"
@@ -364,6 +367,9 @@
         </Tabs.TabPane>
         <Tabs.TabPane key="item"
                       tab="鐗╂枡闇�姹�">
+          <p class="mb-3 text-xs text-gray-400 leading-relaxed">
+            鐗╂枡闇�姹傛寜鍚勫伐搴忋�屾姇鏂欐槑缁嗐�嵜楄鍗曟暟閲忚嚜鍔ㄦ眹鎬荤敓鎴愶紝浠呬緵鏌ョ湅锛涘闇�璋冩暣锛岃鍥炲埌銆屽伐搴忋�嶉〉绛剧淮鎶ゆ姇鏂欍��
+          </p>
           <ItemList v-if="formData?.id"
                     :work-order-id="formData.id" />
           <div v-else class="text-gray-400 text-center py-8">
@@ -374,11 +380,15 @@
     </template>
     <template #prepend-footer>
       <div class="flex flex-auto flex-col gap-2">
-        <div v-if="canUseAi" class="flex items-center gap-2">
-          <PredictMaterial :work-order-id="formData!.id" />
-          <PredictDuration :work-order-id="formData!.id" />
-          <PredictRisk :work-order-id="formData!.id" />
-          <PredictDelivery :work-order-id="formData!.id" />
+        <div v-if="canUseAi" class="flex flex-wrap items-center gap-x-2 gap-y-1">
+          <span class="text-xs font-semibold text-gray-800 dark:text-gray-100">AI 鏅鸿兘鍒嗘瀽</span>
+          <div class="flex items-center gap-2">
+            <PredictMaterial :work-order-id="formData!.id" />
+            <PredictDuration :work-order-id="formData!.id" />
+            <PredictRisk :work-order-id="formData!.id" />
+            <PredictDelivery :work-order-id="formData!.id" />
+          </div>
+          <span class="text-xs text-gray-400">鍩轰簬鍘嗗彶鐢熶骇鏁版嵁浼扮畻锛屼粎渚涘弬鑰冿紝涓嶅弬涓庝笟鍔¤绠�</span>
         </div>
         <div v-if="canUseAi && showMainBtn" class="border-t border-gray-200" />
         <div class="flex items-center">
diff --git a/src/views/mes/pro/workorder/modules/process-list.vue b/src/views/mes/pro/workorder/modules/process-list.vue
index 283424d..687633a 100644
--- a/src/views/mes/pro/workorder/modules/process-list.vue
+++ b/src/views/mes/pro/workorder/modules/process-list.vue
@@ -346,6 +346,9 @@
       width="500px"
       @ok="handleQuantityConfirm"
     >
+      <p class="mb-3 text-xs text-gray-400 leading-relaxed">
+        鐢ㄦ枡姣斾緥 = 鐢熶骇 1 浠朵骇鍝佹秷鑰楄鐗╂枡鐨勬暟閲忥紱銆屽�掑啿銆嶅伐搴忓皢鍦ㄦ姤宸ュ悗鎹鑷姩鎵e噺搴撳瓨銆�
+      </p>
       <div class="space-y-3 py-2">
         <div
           v-for="(item, idx) in pendingItems"
diff --git a/src/views/mes/process-design/route/data.ts b/src/views/mes/process-design/route/data.ts
index 7990b1e..2210b59 100644
--- a/src/views/mes/process-design/route/data.ts
+++ b/src/views/mes/process-design/route/data.ts
@@ -410,8 +410,11 @@
   ];
 }
 
-/** 宸ヨ壓璺嚎浜у搧琛ㄥ崟 */
-export function useRouteProductFormSchema(onItemChange?: (item: any) => void): VbenFormSchema[] {
+/** 宸ヨ壓璺嚎浜у搧琛ㄥ崟锛沵ultiple = true锛堟柊澧炴ā寮忥級鏃朵骇鍝佸彲澶氶�夛紝涓�娆℃�х粦瀹氬涓� */
+export function useRouteProductFormSchema(
+  onItemChange?: (item: any) => void,
+  multiple = false,
+): VbenFormSchema[] {
   return [
     {
       fieldName: "id",
@@ -423,17 +426,29 @@
       component: "Input",
       dependencies: { triggerFields: [""], show: () => false },
     },
-    // 浜у搧鐗╂枡锛氫娇鐢ㄤ笟鍔¤嚜瀹氫箟閫夋嫨鍣紝change 鍥炲~缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅
-    {
-      fieldName: "itemId",
-      label: "浜у搧",
-      component: markRaw(MdItemSelect),
-      componentProps: {
-        onChange: onItemChange,
-      },
-      formItemClass: "col-span-2",
-      rules: "selectRequired",
-    },
+    // 鏂板锛堝閫夛級锛氫竴娆$粦瀹氬涓骇鍝侊紱缂栬緫锛堝崟閫夛級锛歝hange 鍥炲~缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅
+    multiple
+      ? {
+          fieldName: "itemIds",
+          label: "浜у搧锛堝彲澶氶�夛級",
+          component: markRaw(MdItemSelect),
+          componentProps: {
+            multiple: true,
+            placeholder: "璇烽�夋嫨瑕佺粦瀹氱殑浜у搧锛堝彲澶氶�夛級",
+          },
+          formItemClass: "col-span-2",
+          rules: "selectRequired",
+        }
+      : {
+          fieldName: "itemId",
+          label: "浜у搧",
+          component: markRaw(MdItemSelect),
+          componentProps: {
+            onChange: onItemChange,
+          },
+          formItemClass: "col-span-2",
+          rules: "selectRequired",
+        },
     {
       fieldName: "quantity",
       label: "鐢熶骇鏁伴噺",
diff --git a/src/views/mes/process-design/route/modules/form.vue b/src/views/mes/process-design/route/modules/form.vue
index 8c0c230..cf73e0e 100644
--- a/src/views/mes/process-design/route/modules/form.vue
+++ b/src/views/mes/process-design/route/modules/form.vue
@@ -2,24 +2,28 @@
 import type { FormType } from '../data';
 
 import type { MesProRouteApi } from '#/api/mes/pro/route';
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
 
 import { computed, ref } from 'vue';
 
-import { useVbenModal } from '@vben/common-ui';
+import { confirm, useVbenModal } from '@vben/common-ui';
 
-import { message, TabPane, Tabs } from 'ant-design-vue';
+import { Button, Divider, message, Select } from 'ant-design-vue';
 
 import { useVbenForm } from '#/adapter/form';
 import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route';
+import {
+  deleteRouteProduct,
+  getRouteProductListByRoute,
+} from '#/api/mes/pro/route/product';
 import { $t } from '#/locales';
 
 import { useFormSchema } from '../data';
-import ProcessList from './process-list.vue';
-import ProductList from './product-list.vue';
+import ProductForm from './product-form.vue';
+import Workbench from './workbench.vue';
 
 const emit = defineEmits(['success']);
 const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
-const subTab = ref('process'); // 褰撳墠婵�娲荤殑瀛愯〃 Tab
 const formData = ref<MesProRouteApi.Route>();
 
 const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
@@ -32,6 +36,72 @@
     : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎']);
 });
 
+// ==================== 浜у搧缁戝畾 ====================
+const products = ref<MesProRouteProductApi.RouteProduct[]>([]); // 鍏宠仈浜у搧鍒楄〃
+const selectedProductItemId = ref<number>(); // 褰撳墠閫変腑鐨勪骇鍝佺墿鏂欑紪鍙�
+const selectedProduct = computed(() =>
+  products.value.find((item) => item.itemId === selectedProductItemId.value),
+);
+const productOptions = computed(() =>
+  products.value.map((item) => ({ label: item.itemName, value: item.itemId })),
+);
+
+async function loadProducts() {
+  const routeId = formData.value?.id;
+  if (!routeId) {
+    products.value = [];
+    return;
+  }
+  products.value = (await getRouteProductListByRoute(routeId)) || [];
+  if (
+    !products.value.some((item) => item.itemId === selectedProductItemId.value)
+  ) {
+    selectedProductItemId.value = products.value[0]?.itemId;
+  }
+}
+
+const [ProductFormModal, productFormModalApi] = useVbenModal({
+  connectedComponent: ProductForm,
+  destroyOnClose: true,
+});
+
+/** 鎵撳紑浜у搧寮圭獥锛堢粦瀹� / 缂栬緫锛� */
+function openProductForm(row?: MesProRouteProductApi.RouteProduct) {
+  if (!formData.value?.id) {
+    return;
+  }
+  productFormModalApi
+    .setData(
+      row
+        ? { id: row.id, routeId: formData.value.id }
+        : {
+            routeId: formData.value.id,
+            boundItemIds: products.value
+              .map((item) => item.itemId)
+              .filter((id): id is number => id != null),
+          },
+    )
+    .open();
+}
+
+/** 绉婚櫎浜у搧锛堜細绾ц仈鍒犻櫎璇ヤ骇鍝佺殑鎶曞叆鐗╂枡閰嶇疆锛� */
+async function handleRemoveProduct() {
+  if (!selectedProduct.value?.id) {
+    return;
+  }
+  try {
+    await confirm(
+      `纭灏嗕骇鍝併��${selectedProduct.value.itemName}銆嶄粠璇ュ伐鑹鸿矾绾跨Щ闄わ紵鍏跺叏閮ㄦ姇鍏ョ墿鏂欓厤缃皢涓�骞跺垹闄ゃ�俙,
+    );
+  } catch {
+    return;
+  }
+  await deleteRouteProduct(selectedProduct.value.id);
+  message.success($t('ui.actionMessage.operationSuccess'));
+  await loadProducts();
+}
+
+// ==================== 鍩烘湰淇℃伅琛ㄥ崟 ====================
 const [Form, formApi] = useVbenForm({
   commonConfig: {
     componentProps: { class: 'w-full' },
@@ -59,11 +129,12 @@
     const data = (await formApi.getValues()) as MesProRouteApi.Route;
     try {
       if (formType.value === 'create') {
-        // 鏂板鎴愬姛鍚庡垏鍒扮紪杈戞ā寮忥紝鏂逛究缁х画缁存姢缁勬垚宸ュ簭銆佸叧鑱斾骇鍝�
+        // 鏂板鎴愬姛鍚庡垏鍒扮紪杈戞ā寮忥紝杩涘叆銆屼骇鍝佷笌宸ュ簭閰嶇疆銆嶅伐浣滃彴
         const id = await createRoute(data);
         formData.value = { ...data, id };
         await formApi.setFieldValue('id', id);
         formType.value = 'update';
+        await loadProducts();
       } else {
         await updateRoute(data);
         formData.value = { ...formData.value, ...data };
@@ -82,10 +153,11 @@
   async onOpenChange(isOpen: boolean) {
     if (!isOpen) {
       formData.value = undefined;
+      products.value = [];
+      selectedProductItemId.value = undefined;
       return;
     }
     formApi.setState({ schema: useFormSchema(formApi) });
-    subTab.value = 'process';
     // 鍔犺浇鏁版嵁
     const data = modalApi.getData<{ formType: FormType; id?: number }>();
     formType.value = data.formType;
@@ -99,6 +171,7 @@
       formData.value = await getRoute(data.id);
       // 璁剧疆鍒� values
       await formApi.setValues(formData.value);
+      await loadProducts();
     } finally {
       modalApi.unlock();
     }
@@ -107,20 +180,49 @@
 </script>
 
 <template>
-  <Modal :title="getTitle" class="w-4/5">
+  <Modal :title="getTitle" class="w-11/12">
     <Form class="mx-4" />
-    <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀哄瓙琛� Tab锛屾柊澧炴ā寮忎笅闅愯棌 -->
-    <Tabs
-      v-if="formType !== 'create' && formData?.id"
-      v-model:active-key="subTab"
-      class="mx-4 mt-4"
-    >
-      <TabPane key="process" tab="缁勬垚宸ュ簭">
-        <ProcessList :form-type="formType" :route-id="formData.id" />
-      </TabPane>
-      <TabPane key="product" tab="鍏宠仈浜у搧">
-        <ProductList :form-type="formType" :route-id="formData.id" />
-      </TabPane>
-    </Tabs>
+    <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀恒�屼骇鍝佷笌宸ュ簭閰嶇疆銆嶅伐浣滃彴锛屾柊澧炴ā寮忎笅闅愯棌 -->
+    <template v-if="formData?.id">
+      <div class="mx-4 mt-4">
+        <Divider class="!my-2" orientation="left">
+          <span class="text-sm">浜у搧涓庡伐搴忛厤缃�</span>
+        </Divider>
+        <div class="flex flex-wrap items-center gap-3">
+          <span class="shrink-0 text-sm font-medium">鐢熶骇浜у搧</span>
+          <Select
+            v-model:value="selectedProductItemId"
+            class="!w-64"
+            allow-clear
+            :disabled="isDetail"
+            placeholder="璇烽�夋嫨鎴栫粦瀹氫骇鍝�"
+            :options="productOptions"
+          />
+          <template v-if="!isDetail">
+            <Button type="primary" @click="openProductForm()"> 缁戝畾浜у搧 </Button>
+            <Button
+              :disabled="!selectedProduct"
+              @click="openProductForm(selectedProduct)"
+            >
+              缂栬緫浜у搧
+            </Button>
+            <Button danger :disabled="!selectedProduct" @click="handleRemoveProduct">
+              绉婚櫎浜у搧
+            </Button>
+          </template>
+        </div>
+        <p v-if="products.length === 0" class="mt-2 text-xs text-gray-400">
+          鍏堢粦瀹氳鐢熶骇鐨勭墿鏂欙紝鍐嶄负姣忛亾宸ュ簭閰嶇疆銆屾姇鍏� + 浜у嚭銆�
+        </p>
+        <div class="mt-3">
+          <Workbench
+            :form-type="formType"
+            :product="selectedProduct"
+            :route-id="formData.id"
+          />
+        </div>
+      </div>
+    </template>
   </Modal>
-</template>
+  <ProductFormModal @success="loadProducts" />
+</template>
\ No newline at end of file
diff --git a/src/views/mes/process-design/route/modules/product-form.vue b/src/views/mes/process-design/route/modules/product-form.vue
index a3b0e5d..e20baa6 100644
--- a/src/views/mes/process-design/route/modules/product-form.vue
+++ b/src/views/mes/process-design/route/modules/product-form.vue
@@ -5,7 +5,7 @@
 
 import { useVbenModal } from '@vben/common-ui';
 
-import { Divider, message } from 'ant-design-vue';
+import { message } from 'ant-design-vue';
 
 import { useVbenForm } from '#/adapter/form';
 import {
@@ -16,17 +16,17 @@
 import { $t } from '#/locales';
 
 import { useRouteProductFormSchema } from '../data';
-import ProductBomList from './product-bom-list.vue';
 
 const emit = defineEmits(['success']);
 const formData = ref<MesProRouteProductApi.RouteProduct>();
+const boundItemIds = ref<number[]>([]); // 宸茬粦瀹氭湰璺嚎鐨勪骇鍝佺墿鏂欑紪鍙凤紙鏂板澶氶�夋椂璺宠繃锛�
 const getTitle = computed(() =>
   formData.value?.id
     ? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎浜у搧'])
     : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎浜у搧']),
 );
 
-/** MdItemSelect change 鍥炶皟锛氭妸鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅鍥炲~鍒� form */
+/** MdItemSelect 鍗曢�� change 鍥炶皟锛氭妸鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅鍥炲~鍒� form */
 async function handleItemChange(item?: any) {
   if (!item) {
     return;
@@ -58,22 +58,62 @@
       return;
     }
     modalApi.lock();
-    // 鎻愪氦琛ㄥ崟
-    const data =
-      (await formApi.getValues()) as MesProRouteProductApi.RouteProduct;
+    const data = (await formApi.getValues()) as MesProRouteProductApi.RouteProduct & {
+      itemIds?: number[];
+    };
     try {
+      // 缂栬緫妯″紡锛氭洿鏂板崟涓骇鍝�
       if (formData.value?.id) {
         await updateRouteProduct(data);
-        // 鐢ㄦ渶鏂拌〃鍗曞�煎悓姝� formData锛岀‘淇濅骇鍝� BOM 瀛愯〃缁戝畾鐨� itemId 涓庤〃鍗曚竴鑷�
-        formData.value = { ...formData.value, ...data };
-      } else {
-        // 鏂板鎴愬姛鍚庡垏鎹㈠埌缂栬緫妯″紡锛屾柟渚跨户缁淮鎶や骇鍝� BOM
-        const id = await createRouteProduct(data);
-        formData.value = { ...data, id };
-        await formApi.setFieldValue('id', id);
+        await modalApi.close();
+        emit('success');
+        message.success($t('ui.actionMessage.operationSuccess'));
+        return;
       }
-      emit('success');
-      message.success($t('ui.actionMessage.operationSuccess'));
+      // 鏂板妯″紡锛氬閫変骇鍝佷竴娆℃�х粦瀹氾紝璺宠繃宸插湪璺嚎涓殑浜у搧
+      const itemIds = Array.isArray(data.itemIds) ? data.itemIds : [];
+      if (itemIds.length === 0) {
+        message.warning('璇疯嚦灏戦�夋嫨涓�涓骇鍝�');
+        return;
+      }
+      let created = 0;
+      let skipped = 0;
+      const failedIds: number[] = [];
+      for (const itemId of itemIds) {
+        if (itemId == null) {
+          continue;
+        }
+        if (boundItemIds.value.includes(itemId)) {
+          skipped++;
+          continue;
+        }
+        try {
+          await createRouteProduct({
+            routeId: data.routeId,
+            itemId,
+            quantity: data.quantity,
+            productionTime: data.productionTime,
+            timeUnitType: data.timeUnitType,
+            remark: data.remark,
+          });
+          created++;
+        } catch {
+          failedIds.push(itemId);
+        }
+      }
+      if (skipped > 0) {
+        message.warning(`宸茶烦杩� ${skipped} 涓凡缁戝畾鐨勪骇鍝乣);
+      }
+      if (failedIds.length > 0) {
+        message.error(
+          `鏈� ${failedIds.length} 涓骇鍝佺粦瀹氬け璐ワ紙鍙兘宸茶鍏跺畠宸ヨ壓璺嚎鍗犵敤锛塦,
+        );
+      }
+      if (created > 0) {
+        message.success(`鎴愬姛缁戝畾 ${created} 涓骇鍝乣);
+        await modalApi.close();
+        emit('success');
+      }
     } finally {
       modalApi.unlock();
     }
@@ -81,24 +121,42 @@
   async onOpenChange(isOpen: boolean) {
     if (!isOpen) {
       formData.value = undefined;
+      boundItemIds.value = [];
       return;
     }
     // 鍔犺浇鏁版嵁
     const data = modalApi.getData<{
       id?: number;
       routeId: number;
+      boundItemIds?: number[];
     }>();
     if (!data) {
       return;
     }
+    boundItemIds.value = data.boundItemIds || [];
     if (!data.id) {
-      await formApi.setValues({ routeId: data.routeId });
+      // 鏂板妯″紡锛氬閫変骇鍝佺粦瀹�
+      formData.value = undefined;
+      await formApi.setState({
+        schema: useRouteProductFormSchema(handleItemChange, true),
+      });
+      await formApi.setValues({
+        routeId: data.routeId,
+        itemIds: [],
+        quantity: 1,
+        productionTime: 1,
+        timeUnitType: 'MINUTE',
+        remark: undefined,
+      });
       return;
     }
+    // 缂栬緫妯″紡锛氬姞杞藉崟涓骇鍝佸洖濉�
     modalApi.lock();
     try {
       formData.value = await getRouteProduct(data.id);
-      // 璁剧疆鍒� values
+      await formApi.setState({
+        schema: useRouteProductFormSchema(handleItemChange, false),
+      });
       await formApi.setValues(formData.value);
     } finally {
       modalApi.unlock();
@@ -108,18 +166,7 @@
 </script>
 
 <template>
-  <Modal :title="getTitle" class="w-3/5">
+  <Modal :title="getTitle" class="w-2/5">
     <Form class="mx-4" />
-    <!-- 缂栬緫妯″紡涓嬪睍绀轰骇鍝� BOM 瀛愯〃锛屾柊澧炴ā寮忎笅闅愯棌 -->
-    <template v-if="formData?.id && formData?.itemId">
-      <Divider class="!my-3" orientation="left">浜у搧 BOM 閰嶇疆</Divider>
-      <div class="mx-4">
-        <ProductBomList
-          :route-id="formData.routeId!"
-          :product-id="formData.itemId"
-          :product-name="formData.itemName"
-        />
-      </div>
-    </template>
   </Modal>
-</template>
+</template>
\ No newline at end of file
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