feat(mobile): 新增多个移动端功能模块并优化扫码流程
- 添加生产领料、其他出库、库存移库、盘点任务、销售退货、生产退料等6个移动端功能
- 集成TraceScanInput组件替换原有手动输入方式,实现扫码自动识别
- 新增traceQuantity和findStocks工具函数用于追溯对象数量计算和库存查询
- 实现扫码带出功能,在多个出入库单据中支持扫码自动生成明细
- 移动端路由配置中增加新功能页面映射
- 优化移动端访问权限控制和功能可用性检查逻辑
- 更新物料库存API接口参数,支持批次号、物料编号等查询条件
- 在各明细列表组件中集成TraceScanPanel实现扫码操作面板
| | |
| | | |
| | | /** åºåå°è´¦å页æ¥è¯¢åæ° */ |
| | | export interface MaterialStockPageParams extends PageParam { |
| | | warehouseType?: number; // ä»åºç±»åï¼10æååºï¼20åæ ¼åºï¼æç±»åè¿æ»¤ä»åºï¼ |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | batchId?: number; // æ¹æ¬¡ç¼å· |
| | | frozen?: boolean; // æ¯å¦å»ç» |
| | | itemId?: number; // ç©æç¼å· |
| | | stockState?: number; // åºåç¶æï¼10æååºï¼20åæ ¼åºï¼30å·²åºåº |
| | | virtualFilter?: string; // èæä»è¿æ»¤æ¨¡å¼ï¼exclude æé¤èæä»ï¼only åªçèæä» |
| | | warehouseType?: number; // ä»åºç±»åï¼10æååºï¼20åæ ¼åºï¼æç±»åè¿æ»¤ä»åºï¼ |
| | | } |
| | | |
| | | /** æ¥è¯¢åºåå°è´¦å页 */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { Button, Input } from 'ant-design-vue'; |
| | | |
| | | import { extractCode } from '#/utils/traceCode'; |
| | | |
| | | /** |
| | | * 追溯äºç»´ç æ«ç è¾å
¥ã |
| | | * |
| | | * æ¯ææå头æ«ç ãæ«ç æªé®çè¾å
¥åæå¨è¾å
¥ä¸ç§æ¹å¼ï¼ |
| | | * è¯å«ç»æç»ä¸ç»è¿ extractCode æåã追溯ç½å?code=ç å¼ãä¸çç å¼ååæåºã |
| | | */ |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | continuous?: boolean; |
| | | disabled?: boolean; |
| | | placeholder?: string; |
| | | }>(), |
| | | { |
| | | continuous: false, |
| | | disabled: false, |
| | | placeholder: 'æ«æè¿½æº¯äºç»´ç æè¾å
¥è¿½æº¯ç ï¼å车è¯å«', |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ code: [value: string] }>(); |
| | | |
| | | const input = ref(''); |
| | | const error = ref(''); |
| | | const active = ref(false); |
| | | const starting = ref(false); |
| | | const video = ref<HTMLVideoElement>(); |
| | | const inputRef = ref<{ focus: () => void }>(); |
| | | let stream: MediaStream | undefined; |
| | | let frame = 0; |
| | | let generation = 0; |
| | | let last = 0; |
| | | |
| | | function stop() { |
| | | generation++; |
| | | active.value = false; |
| | | starting.value = false; |
| | | cancelAnimationFrame(frame); |
| | | stream?.getTracks().forEach((track) => track.stop()); |
| | | stream = undefined; |
| | | if (video.value) video.value.srcObject = null; |
| | | } |
| | | |
| | | function accept(raw: string) { |
| | | if (props.disabled) return; |
| | | try { |
| | | const code = extractCode(raw); |
| | | error.value = ''; |
| | | stop(); |
| | | input.value = props.continuous ? '' : code; |
| | | emit('code', code); |
| | | if (props.continuous) { |
| | | void nextTick(() => inputRef.value?.focus()); |
| | | } |
| | | } catch (e) { |
| | | error.value = e instanceof Error ? e.message : 'è¿½æº¯ç æ ¼å¼ä¸æ£ç¡®'; |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => props.continuous, |
| | | async (continuous) => { |
| | | if (!props.disabled && continuous) { |
| | | await nextTick(); |
| | | inputRef.value?.focus(); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | async function start() { |
| | | error.value = ''; |
| | | stop(); |
| | | const ticket = generation; |
| | | starting.value = true; |
| | | try { |
| | | if (!window.isSecureContext || !navigator.mediaDevices?.getUserMedia) { |
| | | throw new Error('æå头éè¦ HTTPS åç¸æºææï¼è¯·ä½¿ç¨æå¨è¾å
¥'); |
| | | } |
| | | const decode = (await import('jsqr')).default; |
| | | const media = await navigator.mediaDevices.getUserMedia({ |
| | | audio: false, |
| | | video: { facingMode: { ideal: 'environment' } }, |
| | | }); |
| | | if (ticket !== generation) { |
| | | media.getTracks().forEach((track) => track.stop()); |
| | | return; |
| | | } |
| | | stream = media; |
| | | active.value = true; |
| | | starting.value = false; |
| | | await new Promise<void>((resolve) => requestAnimationFrame(() => resolve())); |
| | | if (!video.value || ticket !== generation) { |
| | | stop(); |
| | | return; |
| | | } |
| | | video.value.srcObject = media; |
| | | await video.value.play(); |
| | | const canvas = document.createElement('canvas'); |
| | | const ctx = canvas.getContext('2d', { willReadFrequently: true }); |
| | | function tick(time: number) { |
| | | if (ticket !== generation || !active.value) return; |
| | | const element = video.value; |
| | | if ( |
| | | element && |
| | | ctx && |
| | | element.readyState >= 2 && |
| | | time - last > 180 && |
| | | !props.disabled |
| | | ) { |
| | | last = time; |
| | | canvas.width = Math.min(element.videoWidth, 640); |
| | | canvas.height = Math.round( |
| | | (element.videoHeight * canvas.width) / element.videoWidth, |
| | | ); |
| | | ctx.drawImage(element, 0, 0, canvas.width, canvas.height); |
| | | const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| | | const result = decode(pixels.data, canvas.width, canvas.height); |
| | | if (result?.data) { |
| | | accept(result.data); |
| | | return; |
| | | } |
| | | } |
| | | frame = requestAnimationFrame(tick); |
| | | } |
| | | frame = requestAnimationFrame(tick); |
| | | } catch (e) { |
| | | if (ticket !== generation) return; |
| | | stop(); |
| | | const name = e instanceof DOMException ? e.name : ''; |
| | | error.value = |
| | | name === 'NotAllowedError' |
| | | ? 'ç¸æºæéæªå¼å¯ï¼è¯·å¨æµè§å¨è®¾ç½®ä¸å
è®¸ç¸æºï¼ææå¨è¾å
¥ç å¼' |
| | | : name === 'NotFoundError' |
| | | ? 'æªæ¾å°å¯ç¨æå头ï¼è¯·æå¨è¾å
¥æä½¿ç¨æ«ç 设å¤' |
| | | : name === 'NotReadableError' |
| | | ? 'æå头æ£è¢«å
¶ä»ç¨åºä½¿ç¨ï¼è¯·å
³éåéè¯ææå¨è¾å
¥' |
| | | : e instanceof Error |
| | | ? e.message |
| | | : 'ç¸æºä¸å¯ç¨ï¼è¯·æ£æ¥æéææå¨è¾å
¥'; |
| | | } |
| | | } |
| | | |
| | | function visibility() { |
| | | if (document.hidden) stop(); |
| | | } |
| | | |
| | | onMounted(() => document.addEventListener('visibilitychange', visibility)); |
| | | onBeforeUnmount(() => { |
| | | stop(); |
| | | document.removeEventListener('visibilitychange', visibility); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="trace-scan-input"> |
| | | <div class="flex items-center gap-2"> |
| | | <Input |
| | | ref="inputRef" |
| | | v-model:value="input" |
| | | autocomplete="off" |
| | | :disabled="disabled" |
| | | :placeholder="placeholder" |
| | | @press-enter="accept(input)" |
| | | /> |
| | | <Button :disabled="disabled || starting" @click="active ? stop() : start()"> |
| | | {{ active ? 'å
³éæå头' : starting ? 'æ£å¨è¯·æ±ç¸æºâ¦' : 'æå¼æå头' }} |
| | | </Button> |
| | | <Button |
| | | type="primary" |
| | | :disabled="disabled || !input.trim()" |
| | | @click="accept(input)" |
| | | > |
| | | è¯å« |
| | | </Button> |
| | | </div> |
| | | <video |
| | | v-if="active" |
| | | ref="video" |
| | | class="mt-2 w-64 rounded bg-black" |
| | | muted |
| | | playsinline |
| | | /> |
| | | <p v-if="error" class="mt-1 text-red-600" role="alert">{{ error }}</p> |
| | | </div> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { TraceResult } from '#/utils/traceLookup'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { lookupTrace } from '#/utils/traceLookup'; |
| | | |
| | | import TraceScanInput from './TraceScanInput.vue'; |
| | | |
| | | /** |
| | | * 追溯äºç»´ç æ«ç æ ¸å¯¹é¢æ¿ã |
| | | * |
| | | * æ«ç ååªè§£æå¯¹è±¡å¹¶å±ç¤ºæ ¸å¯¹ä¿¡æ¯ï¼ç¹å»ã确认带å
¥ãæç±é¡µé¢æ§è¡åå
¥ã |
| | | */ |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | disabled?: boolean; |
| | | title?: string; |
| | | }>(), |
| | | { |
| | | disabled: false, |
| | | title: 'æ«ç 带åº', |
| | | }, |
| | | ); |
| | | |
| | | const emit = defineEmits<{ confirm: [trace: TraceResult] }>(); |
| | | |
| | | const busy = ref(false); |
| | | const error = ref(''); |
| | | const trace = ref<TraceResult>(); |
| | | |
| | | const typeLabel = computed(() => |
| | | trace.value |
| | | ? ({ bag: 'è¢ç ', batch: 'æ¹æ¬¡ç ', pallet: 'æçç ' } as const)[ |
| | | trace.value.type |
| | | ] |
| | | : '', |
| | | ); |
| | | |
| | | const quantity = computed(() => { |
| | | if (!trace.value) return ''; |
| | | if (trace.value.type === 'bag') return '1'; |
| | | if (trace.value.type === 'pallet') return trace.value.data.bagCount ?? ''; |
| | | return trace.value.data.bagQuantity ?? ''; |
| | | }); |
| | | |
| | | async function identify(code: string) { |
| | | if (props.disabled || busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | trace.value = undefined; |
| | | try { |
| | | trace.value = await lookupTrace(code); |
| | | } catch (e) { |
| | | error.value = e instanceof Error ? e.message : '追溯ç è¯å«å¤±è´¥'; |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | function confirm() { |
| | | if (!trace.value) return; |
| | | emit('confirm', trace.value); |
| | | trace.value = undefined; |
| | | } |
| | | |
| | | function cancel() { |
| | | trace.value = undefined; |
| | | error.value = ''; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <section class="mb-3 rounded-lg border border-border p-3"> |
| | | <h3 class="mb-2 text-sm font-semibold">{{ title }}</h3> |
| | | <TraceScanInput |
| | | v-if="!trace" |
| | | :disabled="disabled || busy" |
| | | @code="identify" |
| | | /> |
| | | <div v-else class="space-y-1 text-sm"> |
| | | <p> |
| | | <span class="text-muted-foreground">ç ç±»åï¼</span>{{ typeLabel }} |
| | | <span class="ml-4 text-muted-foreground">ç å¼ï¼</span>{{ trace.code }} |
| | | </p> |
| | | <p> |
| | | <span class="text-muted-foreground">åç§ï¼</span |
| | | >{{ trace.data.itemName || 'æªå½å
¥' }} |
| | | <span class="ml-4 text-muted-foreground">æ¹æ¬¡ï¼</span |
| | | >{{ trace.data.batchCode || 'æªç»å®' }} |
| | | </p> |
| | | <p> |
| | | <span class="text-muted-foreground">å¯å¸¦å
¥æ°éï¼</span |
| | | >{{ quantity || 'â' }} |
| | | </p> |
| | | <div class="flex gap-2 pt-1"> |
| | | <Button type="primary" :disabled="disabled || busy" @click="confirm"> |
| | | 确认带å
¥ |
| | | </Button> |
| | | <Button :disabled="busy" @click="cancel">åæ¶æ¬æ¬¡æ«ç </Button> |
| | | </div> |
| | | </div> |
| | | <p v-if="error" class="mt-1 text-red-600" role="alert">{{ error }}</p> |
| | | </section> |
| | | </template> |
| | |
| | | { key: 'quality', title: 'ç§åè´¨æ£', hint: 'è¢ç ãæç䏿¹æ¬¡', icon: 'scan', permission: 'mes:qc-seed-quality:submit' }, |
| | | { key: 'quality-records', title: 'è´¨æ£è®°å½', hint: 'ç»æä¸ä¸åæ ¼åå ', icon: 'records', permission: 'mes:qc-seed-quality:query' }, |
| | | { key: 'sales', title: 'éå®åºåº', hint: 'æ£è´§ãè¿åä¸åºåº', icon: 'box', permission: 'mes:wm-product-sales:query' }, |
| | | { key: 'productissue', title: 'çäº§é¢æ', hint: 'æ«ç é¢ç¨ç©æ', icon: 'work', permission: 'mes:wm-product-issue:create' }, |
| | | { key: 'miscissue', title: 'å
¶ä»åºåº', hint: 'æ«ç æ°å¢åºåºæç»', icon: 'box', permission: 'mes:wm-misc-issue:create' }, |
| | | { key: 'transfer', title: 'åºåç§»åº', hint: 'æ«ç æ°å¢ç§»åºæç»', icon: 'layers', permission: 'mes:wm-transfer:create' }, |
| | | { key: 'stocktaking', title: 'çç¹ä»»å¡', hint: 'æ«ç æäº¤çç¹æ°é', icon: 'records', permission: 'mes:wm-stock-taking-task:update' }, |
| | | { key: 'returnsales', title: 'éå®éè´§', hint: 'æ«ç æ°å¢éè´§æç»', icon: 'back', permission: 'mes:wm-return-sales:create' }, |
| | | { key: 'returnissue', title: 'ç产éæ', hint: 'æ«ç æ°å¢éææç»', icon: 'warehouse', permission: 'mes:wm-return-issue:create' }, |
| | | ] as const; |
| | | export function can(permission: string) { return useAccessStore().accessCodes.includes(permission); } |
| | | export function availableFeatures() { |
| | |
| | | if (key === 'records') return can('mes:pro-scan-in:query') && useAccessStore().accessMenus.some(m => m.path === '/mobile/inbound'); |
| | | return availableFeatures().some(f => f.key === key); |
| | | } |
| | | export function extractCode(raw: string) { |
| | | let value = raw.trim(); |
| | | if (!value) throw new Error('è¯·æ«ææè¾å
¥ç å¼'); |
| | | if (/^https?:\/\//i.test(value)) { |
| | | const code = new URL(value).searchParams.get('code'); |
| | | if (!code) throw new Error('è¯¥é¾æ¥ä¸å
å«ä¸å¡ç å¼'); |
| | | value = code.trim(); |
| | | } |
| | | if (!value || value.length > 64 || /\s/.test(value)) throw new Error('ç 弿 ¼å¼ä¸æ£ç¡®'); |
| | | return value; |
| | | } |
| | | export { extractCode } from '#/utils/traceCode'; |
| | |
| | | import { getBagCodeByCode, resolveTraceType } from '#/api/mes/pro/bagCode'; |
| | | import { getPalletByCode } from '#/api/mes/pro/pallet'; |
| | | import { getBatchByCode } from '#/api/mes/pro/batch'; |
| | | import { getMaterialStockPage, type MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | import { can, extractCode } from './access'; |
| | | import type { MesProScanEventApi } from '#/api/mes/pro/scanEvent'; |
| | | export interface TraceObject { |
| | |
| | | if(trace.data.batchStatus===40)return '该对象æå±æ¹æ¬¡å·²ä½åºï¼ä¸å¯æ§è¡åæä½'; |
| | | return ''; |
| | | } |
| | | /** æ«ç 对象å¯å¸¦å
¥åæ®çæ°éï¼è¢ç æ 1 è¢ï¼æçæå·²è£
è¢æ°ï¼æ¹æ¬¡ææ¹æ¬¡è¢æ°ã */ |
| | | export function traceQuantity(trace: TraceResult) { |
| | | if (trace.type === 'bag') return 1; |
| | | if (trace.type === 'pallet') return trace.data.bagCount; |
| | | return trace.data.bagQuantity; |
| | | } |
| | | /** ææ«ç 对象çç©æä¸æ¹æ¬¡æ¥æ¾åºåè®°å½ï¼ä¾éè¦éæ©åºåè®°å½çåæ®å¸¦åºæç»ã */ |
| | | export async function findStocks( |
| | | trace: TraceResult, |
| | | options: { stockState?: number; virtualFilter?: string } = {}, |
| | | ): Promise<MesWmMaterialStockApi.MaterialStock[]> { |
| | | if (!trace.data.itemId) return []; |
| | | const result = await getMaterialStockPage({ |
| | | pageNo: 1, |
| | | pageSize: 50, |
| | | batchCode: trace.data.batchCode, |
| | | itemId: trace.data.itemId, |
| | | ...options, |
| | | }); |
| | | return result.list; |
| | | } |
| | | export function myRecords(params: { pageNo: number; pageSize: number; status?: string; eventId?: string; receivedTime?: string[] }) { |
| | | return requestClient.get<{ list: MesProScanEventApi.ScanEvent[]; total: number }>('/mes/pro/scan-in/my-page', { params }); |
| | | } |
| | |
| | | { path: 'quality', name: 'MobileQuality', component: () => import('#/views/mobile/quality.vue'), meta: { title: 'ç§åè´¨æ£' } }, |
| | | { path: 'quality-records', name: 'MobileQualityRecords', component: () => import('#/views/mobile/quality-records.vue'), meta: { title: 'è´¨æ£è®°å½' } }, |
| | | { path: 'sales', name: 'MobileSales', component: () => import('#/views/mobile/sales.vue'), meta: { title: 'éå®åºåº' } }, |
| | | { path: 'productissue', name: 'MobileProductIssue', component: () => import('#/views/mobile/productissue.vue'), meta: { title: 'çäº§é¢æ' } }, |
| | | { path: 'miscissue', name: 'MobileMiscIssue', component: () => import('#/views/mobile/miscissue.vue'), meta: { title: 'å
¶ä»åºåº' } }, |
| | | { path: 'transfer', name: 'MobileTransfer', component: () => import('#/views/mobile/transfer.vue'), meta: { title: 'åºåç§»åº' } }, |
| | | { path: 'stocktaking', name: 'MobileStocktaking', component: () => import('#/views/mobile/stocktaking.vue'), meta: { title: 'çç¹ä»»å¡' } }, |
| | | { path: 'returnsales', name: 'MobileReturnSales', component: () => import('#/views/mobile/returnsales.vue'), meta: { title: 'éå®éè´§' } }, |
| | | { path: 'returnissue', name: 'MobileReturnIssue', component: () => import('#/views/mobile/returnissue.vue'), meta: { title: 'ç产éæ' } }, |
| | | ], |
| | | }]; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æå追溯ç ã |
| | | * |
| | | * ç°åºæ ç¾å°çæ¯è¿½æº¯äºç»´ç ï¼å
容为ã追溯ç½å?code=追溯ç ãï¼ |
| | | * æ«ç æªä¹å¯è½ææ´æ®µç½åç´æ¥æäº¤ï¼æææ«ç å
¥å£é½åºå
ç»è¿è¿éè§èåã |
| | | * å端 MesTraceCodeUtils ä¿æåä¸å£å¾ã |
| | | */ |
| | | export function extractCode(raw: string): string { |
| | | let value = raw.trim(); |
| | | if (!value) throw new Error('è¯·æ«ææè¾å
¥è¿½æº¯ç '); |
| | | if (/^https?:\/\//i.test(value)) { |
| | | let code: string | null; |
| | | try { |
| | | code = new URL(value).searchParams.get('code'); |
| | | } catch { |
| | | throw new Error('追溯ç½åæ ¼å¼ä¸æ£ç¡®'); |
| | | } |
| | | if (!code) throw new Error('该追溯ç½åä¸å
å«è¿½æº¯ç '); |
| | | value = code.trim(); |
| | | } |
| | | if (!value || value.length > 64 || /\s/.test(value)) { |
| | | throw new Error('è¿½æº¯ç æ ¼å¼ä¸æ£ç¡®'); |
| | | } |
| | | return value; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | |
| | | import { getBagCodeByCode, resolveTraceType } from '#/api/mes/pro/bagCode'; |
| | | import { getBatchByCode } from '#/api/mes/pro/batch'; |
| | | import { getPalletByCode } from '#/api/mes/pro/pallet'; |
| | | import { getMaterialStockPage } from '#/api/mes/wm/materialstock'; |
| | | |
| | | import { extractCode } from './traceCode'; |
| | | |
| | | export type TraceType = 'bag' | 'batch' | 'pallet'; |
| | | |
| | | export interface TraceObject { |
| | | bagCount?: number; |
| | | bagQuantity?: number; |
| | | batchCode?: string; |
| | | batchId?: number; |
| | | batchStatus?: number; |
| | | businessCode?: string; |
| | | code?: string; |
| | | id?: number; |
| | | inboundBagQuantity?: number; |
| | | inboundTime?: string; |
| | | itemId?: number; |
| | | itemName?: string; |
| | | lineName?: string; |
| | | palletCode?: string; |
| | | palletId?: number; |
| | | pendingBagQuantity?: number; |
| | | produceDate?: string; |
| | | status?: number; |
| | | tempCode?: string; |
| | | unitMeasureName?: string; |
| | | } |
| | | |
| | | export interface TraceResult { |
| | | code: string; |
| | | data: TraceObject; |
| | | type: TraceType; |
| | | } |
| | | |
| | | /** è§£ææ«æåæï¼æç ç±»åæ¥è¯¢è¢ç / æç / æ¹æ¬¡å¯¹è±¡ã */ |
| | | export async function lookupTrace(raw: string): Promise<TraceResult> { |
| | | const code = extractCode(raw); |
| | | const type = (await resolveTraceType(code)) as TraceType | null; |
| | | if (!type) { |
| | | throw new Error('æªæ¾å°å¯¹åºè¢ç ãæçææ¹æ¬¡'); |
| | | } |
| | | const fetchers: Record<TraceType, (value: string) => Promise<TraceObject>> = { |
| | | bag: getBagCodeByCode, |
| | | batch: getBatchByCode, |
| | | pallet: getPalletByCode, |
| | | }; |
| | | const data = await fetchers[type](code); |
| | | if (!data?.id) { |
| | | throw new Error('æªæ¾å°å¯¹è±¡ä¿¡æ¯'); |
| | | } |
| | | return { code, data, type }; |
| | | } |
| | | |
| | | /** æ«ç 对象å¯å¸¦å
¥åæ®çæ°éï¼è¢ç æ 1 è¢ï¼æçæå·²è£
è¢æ°ï¼æ¹æ¬¡ææ¹æ¬¡è¢æ°ã */ |
| | | export function traceQuantity(trace: TraceResult): number | undefined { |
| | | if (trace.type === 'bag') { |
| | | return 1; |
| | | } |
| | | if (trace.type === 'pallet') { |
| | | return trace.data.bagCount; |
| | | } |
| | | return trace.data.bagQuantity; |
| | | } |
| | | |
| | | /** ææ«ç 对象çç©æä¸æ¹æ¬¡æ¥æ¾å¯¹åºåºåè®°å½ï¼ä¾éè¦éæ©åºåè®°å½çåæ®å¸¦åºæç»ã */ |
| | | export async function findMaterialStocks( |
| | | trace: TraceResult, |
| | | options: { stockState?: number; virtualFilter?: string } = {}, |
| | | ): Promise<MesWmMaterialStockApi.MaterialStock[]> { |
| | | if (!trace.data.itemId) { |
| | | return []; |
| | | } |
| | | const result = await getMaterialStockPage({ |
| | | pageNo: 1, |
| | | pageSize: 50, |
| | | batchCode: trace.data.batchCode, |
| | | itemId: trace.data.itemId, |
| | | ...options, |
| | | }); |
| | | return result.list; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { |
| | | getMiscIssue, |
| | | getMiscIssuePage, |
| | | type MesWmMiscIssueApi, |
| | | } from '#/api/mes/wm/miscissue'; |
| | | import { |
| | | createMiscIssueLine, |
| | | getMiscIssueLinePage, |
| | | type MesWmMiscIssueLineApi, |
| | | } from '#/api/mes/wm/miscissue/line'; |
| | | import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | import { |
| | | errorText, |
| | | findStocks, |
| | | lookup, |
| | | traceQuantity, |
| | | writeBlockReason, |
| | | type TraceResult, |
| | | } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | defineOptions({ name: 'MobileMiscIssue' }); |
| | | |
| | | type Issue = MesWmMiscIssueApi.MiscIssue; |
| | | type Line = MesWmMiscIssueLineApi.MiscIssueLine; |
| | | |
| | | const busy = ref(false); |
| | | const error = ref(''); |
| | | const notice = ref(''); |
| | | const code = ref(''); |
| | | const rows = ref<Issue[]>([]); |
| | | const page = ref(0); |
| | | const total = ref(0); |
| | | const issue = ref<Issue>(); |
| | | const lines = ref<Line[]>([]); |
| | | const trace = ref<TraceResult>(); |
| | | const stock = ref<MesWmMaterialStockApi.MaterialStock>(); |
| | | const quantity = ref(1); |
| | | let filters: { code?: string } = {}; |
| | | |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | const next = reset ? 1 : page.value + 1; |
| | | const query = reset ? { code: code.value.trim() || undefined } : filters; |
| | | try { |
| | | const data = await getMiscIssuePage({ ...query, status: 0, pageNo: next, pageSize: 20 }); |
| | | rows.value = reset ? data.list : [...rows.value, ...data.list]; |
| | | page.value = next; |
| | | total.value = data.total; |
| | | filters = query; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function readLines(issueId: number) { |
| | | const all: Line[] = []; |
| | | for (let n = 1; ; n++) { |
| | | const data = await getMiscIssueLinePage({ issueId, pageNo: n, pageSize: 100 }); |
| | | all.push(...data.list); |
| | | if (all.length >= data.total || !data.list.length) break; |
| | | } |
| | | return all; |
| | | } |
| | | |
| | | async function open(id?: number) { |
| | | if (!id || busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | try { |
| | | const detail = await getMiscIssue(id); |
| | | if (!detail) throw new Error('åºåºåä¸åå¨æå·²è¢«å é¤'); |
| | | issue.value = detail; |
| | | lines.value = await readLines(id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function identify(raw: string) { |
| | | if (busy.value || !issue.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | try { |
| | | const found = await lookup(raw); |
| | | const reason = writeBlockReason(found); |
| | | if (reason) throw new Error(reason); |
| | | const stocks = await findStocks(found); |
| | | const matched = stocks.find((item) => (item.quantity ?? 0) > 0) ?? stocks[0]; |
| | | if (!matched) throw new Error('æªæ¾å°è¯¥è¿½æº¯ç 对åºçåºåè®°å½'); |
| | | trace.value = found; |
| | | stock.value = matched; |
| | | const max = traceQuantity(found); |
| | | quantity.value = max && matched.quantity ? Math.min(max, matched.quantity) : (max ?? matched.quantity ?? 1); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirm() { |
| | | const current = issue.value; |
| | | const matched = stock.value; |
| | | if (!current?.id || !matched?.id || busy.value) return; |
| | | if (!Number.isFinite(quantity.value) || quantity.value <= 0) { |
| | | error.value = 'åºåºæ°éå¿
须大äºé¶'; |
| | | return; |
| | | } |
| | | if (!window.confirm(`确认åºåº ${matched.batchCode || ''} ${quantity.value}ï¼`)) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | try { |
| | | await createMiscIssueLine({ |
| | | issueId: current.id, |
| | | itemId: matched.itemId, |
| | | materialStockId: matched.id, |
| | | quantity: quantity.value, |
| | | batchCode: matched.batchCode, |
| | | batchId: matched.batchId, |
| | | warehouseId: matched.warehouseId, |
| | | locationId: matched.locationId, |
| | | areaId: matched.areaId, |
| | | remark: `æ«ç æ¥æº ${trace.value?.code ?? ''}`, |
| | | }); |
| | | notice.value = '已添å åºåºæç»ã'; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | lines.value = await readLines(current.id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <p v-if="notice" class="m-success" role="status">{{ notice }}</p> |
| | | <p v-if="error" class="m-error" role="alert">{{ error }}</p> |
| | | <template v-if="issue"> |
| | | <button class="m-secondary" :disabled="busy" @click="issue = undefined; trace = undefined; stock = undefined; load()"> |
| | | è¿ååºåºåå表 |
| | | </button> |
| | | <section class="m-panel" style="margin-top: 16px"> |
| | | <h2 class="m-title">{{ issue.code }}</h2> |
| | | <dl> |
| | | <div class="m-row"><dt>åç§°</dt><dd>{{ issue.name || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>夿³¨</dt><dd>{{ issue.remark || 'â' }}</dd></div> |
| | | </dl> |
| | | </section> |
| | | <h2 class="m-section-title">åºåºæç»</h2> |
| | | <p v-if="!lines.length" class="m-empty">è¯¥åææ åºåºæç»</p> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"> |
| | | <h3 class="m-title">{{ line.itemName || line.itemCode }}</h3> |
| | | <p>åºåºæ°é {{ line.quantity ?? 0 }}</p> |
| | | <p class="m-muted">æ¹æ¬¡ {{ line.batchCode || 'æªå½å
¥' }}</p> |
| | | </article> |
| | | <h2 class="m-section-title">æ«ç åºåº</h2> |
| | | <p class="m-muted">æ«æè¢ä¸è¿½æº¯äºç»´ç ï¼æç©æä¸æ¹æ¬¡å¹é
åºååæ°å¢ä¸è¡åºåºæç»ã</p> |
| | | <Scanner v-if="!trace" :disabled="busy" @code="identify" /> |
| | | <section v-else class="m-panel"> |
| | | <h3 class="m-title">æ ¸å¯¹åºåºå¯¹è±¡</h3> |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ trace.data.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ stock?.batchCode || trace.data.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>åºåæ°é</dt><dd>{{ stock?.quantity ?? 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | | <label class="m-field"> |
| | | <span>åºåºæ°é</span> |
| | | <input v-model.number="quantity" type="number" inputmode="decimal" min="0.01" step="any" :disabled="busy" /> |
| | | </label> |
| | | <div class="m-action-bar"> |
| | | <button class="m-primary" :disabled="busy" @click="confirm">ç¡®è®¤æ·»å æç»</button> |
| | | <button class="m-secondary" :disabled="busy" @click="trace = undefined; stock = undefined">éæ°æ«æ</button> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | <template v-else> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | | <label class="m-field"><span>åºåºåå·</span><input v-model="code" :disabled="busy" placeholder="è¾å
¥åºåºåå·" /></label> |
| | | <button class="m-secondary" :disabled="busy">{{ busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢è稿åºåºå' }}</button> |
| | | </form> |
| | | <p class="m-muted" role="status">å
± {{ total }} å¼ è稿åºåºå</p> |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">ææ è稿åºåºå</p> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"> |
| | | <strong>{{ row.code }}</strong><span>è稿</span><small>{{ row.name || 'æªå½å
¥åç§°' }}</small> |
| | | </button> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)"> |
| | | {{ busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤' }} |
| | | </button> |
| | | </template> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { |
| | | getProductIssue, |
| | | getProductIssuePage, |
| | | type MesWmProductIssueApi, |
| | | } from '#/api/mes/wm/productissue'; |
| | | import { |
| | | createProductIssueLine, |
| | | getProductIssueLinePage, |
| | | type MesWmProductIssueLineApi, |
| | | } from '#/api/mes/wm/productissue/line'; |
| | | import { |
| | | errorText, |
| | | lookup, |
| | | traceQuantity, |
| | | writeBlockReason, |
| | | type TraceResult, |
| | | } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | defineOptions({ name: 'MobileProductIssue' }); |
| | | |
| | | type Issue = MesWmProductIssueApi.ProductIssue; |
| | | type Line = MesWmProductIssueLineApi.ProductIssueLine; |
| | | |
| | | const busy = ref(false); |
| | | const error = ref(''); |
| | | const notice = ref(''); |
| | | const code = ref(''); |
| | | const rows = ref<Issue[]>([]); |
| | | const page = ref(0); |
| | | const total = ref(0); |
| | | const issue = ref<Issue>(); |
| | | const lines = ref<Line[]>([]); |
| | | const trace = ref<TraceResult>(); |
| | | const quantity = ref(1); |
| | | let filters: { code?: string } = {}; |
| | | |
| | | function formatTime(value?: number) { |
| | | return value ? new Date(value).toLocaleString() : 'â'; |
| | | } |
| | | |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | const next = reset ? 1 : page.value + 1; |
| | | const query = reset ? { code: code.value.trim() || undefined } : filters; |
| | | try { |
| | | const data = await getProductIssuePage({ |
| | | ...query, |
| | | status: 0, |
| | | pageNo: next, |
| | | pageSize: 20, |
| | | }); |
| | | rows.value = reset ? data.list : [...rows.value, ...data.list]; |
| | | page.value = next; |
| | | total.value = data.total; |
| | | filters = query; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function readLines(issueId: number) { |
| | | const all: Line[] = []; |
| | | for (let n = 1; ; n++) { |
| | | const data = await getProductIssueLinePage({ |
| | | issueId, |
| | | pageNo: n, |
| | | pageSize: 100, |
| | | }); |
| | | all.push(...data.list); |
| | | if (all.length >= data.total || !data.list.length) break; |
| | | } |
| | | return all; |
| | | } |
| | | |
| | | async function open(id?: number) { |
| | | if (!id || busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | try { |
| | | const detail = await getProductIssue(id); |
| | | if (!detail) throw new Error('颿åä¸åå¨æå·²è¢«å é¤'); |
| | | issue.value = detail; |
| | | lines.value = await readLines(id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function identify(raw: string) { |
| | | if (busy.value || !issue.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | try { |
| | | const found = await lookup(raw); |
| | | const reason = writeBlockReason(found); |
| | | if (reason) throw new Error(reason); |
| | | if (!found.data.itemId) throw new Error('è¯¥è¿½æº¯ç æªç»å®ç©æ'); |
| | | trace.value = found; |
| | | quantity.value = traceQuantity(found) ?? 1; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirm() { |
| | | const current = issue.value; |
| | | const found = trace.value; |
| | | if (!current?.id || !found?.data.itemId || busy.value) return; |
| | | if (!Number.isFinite(quantity.value) || quantity.value <= 0) { |
| | | error.value = '颿æ°éå¿
须大äºé¶'; |
| | | return; |
| | | } |
| | | if (!window.confirm(`确认æè¿½æº¯ç æ·»å 颿æç»ï¼${found.data.itemName || ''} ${quantity.value}ï¼`)) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | try { |
| | | await createProductIssueLine({ |
| | | issueId: current.id, |
| | | itemId: found.data.itemId, |
| | | quantity: quantity.value, |
| | | remark: `æ«ç æ¥æº ${found.code}`, |
| | | }); |
| | | notice.value = '已添å 颿æç»ã'; |
| | | trace.value = undefined; |
| | | lines.value = await readLines(current.id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <p v-if="notice" class="m-success" role="status">{{ notice }}</p> |
| | | <p v-if="error" class="m-error" role="alert">{{ error }}</p> |
| | | <template v-if="issue"> |
| | | <button class="m-secondary" :disabled="busy" @click="issue = undefined; trace = undefined; load()"> |
| | | è¿å颿åå表 |
| | | </button> |
| | | <section class="m-panel" style="margin-top: 16px"> |
| | | <h2 class="m-title">{{ issue.code }}</h2> |
| | | <dl> |
| | | <div class="m-row"><dt>éæ±æ¶é´</dt><dd>{{ formatTime(issue.requiredTime) }}</dd></div> |
| | | <div class="m-row"><dt>夿³¨</dt><dd>{{ issue.remark || 'â' }}</dd></div> |
| | | </dl> |
| | | </section> |
| | | <h2 class="m-section-title">颿æç»</h2> |
| | | <p v-if="!lines.length" class="m-empty">è¯¥åææ 颿æç»</p> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"> |
| | | <h3 class="m-title">{{ line.itemName || line.itemCode }}</h3> |
| | | <p>颿æ°é {{ line.quantity ?? 0 }}</p> |
| | | <p v-if="line.remark" class="m-muted" style="overflow-wrap: anywhere">{{ line.remark }}</p> |
| | | </article> |
| | | <h2 class="m-section-title">æ«ç 颿</h2> |
| | | <p class="m-muted">æ«æè¢ä¸è¿½æº¯äºç»´ç ï¼èªå¨å¸¦åºç©æä¸æ°éï¼ç¡®è®¤åæ°å¢ä¸è¡æç»ã</p> |
| | | <Scanner v-if="!trace" :disabled="busy" @code="identify" /> |
| | | <section v-else class="m-panel"> |
| | | <h3 class="m-title">æ ¸å¯¹é¢æå¯¹è±¡</h3> |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ trace.data.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ trace.data.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | | <label class="m-field"> |
| | | <span>颿æ°é</span> |
| | | <input v-model.number="quantity" type="number" inputmode="decimal" min="0.01" step="any" :disabled="busy" /> |
| | | </label> |
| | | <div class="m-action-bar"> |
| | | <button class="m-primary" :disabled="busy" @click="confirm">ç¡®è®¤æ·»å æç»</button> |
| | | <button class="m-secondary" :disabled="busy" @click="trace = undefined">éæ°æ«æ</button> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | <template v-else> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | | <label class="m-field"><span>颿åå·</span><input v-model="code" :disabled="busy" placeholder="è¾å
¥é¢æåå·" /></label> |
| | | <button class="m-secondary" :disabled="busy">{{ busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢èç¨¿é¢æå' }}</button> |
| | | </form> |
| | | <p class="m-muted" role="status">å
± {{ total }} å¼ èç¨¿é¢æå</p> |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">ææ èç¨¿é¢æå</p> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"> |
| | | <strong>{{ row.code }}</strong><span>è稿</span><small>{{ row.remark || 'æªå½å
¥å¤æ³¨' }}</small> |
| | | </button> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)"> |
| | | {{ busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤' }} |
| | | </button> |
| | | </template> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | import { |
| | | getReturnIssue, |
| | | getReturnIssuePage, |
| | | type MesWmReturnIssueApi, |
| | | } from '#/api/mes/wm/returnissue'; |
| | | import { |
| | | createReturnIssueLine, |
| | | getReturnIssueLinePage, |
| | | type MesWmReturnIssueLineApi, |
| | | } from '#/api/mes/wm/returnissue/line'; |
| | | import { |
| | | errorText, |
| | | findStocks, |
| | | lookup, |
| | | traceQuantity, |
| | | writeBlockReason, |
| | | type TraceResult, |
| | | } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | defineOptions({ name: 'MobileReturnIssue' }); |
| | | |
| | | type Issue = MesWmReturnIssueApi.ReturnIssue; |
| | | type Line = MesWmReturnIssueLineApi.ReturnIssueLine; |
| | | |
| | | const busy = ref(false); |
| | | const error = ref(''); |
| | | const notice = ref(''); |
| | | const code = ref(''); |
| | | const rows = ref<Issue[]>([]); |
| | | const page = ref(0); |
| | | const total = ref(0); |
| | | const issue = ref<Issue>(); |
| | | const lines = ref<Line[]>([]); |
| | | const trace = ref<TraceResult>(); |
| | | const stock = ref<MesWmMaterialStockApi.MaterialStock>(); |
| | | const quantity = ref(1); |
| | | let filters: { code?: string } = {}; |
| | | |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | const next = reset ? 1 : page.value + 1; |
| | | const query = reset ? { code: code.value.trim() || undefined } : filters; |
| | | try { |
| | | const data = await getReturnIssuePage({ ...query, status: 0, pageNo: next, pageSize: 20 }); |
| | | rows.value = reset ? data.list : [...rows.value, ...data.list]; |
| | | page.value = next; |
| | | total.value = data.total; |
| | | filters = query; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function readLines(issueId: number) { |
| | | const all: Line[] = []; |
| | | for (let n = 1; ; n++) { |
| | | const data = await getReturnIssueLinePage({ issueId, pageNo: n, pageSize: 100 }); |
| | | all.push(...data.list); |
| | | if (all.length >= data.total || !data.list.length) break; |
| | | } |
| | | return all; |
| | | } |
| | | |
| | | async function open(id?: number) { |
| | | if (!id || busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | try { |
| | | const detail = await getReturnIssue(id); |
| | | if (!detail) throw new Error('ç产éæåä¸åå¨æå·²è¢«å é¤'); |
| | | issue.value = detail; |
| | | lines.value = await readLines(id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function identify(raw: string) { |
| | | if (busy.value || !issue.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | try { |
| | | const found = await lookup(raw); |
| | | const reason = writeBlockReason(found); |
| | | if (reason) throw new Error(reason); |
| | | const stocks = await findStocks(found, { virtualFilter: 'only' }); |
| | | const matched = stocks.find((item) => (item.quantity ?? 0) > 0) ?? stocks[0]; |
| | | if (!matched) throw new Error('æªæ¾å°è¯¥è¿½æº¯ç 对åºç线边åºåºåè®°å½'); |
| | | trace.value = found; |
| | | stock.value = matched; |
| | | const max = traceQuantity(found); |
| | | quantity.value = max && matched.quantity ? Math.min(max, matched.quantity) : (max ?? matched.quantity ?? 1); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirm() { |
| | | const current = issue.value; |
| | | const matched = stock.value; |
| | | if (!current?.id || !matched?.id || busy.value) return; |
| | | if (!Number.isFinite(quantity.value) || quantity.value <= 0) { |
| | | error.value = 'éææ°éå¿
须大äºé¶'; |
| | | return; |
| | | } |
| | | if (!window.confirm(`ç¡®è®¤éæ ${matched.batchCode || ''} ${quantity.value}ï¼`)) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | try { |
| | | await createReturnIssueLine({ |
| | | issueId: current.id, |
| | | itemId: matched.itemId, |
| | | materialStockId: matched.id, |
| | | quantity: quantity.value, |
| | | batchId: matched.batchId, |
| | | batchCode: matched.batchCode, |
| | | rqcCheckFlag: false, |
| | | remark: `æ«ç æ¥æº ${trace.value?.code ?? ''}`, |
| | | }); |
| | | notice.value = '已添å éææç»ã'; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | lines.value = await readLines(current.id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <p v-if="notice" class="m-success" role="status">{{ notice }}</p> |
| | | <p v-if="error" class="m-error" role="alert">{{ error }}</p> |
| | | <template v-if="issue"> |
| | | <button class="m-secondary" :disabled="busy" @click="issue = undefined; trace = undefined; stock = undefined; load()"> |
| | | è¿åç产éæåå表 |
| | | </button> |
| | | <section class="m-panel" style="margin-top: 16px"> |
| | | <h2 class="m-title">{{ issue.code }}</h2> |
| | | <dl> |
| | | <div class="m-row"><dt>åç§°</dt><dd>{{ issue.name || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç产任å¡</dt><dd>{{ issue.taskCode || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>å·¥ä½ç«</dt><dd>{{ issue.workstationName || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>夿³¨</dt><dd>{{ issue.remark || 'â' }}</dd></div> |
| | | </dl> |
| | | </section> |
| | | <h2 class="m-section-title">éææç»</h2> |
| | | <p v-if="!lines.length" class="m-empty">è¯¥åææ éææç»</p> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"> |
| | | <h3 class="m-title">{{ line.itemName || line.itemCode }}</h3> |
| | | <p>éææ°é {{ line.quantity ?? 0 }}</p> |
| | | <p class="m-muted">æ¹æ¬¡ {{ line.batchCode || 'æªå½å
¥' }}</p> |
| | | </article> |
| | | <h2 class="m-section-title">æ«ç éæ</h2> |
| | | <p class="m-muted">æ«æè¢ä¸è¿½æº¯äºç»´ç ï¼æç©æä¸æ¹æ¬¡å¹é
线边åºåºååæ°å¢ä¸è¡éææç»ã</p> |
| | | <Scanner v-if="!trace" :disabled="busy" @code="identify" /> |
| | | <section v-else class="m-panel"> |
| | | <h3 class="m-title">æ ¸å¯¹éæå¯¹è±¡</h3> |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ stock?.itemName || trace.data.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ stock?.batchCode || trace.data.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>åºä½</dt><dd>{{ [stock?.warehouseName, stock?.locationName, stock?.areaName].filter(Boolean).join(' / ') || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>åºåæ°é</dt><dd>{{ stock?.quantity ?? 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | | <label class="m-field"> |
| | | <span>éææ°é</span> |
| | | <input v-model.number="quantity" type="number" inputmode="decimal" min="0.01" step="any" :disabled="busy" /> |
| | | </label> |
| | | <div class="m-action-bar"> |
| | | <button class="m-primary" :disabled="busy" @click="confirm">ç¡®è®¤æ·»å æç»</button> |
| | | <button class="m-secondary" :disabled="busy" @click="trace = undefined; stock = undefined">éæ°æ«æ</button> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | <template v-else> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | | <label class="m-field"><span>éæåå·</span><input v-model="code" :disabled="busy" placeholder="è¾å
¥éæåå·" /></label> |
| | | <button class="m-secondary" :disabled="busy">{{ busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢è稿éæå' }}</button> |
| | | </form> |
| | | <p class="m-muted" role="status">å
± {{ total }} å¼ è稿éæå</p> |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">ææ è稿éæå</p> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"> |
| | | <strong>{{ row.code }}</strong><span>è稿</span><small>{{ row.taskName || 'æªå
³èä»»å¡' }}</small> |
| | | </button> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)"> |
| | | {{ busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤' }} |
| | | </button> |
| | | </template> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { getBatchPage } from '#/api/mes/wm/batch'; |
| | | import { |
| | | getReturnSales, |
| | | getReturnSalesPage, |
| | | getReturnableProductSalesLineList, |
| | | type MesWmReturnSalesApi, |
| | | } from '#/api/mes/wm/returnsales'; |
| | | import { |
| | | createReturnSalesLine, |
| | | getReturnSalesLinePage, |
| | | type MesWmReturnSalesLineApi, |
| | | } from '#/api/mes/wm/returnsales/line'; |
| | | import { |
| | | errorText, |
| | | lookup, |
| | | traceQuantity, |
| | | writeBlockReason, |
| | | type TraceResult, |
| | | } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | defineOptions({ name: 'MobileReturnSales' }); |
| | | |
| | | type Order = MesWmReturnSalesApi.ReturnSales; |
| | | type Line = MesWmReturnSalesLineApi.ReturnSalesLine; |
| | | |
| | | const busy = ref(false); |
| | | const error = ref(''); |
| | | const notice = ref(''); |
| | | const code = ref(''); |
| | | const rows = ref<Order[]>([]); |
| | | const page = ref(0); |
| | | const total = ref(0); |
| | | const order = ref<Order>(); |
| | | const lines = ref<Line[]>([]); |
| | | const trace = ref<TraceResult>(); |
| | | const quantity = ref(1); |
| | | const productSalesLineId = ref<number>(); |
| | | const batchId = ref<number>(); |
| | | const returnable = ref(0); |
| | | let filters: { code?: string } = {}; |
| | | |
| | | function resetScan() { |
| | | trace.value = undefined; |
| | | productSalesLineId.value = undefined; |
| | | batchId.value = undefined; |
| | | returnable.value = 0; |
| | | } |
| | | |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | const next = reset ? 1 : page.value + 1; |
| | | const query = reset ? { code: code.value.trim() || undefined } : filters; |
| | | try { |
| | | const data = await getReturnSalesPage({ ...query, status: 0, pageNo: next, pageSize: 20 }); |
| | | rows.value = reset ? data.list : [...rows.value, ...data.list]; |
| | | page.value = next; |
| | | total.value = data.total; |
| | | filters = query; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function readLines(returnId: number) { |
| | | const all: Line[] = []; |
| | | for (let n = 1; ; n++) { |
| | | const data = await getReturnSalesLinePage({ returnId, pageNo: n, pageSize: 100 }); |
| | | all.push(...data.list); |
| | | if (all.length >= data.total || !data.list.length) break; |
| | | } |
| | | return all; |
| | | } |
| | | |
| | | async function open(id?: number) { |
| | | if (!id || busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | resetScan(); |
| | | try { |
| | | const detail = await getReturnSales(id); |
| | | if (!detail) throw new Error('éå®éè´§åä¸åå¨æå·²è¢«å é¤'); |
| | | order.value = detail; |
| | | lines.value = await readLines(id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function identify(raw: string) { |
| | | const current = order.value; |
| | | if (busy.value || !current?.id) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | resetScan(); |
| | | try { |
| | | const found = await lookup(raw); |
| | | const reason = writeBlockReason(found); |
| | | if (reason) throw new Error(reason); |
| | | const { batchCode } = found.data; |
| | | const itemId = found.data.itemId; |
| | | if (!itemId || !batchCode) throw new Error('è¯¥è¿½æº¯ç æªç»å®ç©æææ¹æ¬¡'); |
| | | if (current.productSalesId) { |
| | | const list = await getReturnableProductSalesLineList(current.productSalesId); |
| | | const matched = list.find((item) => item.itemId === itemId && item.batchCode === batchCode); |
| | | if (!matched) throw new Error('该追溯ç 对åºçæ¹æ¬¡ä¸å¨æ¬åå¯éèå´å
'); |
| | | const max = Number(matched.returnableQuantity ?? 0); |
| | | returnable.value = max; |
| | | productSalesLineId.value = matched.productSalesLineId; |
| | | batchId.value = matched.batchId; |
| | | const wanted = traceQuantity(found) ?? max; |
| | | quantity.value = Math.min(wanted, max); |
| | | trace.value = found; |
| | | return; |
| | | } |
| | | const batches = await getBatchPage({ code: batchCode, itemId, pageNo: 1, pageSize: 20 }); |
| | | const batch = batches.list.find((item) => item.code === batchCode); |
| | | if (!batch?.id) throw new Error('æªæ¾å°è¯¥è¿½æº¯ç 对åºçå
¥åºæ¹æ¬¡'); |
| | | batchId.value = batch.id; |
| | | quantity.value = traceQuantity(found) ?? 1; |
| | | trace.value = found; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirm() { |
| | | const current = order.value; |
| | | const found = trace.value; |
| | | if (!current?.id || !found?.data.itemId || busy.value) return; |
| | | if (!Number.isFinite(quantity.value) || quantity.value <= 0) { |
| | | error.value = 'éè´§æ°éå¿
须大äºé¶'; |
| | | return; |
| | | } |
| | | if (!window.confirm(`确认éè´§ ${found.data.itemName || ''} ${quantity.value}ï¼`)) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | try { |
| | | await createReturnSalesLine({ |
| | | returnId: current.id, |
| | | productSalesLineId: productSalesLineId.value, |
| | | itemId: found.data.itemId, |
| | | batchId: batchId.value, |
| | | batchCode: found.data.batchCode, |
| | | quantity: quantity.value, |
| | | remark: `æ«ç æ¥æº ${found.code}`, |
| | | }); |
| | | notice.value = '已添å éè´§æç»ã'; |
| | | resetScan(); |
| | | lines.value = await readLines(current.id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <p v-if="notice" class="m-success" role="status">{{ notice }}</p> |
| | | <p v-if="error" class="m-error" role="alert">{{ error }}</p> |
| | | <template v-if="order"> |
| | | <button class="m-secondary" :disabled="busy" @click="order = undefined; resetScan(); load()"> |
| | | è¿åéå®éè´§åå表 |
| | | </button> |
| | | <section class="m-panel" style="margin-top: 16px"> |
| | | <h2 class="m-title">{{ order.code }}</h2> |
| | | <dl> |
| | | <div class="m-row"><dt>客æ·</dt><dd>{{ order.clientName || order.clientCode || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>éå®åºåºå</dt><dd>{{ order.productSalesCode || 'æªå
³è' }}</dd></div> |
| | | <div class="m-row"><dt>éè´§åå </dt><dd>{{ order.returnReason || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>夿³¨</dt><dd>{{ order.remark || 'â' }}</dd></div> |
| | | </dl> |
| | | </section> |
| | | <h2 class="m-section-title">éè´§æç»</h2> |
| | | <p v-if="!lines.length" class="m-empty">è¯¥åææ éè´§æç»</p> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"> |
| | | <h3 class="m-title">{{ line.itemName || line.itemCode }}</h3> |
| | | <p>éè´§æ°é {{ line.quantity ?? 0 }}</p> |
| | | <p class="m-muted">æ¹æ¬¡ {{ line.batchCode || 'æªå½å
¥' }}</p> |
| | | </article> |
| | | <h2 class="m-section-title">æ«ç éè´§</h2> |
| | | <p class="m-muted"> |
| | | {{ order.productSalesId ? 'æ«æè¿½æº¯äºç»´ç ï¼å¹é
æ¬åéå®åºåºåçå¯éè¡åæ°å¢éè´§æç»ã' : 'æ«æè¿½æº¯äºç»´ç ï¼æç©æä¸æ¹æ¬¡å¹é
å
¥åºæ¹æ¬¡åæ°å¢éè´§æç»ã' }} |
| | | </p> |
| | | <Scanner v-if="!trace" :disabled="busy" @code="identify" /> |
| | | <section v-else class="m-panel"> |
| | | <h3 class="m-title">æ ¸å¯¹é货对象</h3> |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ trace.data.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ trace.data.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row" v-if="returnable"><dt>å¯éæ°é</dt><dd>{{ returnable }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | | <label class="m-field"> |
| | | <span>éè´§æ°é</span> |
| | | <input v-model.number="quantity" type="number" inputmode="decimal" min="0.01" step="any" :disabled="busy" /> |
| | | </label> |
| | | <div class="m-action-bar"> |
| | | <button class="m-primary" :disabled="busy" @click="confirm">ç¡®è®¤æ·»å æç»</button> |
| | | <button class="m-secondary" :disabled="busy" @click="resetScan">éæ°æ«æ</button> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | <template v-else> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | | <label class="m-field"><span>éè´§åå·</span><input v-model="code" :disabled="busy" placeholder="è¾å
¥éè´§åå·" /></label> |
| | | <button class="m-secondary" :disabled="busy">{{ busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢è稿éè´§å' }}</button> |
| | | </form> |
| | | <p class="m-muted" role="status">å
± {{ total }} å¼ è稿éè´§å</p> |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">ææ è稿éè´§å</p> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"> |
| | | <strong>{{ row.code }}</strong><span>è稿</span><small>{{ row.clientName || 'æªå
³è客æ·' }}</small> |
| | | </button> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)"> |
| | | {{ busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤' }} |
| | | </button> |
| | | </template> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import { |
| | | getStockTaking, |
| | | getStockTakingPage, |
| | | scanStockTaking, |
| | | type MesWmStockTakingTaskApi, |
| | | } from '#/api/mes/wm/stocktaking/task'; |
| | | import { |
| | | getStockTakingTaskLineSimpleList, |
| | | type MesWmStockTakingTaskLineApi, |
| | | } from '#/api/mes/wm/stocktaking/task/line'; |
| | | import { |
| | | errorText, |
| | | lookup, |
| | | traceQuantity, |
| | | writeBlockReason, |
| | | type TraceResult, |
| | | } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | defineOptions({ name: 'MobileStocktaking' }); |
| | | |
| | | type Task = MesWmStockTakingTaskApi.StockTakingTask; |
| | | type Line = MesWmStockTakingTaskLineApi.StockTakingTaskLine; |
| | | |
| | | const busy = ref(false); |
| | | const error = ref(''); |
| | | const notice = ref(''); |
| | | const code = ref(''); |
| | | const rows = ref<Task[]>([]); |
| | | const page = ref(0); |
| | | const total = ref(0); |
| | | const task = ref<Task>(); |
| | | const lines = ref<Line[]>([]); |
| | | const trace = ref<TraceResult>(); |
| | | const matchedLine = ref<Line>(); |
| | | const quantity = ref(1); |
| | | let filters: { code?: string } = {}; |
| | | |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | const next = reset ? 1 : page.value + 1; |
| | | const query = reset ? { code: code.value.trim() || undefined } : filters; |
| | | try { |
| | | const data = await getStockTakingPage({ ...query, status: 2, pageNo: next, pageSize: 20 }); |
| | | rows.value = reset ? data.list : [...rows.value, ...data.list]; |
| | | page.value = next; |
| | | total.value = data.total; |
| | | filters = query; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function readLines(taskId: number) { |
| | | return getStockTakingTaskLineSimpleList(taskId); |
| | | } |
| | | |
| | | async function open(id?: number) { |
| | | if (!id || busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | matchedLine.value = undefined; |
| | | try { |
| | | const detail = await getStockTaking(id); |
| | | if (!detail) throw new Error('çç¹ä»»å¡ä¸åå¨æå·²è¢«å é¤'); |
| | | task.value = detail; |
| | | lines.value = await readLines(id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function identify(raw: string) { |
| | | if (busy.value || !task.value?.id) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | matchedLine.value = undefined; |
| | | try { |
| | | const found = await lookup(raw); |
| | | const reason = writeBlockReason(found); |
| | | if (reason) throw new Error(reason); |
| | | const line = lines.value.find((item) => item.batchCode && item.batchCode === found.data.batchCode); |
| | | trace.value = found; |
| | | matchedLine.value = line; |
| | | const max = traceQuantity(found); |
| | | quantity.value = line?.quantity ?? max ?? 1; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirm() { |
| | | const current = task.value; |
| | | const found = trace.value; |
| | | if (!current?.id || !found || busy.value) return; |
| | | if (!Number.isFinite(quantity.value) || quantity.value < 0) { |
| | | error.value = 'çç¹æ°éä¸è½å°äºé¶'; |
| | | return; |
| | | } |
| | | if (!window.confirm(`确认çç¹ ${found.data.itemName || ''} ${quantity.value}ï¼`)) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | try { |
| | | await scanStockTaking({ |
| | | taskId: current.id, |
| | | scanCode: found.code, |
| | | takingQuantity: quantity.value, |
| | | remark: 'ç§»å¨ç«¯æ«ç çç¹', |
| | | }); |
| | | notice.value = 'å·²æäº¤çç¹ç»æã'; |
| | | trace.value = undefined; |
| | | matchedLine.value = undefined; |
| | | lines.value = await readLines(current.id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <p v-if="notice" class="m-success" role="status">{{ notice }}</p> |
| | | <p v-if="error" class="m-error" role="alert">{{ error }}</p> |
| | | <template v-if="task"> |
| | | <button class="m-secondary" :disabled="busy" @click="task = undefined; trace = undefined; matchedLine = undefined; load()"> |
| | | è¿åçç¹ä»»å¡å表 |
| | | </button> |
| | | <section class="m-panel" style="margin-top: 16px"> |
| | | <h2 class="m-title">{{ task.code }}</h2> |
| | | <dl> |
| | | <div class="m-row"><dt>åç§°</dt><dd>{{ task.name || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>çç¹æ¥æ</dt><dd>{{ task.takingDate || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>çç¹äºº</dt><dd>{{ task.userNickname || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>çç¹æ¹æ¡</dt><dd>{{ task.planName || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>夿³¨</dt><dd>{{ task.remark || 'â' }}</dd></div> |
| | | </dl> |
| | | </section> |
| | | <h2 class="m-section-title">çç¹èå´</h2> |
| | | <p v-if="!lines.length" class="m-empty">è¯¥ä»»å¡ææ çç¹è¡</p> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"> |
| | | <h3 class="m-title">{{ line.itemName || line.itemCode }}</h3> |
| | | <p>è´¦é¢æ°é {{ line.quantity ?? 0 }} <span class="m-muted">æ¹æ¬¡ {{ line.batchCode || 'æªå½å
¥' }}</span></p> |
| | | <p class="m-muted">{{ [line.warehouseName, line.locationName, line.areaName].filter(Boolean).join(' / ') || 'â' }}</p> |
| | | <p v-if="line.takingQuantity != null" class="m-muted">å·²ç {{ line.takingQuantity }}ï¼å·®å¼ {{ line.differenceQuantity ?? 0 }}</p> |
| | | </article> |
| | | <h2 class="m-section-title">æ«ç çç¹</h2> |
| | | <p class="m-muted">æ«æè¢ä¸è¿½æº¯äºç»´ç ææ¹æ¬¡ç ï¼ææ¹æ¬¡å¹é
çç¹è¡åæäº¤çç¹æ°éã</p> |
| | | <Scanner v-if="!trace" :disabled="busy" @code="identify" /> |
| | | <section v-else class="m-panel"> |
| | | <h3 class="m-title">æ ¸å¯¹çç¹å¯¹è±¡</h3> |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ trace.data.itemName || matchedLine?.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ trace.data.batchCode || matchedLine?.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>åºä½</dt><dd>{{ [matchedLine?.warehouseName, matchedLine?.locationName, matchedLine?.areaName].filter(Boolean).join(' / ') || 'ä¸å¨çç¹èå´' }}</dd></div> |
| | | <div class="m-row"><dt>è´¦é¢æ°é</dt><dd>{{ matchedLine?.quantity ?? 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | | <label class="m-field"> |
| | | <span>çç¹æ°é</span> |
| | | <input v-model.number="quantity" type="number" inputmode="decimal" min="0" step="any" :disabled="busy" /> |
| | | </label> |
| | | <div class="m-action-bar"> |
| | | <button class="m-primary" :disabled="busy" @click="confirm">确认æäº¤çç¹</button> |
| | | <button class="m-secondary" :disabled="busy" @click="trace = undefined; matchedLine = undefined">éæ°æ«æ</button> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | <template v-else> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | | <label class="m-field"><span>çç¹ä»»å¡å·</span><input v-model="code" :disabled="busy" placeholder="è¾å
¥çç¹ä»»å¡å·" /></label> |
| | | <button class="m-secondary" :disabled="busy">{{ busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢çç¹ä¸ä»»å¡' }}</button> |
| | | </form> |
| | | <p class="m-muted" role="status">å
± {{ total }} 个çç¹ä¸ä»»å¡</p> |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">ææ çç¹ä¸ä»»å¡</p> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"> |
| | | <strong>{{ row.code }}</strong><span>çç¹ä¸</span><small>{{ row.name || 'æªå½å
¥åç§°' }}</small> |
| | | </button> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)"> |
| | | {{ busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤' }} |
| | | </button> |
| | | </template> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, ref } from 'vue'; |
| | | import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock'; |
| | | import { |
| | | getTransfer, |
| | | getTransferPage, |
| | | type MesWmTransferApi, |
| | | } from '#/api/mes/wm/transfer'; |
| | | import { |
| | | createTransferLine, |
| | | getTransferLineList, |
| | | type MesWmTransferLineApi, |
| | | } from '#/api/mes/wm/transfer/line'; |
| | | import { |
| | | errorText, |
| | | findStocks, |
| | | lookup, |
| | | traceQuantity, |
| | | writeBlockReason, |
| | | type TraceResult, |
| | | } from '#/mobile/api'; |
| | | import Scanner from './scanner.vue'; |
| | | |
| | | defineOptions({ name: 'MobileTransfer' }); |
| | | |
| | | type Transfer = MesWmTransferApi.Transfer; |
| | | type Line = MesWmTransferLineApi.TransferLine; |
| | | |
| | | const busy = ref(false); |
| | | const error = ref(''); |
| | | const notice = ref(''); |
| | | const code = ref(''); |
| | | const rows = ref<Transfer[]>([]); |
| | | const page = ref(0); |
| | | const total = ref(0); |
| | | const transfer = ref<Transfer>(); |
| | | const lines = ref<Line[]>([]); |
| | | const trace = ref<TraceResult>(); |
| | | const stock = ref<MesWmMaterialStockApi.MaterialStock>(); |
| | | const quantity = ref(1); |
| | | let filters: { code?: string } = {}; |
| | | |
| | | async function load(reset = true) { |
| | | if (busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | const next = reset ? 1 : page.value + 1; |
| | | const query = reset ? { code: code.value.trim() || undefined } : filters; |
| | | try { |
| | | const data = await getTransferPage({ ...query, status: 0, pageNo: next, pageSize: 20 }); |
| | | rows.value = reset ? data.list : [...rows.value, ...data.list]; |
| | | page.value = next; |
| | | total.value = data.total; |
| | | filters = query; |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function open(id?: number) { |
| | | if (!id || busy.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | try { |
| | | const detail = await getTransfer(id); |
| | | if (!detail) throw new Error('转移åä¸åå¨æå·²è¢«å é¤'); |
| | | transfer.value = detail; |
| | | lines.value = await getTransferLineList(id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function identify(raw: string) { |
| | | if (busy.value || !transfer.value) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | notice.value = ''; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | try { |
| | | const found = await lookup(raw); |
| | | const reason = writeBlockReason(found); |
| | | if (reason) throw new Error(reason); |
| | | const stocks = await findStocks(found); |
| | | const matched = stocks.find((item) => (item.quantity ?? 0) > 0) ?? stocks[0]; |
| | | if (!matched) throw new Error('æªæ¾å°è¯¥è¿½æº¯ç 对åºçåºåè®°å½'); |
| | | trace.value = found; |
| | | stock.value = matched; |
| | | const max = traceQuantity(found); |
| | | quantity.value = max && matched.quantity ? Math.min(max, matched.quantity) : (max ?? matched.quantity ?? 1); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | async function confirm() { |
| | | const current = transfer.value; |
| | | const matched = stock.value; |
| | | if (!current?.id || !matched?.id || busy.value) return; |
| | | if (!Number.isFinite(quantity.value) || quantity.value <= 0) { |
| | | error.value = '转移æ°éå¿
须大äºé¶'; |
| | | return; |
| | | } |
| | | if (!window.confirm(`ç¡®è®¤ç§»åº ${matched.batchCode || ''} ${quantity.value}ï¼`)) return; |
| | | busy.value = true; |
| | | error.value = ''; |
| | | try { |
| | | await createTransferLine({ |
| | | transferId: current.id, |
| | | materialStockId: matched.id, |
| | | itemId: matched.itemId, |
| | | quantity: quantity.value, |
| | | batchId: matched.batchId, |
| | | fromWarehouseId: matched.warehouseId, |
| | | fromLocationId: matched.locationId, |
| | | fromAreaId: matched.areaId, |
| | | remark: `æ«ç æ¥æº ${trace.value?.code ?? ''}`, |
| | | }); |
| | | notice.value = '已添å ç§»åºæç»ã'; |
| | | trace.value = undefined; |
| | | stock.value = undefined; |
| | | lines.value = await getTransferLineList(current.id); |
| | | } catch (e) { |
| | | error.value = errorText(e); |
| | | } finally { |
| | | busy.value = false; |
| | | } |
| | | } |
| | | |
| | | onMounted(() => load()); |
| | | </script> |
| | | |
| | | <template> |
| | | <p v-if="notice" class="m-success" role="status">{{ notice }}</p> |
| | | <p v-if="error" class="m-error" role="alert">{{ error }}</p> |
| | | <template v-if="transfer"> |
| | | <button class="m-secondary" :disabled="busy" @click="transfer = undefined; trace = undefined; stock = undefined; load()"> |
| | | è¿å转移åå表 |
| | | </button> |
| | | <section class="m-panel" style="margin-top: 16px"> |
| | | <h2 class="m-title">{{ transfer.code }}</h2> |
| | | <dl> |
| | | <div class="m-row"><dt>åç§°</dt><dd>{{ transfer.name || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç®çå°</dt><dd>{{ transfer.destinationAddress || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>夿³¨</dt><dd>{{ transfer.remark || 'â' }}</dd></div> |
| | | </dl> |
| | | </section> |
| | | <h2 class="m-section-title">ç§»åºæç»</h2> |
| | | <p v-if="!lines.length" class="m-empty">è¯¥åææ ç§»åºæç»</p> |
| | | <article v-for="line in lines" :key="line.id" class="m-panel"> |
| | | <h3 class="m-title">{{ line.itemName || line.itemCode }}</h3> |
| | | <p>转移æ°é {{ line.quantity ?? 0 }}</p> |
| | | <p class="m-muted">æ¹æ¬¡ {{ line.batchCode || 'æªå½å
¥' }}</p> |
| | | </article> |
| | | <h2 class="m-section-title">æ«ç ç§»åº</h2> |
| | | <p class="m-muted">æ«æè¢ä¸è¿½æº¯äºç»´ç ï¼æç©æä¸æ¹æ¬¡å¹é
åºååæ°å¢ä¸è¡ç§»åºæç»ã</p> |
| | | <Scanner v-if="!trace" :disabled="busy" @code="identify" /> |
| | | <section v-else class="m-panel"> |
| | | <h3 class="m-title">æ ¸å¯¹ç§»åºå¯¹è±¡</h3> |
| | | <dl> |
| | | <div class="m-row"><dt>ç©æ</dt><dd>{{ trace.data.itemName || 'æªå½å
¥' }}</dd></div> |
| | | <div class="m-row"><dt>æ¹æ¬¡</dt><dd>{{ stock?.batchCode || trace.data.batchCode || 'æªç»å®' }}</dd></div> |
| | | <div class="m-row"><dt>ç§»åºä½ç½®</dt><dd>{{ [stock?.warehouseName, stock?.locationName, stock?.areaName].filter(Boolean).join(' / ') || 'â' }}</dd></div> |
| | | <div class="m-row"><dt>åºåæ°é</dt><dd>{{ stock?.quantity ?? 'â' }}</dd></div> |
| | | <div class="m-row"><dt>ç å¼</dt><dd style="overflow-wrap: anywhere">{{ trace.code }}</dd></div> |
| | | </dl> |
| | | <label class="m-field"> |
| | | <span>转移æ°é</span> |
| | | <input v-model.number="quantity" type="number" inputmode="decimal" min="0.01" step="any" :disabled="busy" /> |
| | | </label> |
| | | <div class="m-action-bar"> |
| | | <button class="m-primary" :disabled="busy" @click="confirm">ç¡®è®¤æ·»å æç»</button> |
| | | <button class="m-secondary" :disabled="busy" @click="trace = undefined; stock = undefined">éæ°æ«æ</button> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | <template v-else> |
| | | <form class="m-panel" @submit.prevent="load()"> |
| | | <label class="m-field"><span>转移åå·</span><input v-model="code" :disabled="busy" placeholder="è¾å
¥è½¬ç§»åå·" /></label> |
| | | <button class="m-secondary" :disabled="busy">{{ busy ? 'æ£å¨æ¥è¯¢â¦' : 'æ¥è¯¢è稿转移å' }}</button> |
| | | </form> |
| | | <p class="m-muted" role="status">å
± {{ total }} å¼ è稿转移å</p> |
| | | <p v-if="!rows.length && !busy && !error" class="m-empty">ææ è稿转移å</p> |
| | | <button v-for="row in rows" :key="row.id" class="m-order-button" :disabled="busy" @click="open(row.id)"> |
| | | <strong>{{ row.code }}</strong><span>è稿</span><small>{{ row.name || 'æªå½å
¥åç§°' }}</small> |
| | | </button> |
| | | <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)"> |
| | | {{ busy ? 'æ£å¨å è½½â¦' : 'å è½½æ´å¤' }} |
| | | </button> |
| | | </template> |
| | | </template> |
| | |
| | | formApi.setState({ schema: useLineFormSchema(formApi) }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | defaults?: Partial<MesWmMiscIssueLineApi.MiscIssueLine>; |
| | | id?: number; |
| | | issueId: number; |
| | | }>(); |
| | | issueId.value = data.issueId; |
| | | if (!data.id) { |
| | | if (data.defaults) { |
| | | await formApi.setValues(data.defaults); |
| | | } |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesWmMiscIssueLineApi } from '#/api/mes/wm/miscissue/line'; |
| | | |
| | | import type { TraceResult } from '#/utils/traceLookup'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | |
| | | deleteMiscIssueLine, |
| | | getMiscIssueLinePage, |
| | | } from '#/api/mes/wm/miscissue/line'; |
| | | import TraceScanPanel from '#/components/TraceScanPanel.vue'; |
| | | import { $t } from '#/locales'; |
| | | import { findMaterialStocks, traceQuantity } from '#/utils/traceLookup'; |
| | | |
| | | import { useLineGridColumns } from '../data'; |
| | | import LineForm from './line-form.vue'; |
| | |
| | | /** æ·»å ç©æ */ |
| | | function handleCreate() { |
| | | lineFormModalApi.setData({ issueId: props.issueId }).open(); |
| | | } |
| | | |
| | | /** æ«ç 带åºï¼æè¿½æº¯ç å¹é
åºåè®°å½åæå¼æ°å¢è¡è¡¨å */ |
| | | async function handleTraceConfirm(trace: TraceResult) { |
| | | try { |
| | | const stocks = await findMaterialStocks(trace); |
| | | const stock = stocks.find((item) => (item.quantity ?? 0) > 0) ?? stocks[0]; |
| | | if (!stock) { |
| | | message.warning('æªæ¾å°è¯¥è¿½æº¯ç 对åºçåºåè®°å½'); |
| | | return; |
| | | } |
| | | const quantity = traceQuantity(trace); |
| | | lineFormModalApi |
| | | .setData({ |
| | | defaults: { |
| | | areaId: stock.areaId, |
| | | batchCode: stock.batchCode, |
| | | itemId: stock.itemId, |
| | | locationId: stock.locationId, |
| | | materialStockId: stock.id, |
| | | quantity: |
| | | quantity && stock.quantity |
| | | ? Math.min(quantity, stock.quantity) |
| | | : (quantity ?? stock.quantity), |
| | | quantityMax: stock.quantity, |
| | | warehouseId: stock.warehouseId, |
| | | }, |
| | | issueId: props.issueId, |
| | | }) |
| | | .open(); |
| | | } catch (e) { |
| | | message.error(e instanceof Error ? e.message : 'æ«ç 带åºå¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | |
| | | <template> |
| | | <div> |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <TraceScanPanel |
| | | v-if="isEditable" |
| | | title="æ«ç 带åºåºåºæç»" |
| | | @confirm="handleTraceConfirm" |
| | | /> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <TableAction |
| | |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ id?: number; issueId: number }>(); |
| | | const data = modalApi.getData<{ |
| | | defaults?: Partial<MesWmProductIssueLineApi.ProductIssueLine>; |
| | | id?: number; |
| | | issueId: number; |
| | | }>(); |
| | | issueId.value = data.issueId; |
| | | if (!data.id) { |
| | | if (data.defaults) { |
| | | await formApi.setValues(data.defaults); |
| | | } |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail'; |
| | | import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line'; |
| | | |
| | | import type { TraceResult } from '#/utils/traceLookup'; |
| | | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | |
| | | deleteProductIssueLine, |
| | | getProductIssueLinePage, |
| | | } from '#/api/mes/wm/productissue/line'; |
| | | import TraceScanPanel from '#/components/TraceScanPanel.vue'; |
| | | import { $t } from '#/locales'; |
| | | import { traceQuantity } from '#/utils/traceLookup'; |
| | | |
| | | import { useLineGridColumns } from '../data'; |
| | | import DetailForm from './detail-form.vue'; |
| | |
| | | /** æ·»å ç©æ */ |
| | | function handleCreate() { |
| | | lineFormModalApi.setData({ issueId: props.issueId }).open(); |
| | | } |
| | | |
| | | /** æ«ç 带åºï¼æè¿½æº¯ç 带åºç©æä¸æ°é */ |
| | | function handleTraceConfirm(trace: TraceResult) { |
| | | if (!trace.data.itemId) { |
| | | message.warning('è¯¥è¿½æº¯ç æªç»å®ç©æ'); |
| | | return; |
| | | } |
| | | lineFormModalApi |
| | | .setData({ |
| | | defaults: { |
| | | itemId: trace.data.itemId, |
| | | quantity: traceQuantity(trace), |
| | | }, |
| | | issueId: props.issueId, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | |
| | | <div> |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <DetailFormModal @success="handleDetailSuccess" /> |
| | | <TraceScanPanel |
| | | v-if="isEditable" |
| | | title="æ«ç 带åºé¢ææç»" |
| | | @confirm="handleTraceConfirm" |
| | | /> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <TableAction |
| | |
| | | } |
| | | formApi.setState({ schema: useLineFormSchema(formApi) }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ id?: number; receiptId: number }>(); |
| | | const data = modalApi.getData<{ |
| | | defaults?: Partial<MesWmProductReceiptLineApi.ProductReceiptLine>; |
| | | id?: number; |
| | | receiptId: number; |
| | | }>(); |
| | | receiptId.value = data.receiptId; |
| | | if (!data.id) { |
| | | if (data.defaults) { |
| | | await formApi.setValues(data.defaults); |
| | | } |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail'; |
| | | import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line'; |
| | | |
| | | import type { TraceResult } from '#/utils/traceLookup'; |
| | | |
| | | import { computed, reactive, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | |
| | | deleteProductReceiptLine, |
| | | getProductReceiptLinePage, |
| | | } from '#/api/mes/wm/productreceipt/line'; |
| | | import TraceScanPanel from '#/components/TraceScanPanel.vue'; |
| | | import { $t } from '#/locales'; |
| | | import { findMaterialStocks, traceQuantity } from '#/utils/traceLookup'; |
| | | import { BarcodeDetail, PrinterLabel } from '#/views/wls/barcode/components'; |
| | | |
| | | import { useLineGridColumns } from '../data'; |
| | |
| | | /** æ·»å ç©æ */ |
| | | function handleCreate() { |
| | | lineFormModalApi.setData({ receiptId: props.receiptId }).open(); |
| | | } |
| | | |
| | | /** æ«ç 带åºï¼æè¿½æº¯ç å¹é
åºåè®°å½åæå¼æ°å¢è¡è¡¨å */ |
| | | async function handleTraceConfirm(trace: TraceResult) { |
| | | try { |
| | | const stocks = await findMaterialStocks(trace, { virtualFilter: 'only' }); |
| | | const stock = stocks[0]; |
| | | if (!stock) { |
| | | message.warning('æªæ¾å°è¯¥è¿½æº¯ç 对åºçåºåè®°å½'); |
| | | return; |
| | | } |
| | | const quantity = traceQuantity(trace); |
| | | lineFormModalApi |
| | | .setData({ |
| | | defaults: { |
| | | batchCode: stock.batchCode, |
| | | batchId: stock.batchId, |
| | | itemId: stock.itemId, |
| | | materialStockId: stock.id, |
| | | quantity: |
| | | quantity && stock.quantity |
| | | ? Math.min(quantity, stock.quantity) |
| | | : (quantity ?? stock.quantity), |
| | | quantityMax: stock.quantity, |
| | | }, |
| | | receiptId: props.receiptId, |
| | | }) |
| | | .open(); |
| | | } catch (e) { |
| | | message.error(e instanceof Error ? e.message : 'æ«ç 带åºå¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | |
| | | <div> |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <DetailFormModal @success="handleDetailSuccess" /> |
| | | <TraceScanPanel |
| | | v-if="isEditable" |
| | | title="æ«ç 带åºç©ææç»" |
| | | @confirm="handleTraceConfirm" |
| | | /> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <TableAction |
| | |
| | | submitProductSales, |
| | | updateProductSales, |
| | | } from '#/api/mes/wm/productsales'; |
| | | import TraceScanInput from '#/components/TraceScanInput.vue'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | |
| | | const formType = ref<FormType>('create'); |
| | | const formData = ref<MesWmProductSalesApi.ProductSales>(); |
| | | const scanContent = ref(''); |
| | | const scanType = ref<'BAG' | 'PALLET' | 'BATCH'>('BAG'); |
| | | const scanType = ref<'' | 'BAG' | 'BATCH' | 'PALLET'>(''); |
| | | const scanPreview = ref<MesWmProductSalesApi.ProductSalesScanResponse>(); |
| | | const selectedBagIds = ref<number[]>([]); |
| | | const scanLoading = ref(false); |
| | |
| | | } |
| | | } |
| | | |
| | | /** å
è¯å«å¯¹è±¡å¹¶éæ©è¢ç ï¼å车ä¸ä¼ç´æ¥åå
¥æ£è´§ã */ |
| | | async function handleScan() { |
| | | if (!formData.value?.id || !scanContent.value.trim() || scanLoading.value) { |
| | | /** å
è¯å«å¯¹è±¡å¹¶éæ©è¢ç ï¼ç¡®è®¤åæåå
¥æ£è´§ã */ |
| | | async function handleScan(raw?: string) { |
| | | const content = (raw ?? scanContent.value).trim(); |
| | | if (!formData.value?.id || !content || scanLoading.value) { |
| | | return; |
| | | } |
| | | scanContent.value = content; |
| | | scanLoading.value = true; |
| | | try { |
| | | scanPreview.value = undefined; |
| | | selectedBagIds.value = []; |
| | | const result = await previewProductSalesScan(scanContent.value.trim(), scanType.value, formData.value.id); |
| | | const result = await previewProductSalesScan(content, scanType.value || undefined, formData.value.id); |
| | | scanPreview.value = result; |
| | | if (result.scanType === 'BAG') selectedBagIds.value = result.bags?.filter(b => b.selectable).map(b => b.id) || []; |
| | | if (!result.scanQuantity) message.warning('å½å对象没æå¯æ£è¢ç ï¼è¯·æ ¸å¯¹è´¨æ£ååºåç¶æ'); |
| | |
| | | <div v-if="isStock" class="mb-3 flex items-center gap-2 px-4"> |
| | | <label for="sales-picking-type" class="sr-only">æ£è´§æ«ç ç±»å</label> |
| | | <a-select id="sales-picking-type" v-model:value="scanType" :disabled="scanLoading" style="width:140px" @change="scanPreview = undefined; selectedBagIds = []"> |
| | | <a-select-option value="BAG">è¢ç </a-select-option><a-select-option value="PALLET">æçç </a-select-option><a-select-option value="BATCH">çäº§æ¹æ¬¡ç </a-select-option> |
| | | <a-select-option value="">èªå¨è¯å«</a-select-option><a-select-option value="BAG">è¢ç </a-select-option><a-select-option value="PALLET">æçç </a-select-option><a-select-option value="BATCH">çäº§æ¹æ¬¡ç </a-select-option> |
| | | </a-select> |
| | | <a-input |
| | | v-model:value="scanContent" |
| | | :disabled="scanLoading" |
| | | autofocus |
| | | <TraceScanInput |
| | | class="flex-1" |
| | | placeholder="æ«ææéç±»åç ï¼å车è¯å«" |
| | | @press-enter="handleScan" |
| | | :disabled="scanLoading" |
| | | placeholder="æ«æè¿½æº¯äºç»´ç æè¾å
¥è¿½æº¯ç ï¼å车è¯å«" |
| | | @code="handleScan" |
| | | /> |
| | | <Button :loading="scanLoading" type="default" @click="handleScan"> |
| | | è¯å«ç å¼ |
| | | </Button> |
| | | </div> |
| | | <section v-if="isStock && scanPreview" class="mb-4 rounded-lg border border-border p-4"> |
| | | <h3 class="font-semibold">æ ¸å¯¹æ£è´§å¯¹è±¡</h3><p>æ¹æ¬¡ {{scanPreview.batchCode}} · 坿£ {{scanPreview.scanQuantity}} è¢</p> |
| | |
| | | } |
| | | formApi.setState({ schema: useLineFormSchema(formApi) }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ id?: number; issueId: number }>(); |
| | | const data = modalApi.getData<{ |
| | | defaults?: Partial<MesWmReturnIssueLineApi.ReturnIssueLine>; |
| | | id?: number; |
| | | issueId: number; |
| | | }>(); |
| | | issueId.value = data.issueId; |
| | | if (!data.id) { |
| | | if (data.defaults) { |
| | | await formApi.setValues(data.defaults); |
| | | } |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | import type { MesWmReturnIssueDetailApi } from '#/api/mes/wm/returnissue/detail'; |
| | | import type { MesWmReturnIssueLineApi } from '#/api/mes/wm/returnissue/line'; |
| | | |
| | | import type { TraceResult } from '#/utils/traceLookup'; |
| | | |
| | | import { computed, reactive, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | |
| | | deleteReturnIssueLine, |
| | | getReturnIssueLinePage, |
| | | } from '#/api/mes/wm/returnissue/line'; |
| | | import TraceScanPanel from '#/components/TraceScanPanel.vue'; |
| | | import { $t } from '#/locales'; |
| | | import { findMaterialStocks, traceQuantity } from '#/utils/traceLookup'; |
| | | import { BarcodeDetail, PrinterLabel } from '#/views/wls/barcode/components'; |
| | | |
| | | import { useLineGridColumns } from '../data'; |
| | |
| | | /** æ·»å ç©æ */ |
| | | function handleCreate() { |
| | | lineFormModalApi.setData({ issueId: props.issueId }).open(); |
| | | } |
| | | |
| | | /** æ«ç 带åºï¼æè¿½æº¯ç å¹é
线边åºåºåè®°å½åæå¼æ°å¢éæè¡ */ |
| | | async function handleTraceConfirm(trace: TraceResult) { |
| | | try { |
| | | const stocks = await findMaterialStocks(trace, { virtualFilter: 'only' }); |
| | | const stock = stocks.find((item) => (item.quantity ?? 0) > 0) ?? stocks[0]; |
| | | if (!stock) { |
| | | message.warning('æªæ¾å°è¯¥è¿½æº¯ç 对åºç线边åºåºåè®°å½'); |
| | | return; |
| | | } |
| | | const quantity = traceQuantity(trace); |
| | | lineFormModalApi |
| | | .setData({ |
| | | defaults: { |
| | | batchCode: stock.batchCode, |
| | | batchId: stock.batchId, |
| | | itemId: stock.itemId, |
| | | materialDisplayName: [stock.itemName, stock.specification] |
| | | .filter(Boolean) |
| | | .join(' | '), |
| | | materialStockId: stock.id, |
| | | quantity: |
| | | quantity && stock.quantity |
| | | ? Math.min(quantity, stock.quantity) |
| | | : (quantity ?? stock.quantity), |
| | | quantityMax: stock.quantity, |
| | | }, |
| | | issueId: props.issueId, |
| | | }) |
| | | .open(); |
| | | } catch (e) { |
| | | message.error(e instanceof Error ? e.message : 'æ«ç 带åºå¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | |
| | | <div> |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <DetailFormModal @success="handleDetailSuccess" /> |
| | | <TraceScanPanel |
| | | v-if="isEditable" |
| | | title="æ«ç 带åºéææç»" |
| | | @confirm="handleTraceConfirm" |
| | | /> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <TableAction |
| | |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | clientId?: number; |
| | | defaults?: Partial<MesWmReturnSalesLineApi.ReturnSalesLine>; |
| | | id?: number; |
| | | productSalesId?: number; |
| | | returnId: number; |
| | |
| | | ), |
| | | }); |
| | | if (!data.id) { |
| | | if (data.defaults) { |
| | | await formApi.setValues(data.defaults); |
| | | } |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | import type { MesWmReturnSalesDetailApi } from '#/api/mes/wm/returnsales/detail'; |
| | | import type { MesWmReturnSalesLineApi } from '#/api/mes/wm/returnsales/line'; |
| | | |
| | | import type { TraceResult } from '#/utils/traceLookup'; |
| | | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getReturnSalesDetailListByLineId } from '#/api/mes/wm/returnsales/detail'; |
| | | import { getBatchPage } from '#/api/mes/wm/batch'; |
| | | import { getReturnableProductSalesLineList } from '#/api/mes/wm/returnsales'; |
| | | import { |
| | | deleteReturnSalesLine, |
| | | getReturnSalesLinePage, |
| | | } from '#/api/mes/wm/returnsales/line'; |
| | | import TraceScanPanel from '#/components/TraceScanPanel.vue'; |
| | | import { $t } from '#/locales'; |
| | | import { traceQuantity } from '#/utils/traceLookup'; |
| | | import { PrinterLabel } from '#/views/wls/barcode/components'; |
| | | |
| | | import { useLineGridColumns } from '../data'; |
| | |
| | | returnId: props.returnId, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | /** æ«ç 带åºï¼å
³èåºåºåæ¶å¹é
å¯éåºåºè¡ï¼å¦åææ¹æ¬¡å¸¦åºç©æä¸æ°é */ |
| | | async function handleTraceConfirm(trace: TraceResult) { |
| | | const { batchCode, itemId } = trace.data; |
| | | if (!itemId || !batchCode) { |
| | | message.warning('è¯¥è¿½æº¯ç æªç»å®ç©æææ¹æ¬¡'); |
| | | return; |
| | | } |
| | | try { |
| | | if (props.productSalesId) { |
| | | const lines = await getReturnableProductSalesLineList( |
| | | props.productSalesId, |
| | | ); |
| | | const matched = lines.find( |
| | | (line) => line.itemId === itemId && line.batchCode === batchCode, |
| | | ); |
| | | if (!matched) { |
| | | message.warning('该追溯ç 对åºçæ¹æ¬¡ä¸å¨æ¬åå¯éèå´å
'); |
| | | return; |
| | | } |
| | | const returnable = Number(matched.returnableQuantity ?? 0); |
| | | const quantity = Math.min(traceQuantity(trace) ?? returnable, returnable); |
| | | lineFormModalApi |
| | | .setData({ |
| | | clientId: props.clientId, |
| | | defaults: { |
| | | batchCode: matched.batchCode, |
| | | itemId: matched.itemId, |
| | | productSalesLineId: matched.productSalesLineId, |
| | | quantity, |
| | | returnableQuantity: returnable, |
| | | }, |
| | | productSalesId: props.productSalesId, |
| | | returnId: props.returnId, |
| | | }) |
| | | .open(); |
| | | return; |
| | | } |
| | | const batches = await getBatchPage({ |
| | | code: batchCode, |
| | | itemId, |
| | | pageNo: 1, |
| | | pageSize: 20, |
| | | }); |
| | | const batch = batches.list.find((item) => item.code === batchCode); |
| | | if (!batch?.id) { |
| | | message.warning('æªæ¾å°è¯¥è¿½æº¯ç 对åºçå
¥åºæ¹æ¬¡'); |
| | | return; |
| | | } |
| | | lineFormModalApi |
| | | .setData({ |
| | | clientId: props.clientId, |
| | | defaults: { |
| | | batchCode, |
| | | batchId: batch.id, |
| | | itemId, |
| | | quantity: traceQuantity(trace), |
| | | }, |
| | | productSalesId: props.productSalesId, |
| | | returnId: props.returnId, |
| | | }) |
| | | .open(); |
| | | } catch (e) { |
| | | message.error(e instanceof Error ? e.message : 'æ«ç 带åºå¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾ç©æ */ |
| | |
| | | <div> |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <DetailFormModal @success="handleDetailSuccess" /> |
| | | <TraceScanPanel |
| | | v-if="isEditable" |
| | | title="æ«ç 带åºéè´§æç»" |
| | | @confirm="handleTraceConfirm" |
| | | /> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <TableAction |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, Input, InputNumber, message } from 'ant-design-vue'; |
| | | import { Button, InputNumber, message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { scanStockTaking } from '#/api/mes/wm/stocktaking/task'; |
| | |
| | | deleteStockTakingResult, |
| | | getStockTakingResultPage, |
| | | } from '#/api/mes/wm/stocktaking/task/result'; |
| | | import TraceScanInput from '#/components/TraceScanInput.vue'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useResultGridColumns } from '../data'; |
| | |
| | | const scanning = ref(false); |
| | | |
| | | /** æ«ç çç¹ */ |
| | | async function handleScan() { |
| | | if (!scanCode.value) { |
| | | async function handleScan(raw?: string) { |
| | | const code = (raw ?? scanCode.value).trim(); |
| | | if (!code) { |
| | | message.warning('请è¾å
¥è¿½æº¯ç /è¢ç /æçç /æ¹æ¬¡å·'); |
| | | return; |
| | | } |
| | | scanCode.value = code; |
| | | if (takingQuantity.value === undefined || takingQuantity.value === null) { |
| | | message.warning('请è¾å
¥çç¹æ°é'); |
| | | return; |
| | |
| | | class="mb-3 flex flex-wrap items-center gap-2 rounded-md border border-dashed border-yellow-300 bg-yellow-50 px-3 py-2" |
| | | > |
| | | <span class="font-medium text-yellow-700">PDA æ«ç çç¹ï¼</span> |
| | | <Input |
| | | v-model:value="scanCode" |
| | | allow-clear |
| | | class="!w-56" |
| | | placeholder="追溯ç /è¢ç /æçç /æ¹æ¬¡å·" |
| | | @press-enter="handleScan" |
| | | <TraceScanInput |
| | | class="!w-72" |
| | | :disabled="scanning" |
| | | placeholder="æ«æè¿½æº¯äºç»´ç æè¾å
¥è¿½æº¯ç ï¼å车è¯å«" |
| | | @code="handleScan" |
| | | /> |
| | | <InputNumber |
| | | v-model:value="takingQuantity" |
| | |
| | | :precision="2" |
| | | class="!w-32" |
| | | placeholder="çç¹æ°é" |
| | | @press-enter="handleScan" |
| | | @press-enter="() => handleScan()" |
| | | /> |
| | | <Button |
| | | type="primary" |
| | | :loading="scanning" |
| | | @click="handleScan" |
| | | @click="() => handleScan()" |
| | | > |
| | | æ«ç çç¹ |
| | | </Button> |
| | |
| | | } |
| | | formApi.setState({ schema: useLineFormSchema(formApi) }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ id?: number; transferId: number }>(); |
| | | const data = modalApi.getData<{ |
| | | defaults?: Partial<MesWmTransferLineApi.TransferLine>; |
| | | id?: number; |
| | | transferId: number; |
| | | }>(); |
| | | transferId.value = data.transferId; |
| | | if (!data.id) { |
| | | if (data.defaults) { |
| | | await formApi.setValues(data.defaults); |
| | | } |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | |
| | | import type { MesWmTransferDetailApi } from '#/api/mes/wm/transfer/detail'; |
| | | import type { MesWmTransferLineApi } from '#/api/mes/wm/transfer/line'; |
| | | |
| | | import type { TraceResult } from '#/utils/traceLookup'; |
| | | |
| | | import { computed, reactive } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | |
| | | deleteTransferLine, |
| | | getTransferLineList, |
| | | } from '#/api/mes/wm/transfer/line'; |
| | | import TraceScanPanel from '#/components/TraceScanPanel.vue'; |
| | | import { $t } from '#/locales'; |
| | | import { findMaterialStocks, traceQuantity } from '#/utils/traceLookup'; |
| | | import { PrinterLabel } from '#/views/wls/barcode/components'; |
| | | |
| | | import { useLineGridColumns } from '../data'; |
| | |
| | | /** æ·»å è°æ¨ç©æ */ |
| | | function handleCreate() { |
| | | lineFormModalApi.setData({ transferId: props.transferId }).open(); |
| | | } |
| | | |
| | | /** æ«ç 带åºï¼æè¿½æº¯ç å¹é
åºåè®°å½åæå¼æ°å¢è°æ¨è¡ */ |
| | | async function handleTraceConfirm(trace: TraceResult) { |
| | | try { |
| | | const stocks = await findMaterialStocks(trace); |
| | | const stock = stocks.find((item) => (item.quantity ?? 0) > 0) ?? stocks[0]; |
| | | if (!stock) { |
| | | message.warning('æªæ¾å°è¯¥è¿½æº¯ç 对åºçåºåè®°å½'); |
| | | return; |
| | | } |
| | | const quantity = traceQuantity(trace); |
| | | lineFormModalApi |
| | | .setData({ |
| | | defaults: { |
| | | batchCode: stock.batchCode, |
| | | batchId: stock.batchId, |
| | | fromAreaId: stock.areaId, |
| | | fromLocationId: stock.locationId, |
| | | fromWarehouseId: stock.warehouseId, |
| | | itemId: stock.itemId, |
| | | materialStockId: stock.id, |
| | | quantity: |
| | | quantity && stock.quantity |
| | | ? Math.min(quantity, stock.quantity) |
| | | : (quantity ?? stock.quantity), |
| | | quantityMax: stock.quantity, |
| | | }, |
| | | transferId: props.transferId, |
| | | }) |
| | | .open(); |
| | | } catch (e) { |
| | | message.error(e instanceof Error ? e.message : 'æ«ç 带åºå¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾è°æ¨ç©æ */ |
| | |
| | | <div> |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <DetailFormModal @success="handleDetailSuccess" /> |
| | | <TraceScanPanel |
| | | v-if="isEditable" |
| | | title="æ«ç 带åºè°æ¨æç»" |
| | | @confirm="handleTraceConfirm" |
| | | /> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <TableAction |