refactor(mes): 优化工艺路线配置与工单表单功能
- 移除安灯呼叫相关的统计计数和快速操作入口
- 在工单表单中为多个字段添加帮助提示信息
- 优化工艺路线产品绑定表单,支持多选批量绑定产品
- 更新工艺路线配置界面,采用工作台模式替代原有的标签页
- 为产品BOM选择组件添加多选功能支持
- 在工单工序配置中添加物料需求说明信息
- 优化AI智能分析区域的布局显示效果
| | |
| | | </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" |
| | | /> |
| | |
| | | defineOptions({ name: 'DashboardAlertPanel' }); |
| | | |
| | | const props = defineProps<{ |
| | | andonActiveCount: number; |
| | | repairActiveCount: number; |
| | | }>(); |
| | | |
| | |
| | | <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')" |
| | |
| | | { 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' }, |
| | |
| | | <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" |
| | |
| | | |
| | | 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, |
| | |
| | | }, |
| | | } 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 ç©æéæ©å¼¹çªï¼selectedBomItemIds åéä¼ å个ç¼å·ï¼å¤éä¼ ç¼å·æ°ç»ï¼ */ |
| | | 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); |
| | | } |
| | |
| | | /** å
³é 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; |
| | | } |
| | | |
| | |
| | | allowClear?: boolean; |
| | | disabled?: boolean; |
| | | itemId?: number; |
| | | modelValue?: number; |
| | | modelValue?: number | number[]; |
| | | multiple?: boolean; |
| | | placeholder?: string; |
| | | }>(), |
| | | { |
| | |
| | | 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 è§£æåº 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 }, |
| | | ); |
| | |
| | | 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 ç©æéæ©å¼¹çª */ |
| | |
| | | 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> |
| | | |
| | |
| | | @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> |
| | |
| | | </Input> |
| | | </Tooltip> |
| | | </div> |
| | | <MdProductBomSelectDialog ref="dialogRef" @selected="handleSelected" /> |
| | | <MdProductBomSelectDialog |
| | | ref="dialogRef" |
| | | :multiple="multiple" |
| | | @selected="handleSelected" |
| | | /> |
| | | </template> |
| | |
| | | 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); |
| | |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | }, |
| | | } as VxeTableGridOptions<MesMdItemApi.Item>, |
| | | gridEvents: { |
| | | cellClick: handleCellClick, |
| | | cellDblclick: handleCellDblclick, |
| | | checkboxAll: handleCheckboxSelectChange, |
| | | checkboxChange: handleCheckboxSelectChange, |
| | |
| | | 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" |
| | |
| | | } |
| | | }, |
| | | }, |
| | | help: (values: { orderSourceType?: number }) => |
| | | values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER |
| | | ? 'å·²é客æ·è®¢åï¼éè¡¥å
æ¥æºåæ®ç¼å·ä¸å®¢æ·ï¼ä¾¿äºè¿½æº¯è®¢åæ¥æºã' |
| | | : 'ç¨äºåºå订åä¸å¡å½å±ï¼éã客æ·è®¢åãæ¶æéå
³èæ¥æºåæ®ä¸å®¢æ·ã', |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | |
| | | show: (values) => |
| | | values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER, |
| | | }, |
| | | help: 'å¡«å䏿¸¸åæ®ï¼å¦éå®è®¢åï¼çç¼å·ï¼ç¨äºè¿½æº¯è¯¥è®¢åçæ¥æºã', |
| | | }, |
| | | { |
| | | fieldName: 'type', |
| | |
| | | } |
| | | }, |
| | | }, |
| | | help: (values: { type?: number }) => |
| | | values.type === MesProWorkOrderTypeEnum.OUTSOURCE || |
| | | values.type === MesProWorkOrderTypeEnum.PURCHASE |
| | | ? 'å·²éå§å¤/éè´ç产ï¼éæå®ä¾åºåï¼å§å¤æ¥å·¥ä¸å
¥åºå°æè¯¥ä¾åºåç»ç®ã' |
| | | : 'èªè¥çäº§ææ¬åå·¥åºæäº§ï¼éãå§å¤å å·¥ãæãéè´çäº§ãæ¶éé¢å¤æå®ä¾åºåã', |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | |
| | | disabled: headerReadonly, |
| | | placeholder: 'è¯·éæ©äº§å', |
| | | }, |
| | | help: 'éæ©äº§ååï¼å·¥åºå°èªå¨ä»å
¶å·¥èºè·¯çº¿å¸¦åºå¹¶æéè°æ´ï¼äº§åéå
å¨ãå·¥èºæµç¨ãä¸é
ç½®è·¯çº¿ä¸ææ BOMã', |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | |
| | | show: (values) => |
| | | values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER, |
| | | }, |
| | | help: 'å
³è客æ·åï¼è®¢åå¯å¨å®¢æ·ç»´åº¦è¿½æº¯å¹¶å¯¹æ¥åç»ç»ç®ã', |
| | | }, |
| | | { |
| | | fieldName: 'vendorId', |
| | |
| | | values.type === MesProWorkOrderTypeEnum.OUTSOURCE || |
| | | values.type === MesProWorkOrderTypeEnum.PURCHASE, |
| | | }, |
| | | help: 'å§å¤/éè´è®¢åéæå®ä¾åºåï¼å§å¤æ¥å·¥ä¸å
¥åºå以该ä¾åºå为åã', |
| | | }, |
| | | { |
| | | fieldName: 'batchCode', |
| | |
| | | placeholder: 'è¯·éæ©éæ±æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | help: 'é¢è®¡äº¤ä»/å®ææ¥æï¼ç¨äºæäº§ãè¿åº¦è·è¸ªä¸äº¤æé¢è¦ã', |
| | | rules: 'required', |
| | | }, |
| | | // { |
| | |
| | | disabled: headerReadonly, |
| | | }, |
| | | defaultValue: false, |
| | | help: 'å¼å¯åï¼ç产æ¥å·¥æäº¤éç»å®¡æ¹éè¿æè®¡å
¥äº§éï¼å
³éåæ¥å·¥ç´æ¥çæã', |
| | | rules: 'required', |
| | | }, |
| | | { |
| | |
| | | return undefined; |
| | | }, |
| | | }, |
| | | help: 'éæ©æ¥å·¥æäº¤åèµ°çå®¡æ¹æµç¨ï¼æµç¨éå
å¨ç³»ç»ãæµç¨ç®¡çãä¸é¨ç½²ï¼ä»
ãæ¥å·¥å®¡æ¹ãç±»æµç¨å¯éã', |
| | | }, |
| | | |
| | | { |
| | |
| | | <Tabs.TabPane key="process" |
| | | tab="å·¥åº" |
| | | forceRender> |
| | | <p class="mb-3 text-xs text-gray-400 leading-relaxed"> |
| | | æ°å¢æ¨¡å¼ä¼èªå¨ä»æé产åçå·¥èºè·¯çº¿å¸¦åºå·¥åºï¼ç¹å»è¡é¦ææå¯ææ½è°æ´é¡ºåºï¼ãå
³é®å·¥åºãééç¹ç®¡æ§ããè´¨æ£å·¥åºãç±è´¨æ£äººåæ£éªãå¾éãåå²ãçå·¥åºå¨æ¥å·¥åæææç¨éèªå¨æ£åç©æåºåãæ¯éå·¥åºå¯ç¹ãæ°å¢ç©æãé
ç½®æææç»ã |
| | | </p> |
| | | <ProcessList |
| | | ref="processListRef" |
| | | :form-type="formType" |
| | |
| | | </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"> |
| | |
| | | </template> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto flex-col gap-2"> |
| | | <div v-if="canUseAi" class="flex items-center gap-2"> |
| | | <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"> |
| | | <Button v-if="showMainBtn" |
| | |
| | | width="500px" |
| | | @ok="handleQuantityConfirm" |
| | | > |
| | | <p class="mb-3 text-xs text-gray-400 leading-relaxed"> |
| | | ç¨ææ¯ä¾ = ç产 1 ä»¶äº§åæ¶èè¯¥ç©æçæ°éï¼ãåå²ãå·¥åºå°å¨æ¥å·¥åæ®æ¤èªå¨æ£ååºåã |
| | | </p> |
| | | <div class="space-y-3 py-2"> |
| | | <div |
| | | v-for="(item, idx) in pendingItems" |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** å·¥èºè·¯çº¿äº§å表å */ |
| | | export function useRouteProductFormSchema(onItemChange?: (item: any) => void): VbenFormSchema[] { |
| | | /** å·¥èºè·¯çº¿äº§å表åï¼multiple = trueï¼æ°å¢æ¨¡å¼ï¼æ¶äº§åå¯å¤éï¼ä¸æ¬¡æ§ç»å®å¤ä¸ª */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | multiple = false, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: "id", |
| | |
| | | component: "Input", |
| | | dependencies: { triggerFields: [""], show: () => false }, |
| | | }, |
| | | // 产åç©æï¼ä½¿ç¨ä¸å¡èªå®ä¹éæ©å¨ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | { |
| | | // æ°å¢ï¼å¤éï¼ï¼ä¸æ¬¡ç»å®å¤ä¸ªäº§åï¼ç¼è¾ï¼åéï¼ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | multiple |
| | | ? { |
| | | fieldName: "itemIds", |
| | | label: "产åï¼å¯å¤éï¼", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | multiple: true, |
| | | placeholder: "è¯·éæ©è¦ç»å®ç产åï¼å¯å¤éï¼", |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | } |
| | | : { |
| | | fieldName: "itemId", |
| | | label: "产å", |
| | | component: markRaw(MdItemSelect), |
| | |
| | | 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'); // æ¯å¦æ¥çæ¨¡å¼ |
| | |
| | | : $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' }, |
| | |
| | | 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 }; |
| | |
| | | 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; |
| | |
| | | formData.value = await getRoute(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | await loadProducts(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | </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" |
| | | <!-- ç¼è¾/详æ
模å¼ä¸å±ç¤ºã产åä¸å·¥åºé
ç½®ãå·¥ä½å°ï¼æ°å¢æ¨¡å¼ä¸éè --> |
| | | <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)" |
| | | > |
| | | <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> |
| | | ç¼è¾äº§å |
| | | </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> |
| | | <ProductFormModal @success="loadProducts" /> |
| | | </template> |
| | |
| | | |
| | | 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 { |
| | |
| | | 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; |
| | |
| | | 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; |
| | | } |
| | | // æ°å¢æ¨¡å¼ï¼å¤é产å䏿¬¡æ§ç»å®ï¼è·³è¿å·²å¨è·¯çº¿ä¸ç产å |
| | | 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(); |
| | | } |
| | |
| | | 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(); |
| | |
| | | </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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |