feat(mes): 完善工艺路线管理功能
- 新增工艺路线导入导出功能,支持批量导入完整工艺路线
- 实现工序产出物料归属配置,支持多产品通用和产品专属设置
- 添加报工仓库、库区、库位配置,优化库存管理流程
- 集成报工自动扣料功能,提升生产执行效率
- 更新表单验证规则,增强数据录入准确性
- 重构界面布局,采用标签页方式组织工序和产品配置
- 优化API接口设计,完善仓库相关数据查询功能
- 调整字段显示格式,统一日期时间显示规范
- 增加工序收率说明帮助信息,提升用户体验
- 实现出产物料与仓库信息的联动选择功能
| | |
| | | export function exportRoute(params: any) { |
| | | return requestClient.download('/mes/pro/route/export-excel', { params }); |
| | | } |
| | | |
| | | /** ä¸è½½å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importRouteTemplate() { |
| | | return requestClient.download('/mes/pro/route/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | export function importRoute(file: File, updateSupport: boolean) { |
| | | return requestClient.upload( |
| | | `/mes/pro/route/import?updateSupport=${updateSupport}`, |
| | | { file }, |
| | | ); |
| | | } |
| | | |
| | | /** ä¸è½½å
å«è·¯çº¿ã产åãå·¥åºãå·¥åºäº§åºãå·¥åºææäºå¼ å·¥ä½è¡¨çæ¨¡æ¿ */ |
| | | export function importCompleteRouteTemplate() { |
| | | return requestClient.download('/mes/pro/route/get-complete-import-template'); |
| | | } |
| | | |
| | | /** æ¹é导å
¥å®æ´å·¥èºè·¯çº¿ */ |
| | | export function importCompleteRoute(file: File) { |
| | | return requestClient.upload('/mes/pro/route/import-complete', { file }); |
| | | } |
| | |
| | | remark?: string; |
| | | } |
| | | |
| | | /** 产åºç©æå½å± */ |
| | | export interface OutputItem { |
| | | id?: number; |
| | | routeProcessId?: number; |
| | | productId?: number; // å½å±äº§åç¼å·ï¼ä¸ºç©ºè¡¨ç¤ºå¤äº§åéç¨ |
| | | productName?: string; |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | sort?: number; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** MES å·¥èºè·¯çº¿å·¥åº */ |
| | | export interface RouteProcess { |
| | | id?: number; |
| | | routeId?: number; |
| | | processId?: number; |
| | | paramTemplateId?: number; |
| | | /** å·¥åºæ¶çï¼äº§åº/æå
¥ï¼0~1ï¼ */ |
| | | yieldRate?: number; |
| | | processCode?: string; |
| | | processName?: string; |
| | |
| | | outputItemName?: string; |
| | | outputItemIds?: number[]; |
| | | outputItemNames?: string; |
| | | outputs?: OutputItem[]; // 产åºç©æå½å±é
ç½® |
| | | warehouseId?: number; // é»è®¤æ¥å·¥ä»åºç¼å· |
| | | warehouseName?: string; // é»è®¤æ¥å·¥ä»åºåç§° |
| | | locationId?: number; // é»è®¤æ¥å·¥åºåºç¼å· |
| | | locationName?: string; // é»è®¤æ¥å·¥åºåºåç§° |
| | | areaId?: number; // é»è®¤æ¥å·¥åºä½ç¼å· |
| | | areaName?: string; // é»è®¤æ¥å·¥åºä½åç§° |
| | | remark?: string; |
| | | bomItems?: ProcessBomItem[]; |
| | | createTime?: Date; |
| | |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | warehouseId?: number; |
| | | warehouseName?: string; |
| | | locationId?: number; |
| | | locationName?: string; |
| | | areaId?: number; |
| | | areaName?: string; |
| | | specification?: string; |
| | | unitName?: string; |
| | | quantity?: number; |
| | |
| | | import type { MesProWorkOrderProcessApi } from './index'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProWorkOrderProcessApi { |
| | |
| | | outputItemName?: string; // 产åºäº§åç©æåç§°ï¼ç¬¬ä¸ä¸ªï¼ |
| | | outputItemIds?: number[]; // 产åºäº§åç©æç¼å·å表 |
| | | outputItemNames?: string; // 产åºäº§åç©æåç§°ï¼éå·æ¼æ¥ï¼ |
| | | warehouseId?: number; // æ¥å·¥ä»åºç¼å· |
| | | warehouseName?: string; // æ¥å·¥ä»åºåç§° |
| | | locationId?: number; // æ¥å·¥åºåºç¼å· |
| | | locationName?: string; // æ¥å·¥åºåºåç§° |
| | | areaId?: number; // æ¥å·¥åºä½ç¼å· |
| | | areaName?: string; // æ¥å·¥åºä½åç§° |
| | | bomItems?: ProcessBomItem[]; // å·¥åºææBOMæç» |
| | | remark?: string; // 夿³¨ |
| | | } |
| | |
| | | /** å é¤å·¥åå·¥åº */ |
| | | export function deleteWorkOrderProcess(id: number) { |
| | | return requestClient.delete(`/mes/pro/work-order-process/delete?id=${id}`); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | |
| | | import { h, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Modal as AntModal, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm, z } from '#/adapter/form'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | export interface ImportModalProps { |
| | | /** å¼¹çªæ é¢ï¼å¦ã导å
¥ç¨æ·ã */ |
| | | title: string; |
| | | /** ä¸è½½çæ¨¡æ¿æä»¶åï¼å¦ãç¨æ·å¯¼å
¥æ¨¡æ¿.xlsã */ |
| | | fileName: string; |
| | | /** æ§è¡å¯¼å
¥çæ¥å£å½æ° (file, updateSupport) => Promise */ |
| | | importFn: (file: File, updateSupport: boolean) => Promise<unknown>; |
| | | /** ä¸è½½æ¨¡æ¿çæ¥å£å½æ° () => Promise(blob) */ |
| | | templateFn: () => Promise<BlobPart>; |
| | | /** 宿´è·¯çº¿åªæ¯ææ°å¢ï¼ä¸å±ç¤ºè¦çå¼å
³ */ |
| | | showUpdateSupport?: boolean; |
| | | } |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const config = ref<ImportModalProps>({ |
| | | title: '', |
| | | fileName: '', |
| | | importFn: async () => {}, |
| | | templateFn: () => Promise.resolve(new Blob()), |
| | | }); |
| | | |
| | | function createSchema(showUpdateSupport = true): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'file', |
| | | label: 'æ°æ®æä»¶', |
| | | component: 'Upload' as const, |
| | | rules: 'required', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶', |
| | | }, |
| | | ...(showUpdateSupport ? [{ |
| | | fieldName: 'updateSupport', |
| | | label: 'æ¯å¦è¦ç', |
| | | component: 'Switch' as const, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | help: 'æ¯å¦æ´æ°å·²ç»åå¨çæ°æ®', |
| | | }] : []), |
| | | ]; |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: createSchema(), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (isOpen) { |
| | | config.value = modalApi.getData<ImportModalProps>() ?? config.value; |
| | | await formApi.updateSchema(createSchema(config.value.showUpdateSupport !== false)); |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | const result = await config.value.importFn(data.file, data.updateSupport ?? false) as { |
| | | createCodes?: string[]; |
| | | updateCodes?: string[]; |
| | | failureCodes?: Record<string, string>; |
| | | } | undefined; |
| | | // å
³éå¹¶æç¤º |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | const failures = Object.entries(result?.failureCodes ?? {}); |
| | | if (failures.length > 0) { |
| | | AntModal.warning({ |
| | | title: `导å
¥å®æï¼æå ${(result?.createCodes?.length ?? 0) + (result?.updateCodes?.length ?? 0)} æ¡ï¼å¤±è´¥ ${failures.length} æ¡`, |
| | | content: h('div', { style: { whiteSpace: 'pre-wrap', maxHeight: '50vh', overflowY: 'auto' } }, |
| | | failures.map(([code, reason]) => `${code}ï¼${reason}`).join('\n')), |
| | | }); |
| | | } else { |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** ä¸ä¼ å */ |
| | | function beforeUpload(file: FileType) { |
| | | formApi.setFieldValue('file', file); |
| | | return false; |
| | | } |
| | | |
| | | /** ä¸è½½æ¨¡ç */ |
| | | async function handleDownload() { |
| | | const data = await config.value.templateFn(); |
| | | downloadFileFromBlobPart({ fileName: config.value.fileName, source: data }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="config.title" class="w-1/3"> |
| | | <Form class="mx-4"> |
| | | <template #file> |
| | | <div class="w-full"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <Button type="primary"> éæ© Excel æä»¶ </Button> |
| | | </Upload> |
| | | </div> |
| | | </template> |
| | | </Form> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center"> |
| | | <Button @click="handleDownload"> ä¸è½½å¯¼å
¥æ¨¡æ¿ </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export { default } from './import-modal.vue'; |
| | |
| | | import type { MesMdItemApi } from "#/api/mes/md/item"; |
| | | import type { MesMdItemTypeApi } from "#/api/mes/md/item/type"; |
| | | |
| | | import { nextTick, ref } from "vue"; |
| | | import { computed, nextTick, ref } from "vue"; |
| | | |
| | | import { CommonStatusEnum } from "@vben/constants"; |
| | | |
| | | import { Card, message, Modal } from "ant-design-vue"; |
| | | import { Alert, Card, message, Modal, Tag } from "ant-design-vue"; |
| | | |
| | | import { useVbenVxeGrid } from "#/adapter/vxe-table"; |
| | | import { getItemPage } from "#/api/mes/md/item"; |
| | |
| | | const selectedItemTypeId = ref<number>(); // å½åçéåç±»ç¼å· |
| | | const preSelectedIds = ref<number[]>([]); // é¢éç©æç¼å·å表 |
| | | const productIds = ref<number[]>([]); // éå®å¯éçç©æç¼å·å表ï¼ä¸ºç©ºåä¸éå¶ï¼ |
| | | const limitedToProducts = computed(() => productIds.value.length > 0); |
| | | const dialogTitle = computed(() => limitedToProducts.value ? "路线产åºéæ©" : "ç©æäº§åéæ©"); |
| | | const tableTitle = computed(() => limitedToProducts.value ? "å·²é
ç½®ç产åºäº§å" : "ç©æäº§åå表"); |
| | | const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // ç©æåç±»æ |
| | | |
| | | /** è·åå½åè¡¨æ ¼æ°æ® */ |
| | |
| | | }, |
| | | gridOptions: { |
| | | columns: useItemSelectGridColumns(true), |
| | | height: 560, |
| | | height: '100%', |
| | | keepSource: true, |
| | | checkboxConfig: { |
| | | highlight: true, |
| | |
| | | |
| | | <template> |
| | | <Modal v-model:open="open" |
| | | title="ç©æäº§åéæ©" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | :destroy-on-close="true" |
| | | @ok="handleConfirm" |
| | | @cancel="closeModal"> |
| | | <div class="flex h-full w-full"> |
| | | <Card class="mr-4 h-full w-1/5"> |
| | | <MdItemTypeTree ref="typeTreeRef" |
| | | @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <div class="w-4/5"> |
| | | <Grid table-title="ç©æäº§åå表" /> |
| | | <div class="flex h-[calc(100vh-300px)] min-h-[380px] w-full flex-col"> |
| | | <Alert |
| | | v-if="limitedToProducts" |
| | | class="mb-3 shrink-0" |
| | | message="ä»
å¯éæ©æ¬äº§åå¨å·¥èºè·¯çº¿ä¸å·²é
ç½®ç产åºç©æ" |
| | | description="å¦éå¢å å¯é项ï¼è¯·å
å¨å·¥èºè·¯çº¿å¯¹åºå·¥åºä¸é
置产åºå¹¶ä¿åã" |
| | | show-icon |
| | | type="info" |
| | | > |
| | | <template #action><Tag color="blue">{{ productIds.length }} 个产åº</Tag></template> |
| | | </Alert> |
| | | <div class="flex min-h-0 w-full flex-1 gap-4"> |
| | | <Card v-if="!limitedToProducts" class="h-full w-1/5 shrink-0" :body-style="{ height: '100%', overflow: 'hidden' }"> |
| | | <MdItemTypeTree ref="typeTreeRef" @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <div class="h-full min-h-0 min-w-0 flex-1"> |
| | | <Grid class="h-full" :table-title="tableTitle" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | |
| | | export { default as RouteColorPicker } from './color-picker.vue'; |
| | | export { default as RouteOutputAttribution } from './output-attribution.vue'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Button, Select } from 'ant-design-vue'; |
| | | |
| | | defineOptions({ name: 'RouteOutputAttribution' }); |
| | | |
| | | /** å½å±éæ©ä¸ä»£è¡¨âå¤äº§åéç¨âçå¨å
µå¼ï¼äº§åç¼å·åä¸ºæ£æ°ï¼ */ |
| | | const GENERAL_PRODUCT = 0; |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | modelValue?: MesProRouteProcessApi.OutputItem[]; |
| | | products?: MesProRouteProductApi.RouteProduct[]; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | modelValue: () => [], |
| | | products: () => [], |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ |
| | | 'update:modelValue': [value: MesProRouteProcessApi.OutputItem[]]; |
| | | }>(); |
| | | |
| | | const rows = computed(() => props.modelValue ?? []); |
| | | const productOptions = computed(() => |
| | | props.products |
| | | .filter((product) => product.itemId != null) |
| | | .map((product) => ({ |
| | | label: product.itemName ?? product.itemCode ?? String(product.itemId), |
| | | value: product.itemId!, |
| | | })), |
| | | ); |
| | | const attributionOptions = computed(() => [ |
| | | { label: 'éç¨ï¼ææäº§åï¼', value: GENERAL_PRODUCT }, |
| | | ...productOptions.value, |
| | | ]); |
| | | |
| | | function commit(next: MesProRouteProcessApi.OutputItem[]) { |
| | | emit('update:modelValue', next); |
| | | } |
| | | |
| | | function updateRow( |
| | | index: number, |
| | | patch: Partial<MesProRouteProcessApi.OutputItem>, |
| | | ) { |
| | | commit(rows.value.map((row, i) => (i === index ? { ...row, ...patch } : row))); |
| | | } |
| | | |
| | | /** éæ©äº§åºç©ææ¶ï¼é»è®¤å½å±å°èªèº«äº§åï¼åæåå
±ç¨æ¶åæå¨æ¹æâéç¨âï¼ */ |
| | | function handleItemChange(index: number, itemId: number | undefined) { |
| | | const current = rows.value[index]; |
| | | const patch: Partial<MesProRouteProcessApi.OutputItem> = { itemId }; |
| | | // 忥å½å±èªèº«æ¶ï¼åæ¢äº§åºç©æä¹è¦åæ¥å½å±ï¼é¿å
ç䏿§äº§åç¼å·ã |
| | | if ((current?.productId == null || current.productId === current.itemId) && itemId != null) { |
| | | patch.productId = itemId; |
| | | } |
| | | updateRow(index, patch); |
| | | } |
| | | |
| | | function handleAttributionChange(index: number, value: number | undefined) { |
| | | updateRow(index, { |
| | | productId: value === GENERAL_PRODUCT || value == null ? undefined : value, |
| | | }); |
| | | } |
| | | |
| | | function addRow() { |
| | | commit([...rows.value, { itemId: undefined, productId: undefined }]); |
| | | } |
| | | |
| | | function removeRow(index: number) { |
| | | commit(rows.value.filter((_, i) => i !== index)); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="w-full"> |
| | | <div class="flex flex-col gap-2"> |
| | | <div |
| | | v-for="(row, index) in rows" |
| | | :key="index" |
| | | class="flex items-center gap-2" |
| | | > |
| | | <Select |
| | | :disabled="disabled" |
| | | :options="productOptions" |
| | | :placeholder="'éæ©äº§åºç©æ'" |
| | | :value="row.itemId" |
| | | class="min-w-0 flex-1" |
| | | option-filter-prop="label" |
| | | show-search |
| | | @change="(value) => handleItemChange(index, value as number | undefined)" |
| | | /> |
| | | <Select |
| | | :disabled="disabled || row.itemId == null" |
| | | :options="attributionOptions" |
| | | :value="row.productId ?? GENERAL_PRODUCT" |
| | | class="min-w-0 flex-1" |
| | | @change="(value) => handleAttributionChange(index, value as number | undefined)" |
| | | /> |
| | | <Button |
| | | :aria-label="`å é¤ç¬¬ ${index + 1} æ¡äº§åºé
ç½®`" |
| | | :disabled="disabled" |
| | | danger |
| | | size="small" |
| | | type="text" |
| | | @click="removeRow(index)" |
| | | > |
| | | <IconifyIcon class="size-4" icon="lucide:trash-2" /> |
| | | </Button> |
| | | </div> |
| | | <div v-if="rows.length === 0" class="text-sm text-gray-500"> |
| | | ææ äº§åºï¼å½¢æåæåææåæ¶æ·»å |
| | | </div> |
| | | </div> |
| | | <Button |
| | | v-if="!disabled" |
| | | class="mt-2" |
| | | size="small" |
| | | type="dashed" |
| | | @click="addRow" |
| | | > |
| | | <IconifyIcon class="mr-1 size-4" icon="lucide:plus" /> |
| | | æ·»å äº§åº |
| | | </Button> |
| | | </div> |
| | | </template> |
| | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getTemplatePage } from '#/api/mes/pro/processdesign/template'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { |
| | | MdItemSelect, |
| | | MdProductBomSelect, |
| | | } from '#/views/mes/md/item/components'; |
| | | |
| | | import { RouteColorPicker } from './components'; |
| | | import { RouteColorPicker, RouteOutputAttribution } from './components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | |
| | | /** å·¥èºè·¯çº¿å·¥åºæç»è¡¨å */ |
| | | export function useRouteProcessFormSchema( |
| | | processOptions: Array<{ label: string; value: number }>, |
| | | products: MesProRouteProductApi.RouteProduct[] = [], |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | }, |
| | | { |
| | | fieldName: 'yieldRate', |
| | | label: 'æ¶ç', |
| | | label: 'å·¥åºæ¶ç', |
| | | component: 'InputNumber', |
| | | help: '产åºé ÷ æå
¥éï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0.0001, |
| | | max: 1, |
| | | step: 0.01, |
| | | precision: 4, |
| | | placeholder: '产åº/æå
¥ï¼å¦ 0.98', |
| | | placeholder: 'å¦ 0.98ï¼98%ï¼', |
| | | }, |
| | | rules: z.number().min(0.0001).max(1).nullish(), |
| | | }, |
| | | { |
| | | fieldName: 'outputs', |
| | | label: 'æ¬å·¥åºäº§åº', |
| | | component: markRaw(RouteOutputAttribution), |
| | | componentProps: { |
| | | products, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'linkType', |
| | | label: 'ä¸ä¸éå·¥åºå
³ç³»', |
| | | component: 'Select', |
| | | help: () => |
| | | h( |
| | | 'div', |
| | | { |
| | | style: |
| | | 'white-space: pre-line; max-width: 320px; line-height: 1.6;', |
| | | }, |
| | | '0=å¼å§-å¼å§(SS)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½å¼å§ï¼å¯éå å¹¶è¡å¼å·¥ï¼\n1=ç»æ-ç»æ(FF)ï¼ä¸éå·¥åºå¿
é¡»çå°æ¬å·¥åºç»ææè½ç»æï¼å¹¶è¡ä½åæ¥å®å·¥ï¼\n2=å¼å§-ç»æ(SF)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½ç»æ\n3=ç»æ-å¼å§(FS)ï¼æ¬å·¥åºç»æåï¼ä¸éå·¥åºæè½å¼å§ï¼é»è®¤å¼ï¼', |
| | | ), |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©', |
| | | }, |
| | | rules: z.number().optional(), |
| | | rules: z.number().default(3), |
| | | }, |
| | | { |
| | | fieldName: 'colorCode', |
| | |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'backflushFlag', |
| | | label: 'æ¥å·¥èªå¨æ£æ', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'èªå¨', |
| | | unCheckedChildren: 'æå¨', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | }, |
| | |
| | | { field: 'sort', title: 'åºå·', width: 70, align: 'center', fixed: 'left' }, |
| | | { field: 'processCode', title: 'å·¥åºç¼ç ', width: 140, fixed: 'left' }, |
| | | { field: 'processName', title: 'å·¥åºåç§°', width: 140, fixed: 'left' }, |
| | | { |
| | | field: 'outputItemNames', |
| | | title: 'æ¬å·¥åºäº§åº', |
| | | minWidth: 240, |
| | | formatter: ({ cellValue, row }) => { |
| | | const outputs = row.outputs ?? []; |
| | | if (!outputs.length) { |
| | | return cellValue ?? ''; |
| | | } |
| | | return outputs |
| | | .map((output) => { |
| | | const name = output.itemName ?? output.itemCode ?? output.itemId; |
| | | return output.productId == null |
| | | ? `${name}ï¼éç¨ï¼` |
| | | : `${name}ï¼${output.productName ?? output.productId}ï¼`; |
| | | }) |
| | | .join('ã'); |
| | | }, |
| | | }, |
| | | { field: 'nextProcessName', title: 'ä¸ä¸éå·¥åº', width: 140 }, |
| | | { |
| | | field: 'linkType', |
| | |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'backflushFlag', |
| | | title: 'èªå¨æ£æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { field: 'prepareTime', title: 'å夿¶é´(å)', width: 110 }, |
| | | { field: 'waitTime', title: 'çå¾
æ¶é´(å)', width: 110 }, |
| | | { |
| | |
| | | /** å·¥èºè·¯çº¿äº§ååè¡¨åæ®µ */ |
| | | export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] { |
| | | return [ |
| | | { field: 'itemCode', title: '产åç©æç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: '产åç©æåç§°', width: 150 }, |
| | | { field: 'itemCode', title: '产åºç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: '产åºåç§°', width: 150 }, |
| | | { field: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { field: 'unitName', title: 'åä½', width: 80 }, |
| | | { field: 'quantity', title: 'ç产æ°é', width: 100 }, |
| | | { field: 'warehouseName', title: 'æ¥å·¥å
¥åºä»', width: 140 }, |
| | | { field: 'locationName', title: 'å
¥åºåºåº', width: 120 }, |
| | | { field: 'areaName', title: 'å
¥åºåºä½', width: 120 }, |
| | | { |
| | | field: 'productionTime', |
| | | title: 'çäº§ç¨æ¶', |
| | |
| | | /** å·¥èºè·¯çº¿äº§å BOM åè¡¨åæ®µ */ |
| | | export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] { |
| | | return [ |
| | | { field: 'itemCode', title: 'BOM ç©æç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: 'BOM ç©æåç§°', width: 150 }, |
| | | { field: 'itemCode', title: 'ææç¼ç ', width: 150 }, |
| | | { field: 'itemName', title: 'ææåç§°', width: 150 }, |
| | | { field: 'specification', title: 'è§æ ¼åå·', width: 150 }, |
| | | { field: 'unitName', title: 'åä½', width: 80 }, |
| | | { field: 'quantity', title: 'ç¨ææ¯ä¾', width: 100 }, |
| | |
| | | /** å·¥èºè·¯çº¿äº§å表å */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | getFormApi?: () => VbenFormApi, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | // 产åç©æï¼ä½¿ç¨ä¸å¡èªå®ä¹éæ©å¨ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | { |
| | | fieldName: 'itemId', |
| | | label: '产å', |
| | | label: '产åºç©æ', |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | help: 'å¯éæ©ä¸»æ°æ®ç±»å为æåæåæåçç©æï¼ä¹å
¼å®¹ MES æå/åæååç±»ï¼å·²æè·¯çº¿äº§åå¯ç»§ç»ä¿®æ¹ä»ä½ã', |
| | | formItemClass: 'col-span-2', |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | rules: z.number().default(1), |
| | | }, |
| | | { |
| | | fieldName: 'warehouseId', |
| | | label: 'æ¥å·¥ä»åº', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getWarehouseSimpleList, |
| | | labelField: 'name', |
| | | valueField: 'id', |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºä»åº', |
| | | onChange: async () => { |
| | | await getFormApi?.().setFieldValue('locationId', undefined); |
| | | await getFormApi?.().setFieldValue('areaId', undefined); |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'æ¥å·¥åºåº', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const options = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºåºåº', |
| | | onChange: async () => getFormApi?.().setFieldValue('areaId', undefined), |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'æ¥å·¥åºä½', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const options = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥äº§åºåºä½', |
| | | }; |
| | | }, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'productionTime', |
| | | label: 'çäº§ç¨æ¶', |
| | | component: 'InputNumber', |
| | |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tooltip } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteRoute, |
| | | exportRoute, |
| | | getRoutePage, |
| | | importRoute, |
| | | importRouteTemplate, |
| | | updateRouteStatus, |
| | | } from '#/api/mes/pro/route'; |
| | | import ImportModalComponent from '#/components/import-modal'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | import Form from './modules/route-configurator.vue'; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ¯å¦å¯åæ¢ç¶æ |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportModalComponent, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | /** å建工èºè·¯çº¿ */ |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | function handleImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | fileName: 'å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xls', |
| | | importFn: importRoute, |
| | | templateFn: importRouteTemplate, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** ç¼è¾å·¥èºè·¯çº¿ï¼ä»
åç¨ç¶æå¯ç¼è¾ï¼ */ |
| | |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | `确认è¦å°"${row.name}"å·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `确认å¯ç¨â${row.name}âåï¼ç³»ç»å°æ£æ¥äº§åºå
¥åºä½ç½®ãå·¥åºäº§åºåå·¥åºæææ¯å¦å®æ´ã` |
| | | : `确认è¦å°â${row.name}âå·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-3" |
| | | message="路线é
置顺åº" |
| | | description="åºæ¬ä¿¡æ¯ â 产åºäº§ååå
¥åºä» â å·¥åºååå·¥åºäº§åº â å·¥åºææ â æ ¡éªå¯ç¨" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="å·¥èºè·¯çº¿å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | 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 { Alert, Button, Empty, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getRouteProductBomList } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import ProductBomList from './product-bom-list.vue'; |
| | | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | processes: MesProRouteProcessApi.RouteProcess[]; |
| | | products: MesProRouteProductApi.RouteProduct[]; |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProductBomApi.RouteProductBom[]]; |
| | | }>(); |
| | | |
| | | const activeProductId = ref<number>(); |
| | | const allBomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); |
| | | |
| | | const activeProduct = computed(() => |
| | | props.products.find((item) => item.itemId === activeProductId.value), |
| | | ); |
| | | |
| | | function materialCount(productId?: number) { |
| | | if (!productId) return 0; |
| | | return allBomList.value.filter((item) => item.productId === productId).length; |
| | | } |
| | | |
| | | async function loadAll() { |
| | | allBomList.value = (await getRouteProductBomList({ routeId: props.routeId })) || []; |
| | | emit('change', allBomList.value); |
| | | } |
| | | |
| | | watch( |
| | | () => props.products.map((item) => item.itemId).filter(Boolean), |
| | | (productIds) => { |
| | | if (!productIds.length) { |
| | | activeProductId.value = undefined; |
| | | return; |
| | | } |
| | | if (!activeProductId.value || !productIds.includes(activeProductId.value)) { |
| | | activeProductId.value = productIds[0]; |
| | | } |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | watch( |
| | | () => props.routeId, |
| | | (value) => value && loadAll(), |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <Alert |
| | | class="mb-4" |
| | | message="æäº§åºäº§åé
ç½®æ¯éå·¥åºéè¦æå
¥çç©æ" |
| | | description="å
éæ©äº§åºäº§åï¼å忢å°å
·ä½å·¥åºæ·»å ææç©æåç¨ææ¯ä¾ãå¼å¯âæ¥å·¥èªå¨æ£æâçå·¥åºï¼ä¼å¨æ¥å·¥æ¶æè¿éçé
ç½®èªå¨æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | |
| | | <Empty |
| | | v-if="!products.length || !processes.length" |
| | | :description="!products.length ? '请å
å®æäº§åºäº§åé
ç½®' : '请å
宿工åºé
ç½®'" |
| | | /> |
| | | |
| | | <div v-else class="material-layout"> |
| | | <aside class="product-selector" aria-label="éæ©è¦é
ç½®ææç产åºäº§å"> |
| | | <div class="selector-heading"> |
| | | <strong>产åºäº§å</strong> |
| | | <span>{{ products.length }} 个</span> |
| | | </div> |
| | | <Button |
| | | v-for="product in products" |
| | | :key="product.itemId" |
| | | :class="['product-button', { active: product.itemId === activeProductId }]" |
| | | type="text" |
| | | @click="activeProductId = product.itemId" |
| | | > |
| | | <span class="product-copy"> |
| | | <strong>{{ product.itemName }}</strong> |
| | | <small>{{ product.itemCode }}</small> |
| | | </span> |
| | | <Tag :color="materialCount(product.itemId) ? 'success' : 'warning'"> |
| | | {{ materialCount(product.itemId) ? `${materialCount(product.itemId)} æ¡` : 'æªé
ç½®' }} |
| | | </Tag> |
| | | </Button> |
| | | </aside> |
| | | |
| | | <section v-if="activeProduct" class="material-detail"> |
| | | <header class="material-detail-header"> |
| | | <div> |
| | | <h3>{{ activeProduct.itemName }} · å·¥åºææ</h3> |
| | | <p>便¬¡åæ¢å·¥åºï¼ç»´æ¤è¯¥äº§åå¨è¯¥å·¥åºå®é
æ¶èçç©æã</p> |
| | | </div> |
| | | <Tag color="blue">{{ processes.length }} éå·¥åº</Tag> |
| | | </header> |
| | | <ProductBomList |
| | | :form-type="formType" |
| | | :product-id="activeProduct.itemId!" |
| | | :product-name="activeProduct.itemName" |
| | | :route-id="routeId" |
| | | @change="loadAll" |
| | | /> |
| | | </section> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .material-layout { display: grid; grid-template-columns: minmax(210px, 260px) minmax(0, 1fr); gap: 18px; } |
| | | .product-selector { overflow: hidden; align-self: start; border: 1px solid rgb(226 232 240); border-radius: 12px; background: rgb(248 250 252); } |
| | | .selector-heading { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid rgb(226 232 240); color: rgb(15 23 42); } |
| | | .selector-heading span { color: rgb(100 116 139); font-size: 12px; } |
| | | .product-button { display: flex; width: 100%; height: auto; min-height: 62px; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border-radius: 0; text-align: left; } |
| | | .product-button + .product-button { border-top: 1px solid rgb(226 232 240); } |
| | | .product-button.active { background: rgb(219 234 254); color: rgb(29 78 216); } |
| | | .product-copy { display: grid; min-width: 0; gap: 2px; } |
| | | .product-copy strong, .product-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .product-copy small { color: rgb(100 116 139); } |
| | | .material-detail { min-width: 0; padding: 18px; border: 1px solid rgb(226 232 240); border-radius: 12px; } |
| | | .material-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } |
| | | .material-detail-header h3 { margin: 0; color: rgb(15 23 42); font-size: 16px; font-weight: 650; } |
| | | .material-detail-header p { margin: 4px 0 0; color: rgb(100 116 139); font-size: 13px; } |
| | | :global(.dark) .product-selector, :global(.dark) .material-detail { border-color: rgb(51 65 85); background: rgb(15 23 42); } |
| | | :global(.dark) .selector-heading, :global(.dark) .product-button + .product-button { border-color: rgb(51 65 85); color: rgb(241 245 249); } |
| | | :global(.dark) .product-button.active { background: rgb(30 64 175 / 35%); color: rgb(147 197 253); } |
| | | :global(.dark) .material-detail-header h3 { color: rgb(241 245 249); } |
| | | @media (max-width: 900px) { |
| | | .material-layout { grid-template-columns: 1fr; } |
| | | .product-selector { display: flex; overflow-x: auto; } |
| | | .selector-heading { display: none; } |
| | | .product-button { min-width: 210px; border-top: 0 !important; border-right: 1px solid rgb(226 232 240); } |
| | | } |
| | | </style> |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Alert, message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | |
| | | getRouteProcess, |
| | | updateRouteProcess, |
| | | } from '#/api/mes/pro/route/process'; |
| | | import { getRouteProductListByRoute } from '#/api/mes/pro/route/product'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useRouteProcessFormSchema } from '../data'; |
| | |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 150, |
| | | labelWidth: 130, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | |
| | | }); |
| | | |
| | | /** å 载工åºé项ååçæ schemaï¼é¿å
ä¸æç©ºéé¡¹ï¼ */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | async function loadSchema(routeId: number): Promise<VbenFormSchema[]> { |
| | | const list = await getProcessSimpleList(); |
| | | const options = (list || []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })); |
| | | return useRouteProcessFormSchema(options); |
| | | const products = routeId ? await getRouteProductListByRoute(routeId) : []; |
| | | return useRouteProcessFormSchema(options, products); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // å·¥åºä¸æä¾èµè¿ç¨æ°æ®ï¼schema å¨å¼¹çªæå¼æ¶åçæ |
| | | const schema = await loadSchema(); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | maxSort?: number; |
| | |
| | | if (!data) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // å·¥åºä¸æä¾èµè¿ç¨æ°æ®ï¼schema å¨å¼¹çªæå¼æ¶åçæ |
| | | const schema = await loadSchema(data.routeId); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | // å è½½æ°æ® |
| | | if (!data.id) { |
| | | // æ°å¢æ¶ï¼é»è®¤åºå· = maxSort + 1ï¼å¹¶ç»ä¸ä¸ªé»è®¤çç¹å¾é¢è² |
| | | await formApi.setValues({ |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="æå®é
ç产èç¹é
置产åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æ«éå·¥åºå¿
须鿩å
¨é¨æåãå½å±è¡¨ç¤ºè¯¥äº§åºå±äºåªä¸ªäº§åï¼å¤äº§åå
±ç¨çåæåéâéç¨âï¼äº§åä¸å±ï¼å°¤å
¶æ¯æåï¼å½å±å°èªèº«äº§åãå¦å¼å¯æ¥å·¥èªå¨æ£æï¼æ¥å·¥æ¶ä¼æå·¥åºææé
ç½®æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const isEditable = ref(props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProcessApi.RouteProcess[]]; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const list = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥èºè·¯çº¿å·¥åºå表 |
| | | const sortedList = computed(() => |
| | | [...list.value].sort((a, b) => (a.sort || 0) - (b.sort || 0)), |
| | | ); |
| | | |
| | | const [ProcessFormModal, processFormModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessForm, |
| | |
| | | try { |
| | | list.value = await getRouteProcessListByRoute(props.routeId); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | emit('change', list.value); |
| | | } finally { |
| | | gridApi.setLoading(false); |
| | | } |
| | |
| | | |
| | | <template> |
| | | <ProcessFormModal @success="getList" /> |
| | | <div v-if="sortedList.length" class="mb-4 flex flex-wrap items-center gap-2 rounded-lg bg-slate-50 p-3 dark:bg-slate-900"> |
| | | <template v-for="(item, index) in sortedList" :key="item.id"> |
| | | <div class="flex items-center gap-2 rounded-md border border-slate-200 bg-white px-3 py-2 dark:border-slate-700 dark:bg-slate-800"> |
| | | <span class="text-xs font-semibold text-blue-600">{{ item.sort }}</span> |
| | | <span class="font-medium">{{ item.processName }}</span> |
| | | <Tag v-if="index === sortedList.length - 1" color="blue">æ«é</Tag> |
| | | </div> |
| | | <span v-if="index < sortedList.length - 1" class="text-slate-400">â</span> |
| | | </template> |
| | | </div> |
| | | <div v-if="isEditable" class="mb-3 flex items-center justify-start"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="ç»æå·¥åº"> |
| | | <Grid class="w-full" table-title="å·¥åºé¡ºåºååå·¥åºäº§åº"> |
| | | <template #colorCode="{ row }"> |
| | | <div v-if="row.colorCode" class="flex items-center justify-center gap-1"> |
| | | <div |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | |
| | | import BomForm from './bom-form.vue'; |
| | | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | productId: number; |
| | | productName?: string; |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: []; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); |
| | | |
| | | const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // å·¥åºå表ï¼ç¨äº Tabï¼ |
| | | const activeProcessId = ref<number>(); // å½åéä¸çå·¥åº Tab |
| | |
| | | await deleteRouteProductBom(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', ['BOM ç©æ'])); |
| | | await getList(); |
| | | emit('change'); |
| | | } |
| | | |
| | | async function handleSaved() { |
| | | await getList(); |
| | | emit('change'); |
| | | } |
| | | |
| | | watch( |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <BomFormModal @success="getList" /> |
| | | <BomFormModal @success="handleSaved" /> |
| | | <Tabs v-model:active-key="activeProcessId" @change="getList"> |
| | | <TabPane |
| | | v-for="item in processList" |
| | |
| | | :tab="item.processName" |
| | | /> |
| | | </Tabs> |
| | | <div class="mb-3 flex items-center justify-start"> |
| | | <div v-if="isEditable" class="mb-3 flex items-center justify-start"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['BOM ç©æ']), |
| | | label: $t('ui.actionTitle.create', ['ææç©æ']), |
| | | type: 'primary', |
| | | disabled: !activeProcessId, |
| | | onClick: handleCreate, |
| | |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="产å BOM"> |
| | | <Grid class="w-full" table-title="å½åå·¥åºææ"> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | ifShow: () => isEditable, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | ifShow: () => isEditable, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', ['BOM ç©æ']), |
| | | title: $t('ui.actionMessage.deleteConfirm', ['ææç©æ']), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Divider, message } from 'ant-design-vue'; |
| | | import { Alert, 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>(); |
| | |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: useRouteProductFormSchema(handleItemChange), |
| | | schema: useRouteProductFormSchema(handleItemChange, () => formApi), |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | |
| | | 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 createRouteProduct(data); |
| | | } |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="è¿éç»´æ¤äº§åºåå
¶æ¥å·¥å
¥åºä½ç½®" |
| | | description="ææç©æå°å¨è·¯çº¿é
ç½®çâå·¥åºææâæ¥éª¤ä¸æäº§åãæå·¥åºç»ä¸ç»´æ¤ã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <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> |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProRouteProductApi } from '#/api/mes/pro/route/product'; |
| | | |
| | | import { ref, watch } from 'vue'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | |
| | | routeId: number; |
| | | }>(); |
| | | |
| | | const isEditable = ref(props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const emit = defineEmits<{ |
| | | change: [list: MesProRouteProductApi.RouteProduct[]]; |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => props.formType !== 'detail'); // æ¯å¦å¯ç¼è¾ |
| | | const list = ref<MesProRouteProductApi.RouteProduct[]>([]); // å·¥èºè·¯çº¿äº§åå表 |
| | | |
| | | const [ProductFormModal, productFormModalApi] = useVbenModal({ |
| | |
| | | try { |
| | | list.value = await getRouteProductListByRoute(props.routeId); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | emit('change', list.value); |
| | | } finally { |
| | | gridApi.setLoading(false); |
| | | } |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['å
³è产å']), |
| | | label: $t('ui.actionTitle.create', ['产åºäº§å']), |
| | | type: 'primary', |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </div> |
| | | <Grid class="w-full" table-title="å
³è产å"> |
| | | <Grid class="w-full" table-title="产åºäº§å忥工å
¥åºä½ç½®"> |
| | | <template #productionTime="{ row }"> |
| | | <span v-if="row.productionTime"> |
| | | {{ row.productionTime }} |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { MesProRouteApi } from '#/api/mes/pro/route'; |
| | | 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 } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { Alert, Button, Empty, message, Steps, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route'; |
| | | import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process'; |
| | | import { getRouteProductListByRoute } from '#/api/mes/pro/route/product'; |
| | | import { getRouteProductBomList } from '#/api/mes/pro/route/productbom'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import MaterialInputList from './material-input-list.vue'; |
| | | import ProcessList from './process-list.vue'; |
| | | import ProductList from './product-list.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | | const formData = ref<MesProRouteApi.Route>(); |
| | | const activeStep = ref(0); |
| | | const productList = ref<MesProRouteProductApi.RouteProduct[]>([]); |
| | | const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); |
| | | const bomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); |
| | | |
| | | const isDetail = computed(() => formType.value === 'detail'); |
| | | const isNew = computed(() => !formData.value?.id); |
| | | const routeId = computed(() => formData.value?.id); |
| | | const sortedProcessList = computed(() => |
| | | [...processList.value].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0)), |
| | | ); |
| | | const title = computed(() => |
| | | isDetail.value ? 'æ¥çå·¥èºè·¯çº¿' : isNew.value ? 'æ°å»ºå·¥èºè·¯çº¿' : 'ç¼è¾å·¥èºè·¯çº¿', |
| | | ); |
| | | |
| | | const stepItems = [ |
| | | { title: 'åºæ¬ä¿¡æ¯', description: '路线åç§°ä¸è¯´æ' }, |
| | | { title: '产åºä¸å
¥åº', description: '产å忥工ä»ä½' }, |
| | | { title: 'å·¥åºä¸äº§åº', description: '顺åºååæå/æå' }, |
| | | { title: 'å·¥åºææ', description: 'æäº§åä¸å·¥åºé
ç½®' }, |
| | | { title: 'å¯ç¨åæ£æ¥', description: 'é䏿£æ¥ç¼ºå¤±é¡¹' }, |
| | | ]; |
| | | |
| | | const productsWithWarehouse = computed(() => |
| | | productList.value.filter((item) => item.warehouseId && item.locationId && item.areaId).length, |
| | | ); |
| | | const processesWithOutput = computed(() => |
| | | processList.value.filter((item) => item.outputItemIds?.length).length, |
| | | ); |
| | | const lastProcess = computed(() => sortedProcessList.value.at(-1)); |
| | | const configuredProductIds = computed( |
| | | () => new Set(bomList.value.map((item) => item.productId).filter(Boolean)), |
| | | ); |
| | | const productsWithBom = computed(() => configuredProductIds.value.size); |
| | | const reviewIssues = computed(() => { |
| | | const issues: Array<{ step: number; text: string }> = []; |
| | | if (!productList.value.length) issues.push({ step: 1, text: 'è¿æ²¡æé
置产åºäº§å' }); |
| | | const missingWarehouse = productList.value.filter( |
| | | (item) => !item.warehouseId || !item.locationId || !item.areaId, |
| | | ); |
| | | if (missingWarehouse.length) { |
| | | issues.push({ |
| | | step: 1, |
| | | text: `${missingWarehouse.length} 个产åºäº§åæªé
ç½®å®æ´ä»åºãåºåºååºä½`, |
| | | }); |
| | | } |
| | | if (!processList.value.length) issues.push({ step: 2, text: 'è¿æ²¡æé
ç½®ç产工åº' }); |
| | | if (processList.value.length && !processList.value.some((item) => item.keyFlag)) { |
| | | issues.push({ step: 2, text: 'æªè®¾ç½®å
³é®å·¥åº' }); |
| | | } |
| | | if (lastProcess.value && !lastProcess.value.outputItemIds?.length) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: `æ«éå·¥åºâ${lastProcess.value.processName || lastProcess.value.processCode || lastProcess.value.sort}âå¿
é¡»é
ç½®æå产åº`, |
| | | }); |
| | | } |
| | | const lastOutputs = lastProcess.value?.outputs ?? []; |
| | | if ( |
| | | lastOutputs.length && |
| | | lastOutputs.some( |
| | | (output) => output.productId == null || output.productId !== output.itemId, |
| | | ) |
| | | ) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: 'æ«éå·¥åºçæå产åºå¿
é¡»å½å±å°èªèº«äº§åï¼ä¸è½ä½¿ç¨âéç¨â', |
| | | }); |
| | | } |
| | | const productIds = new Set(productList.value.map((item) => item.itemId)); |
| | | const invalidOutputCount = processList.value.filter((process) => |
| | | process.outputItemIds?.some((itemId) => !productIds.has(itemId)), |
| | | ).length; |
| | | if (invalidOutputCount) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: `${invalidOutputCount} éå·¥åºçäº§åºæªå
å«å¨è·¯çº¿äº§åºäº§åä¸`, |
| | | }); |
| | | } |
| | | const noBomProducts = productList.value.filter( |
| | | (item) => item.itemId && !configuredProductIds.value.has(item.itemId), |
| | | ); |
| | | if (noBomProducts.length) { |
| | | issues.push({ step: 3, text: `${noBomProducts.length} 个产åè¿æ²¡æé
置任ä½å·¥åºææ` }); |
| | | } |
| | | return issues; |
| | | }); |
| | | const primaryText = computed(() => { |
| | | if (activeStep.value === stepItems.length - 1) return isDetail.value ? 'å
³é' : '宿é
ç½®'; |
| | | if (activeStep.value === 0 && isNew.value) return 'ä¿åå¹¶é
置产åº'; |
| | | return 'ä¸ä¸æ¥'; |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | async function loadConfiguration() { |
| | | if (!routeId.value) return; |
| | | const [products, processes, boms] = await Promise.all([ |
| | | getRouteProductListByRoute(routeId.value), |
| | | getRouteProcessListByRoute(routeId.value), |
| | | getRouteProductBomList({ routeId: routeId.value }), |
| | | ]); |
| | | productList.value = products || []; |
| | | processList.value = processes || []; |
| | | bomList.value = boms || []; |
| | | } |
| | | |
| | | async function saveBaseInformation() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return false; |
| | | modalApi.lock(); |
| | | try { |
| | | const data = (await formApi.getValues()) as MesProRouteApi.Route; |
| | | if (!routeId.value) { |
| | | const id = await createRoute(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | formType.value = 'update'; |
| | | message.success('åºæ¬ä¿¡æ¯å·²ä¿åï¼è¯·ç»§ç»é
置产åºäº§ååå
¥åºä½ç½®'); |
| | | } else { |
| | | await updateRoute({ ...data, id: routeId.value }); |
| | | formData.value = { ...formData.value, ...data, id: routeId.value }; |
| | | } |
| | | emit('success'); |
| | | return true; |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | function validateProducts() { |
| | | if (!productList.value.length) { |
| | | message.warning('请è³å°æ·»å ä¸ä¸ªäº§åºäº§åï¼åé
置工åº'); |
| | | return false; |
| | | } |
| | | if (productsWithWarehouse.value !== productList.value.length) { |
| | | message.warning('请为æ¯ä¸ªäº§åºäº§åé
ç½®å®æ´çä»åºãåºåºååºä½'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function validateProcesses() { |
| | | if (!processList.value.length) { |
| | | message.warning('请è³å°æ·»å ä¸éç产工åº'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function validateBom() { |
| | | if (productsWithBom.value !== productList.value.length) { |
| | | message.warning('æ¯ä¸ªäº§åºäº§åè³å°è¦é
ç½®ä¸æ¡å·¥åºææ'); |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | async function goToStep(target: number) { |
| | | if (target < 0 || target >= stepItems.length) return; |
| | | if (target <= activeStep.value || isDetail.value) { |
| | | activeStep.value = target; |
| | | return; |
| | | } |
| | | if (!(await saveBaseInformation())) return; |
| | | await loadConfiguration(); |
| | | if (target >= 2 && !validateProducts()) return void (activeStep.value = 1); |
| | | if (target >= 3 && !validateProcesses()) return void (activeStep.value = 2); |
| | | if (target >= 4 && !validateBom()) return void (activeStep.value = 3); |
| | | activeStep.value = target; |
| | | } |
| | | |
| | | async function handlePrimaryAction() { |
| | | if (activeStep.value === stepItems.length - 1) { |
| | | if (!isDetail.value && reviewIssues.value.length) { |
| | | message.warning('ä»æç¼ºå¤±é
ç½®ï¼è¯·æ ¹æ®æ£æ¥ç»æè¡¥å
åå宿'); |
| | | return; |
| | | } |
| | | await modalApi.close(); |
| | | return; |
| | | } |
| | | await goToStep(activeStep.value + 1); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | showConfirmButton: false, |
| | | cancelText: 'å
³é', |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | productList.value = []; |
| | | processList.value = []; |
| | | bomList.value = []; |
| | | activeStep.value = 0; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema(formApi) }); |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(isDetail.value); |
| | | if (!data.id) return; |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getRoute(data.id); |
| | | await formApi.setValues(formData.value); |
| | | await loadConfiguration(); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="title" class="w-[94vw] max-w-[1560px]"> |
| | | <div class="route-config-shell"> |
| | | <header class="route-config-header"> |
| | | <div class="min-w-0"> |
| | | <div class="flex flex-wrap items-center gap-2"> |
| | | <h2 class="m-0 text-lg font-semibold text-gray-900 dark:text-gray-100"> |
| | | {{ formData?.name || 'æ°å·¥èºè·¯çº¿' }} |
| | | </h2> |
| | | <Tag :color="isDetail ? 'default' : 'processing'"> |
| | | {{ isDetail ? 'åªè¯»æ¥ç' : 'é
ç½®ä¸' }} |
| | | </Tag> |
| | | </div> |
| | | <p class="mb-0 mt-1 text-sm text-gray-600 dark:text-gray-300"> |
| | | å·¥åºææä¾èµäº§åºäº§ååå·¥åºï¼å æ¤å¨å·¥åºç¼æå®æåéä¸é
ç½®ã |
| | | </p> |
| | | </div> |
| | | <div class="route-config-summary" aria-label="å½åé
ç½®æ¦è§"> |
| | | <span>äº§åº {{ productList.length }}</span> |
| | | <span>å·¥åº {{ processList.length }}</span> |
| | | <span>ææ {{ bomList.length }}</span> |
| | | </div> |
| | | </header> |
| | | |
| | | <Steps |
| | | :current="activeStep" |
| | | :items="stepItems" |
| | | class="route-config-steps" |
| | | responsive |
| | | size="small" |
| | | type="navigation" |
| | | @change="goToStep" |
| | | /> |
| | | |
| | | <main class="route-config-content"> |
| | | <section v-show="activeStep === 0" class="route-step-panel"> |
| | | <div class="route-step-copy"> |
| | | <IconifyIcon icon="lucide:route" /> |
| | | <div> |
| | | <h3>å
å®ä¹è¿æ¡è·¯çº¿æ¯ä»ä¹</h3> |
| | | <p>ç¼ç ç¨äºç³»ç»è¯å«ï¼åç§°å说æå¸®å©ç°åºäººåå¿«ééæ©ã</p> |
| | | </div> |
| | | </div> |
| | | <Form class="route-base-form" /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 1" class="route-step-panel"> |
| | | <Alert |
| | | class="mb-4" |
| | | message="å
ç»´æ¤ææå¯è½çå·¥åºäº§åº" |
| | | description="å®é
å½¢æçä¸é´åæååæ«éå·¥åºçæåé½è¦å å
¥ï¼å¹¶å嫿宿¥å·¥åè¿å
¥çä»åºãåºåºååºä½ã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <ProductList |
| | | :form-type="formType" |
| | | :route-id="routeId" |
| | | @change="productList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 2" class="route-step-panel"> |
| | | <Alert |
| | | class="mb-4" |
| | | message="æå®é
ç产顺åºç¼æå·¥åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æåä¸éå·¥åºå¿
é¡»éæ©è·¯çº¿ç»å®çå
¨é¨æåãå¤äº§åå
±ç¨çåæåå½å±éâéç¨âï¼æåå¿
é¡»å½å±å°èªèº«äº§åã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <ProcessList |
| | | :form-type="formType" |
| | | :route-id="routeId" |
| | | @change="processList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-if="routeId" v-show="activeStep === 3" class="route-step-panel"> |
| | | <MaterialInputList |
| | | :form-type="formType" |
| | | :processes="sortedProcessList" |
| | | :products="productList" |
| | | :route-id="routeId" |
| | | @change="bomList = $event" |
| | | /> |
| | | </section> |
| | | |
| | | <section v-show="activeStep === 4" class="route-step-panel"> |
| | | <div class="review-heading"> |
| | | <div> |
| | | <h3>å¯ç¨åæ£æ¥</h3> |
| | | <p>确认产åºãä»ä½ãå·¥åºåææå½¢æéç¯åï¼ååå°å表å¯ç¨è·¯çº¿ã</p> |
| | | </div> |
| | | <Tag :color="reviewIssues.length ? 'warning' : 'success'"> |
| | | {{ reviewIssues.length ? `${reviewIssues.length} 项å¾
å¤ç` : 'é
ç½®å®æ´' }} |
| | | </Tag> |
| | | </div> |
| | | |
| | | <div class="review-metrics"> |
| | | <div><strong>{{ productsWithWarehouse }}/{{ productList.length }}</strong><span>产åä»ä½å®æ´</span></div> |
| | | <div><strong>{{ processesWithOutput }}</strong><span>é
置产åºçå·¥åº</span></div> |
| | | <div><strong>{{ productsWithBom }}/{{ productList.length }}</strong><span>产åå·²é
ç½®ææ</span></div> |
| | | </div> |
| | | |
| | | <div v-if="reviewIssues.length" class="issue-list" role="status"> |
| | | <button |
| | | v-for="issue in reviewIssues" |
| | | :key="issue.text" |
| | | class="issue-item" |
| | | type="button" |
| | | @click="activeStep = issue.step" |
| | | > |
| | | <IconifyIcon icon="lucide:circle-alert" /> |
| | | <span>{{ issue.text }}</span> |
| | | <IconifyIcon class="ml-auto" icon="lucide:chevron-right" /> |
| | | </button> |
| | | </div> |
| | | <Empty v-else description="ææå¿
éé
ç½®å已宿"> |
| | | <template #image> |
| | | <div class="review-success-icon"><IconifyIcon icon="lucide:badge-check" /></div> |
| | | </template> |
| | | </Empty> |
| | | </section> |
| | | </main> |
| | | </div> |
| | | |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center justify-between gap-3"> |
| | | <Button v-if="activeStep > 0" @click="activeStep--"> |
| | | <IconifyIcon class="mr-1" icon="lucide:arrow-left" />ä¸ä¸æ¥ |
| | | </Button> |
| | | <span v-else class="text-sm text-gray-500">æ°å»ºè·¯çº¿é»è®¤åç¨ï¼é
ç½®å®æ´ååå¯ç¨</span> |
| | | <Button type="primary" @click="handlePrimaryAction"> |
| | | {{ primaryText }} |
| | | <IconifyIcon |
| | | v-if="activeStep < stepItems.length - 1" |
| | | class="ml-1" |
| | | icon="lucide:arrow-right" |
| | | /> |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .route-config-shell { min-height: min(720px, 72vh); background: rgb(248 250 252); color: rgb(15 23 42); } |
| | | .route-config-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 18px 24px 14px; background: white; border-bottom: 1px solid rgb(226 232 240); } |
| | | .route-config-summary { display: flex; flex-shrink: 0; gap: 8px; } |
| | | .route-config-summary span { padding: 5px 10px; border-radius: 8px; background: rgb(239 246 255); color: rgb(29 78 216); font-size: 12px; font-weight: 600; } |
| | | .route-config-steps { padding: 12px 20px 0; background: white; } |
| | | .route-config-content { padding: 20px 24px 28px; } |
| | | .route-step-panel { min-height: 460px; padding: 22px; border-radius: 14px; background: white; box-shadow: 0 10px 28px rgb(15 23 42 / 6%); } |
| | | .route-step-copy, .review-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 22px; } |
| | | .route-step-copy { justify-content: flex-start; } |
| | | .route-step-copy > svg { width: 38px; height: 38px; padding: 9px; border-radius: 10px; background: rgb(219 234 254); color: rgb(29 78 216); } |
| | | .route-step-copy h3, .review-heading h3 { margin: 0; color: rgb(15 23 42); font-size: 17px; font-weight: 650; } |
| | | .route-step-copy p, .review-heading p { margin: 4px 0 0; color: rgb(71 85 105); font-size: 14px; } |
| | | .route-base-form { max-width: 1040px; margin: 34px auto 0; } |
| | | .review-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; margin: 24px 0; } |
| | | .review-metrics > div { display: flex; align-items: baseline; gap: 10px; padding: 18px; border-radius: 12px; background: rgb(248 250 252); } |
| | | .review-metrics strong { color: rgb(29 78 216); font-size: 24px; } |
| | | .review-metrics span { color: rgb(71 85 105); font-size: 13px; } |
| | | .issue-list { display: grid; gap: 10px; } |
| | | .issue-item { display: flex; width: 100%; min-height: 48px; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid rgb(253 230 138); border-radius: 10px; background: rgb(255 251 235); color: rgb(146 64 14); cursor: pointer; text-align: left; transition: background-color 180ms ease, border-color 180ms ease; } |
| | | .issue-item:hover, .issue-item:focus-visible { border-color: rgb(245 158 11); background: rgb(254 243 199); outline: none; } |
| | | .review-success-icon { display: grid; width: 72px; height: 72px; place-items: center; margin: 0 auto; border-radius: 50%; background: rgb(220 252 231); color: rgb(22 163 74); font-size: 38px; } |
| | | :global(.dark) .route-config-shell { background: rgb(15 23 42); color: rgb(226 232 240); } |
| | | :global(.dark) .route-config-header, :global(.dark) .route-config-steps, :global(.dark) .route-step-panel { background: rgb(30 41 59); border-color: rgb(51 65 85); } |
| | | :global(.dark) .route-step-copy h3, :global(.dark) .review-heading h3 { color: rgb(241 245 249); } |
| | | :global(.dark) .route-step-copy p, :global(.dark) .review-heading p, :global(.dark) .review-metrics span { color: rgb(203 213 225); } |
| | | :global(.dark) .review-metrics > div { background: rgb(15 23 42); } |
| | | @media (max-width: 900px) { |
| | | .route-config-header { align-items: flex-start; flex-direction: column; } |
| | | .review-metrics { grid-template-columns: 1fr; } |
| | | .route-step-panel { padding: 16px; } |
| | | } |
| | | </style> |
| | |
| | | width: 160, |
| | | }, |
| | | { |
| | | field: 'warehouseName', |
| | | title: 'æ¥å·¥ä»åº', |
| | | width: 130, |
| | | formatter: ({ cellValue, row }) => { |
| | | if (!cellValue) return '-'; |
| | | return row.areaName |
| | | ? `${cellValue}/${row.locationName ?? ''}/${row.areaName}` |
| | | : cellValue; |
| | | }, |
| | | }, |
| | | { |
| | | field: 'remark', |
| | | title: '夿³¨', |
| | | minWidth: 120, |
| | |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | | import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process'; |
| | | import { |
| | | createWorkOrderProcess, |
| | | getWorkOrderProcess, |
| | | updateWorkOrderProcess, |
| | | } from '#/api/mes/pro/workorder/process'; |
| | | import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse'; |
| | | import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area'; |
| | | import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location'; |
| | | import { $t } from '#/locales'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | |
| | |
| | | const workOrderId = ref<number>(); |
| | | const processList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]); |
| | | const isCreateMode = ref(false); |
| | | const allowedOutputItemIds = ref<number[]>([]); |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * è½½å
¥æ¬å·¥å产åå¯éç产åºç©æèå´ |
| | | * |
| | | * åå·¥èºè·¯çº¿æäº§åºå½å±è¿æ»¤åç产åºï¼éç¨åæå + æ¬äº§åä¸å±æåï¼ï¼ |
| | | * å¹¶å¹¶å
¥è¯¥å·¥åºå·²é
ç½®ç产åºï¼é¿å
å岿°æ®å¨éæ°éæ©æ¶è¢«è¯¯æ¸
空ã |
| | | */ |
| | | async function loadAllowedOutputItemIds( |
| | | productId?: number, |
| | | currentOutputItemIds: number[] = [], |
| | | ): Promise<number[]> { |
| | | if (!productId) { |
| | | return []; |
| | | } |
| | | const ids = new Set<number>([productId, ...currentOutputItemIds]); |
| | | const routeProcessList = await getRouteProcessListByProduct(productId); |
| | | (routeProcessList || []).forEach((process) => { |
| | | (process.outputItemIds || []).forEach((id) => { |
| | | if (id) { |
| | | ids.add(id); |
| | | } |
| | | }); |
| | | }); |
| | | return [...ids]; |
| | | } |
| | | |
| | | /** 表å schema */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | const processOptions = await loadProcessOptions(); |
| | | const [processOptions, warehouseList] = await Promise.all([ |
| | | loadProcessOptions(), |
| | | getWarehouseSimpleList(), |
| | | ]); |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | |
| | | }, |
| | | { |
| | | fieldName: 'workOrderId', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'warehouseName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'locationName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'areaName', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | |
| | | componentProps: { |
| | | multiple: true, |
| | | placeholder: 'è¯·éæ©äº§åºç©æ', |
| | | productIds: allowedOutputItemIds.value, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'warehouseId', |
| | | label: 'æ¥å·¥ä»åº', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: (warehouseList || []).map((item) => ({ |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })), |
| | | placeholder: 'é»è®¤åå·¥èºè·¯çº¿é
ç½®ï¼å¯ä¿®æ¹', |
| | | onChange: async (value: number) => { |
| | | const warehouse = (warehouseList || []).find( |
| | | (item) => item.id === value, |
| | | ); |
| | | await formApi.setFieldValue('warehouseName', warehouse?.name); |
| | | await formApi.setFieldValue('locationId', undefined); |
| | | await formApi.setFieldValue('locationName', undefined); |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | await formApi.setFieldValue('areaName', undefined); |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'locationId', |
| | | label: 'æ¥å·¥åºåº', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['warehouseId'], |
| | | disabled: (values) => !values.warehouseId, |
| | | async componentProps(values) { |
| | | const options = values.warehouseId |
| | | ? await getWarehouseLocationSimpleList(values.warehouseId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥åºåº', |
| | | onChange: async (value: number) => { |
| | | const location = options.find((item) => item.id === value); |
| | | await formApi.setFieldValue('locationName', location?.name); |
| | | await formApi.setFieldValue('areaId', undefined); |
| | | await formApi.setFieldValue('areaName', undefined); |
| | | }, |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'areaId', |
| | | label: 'æ¥å·¥åºä½', |
| | | component: 'Select', |
| | | dependencies: { |
| | | triggerFields: ['locationId'], |
| | | disabled: (values) => !values.locationId, |
| | | async componentProps(values) { |
| | | const options = values.locationId |
| | | ? await getWarehouseAreaSimpleList(values.locationId) |
| | | : []; |
| | | return { |
| | | allowClear: true, |
| | | fieldNames: { label: 'name', value: 'id' }, |
| | | options, |
| | | placeholder: 'è¯·éæ©æ¥å·¥åºä½', |
| | | onChange: async (value: number) => { |
| | | const area = options.find((item) => item.id === value); |
| | | await formApi.setFieldValue('areaName', area?.name); |
| | | }, |
| | | }; |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | formData.value = undefined; |
| | | processList.value = []; |
| | | isCreateMode.value = false; |
| | | allowedOutputItemIds.value = []; |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | const schema = await loadSchema(); |
| | | formApi.setState({ schema }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | |
| | | const data = modalApi.getData<{ |
| | | id?: number; |
| | | productId?: number; |
| | |
| | | processList.value = data.processList || []; |
| | | isCreateMode.value = !data.workOrderId; |
| | | |
| | | if (data.row) { |
| | | // ç¼è¾ä¸´æ¶å表ä¸çå·¥åº |
| | | formData.value = data.row; |
| | | await formApi.setValues(data.row); |
| | | } else if (data.id) { |
| | | // ç¼è¾å·²ä¿åçå·¥åº |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getWorkOrderProcess(data.id); |
| | | modalApi.lock(); |
| | | try { |
| | | // å
åå·¥åºæ°æ®ï¼ç¼è¾å·²ä¿åå·¥åºæ¶ç¨äºåæ¾ï¼ä¹ç¨äºä¿çå
¶å·²æäº§åº |
| | | const savedProcess = !data.row && data.id |
| | | ? await getWorkOrderProcess(data.id) |
| | | : undefined; |
| | | formData.value = data.row ?? savedProcess; |
| | | |
| | | allowedOutputItemIds.value = await loadAllowedOutputItemIds( |
| | | data.productId, |
| | | formData.value?.outputItemIds || [], |
| | | ); |
| | | formApi.setState({ schema: await loadSchema() }); |
| | | |
| | | if (formData.value) { |
| | | // ç¼è¾ä¸´æ¶å表ä¸çå·¥åº / ç¼è¾å·²ä¿åçå·¥åº |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } else { |
| | | // æ°å¢ |
| | | const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0)); |
| | | await formApi.setValues({ |
| | | colorCode: '#00AEF3', |
| | | sort: maxSort + 1, |
| | | }); |
| | | } |
| | | } else { |
| | | // æ°å¢ |
| | | formData.value = undefined; |
| | | const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0)); |
| | | await formApi.setValues({ |
| | | colorCode: '#00AEF3', |
| | | sort: maxSort + 1, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | |
| | | backflushFlag: item.backflushFlag, |
| | | outputItemIds: item.outputItemIds, |
| | | outputItemNames: item.outputItemNames, |
| | | warehouseId: item.warehouseId, |
| | | warehouseName: item.warehouseName, |
| | | locationId: item.locationId, |
| | | locationName: item.locationName, |
| | | areaId: item.areaId, |
| | | areaName: item.areaName, |
| | | remark: item.remark, |
| | | bomItems: item.bomItems, |
| | | })); |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: "productKeyword", |
| | | label: "产å", |
| | | component: "Input", |
| | | componentProps: { |
| | | allowClear: true, |
| | | maxLength: 100, |
| | | placeholder: "è¾å
¥äº§ååç§°æç¼ç ", |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: "status", |
| | | label: "ç¶æ", |
| | | component: "Select", |
| | |
| | | field: "createTime", |
| | | title: "å建æ¶é´", |
| | | width: 180, |
| | | formatter: 'formatDate', |
| | | formatter: "formatDateTime", |
| | | }, |
| | | { |
| | | title: "æä½", |
| | |
| | | fieldName: "yieldRate", |
| | | label: "å·¥åºæ¶ç", |
| | | component: "InputNumber", |
| | | help: "产åºé ÷ æå
¥éãå¡«å 0.98 表示 98%ï¼1 è¡¨ç¤ºæ æèï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã", |
| | | help: "产åºé ÷ æå
¥éãå¡«å 0.98 表示 98%ï¼æªå¡«åæ 100% åä¸æäº§è®¡ç®ã", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0.0001, |
| | |
| | | fieldName: "linkType", |
| | | label: "ä¸ä¸éå·¥åºå
³ç³»", |
| | | component: "Select", |
| | | help: () => |
| | | h( |
| | | "div", |
| | | { |
| | | style: |
| | | "white-space: pre-line; max-width: 320px; line-height: 1.6;", |
| | | }, |
| | | "0=å¼å§-å¼å§(SS)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½å¼å§ï¼å¯éå å¹¶è¡å¼å·¥ï¼\n1=ç»æ-ç»æ(FF)ï¼ä¸éå·¥åºå¿
é¡»çå°æ¬å·¥åºç»ææè½ç»æï¼å¹¶è¡ä½åæ¥å®å·¥ï¼\n2=å¼å§-ç»æ(SF)ï¼æ¬å·¥åºå¼å§åï¼ä¸éå·¥åºæè½ç»æ\n3=ç»æ-å¼å§(FS)ï¼æ¬å·¥åºç»æåï¼ä¸éå·¥åºæè½å¼å§ï¼é»è®¤å¼ï¼", |
| | | ), |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, "number"), |
| | | placeholder: "è¯·éæ©", |
| | | }, |
| | | rules: z.number().optional(), |
| | | rules: z.number().default(3), |
| | | }, |
| | | { |
| | | fieldName: "colorCode", |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** å·¥èºè·¯çº¿äº§å表åï¼multiple = trueï¼æ°å¢æ¨¡å¼ï¼æ¶äº§åå¯å¤éï¼ä¸æ¬¡æ§ç»å®å¤ä¸ª */ |
| | | export function useRouteProductFormSchema( |
| | | onItemChange?: (item: any) => void, |
| | | multiple = false, |
| | | ): VbenFormSchema[] { |
| | | /** å·¥èºè·¯çº¿äº§å表å */ |
| | | export function useRouteProductFormSchema(onItemChange?: (item: any) => void): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: "id", |
| | |
| | | component: "Input", |
| | | dependencies: { triggerFields: [""], show: () => false }, |
| | | }, |
| | | // æ°å¢ï¼å¤éï¼ï¼ä¸æ¬¡ç»å®å¤ä¸ªäº§åï¼ç¼è¾ï¼åéï¼ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | 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", |
| | | }, |
| | | // 产åç©æï¼ä½¿ç¨ä¸å¡èªå®ä¹éæ©å¨ï¼change åå¡«ç¼ç /åç§°/è§æ ¼/åä½ |
| | | { |
| | | fieldName: "itemId", |
| | | label: "产å", |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | onChange: onItemChange, |
| | | }, |
| | | formItemClass: "col-span-2", |
| | | rules: "selectRequired", |
| | | }, |
| | | { |
| | | fieldName: "quantity", |
| | | label: "ç产æ°é", |
| | |
| | | import { getDictLabel } from '@vben/hooks'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tooltip } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Tooltip } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteRoute, |
| | | exportRoute, |
| | | getRoutePage, |
| | | importRoute, |
| | | importRouteTemplate, |
| | | importCompleteRoute, |
| | | importCompleteRouteTemplate, |
| | | updateRouteStatus, |
| | | } from '#/api/mes/pro/route'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import ImportModalComponent from '#/components/import-modal'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | // å·¥èºè®¾è®¡ä¸ç产åºç¡æ°æ®å
±ç¨åä¸ä¸ªé
ç½®å¨ï¼é¿å
两å¥é¡µé¢è½åä¸ä¸è´ã |
| | | import Form from '#/views/mes/pro/route/modules/route-configurator.vue'; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // æ¯å¦å¯åæ¢ç¶æ |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportModalComponent, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** 导å
¥å·¥èºè·¯çº¿ */ |
| | | function handleImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | fileName: 'å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xls', |
| | | importFn: importRoute, |
| | | templateFn: importRouteTemplate, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | function handleCompleteImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: 'æ¹é导å
¥å®æ´å·¥èºè·¯çº¿', |
| | | fileName: '宿´å·¥èºè·¯çº¿å¯¼å
¥æ¨¡æ¿.xlsx', |
| | | importFn: importCompleteRoute, |
| | | templateFn: importCompleteRouteTemplate, |
| | | showUpdateSupport: false, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** å建工èºè·¯çº¿ */ |
| | |
| | | ): Promise<boolean | undefined> { |
| | | try { |
| | | await confirm( |
| | | `确认è¦å°"${row.name}"å·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | newStatus === CommonStatusEnum.ENABLE |
| | | ? `确认å¯ç¨â${row.name}âåï¼ç³»ç»å°æ£æ¥äº§åºå
¥åºä½ç½®ãå·¥åºäº§åºåå·¥åºæææ¯å¦å®æ´ã` |
| | | : `确认è¦å°â${row.name}âå·¥èºè·¯çº¿åæ¢ä¸ºã${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}ãåï¼`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height><FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Alert |
| | | class="mb-3" |
| | | message="路线é
置顺åº" |
| | | description="åºæ¬ä¿¡æ¯ â 产åºäº§ååå
¥åºä» â å·¥åºååå·¥åºäº§åº â å·¥åºææ â æ ¡éªå¯ç¨" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | | <Grid table-title="å·¥èºè·¯çº¿å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | auth: ['mes:pro-route:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['å·¥èºè·¯çº¿']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: 'æ¹é导å
¥å®æ´è·¯çº¿', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-route:import'], |
| | | onClick: handleCompleteImport, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | |
| | | 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 { confirm, useVbenModal } from '@vben/common-ui'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, Divider, message, Select } from 'ant-design-vue'; |
| | | import { message, TabPane, Tabs } 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 ProductForm from './product-form.vue'; |
| | | import Workbench from './workbench.vue'; |
| | | import ProcessList from './process-list.vue'; |
| | | import ProductList from './product-list.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-11/12"> |
| | | <Modal :title="getTitle" class="w-4/5"> |
| | | <Form class="mx-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)" |
| | | > |
| | | ç¼è¾äº§å |
| | | </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> |
| | | <!-- ç¼è¾/详æ
模å¼ä¸å±ç¤ºå表 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> |
| | | </Modal> |
| | | <ProductFormModal @success="loadProducts" /> |
| | | </template> |
| | | </template> |
| | |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 150, |
| | | labelWidth: 130, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Divider, 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 & { |
| | | itemIds?: number[]; |
| | | }; |
| | | // æäº¤è¡¨å |
| | | const data = |
| | | (await formApi.getValues()) as MesProRouteProductApi.RouteProduct; |
| | | try { |
| | | // ç¼è¾æ¨¡å¼ï¼æ´æ°å个产å |
| | | if (formData.value?.id) { |
| | | await updateRouteProduct(data); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | return; |
| | | // ç¨ææ°è¡¨åå¼åæ¥ formDataï¼ç¡®ä¿äº§å BOM å表ç»å®ç itemId ä¸è¡¨åä¸è´ |
| | | formData.value = { ...formData.value, ...data }; |
| | | } else { |
| | | // æ°å¢æåå忢å°ç¼è¾æ¨¡å¼ï¼æ¹ä¾¿ç»§ç»ç»´æ¤äº§å BOM |
| | | const id = await createRouteProduct(data); |
| | | formData.value = { ...data, id }; |
| | | await formApi.setFieldValue('id', id); |
| | | } |
| | | // æ°å¢æ¨¡å¼ï¼å¤é产å䏿¬¡æ§ç»å®ï¼è·³è¿å·²å¨è·¯çº¿ä¸ç产å |
| | | 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'); |
| | | } |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } 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) { |
| | | // æ°å¢æ¨¡å¼ï¼å¤é产åç»å® |
| | | 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, |
| | | }); |
| | | await formApi.setValues({ routeId: data.routeId }); |
| | | return; |
| | | } |
| | | // ç¼è¾æ¨¡å¼ï¼å è½½å个产ååå¡« |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getRouteProduct(data.id); |
| | | await formApi.setState({ |
| | | schema: useRouteProductFormSchema(handleItemChange, false), |
| | | }); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/5"> |
| | | <Modal :title="getTitle" class="w-3/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> |