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