Merge remote-tracking branch 'origin/dev_新疆_新高电力' into dev_新疆_新高电力
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # Product |
| | | |
| | | <!-- impeccable:product-schema 1 --> |
| | | |
| | | ## Platform |
| | | |
| | | web |
| | | |
| | | ## Users |
| | | |
| | | - ç产工èºã计åå车é´ç®¡ç人å卿¡é¢ç«¯ç»´æ¤ç产åºç¡æ°æ®ã |
| | | - ç¨æ·éè¦å¨è¾å°å¹è®çæ
åµä¸å®æå·¥èºè·¯çº¿ã产åºäº§åãå
¥åºä½ç½®åå·¥åºäº§åºçé
ç½®ã |
| | | |
| | | ## Product Purpose |
| | | |
| | | æ°é«çµåæºé å¹³å°ç¨äºè´¯é产åãå·¥èºãç产ãè´¨éåä»å¨ä¸å¡ãå·¥èºè·¯çº¿é
ç½®æåçæ åï¼æ¯ç¨æ·è½æç¡®ç¥éæ¯éå·¥åºäº§åºä»ä¹ï¼ä»¥ååæ ¼äº§åºæç»è¿å
¥åªä¸ªä»åºãåºåºååºä½ã |
| | | |
| | | ## Operating Context |
| | | |
| | | - 䏿¡å·¥èºè·¯çº¿å¯å
³èå¤ä¸ªæç»äº§åã |
| | | - æ¯ä¸ªæç»äº§åå¿
é¡»é
ç½®æ¥å·¥å¯¹åºçä»åºãåºåºååºä½ã |
| | | - ä¸é´å·¥åºäº§åºåæåï¼æåä¸éå·¥åºå¿
须产åºè·¯çº¿ç»å®çå
¨é¨æåã |
| | | - 路线åç¨æ¶æå
è®¸ä¿®æ¹æå é¤ï¼é
ç½®å®æ´åå¨å表ä¸å¯ç¨ã |
| | | |
| | | ## Capabilities and Constraints |
| | | |
| | | - ææ¯æ 为 Vue 3ãTypeScriptãVben AdminãAnt Design Vue å VXE Tableã |
| | | - ä¿çç°æå端æ¥å£åä¸å¡æ ¡éªï¼ä¸æ¹åæ°æ®æ¨¡åã |
| | | - 页é¢åºå
¼å®¹äº®è²ãæè²ä¸»é¢åå¸¸è§æ¡é¢å辨çã |
| | | - å½åäºå®æ¥èªç¨æ·æç¡®éæ±åç°æä»£ç ï¼æ¯å¦éè¦ç§»å¨ç«¯ç»´æ¤å·¥èºè·¯çº¿å°æªç¡®è®¤ã |
| | | |
| | | ## Brand Commitments |
| | | |
| | | - 产ååç§°ä¸ºâæ°é«çµåæºé å¹³å°âã |
| | | - 沿ç¨å½åç³»ç»çèè²ä¸»è²ãAnt Design æ§ä»¶å䏿ä¸å¡æ¯è¯ã |
| | | |
| | | ## Evidence on Hand |
| | | |
| | | - ç°æå·¥èºè·¯çº¿é¡µé¢åæ¥å£ä½äº `src/views/mes/pro/route` ä¸ `src/api/mes/pro/route`ã |
| | | - å端已å®ç°å·¥èºè·¯çº¿äº§åä»ä½åå·¥åºäº§åºå®æ´æ§æ ¡éªã |
| | | - 没æç¬ç«ç¨æ·ç ç©¶æææå¯ç¨æ§æµè¯å½åï¼ä¸èæéåææã |
| | | |
| | | ## Product Principles |
| | | |
| | | - ä¸å¡é¡ºåºå°±æ¯çé¢é¡ºåºï¼å
产ååå
¥åºä½ç½®ï¼åå·¥åºå产åºï¼æåæäº§åä¸å·¥åºé
ç½®ææã |
| | | - å¨ç¨æ·æäº¤åè§£éè§åï¼å¹¶å¨å½åæ¥éª¤å°±è¿æ´é²ç¼ºå¤±é¡¹ã |
| | | - éè¦ç¶æåæ¶ä½¿ç¨æåã徿 åé¢è²è¡¨è¾¾ã |
| | | - é»è®¤æµç¨æå¡æ®éä¸å¡ç¨æ·ï¼é«çº§åæ®µä¸æ¢å ä¸»è¦æä½è·¯å¾ã |
| | | |
| | | ## Accessibility & Inclusion |
| | | |
| | | - 交äºç®æ åºæ»¡è¶³é®çæä½åæ¸
æ°ç¦ç¹ç¶æã |
| | | - æ£æä¸æ§ä»¶æåä¿æå¯è¯»å¯¹æ¯åº¦ï¼ä¸è½ä»
ä¾èµé¢è²åºåå®æç¶æã |
| | |
| | | const pageMap: ComponentRecordType = import.meta.glob([ |
| | | '../views/**/*.vue', |
| | | '!../views/ai/**/*.vue', |
| | | '!../views/dashboard/analytics/modules/ai-insights.vue', |
| | | '!../views/mes/**/modules/ai/**/*.vue', |
| | | '!../views/mes/qc/components/ai-*.vue', |
| | | '!../views/crm/saleQuotation/modules/ocr-upload.vue', |
| | | ]); |
| | | const accessStore = useAccessStore(); |
| | | |
| | |
| | | |
| | | import { mergeRouteModules, traverseTreeValues } from '@vben/utils'; |
| | | |
| | | import { isAiFeatureEnabled } from '#/utils/features'; |
| | | |
| | | import { coreRoutes, fallbackNotFoundRoute } from './core'; |
| | | |
| | | const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', { |
| | | const dynamicRouteFiles = import.meta.glob([ |
| | | './modules/**/*.ts', |
| | | '!./modules/ai.ts', |
| | | ], { |
| | | eager: true, |
| | | }); |
| | | |
| | |
| | | |
| | | // add by èè¿ï¼from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/index.ts#L38-L45 |
| | | const componentKeys: string[] = Object.keys( |
| | | import.meta.glob('../../views/**/*.vue'), |
| | | import.meta.glob([ |
| | | '../../views/**/*.vue', |
| | | '!../../views/**/modules/**', |
| | | '!../../views/ai/**', |
| | | ]), |
| | | ) |
| | | .filter((item) => !item.includes('/modules/')) |
| | | .filter((item) => isAiFeatureEnabled || !item.includes('/views/ai/')) |
| | | .map((v) => { |
| | | const path = v.replace('../../views/', '/'); |
| | | return path.endsWith('.vue') ? path.slice(0, -4) : path; |
| | |
| | | |
| | | import { defineAsyncComponent } from 'vue'; |
| | | |
| | | const modules = import.meta.glob('../views/**/*.{vue,tsx}'); |
| | | const modules = import.meta.glob([ |
| | | '../views/**/*.{vue,tsx}', |
| | | '!../views/ai/**', |
| | | '!../views/dashboard/analytics/modules/ai-insights.vue', |
| | | '!../views/mes/**/modules/ai/**', |
| | | '!../views/mes/qc/components/ai-*.vue', |
| | | '!../views/crm/saleQuotation/modules/ocr-upload.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 preSelectedIds = ref<number[]>([]); // é¢éç©æç¼å·å表 |
| | | const productIds = ref<number[]>([]); // éå®å¯éçç©æç¼å·å表ï¼ä¸ºç©ºåä¸éå¶ï¼ |
| | | const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); // ç©æåç±»æ |
| | | const limitedToProducts = computed(() => productIds.value.length > 0); |
| | | const dialogTitle = computed(() => |
| | | limitedToProducts.value ? "路线产åºéæ©" : "ç©æäº§åéæ©", |
| | | ); |
| | | const tableTitle = computed(() => |
| | | limitedToProducts.value ? "å·²é
ç½®ç产åºäº§å" : "ç©æäº§åå表", |
| | | ); |
| | | |
| | | /** è·åå½åè¡¨æ ¼æ°æ® */ |
| | | function getTableRows() { |
| | |
| | | }; |
| | | // éå®å¯éç©æèå´ |
| | | if (productIds.value.length > 0) { |
| | | params.pageSize = 1000; |
| | | params.ids = productIds.value.join(','); |
| | | } |
| | | const result = await getItemPage(params as never); |
| | |
| | | |
| | | <template> |
| | | <Modal v-model:open="open" |
| | | title="ç©æäº§åéæ©" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | :destroy-on-close="true" |
| | | @ok="handleConfirm" |
| | | @cancel="closeModal"> |
| | | <div class="flex h-[60vh] w-full"> |
| | | <Card |
| | | v-if="!limitedToProducts" |
| | | class="mr-4 h-full w-1/5" |
| | | :body-style="{ height: '100%', overflow: 'hidden' }" |
| | | > |
| | | <MdItemTypeTree ref="typeTreeRef" |
| | | @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <div class="w-4/5"> |
| | | <Grid table-title="ç©æäº§åå表" /> |
| | | <div :class="limitedToProducts ? 'w-full' : 'w-4/5'"> |
| | | <Alert |
| | | v-if="limitedToProducts" |
| | | class="mb-3" |
| | | message="ä»
å¯éæ©æ¬è·¯çº¿å·²é
ç½®ç产åºäº§å" |
| | | description="å¦éå¢å å¯é项ï¼è¯·è¿åâ产åºäº§ååå
¥åºä»âæ¥éª¤å
æ·»å 产åºã" |
| | | show-icon |
| | | type="info" |
| | | > |
| | | <template #action> |
| | | <Tag color="blue">{{ productIds.length }} 个产åº</Tag> |
| | | </template> |
| | | </Alert> |
| | | <Grid :table-title="tableTitle" /> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'outputItemIds', |
| | | label: 'æ¬å·¥åºäº§åº', |
| | | component: markRaw(MdItemSelect), |
| | | componentProps: { |
| | | multiple: true, |
| | | productIds, |
| | | placeholder: 'å¯ç空ï¼å½¢æåæåæ¶éæ©ï¼æ«éå·¥åºéæ©å
¨é¨æå', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | | fieldName: 'linkType', |
| | | label: 'ä¸ä¸éå·¥åºå
³ç³»', |
| | | component: 'Select', |
| | |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'outputItemIds', |
| | | label: '产åºäº§å', |
| | | component: markRaw(MdItemSelect), |
| | | fieldName: 'backflushFlag', |
| | | label: 'æ¥å·¥èªå¨æ£æ', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | multiple: true, |
| | | productIds, |
| | | placeholder: 'ä¸é´å·¥åºéåæåï¼æ«éå·¥åºéå
¨é¨æå', |
| | | checkedChildren: 'èªå¨', |
| | | unCheckedChildren: 'æå¨', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | rules: z.boolean().default(false), |
| | | }, |
| | | { |
| | | fieldName: 'prepareTime', |
| | |
| | | { 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: 190 }, |
| | | { 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 }, |
| | | { field: 'outputItemNames', title: '产åºäº§å', minWidth: 180 }, |
| | | { |
| | | field: 'colorCode', |
| | | title: 'çç¹å¾é¢è²', |
| | |
| | | /** å·¥èºè·¯çº¿äº§ååè¡¨åæ®µ */ |
| | | 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: 130 }, |
| | | { field: 'locationName', title: 'åºåº', width: 120 }, |
| | | { field: 'areaName', title: 'åºä½', width: 120 }, |
| | | { 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 }, |
| | |
| | | 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 { |
| | |
| | | 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']); // æ¯å¦å¯åæ¢ç¶æ |
| | |
| | | ): 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 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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'; |
| | |
| | | |
| | | <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>(); |
| | |
| | | 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 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> |
| | |
| | | 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 { |
| | |
| | | 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']); // æ¯å¦å¯åæ¢ç¶æ |
| | |
| | | ): 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 |