云
4 小时以前 fb4db4ae9b6a7f113ba049f6789e6916c0dcc276
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<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>