<script setup lang="ts">
|
import { computed, onMounted, ref } from 'vue';
|
import { onBeforeRouteLeave } from 'vue-router';
|
import { useUserStore } from '@vben/stores';
|
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
|
import { getWarehouseLocationSimpleList, type MesWmWarehouseLocationApi } from '#/api/mes/wm/warehouse/location';
|
import { getWarehouseAreaSimpleList, type MesWmWarehouseAreaApi } from '#/api/mes/wm/warehouse/area';
|
import { inboundScanEvent, type MesProScanEventApi } from '#/api/mes/pro/scanEvent';
|
import { lookup, myEvent, errorText, writeBlockReason, type TraceResult } from '#/mobile/api';
|
import Scanner from './scanner.vue';
|
import Picker from './picker.vue';
|
const user=useUserStore(),storageKey='mom-mobile-inbound-'+user.userInfo?.id;
|
const positionKey='mom-mobile-position-'+user.userInfo?.id;
|
const locations=ref<MesWmWarehouseLocationApi.WarehouseLocation[]>([]),areas=ref<MesWmWarehouseAreaApi.WarehouseArea[]>([]);
|
const locationId=ref<number>(),areaId=ref<number>(),busy=ref(false),error=ref(''),trace=ref<TraceResult>();
|
const quantity=ref(1),result=ref<MesProScanEventApi.ScanEvent>(),pending=ref<MesProScanEventApi.InboundParams>(),unknown=ref(false),ready=ref(false);
|
const remembered=ref('');
|
const areaLoading=ref(false),positionLoading=ref(false);
|
let areaRequest=0;
|
const place=computed(()=>[locations.value.find(l=>l.id===locationId.value)?.name,areas.value.find(a=>a.id===areaId.value)?.name].filter(Boolean).join(' / ')||remembered.value);
|
async function loadAreas(){
|
const ticket=++areaRequest, id=locationId.value;
|
areas.value=[];areaId.value=undefined;areaLoading.value=!!id;
|
try{if(id){const list=await getWarehouseAreaSimpleList(id);if(ticket===areaRequest)areas.value=list.filter(a=>!a.frozen&&(a.status==null||a.status===0));}}
|
finally{if(ticket===areaRequest)areaLoading.value=false;}
|
}
|
async function changeLocation(){error.value='';trace.value=undefined;try{await loadAreas();}catch(e){error.value=errorText(e);}}
|
async function loadPositions(){
|
if(positionLoading.value)return;positionLoading.value=true;
|
error.value='';
|
try{
|
const warehouses=await getWarehouseSimpleList();
|
const wip=warehouses.find(w=>w.code==='WIP_VIRTUAL_WAREHOUSE');
|
if(!wip?.id)throw new Error('未配置生产暂存仓,请联系管理员');
|
locations.value=(await getWarehouseLocationSimpleList(wip.id)).filter(l=>!l.frozen);
|
const saved=JSON.parse(sessionStorage.getItem(positionKey)||'null');
|
if(saved&&locations.value.some(l=>l.id===saved.locationId)){locationId.value=saved.locationId;await loadAreas();if(areas.value.some(a=>a.id===saved.areaId))areaId.value=saved.areaId;}
|
ready.value=true;
|
}catch(e){error.value=errorText(e);}finally{positionLoading.value=false;}
|
}
|
onMounted(async()=>{
|
try {
|
const savedPosition=JSON.parse(sessionStorage.getItem(positionKey)||'null');
|
remembered.value=savedPosition?.name||'';
|
const savedPending=JSON.parse(sessionStorage.getItem(storageKey)||'null');
|
if(savedPending){pending.value=savedPending;unknown.value=true;return;}
|
} catch { error.value='上次操作信息无法读取,请通过作业记录核实后再入库。'; return; }
|
await loadPositions();
|
});
|
function eventId(){
|
const bytes=crypto.getRandomValues(new Uint8Array(16));
|
return Array.from(bytes,b=>b.toString(16).padStart(2,'0')).join('');
|
}
|
async function identify(code:string){
|
if(busy.value||pending.value)return;busy.value=true;error.value='';trace.value=undefined;result.value=undefined;
|
try{
|
const found=await lookup(code),reason=writeBlockReason(found);
|
if(reason)throw new Error(reason);
|
if (found.type === 'bag' && found.data.inboundTime || found.type !== 'bag' && found.data.pendingBagQuantity === 0) throw new Error('该对象没有待入库袋码,请勿重复入库');
|
trace.value=found;quantity.value=1;
|
}catch(e){error.value=errorText(e);}finally{busy.value=false;}
|
}
|
function complete(r:MesProScanEventApi.ScanEvent){
|
result.value=r;unknown.value=false;pending.value=undefined;sessionStorage.removeItem(storageKey);
|
error.value=r.status==='SUCCESS'?'':(r.errorMessage||'入库失败');
|
}
|
async function submit(){
|
if(busy.value)return;
|
if(!pending.value){
|
if(!trace.value||!locationId.value||!areaId.value){error.value='请先选择库区、库位并识别码值';return;}
|
if(!Number.isFinite(quantity.value)||quantity.value<=0){error.value='入库数量必须大于零';return;}
|
pending.value={eventId:eventId(),bagCode:trace.value.code,scanType:trace.value.type.toUpperCase() as MesProScanEventApi.ScanType,quantity:quantity.value,locationId:locationId.value,areaId:areaId.value};
|
sessionStorage.setItem(positionKey,JSON.stringify({locationId:locationId.value,areaId:areaId.value,name:place.value}));
|
}
|
sessionStorage.setItem(storageKey,JSON.stringify(pending.value));
|
busy.value=true;error.value='';
|
try{complete(await inboundScanEvent(pending.value));}
|
catch(e){unknown.value=true;error.value='结果待确认:'+errorText(e)+'。请先核实原操作,勿重新扫码入库。';}
|
finally{busy.value=false;}
|
}
|
async function verify(){
|
if(!pending.value||busy.value)return;busy.value=true;error.value='';
|
try{const r=await myEvent(pending.value.eventId);if(r)complete(r);else error.value='尚未查到记录。请稍后核实,或使用原操作号重试。';}
|
catch(e){error.value=errorText(e);}finally{busy.value=false;}
|
}
|
function next(){result.value=undefined;trace.value=undefined;error.value='';if(!ready.value)void loadPositions();}
|
onBeforeRouteLeave(()=>!busy.value&&(!trace.value||!!result.value||unknown.value||window.confirm('离开将放弃尚未提交的对象,是否继续?')));
|
</script>
|
<template>
|
<p class="m-muted">生产暂存入库 · 质检前暂存</p>
|
<div v-if="pending && unknown" class="m-panel">
|
<h2 class="m-title">核实上次入库结果</h2><p>{{pending.bagCode}}</p><p>目标位置:{{place||('库区 '+pending.locationId+' / 库位 '+pending.areaId)}}</p><p class="m-muted">操作号:{{pending.eventId}}</p>
|
<p class="m-warning">同一操作重试将沿用原操作号,不创建新的入库请求。</p>
|
<div class="m-stack"><button class="m-primary" :disabled="busy" @click="verify">核实结果</button><button class="m-secondary" :disabled="busy" @click="submit">使用原操作号重试</button></div>
|
</div>
|
<template v-else-if="result">
|
<div :class="result.status==='SUCCESS'?'m-success':'m-error'" role="status"><h2 class="m-title">{{result.status==='SUCCESS'?'入库成功':'入库失败'}}</h2><p>{{result.bagCode}}</p><p>{{result.errorMessage||place}}</p><p class="m-muted">{{result.eventTime||result.receivedTime}}</p></div>
|
<button class="m-primary" @click="next">继续扫码</button>
|
<RouterLink to="/mobile/records" class="m-link">查看作业记录</RouterLink>
|
</template>
|
<template v-else>
|
<div v-if="!trace" class="m-panel">
|
<h2 class="m-title">目标位置</h2>
|
<Picker v-model="locationId" label="库区" :options="locations" :disabled="busy||!!trace" :loading="positionLoading" placeholder="请选择库区" @update:model-value="changeLocation"/>
|
<Picker v-model="areaId" label="库位" :options="areas" :disabled="busy||!!trace||!locationId" :loading="areaLoading" placeholder="请选择库位"/>
|
<p v-if="!ready" class="m-muted">正在获取作业位置</p>
|
<button v-if="!ready&&error" class="m-secondary" @click="loadPositions">重新获取位置</button>
|
</div>
|
<Scanner v-if="!trace" :disabled="busy||!areaId||!ready" @code="identify"/>
|
<div v-else class="m-panel">
|
<h2 class="m-title">核对入库对象</h2>
|
<dl><div class="m-row"><dt>品种</dt><dd>{{trace.data.itemName||'未录入'}}</dd></div><div class="m-row"><dt>码值</dt><dd>{{trace.code}}</dd></div><div class="m-row"><dt>类型</dt><dd>{{{bag:'袋码',pallet:'托盘码',batch:'批次码'}[trace.type]}}</dd></div><div class="m-row"><dt>目标位置</dt><dd>{{place}}</dd></div><div class="m-row"><dt>批次</dt><dd>{{trace.data.batchCode||'未绑定'}}</dd></div></dl>
|
<label v-if="trace.type==='bag'" class="m-field"><span>入库数量(按库存计量单位)</span><input v-model.number="quantity" type="number" inputmode="decimal" min="0.000001" step="any" :disabled="busy"/></label>
|
<p v-else class="m-warning">整托 / 整批入库,本次待入库 {{trace.data.pendingBagQuantity ?? '—'}} 袋,最终由服务端按未入库有效袋数计算。</p>
|
<div class="m-action-bar"><button class="m-primary" :disabled="busy" @click="submit">{{busy?'正在入库…':'确认入库'}}</button><button class="m-secondary" :disabled="busy" @click="next">返回重新扫描</button></div>
|
</div>
|
</template>
|
<p v-if="error" class="m-error" role="alert">{{error}}</p>
|
</template>
|