feat(mes): 完善工艺路线产出配置功能
- 添加产出物料归属选择组件,支持通用和指定产品归属
- 在销售订单中增加已取消状态显示和筛选功能
- 优化工作台界面,显示工作站待报工任务数量
- 在工单工序中添加仓库、库区、库位配置字段
- 更新百度地图密钥配置,修复HTTP访问定位问题
- 重构工序产出配置逻辑,支持更精确的物料归属管理
- 添加工艺路线配置验证,确保末道工序成品归属正确
- 优化物料选择对话框界面布局和交互体验
| | |
| | | # VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ== |
| | | |
| | | # ç¾åº¦å°å¾ |
| | | VITE_BAIDU_MAP_KEY=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN |
| | | VITE_BAIDU_MAP_KEY=5YhFUgsoi8KMm6tafbSljgz9NNpOYP1Z |
| | |
| | | contractId?: number; // å
³è CRM ååç¼å· |
| | | contractNo?: string; // CRM åååå· |
| | | needProduction?: number; // æ¯å¦éè¦ç产ï¼0-ä¸éè¦ï¼1-éè¦ |
| | | productionStatus?: number; // çäº§ç¶æï¼0-æªå®æï¼1-已宿 |
| | | productionStatus?: number; // çäº§ç¶æï¼0-æªå®æï¼1-已宿ï¼2-已忶 |
| | | productionFinishTime?: Date; // çäº§å®ææ¶é´ |
| | | hasSalesNotice?: boolean; // æ¯å¦å·²çæåè´§éç¥å |
| | | canCreateSalesNotice?: boolean; // æ¯å¦å¯ä»¥çæåè´§éç¥å |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æå·¥ä½ç«æ¥è¯¢å¾
æ¥å·¥ä»»å¡æ°ï¼key 为工ä½ç«ç¼å·ï¼ */ |
| | | export function getPendingFeedbackCountByWorkstation() { |
| | | return requestClient.get<Record<string, number>>( |
| | | '/mes/pro/task/pending-count-by-workstation', |
| | | ); |
| | | } |
| | | |
| | | /** æ ¹æ®å·¥ä½ç«åæ°é计ç®ç产æ¶é¿ */ |
| | | export function calculateTaskDuration(workstationId: number, quantity: number) { |
| | | return requestClient.get<{ duration: number }>( |
| | |
| | | outputItemName?: string; // 产åºäº§åç©æåç§°ï¼ç¬¬ä¸ä¸ªï¼ |
| | | outputItemIds?: number[]; // 产åºäº§åç©æç¼å·å表 |
| | | outputItemNames?: string; // 产åºäº§åç©æåç§°ï¼éå·æ¼æ¥ï¼ |
| | | warehouseId?: number; // æ¥å·¥ä»åºç¼å· |
| | | warehouseName?: string; // æ¥å·¥ä»åºåç§° |
| | | locationId?: number; // æ¥å·¥åºåºç¼å· |
| | | locationName?: string; // æ¥å·¥åºåºåç§° |
| | | areaId?: number; // æ¥å·¥åºä½ç¼å· |
| | | areaName?: string; // æ¥å·¥åºä½åç§° |
| | | bomItems?: ProcessBomItem[]; // å·¥åºææBOMæç» |
| | | remark?: string; // 夿³¨ |
| | | } |
| | |
| | | { label: 'æ éç产', value: -1 }, |
| | | { label: 'æªå®æ', value: 0 }, |
| | | { label: '已宿', value: 1 }, |
| | | { label: '已忶', value: 2 }, |
| | | ], |
| | | placeholder: 'è¯·éæ©çäº§ç¶æ', |
| | | allowClear: true, |
| | |
| | | <template v-else> |
| | | <Tag v-if="row.productionStatus === 0" color="warning">æªå®æ</Tag> |
| | | <Tag v-else-if="row.productionStatus === 1" color="success">已宿</Tag> |
| | | <Tag v-else-if="row.productionStatus === 2" color="error">已忶</Tag> |
| | | </template> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | |
| | | <template #productionStatus="{ modelValue }"> |
| | | <Tag v-if="modelValue === 0" color="warning">æªå®æ</Tag> |
| | | <Tag v-else-if="modelValue === 1" color="success">已宿</Tag> |
| | | <Tag v-else-if="modelValue === 2" color="error">已忶</Tag> |
| | | </template> |
| | | <template #items> |
| | | <ItemForm |
| | |
| | | const env = (import.meta as any).env ?? {}; |
| | | const baiduMapKey = String(env.VITE_BAIDU_MAP_KEY ?? '').trim(); |
| | | const hasMapConfig = computed(() => Boolean(baiduMapKey)); |
| | | // æµè§å¨ä»
å¨å®å
¨ä¸ä¸æï¼HTTPS æ localhostï¼æ¾å¼å®ä½è½åï¼HTTP 访é®ä¸æé®ç¹äºå¿
ç¶å¤±è´¥ |
| | | const canUseBrowserLocation = computed( |
| | | () => Boolean(navigator.geolocation) && window.isSecureContext, |
| | | ); |
| | | |
| | | function outOfChina(longitude: number, latitude: number) { |
| | | return longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271; |
| | |
| | | placeholder="è¾å
¥çãå¸ãåºåå
·ä½å°ç¹ï¼ä¾å¦ï¼ä¹é²æ¨é½å¸æ°é«åååº" |
| | | @search="handleSearch" |
| | | /> |
| | | <a-button @click="handleCurrentLocation">å®ä½å°å½åä½ç½®</a-button> |
| | | <a-button v-if="canUseBrowserLocation" @click="handleCurrentLocation">å®ä½å°å½åä½ç½®</a-button> |
| | | </div> |
| | | <p |
| | | v-if="!disabled && !canUseBrowserLocation" |
| | | class="mb-2 text-xs text-gray-500" |
| | | > |
| | | å½åéè¿ HTTP 访é®ï¼æµè§å¨ä¸å
许ç½é¡µè·åä½ç½®ã请ç¨ãæç´¢å°åãæç´æ¥ç¹å»å°å¾éç¹ï¼å¦éç½é¡µå®ä½è¯·æ¹ç¨ HTTPS 访é®ã |
| | | </p> |
| | | <div ref="mapContainer" class="h-[360px] w-full overflow-hidden rounded border border-gray-200"></div> |
| | | <div class="mt-2 flex flex-wrap justify-between gap-2 text-xs text-gray-500"> |
| | | <span>{{ disabled ? 'æå¡ä½ç½®åå
许èå´' : 'ç¹å»å°å¾éæ©æå¡ä¸å¿ç¹ï¼èè²åå表示å
许æå¡èå´' }}</span> |
| | |
| | | }, |
| | | gridOptions: { |
| | | columns: useItemSelectGridColumns(true), |
| | | height: 560, |
| | | height: '100%', |
| | | keepSource: true, |
| | | checkboxConfig: { |
| | | highlight: true, |
| | |
| | | :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' }" |
| | | <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" |
| | | > |
| | | <MdItemTypeTree ref="typeTreeRef" |
| | | @node-click="handleItemTypeNodeClick" /> |
| | | </Card> |
| | | <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> |
| | | <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' }" |
| | | > |
| | | <template #action> |
| | | <Tag color="blue">{{ productIds.length }} 个产åº</Tag> |
| | | </template> |
| | | </Alert> |
| | | <Grid :table-title="tableTitle" /> |
| | | <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 && 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 |
| | | :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> |
| | |
| | | MdProductBomSelect, |
| | | } from '#/views/mes/md/item/components'; |
| | | |
| | | import { RouteColorPicker } from './components'; |
| | | import { RouteColorPicker, RouteOutputAttribution } from './components'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'update'; |
| | |
| | | /** å·¥èºè·¯çº¿å·¥åºæç»è¡¨å */ |
| | | export function useRouteProcessFormSchema( |
| | | processOptions: Array<{ label: string; value: number }>, |
| | | productIds: number[] = [], |
| | | products: MesProRouteProductApi.RouteProduct[] = [], |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'outputItemIds', |
| | | fieldName: 'outputs', |
| | | label: 'æ¬å·¥åºäº§åº', |
| | | component: markRaw(MdItemSelect), |
| | | component: markRaw(RouteOutputAttribution), |
| | | componentProps: { |
| | | multiple: true, |
| | | productIds, |
| | | placeholder: 'å¯ç空ï¼å½¢æåæåæ¶éæ©ï¼æ«éå·¥åºéæ©å
¨é¨æå', |
| | | products, |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | |
| | | { 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: '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', |
| | |
| | | value: item.id!, |
| | | })); |
| | | const products = routeId ? await getRouteProductListByRoute(routeId) : []; |
| | | return useRouteProcessFormSchema( |
| | | options, |
| | | products.map((item) => item.itemId!).filter(Boolean), |
| | | ); |
| | | return useRouteProcessFormSchema(options, products); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="æå®é
ç产èç¹é
置产åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æ«éå·¥åºå¿
须鿩å
¨é¨æåãå¦å¼å¯æ¥å·¥èªå¨æ£æï¼æ¥å·¥æ¶ä¼æå·¥åºææé
ç½®æ£ååºåã" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æ«éå·¥åºå¿
须鿩å
¨é¨æåãå½å±è¡¨ç¤ºè¯¥äº§åºå±äºåªä¸ªäº§åï¼å¤äº§åå
±ç¨çåæåéâéç¨âï¼äº§åä¸å±ï¼å°¤å
¶æ¯æåï¼å½å±å°èªèº«äº§åãå¦å¼å¯æ¥å·¥èªå¨æ£æï¼æ¥å·¥æ¶ä¼æå·¥åºææé
ç½®æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | |
| | | 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)), |
| | |
| | | <Alert |
| | | class="mb-4" |
| | | message="æå®é
ç产顺åºç¼æå·¥åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æåä¸éå·¥åºå¿
é¡»éæ©è·¯çº¿ç»å®çå
¨é¨æåã" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æåä¸éå·¥åºå¿
é¡»éæ©è·¯çº¿ç»å®çå
¨é¨æåãå¤äº§åå
±ç¨çåæåå½å±éâéç¨âï¼æåå¿
é¡»å½å±å°èªèº«äº§åã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | |
| | | 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, |
| | | })); |
| | |
| | | import { Input, Modal, Spin } from 'ant-design-vue'; |
| | | |
| | | import { getWorkstationPage } from '#/api/mes/md/workstation'; |
| | | import { getPendingFeedbackCountByWorkstation } from '#/api/mes/pro/task'; |
| | | |
| | | defineOptions({ name: 'WorkbenchWorkstationSelectDialog' }); |
| | | |
| | |
| | | const loading = ref(false); |
| | | const workstations = ref<MesMdWorkstationApi.Workstation[]>([]); |
| | | const searchKeyword = ref(''); |
| | | /** å·¥ä½ç«å¾
æ¥å·¥ä»»å¡æ°ï¼key 为工ä½ç«ç¼å·ï¼null 表示ç»è®¡å 载失败ï¼ä¸å±ç¤º */ |
| | | const pendingCounts = ref<null | Record<string, number>>(null); |
| | | |
| | | /** æå¼å¼¹çª */ |
| | | async function open() { |
| | |
| | | async function loadWorkstations() { |
| | | loading.value = true; |
| | | try { |
| | | const result = await getWorkstationPage({ pageNo: 1, pageSize: 200 }); |
| | | const [result, counts] = await Promise.all([ |
| | | getWorkstationPage({ pageNo: 1, pageSize: 200 }), |
| | | getPendingFeedbackCountByWorkstation().catch(() => null), |
| | | ]); |
| | | workstations.value = result.list ?? []; |
| | | pendingCounts.value = counts; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** 该工ä½ç«çå¾
宿ï¼å¾
æ¥å·¥ï¼ä»»å¡æ° */ |
| | | function getPendingCount(ws: MesMdWorkstationApi.Workstation) { |
| | | return pendingCounts.value?.[String(ws.id)] ?? 0; |
| | | } |
| | | |
| | | /** è¿æ»¤åçå·¥ä½ç« */ |
| | |
| | | {{ ws.processName }} |
| | | </span> |
| | | </div> |
| | | <div class="ws-card__name">{{ ws.name }}</div> |
| | | <div class="ws-card__name-row"> |
| | | <span class="ws-card__name">{{ ws.name }}</span> |
| | | <span |
| | | v-if="pendingCounts" |
| | | class="ws-card__pending" |
| | | :class="{ 'ws-card__pending--empty': getPendingCount(ws) === 0 }" |
| | | :title="`该工ä½ç«å¾
æ¥å·¥ä»»å¡æ°ï¼${getPendingCount(ws)}`" |
| | | > |
| | | <IconifyIcon icon="lucide:list-todo" class="ws-card__pending-icon" /> |
| | | å¾
宿 {{ getPendingCount(ws) }} |
| | | </span> |
| | | </div> |
| | | <div class="ws-card__info"> |
| | | <div v-if="ws.workshopName" class="ws-card__info-row"> |
| | | <IconifyIcon icon="lucide:building-2" class="ws-card__info-icon" /> |
| | |
| | | color: #fff; |
| | | } |
| | | |
| | | .ws-card__name-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .ws-card__name { |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1e3a5f; |
| | | line-height: 1.3; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .ws-card__pending { |
| | | display: inline-flex; |
| | | flex-shrink: 0; |
| | | align-items: center; |
| | | gap: 4px; |
| | | padding: 3px 9px; |
| | | border: 1px solid #fdba74; |
| | | border-radius: 999px; |
| | | background: #fff7ed; |
| | | color: #c2410c; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | font-variant-numeric: tabular-nums; |
| | | line-height: 1.4; |
| | | |
| | | &--empty { |
| | | border-color: #cbd5e1; |
| | | background: rgb(255 255 255 / 55%); |
| | | color: #64748b; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | |
| | | .ws-card__pending-icon { |
| | | flex-shrink: 0; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .ws-card__info { |
| | |
| | | |
| | | async function getAlignedData() { |
| | | const data = (await formApi.getValues()) as any; |
| | | // æ°å»ºæ¶ä¸è½æºå¸¦ä¸ä¸å¼ æ¥å·¥åæ®çç idï¼å¦åå端æä¸»é®æå
¥ä¼å²çª |
| | | if (formType.value === 'create') { |
| | | delete data.id; |
| | | } |
| | | alignQuantity(data); |
| | | return data; |
| | | } |
| | |
| | | itemSpecification: task.itemSpecification, |
| | | unitMeasureName: task.unitMeasureName, |
| | | scheduledQuantity: task.quantity, |
| | | reportableQuantity: task.reportableQuantity, |
| | | checkFlag, |
| | | }); |
| | | |