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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { onBeforeRouteLeave, useRoute } from 'vue-router';
import { useUserStore } from '@vben/stores';
import { bindBagCodePallet, getBagCode, getBagCodePage } from '#/api/mes/pro/bagCode';
import { batchCreatePallet, bindPalletBatch, getPallet, getPalletPage, preGeneratePallet } from '#/api/mes/pro/pallet';
import { getBatchPage } from '#/api/mes/pro/batch';
import { can } from '#/mobile/access';
import { lookup, errorText, writeBlockReason, type TraceResult, type TraceObject } from '#/mobile/api';
import Scanner from './scanner.vue';
defineOptions({name:'MobileQuery'});
const route=useRoute(),key=route.path.split('/')[2]||'bags';
const expected={bags:'bag',pallets:'pallet',batches:'batch'}[key];
const busy=ref(false),error=ref(''),notice=ref(''),detail=ref<TraceResult>(),rows=ref<TraceObject[]>([]),page=ref(1),total=ref(0),filter=ref('');
const binding=ref(false),target=ref<TraceResult>(),bags=ref<TraceResult[]>([]);
const autoPacking=ref(false),palletSize=ref<48|100>(48),thresholdPrompt=ref(false),thresholdPassed=ref(false);
type PendingBind={kind:string;ids:number[];targetId:number;stage?:'creating'|'binding';palletCode?:string};
const pending=ref<PendingBind>();
const storageKey='mom-mobile-bind-'+useUserStore().userInfo?.id+'-'+key;
const canBind=computed(()=>key==='bags'?can('mes:pro-bag-code:update')&&can('mes:pro-pallet:query'):key==='pallets'&&can('mes:pro-pallet:update')&&can('mes:pro-batch:query'));
const canAutoPack=computed(()=>key==='bags'&&can('mes:pro-bag-code:update')&&can('mes:pro-pallet:create')&&can('mes:pro-pallet:query'));
let appliedFilter='';
async function load(reset=false){
  if(busy.value)return;busy.value=true;error.value='';
  const nextPage=reset?1:page.value+1;
  const search=reset?filter.value.trim():appliedFilter;
  try{
    const params={pageNo:nextPage,pageSize:20,code:search||undefined};
    const r=await (key==='bags'?getBagCodePage(params):key==='pallets'?getPalletPage(params):getBatchPage(params));
    rows.value=reset?r.list:[...rows.value,...r.list];total.value=r.total;page.value=nextPage;
    appliedFilter=search;
  }catch(e){error.value=errorText(e);}finally{busy.value=false;}
}
onMounted(()=>{try{pending.value=JSON.parse(sessionStorage.getItem(storageKey)||'null')||undefined;}catch{sessionStorage.removeItem(storageKey);}load(true);});
async function identify(code:string){
  if(busy.value||pending.value||thresholdPrompt.value)return;busy.value=true;error.value='';notice.value='';
  try{
    const r=await lookup(code);
    if((binding.value||autoPacking.value)&&writeBlockReason(r))throw new Error(writeBlockReason(r));
    if(autoPacking.value){
      if(r.type!=='bag')throw new Error('请扫描袋码');
      if(r.data.palletId)throw new Error('该袋码已绑定托盘,请核实后操作');
      if(!r.data.itemId)throw new Error('该袋码缺少品种,无法绑定托盘');
      if(bags.value.some(b=>b.data.id===r.data.id))throw new Error('本次已经采集该袋码');
      const first=bags.value[0];
      if(first&&first.data.itemId!==r.data.itemId)throw new Error('袋码品种与本次托盘不一致');
      if(first&&first.data.batchId!==r.data.batchId)throw new Error('袋码所属批次与本次托盘不一致');
      bags.value.push(r);
      if(bags.value.length===palletSize.value&&!thresholdPassed.value)thresholdPrompt.value=true;
      return;
    }
    if(!binding.value){
      if(r.type!==expected)throw new Error('该码类型与当前查询页面不一致,请使用对应查询入口');
      detail.value=r;
    }else if(!target.value){
      if(r.type!==(key==='bags'?'pallet':'batch'))throw new Error(key==='bags'?'请先扫描目标托盘':'请先扫描目标批次');
      target.value=r;
    }else{
      if(r.type!==expected)throw new Error('请扫描'+(key==='bags'?'袋码':'托盘码'));
      if(key==='bags'){
        if(r.data.palletId)throw new Error('该袋码已绑定托盘,请核实后操作');
        if(r.data.batchId&&target.value.data.batchId&&r.data.batchId!==target.value.data.batchId)throw new Error('袋码与目标托盘批次不一致');
        if(bags.value.some(b=>b.data.id===r.data.id))throw new Error('本次已经采集该袋码');
        bags.value.push(r);
      }else{
        if(r.data.batchId)throw new Error('该托盘已绑定批次');
        detail.value=r;
      }
    }
  }catch(e){error.value=errorText(e);}finally{busy.value=false;}
}
function startBind(){binding.value=true;detail.value=undefined;target.value=undefined;bags.value=[];notice.value='';error.value='';}
function startAutoPack(){autoPacking.value=true;binding.value=false;detail.value=undefined;target.value=undefined;bags.value=[];thresholdPrompt.value=false;thresholdPassed.value=false;notice.value='';error.value='';}
function stopAutoPack(){autoPacking.value=false;bags.value=[];thresholdPrompt.value=false;thresholdPassed.value=false;}
function removeBag(index:number){bags.value.splice(index,1);if(bags.value.length<palletSize.value){thresholdPrompt.value=false;thresholdPassed.value=false;}}
function clearPending(){pending.value=undefined;sessionStorage.removeItem(storageKey);}
async function createAndBindPallet(){
  if(busy.value||pending.value||bags.value.length<palletSize.value)return;
  const ids=bags.value.map(b=>b.data.id!);
  const operation:PendingBind={kind:'auto-bags',ids,targetId:0,stage:'creating'};
  pending.value=operation;sessionStorage.setItem(storageKey,JSON.stringify(operation));busy.value=true;error.value='';thresholdPrompt.value=false;
  try{
    const batchId=bags.value[0]?.data.batchId;
    if(batchId){
      const [id]=await batchCreatePallet({batchId,count:1,capacity:palletSize.value});
      if(!id)throw new Error('未取得新托盘编号');
      operation.targetId=id;operation.stage='binding';sessionStorage.setItem(storageKey,JSON.stringify(operation));
      const created=await getPallet(id);
      operation.palletCode=created.code||created.tempCode||String(id);
    }else{
      const [created]=await preGeneratePallet({count:1,capacity:palletSize.value});
      if(!created?.id)throw new Error('未取得新托盘编号');
      operation.targetId=created.id;operation.palletCode=created.tempCode;operation.stage='binding';
    }
    sessionStorage.setItem(storageKey,JSON.stringify(operation));
    await bindBagCodePallet({ids,palletId:operation.targetId});
    clearPending();stopAutoPack();notice.value=`已生成并绑定托盘码 ${operation.palletCode},共 ${ids.length} 袋。`;
  }catch(e){error.value=operation.targetId?`托盘码 ${operation.palletCode} 的绑定结果待确认:${errorText(e)}。请先核实,勿重复生成。`:`生成结果待确认:${errorText(e)}。请在托盘列表核实,勿重复生成。`;}
  finally{busy.value=false;}
}
async function submitBind(){
  if(busy.value||!target.value?.data.id)return;
  const ids=key==='bags'?bags.value.map(b=>b.data.id!):detail.value?.data.id?[detail.value.data.id]:[];
  if(!ids.length)return;
  const operation={kind:key,ids,targetId:target.value.data.id};
  pending.value=operation;sessionStorage.setItem(storageKey,JSON.stringify(operation));busy.value=true;error.value='';
  try{
    if(key==='bags')await bindBagCodePallet({ids,palletId:operation.targetId});
    else await bindPalletBatch({ids,batchId:operation.targetId});
    clearPending();binding.value=false;bags.value=[];detail.value=undefined;notice.value='绑定成功,已更新对象的绑定关系。';
  }catch(e){error.value='绑定结果待确认:'+errorText(e)+'。请先核实,勿重复提交。';}
  finally{busy.value=false;}
}
async function verifyBind(){
  if(!pending.value||busy.value)return;busy.value=true;error.value='';
  try{
    const p=pending.value;
    if(p.kind==='auto-bags'&&!p.targetId){error.value='尚未取得托盘编号,请在托盘列表核实生成结果后联系负责人处理。';return;}
    const states=await Promise.all(p.ids.map(id=>p.kind==='bags'||p.kind==='auto-bags'?getBagCode(id):getPallet(id)));
    const bagOperation=p.kind==='bags'||p.kind==='auto-bags';
    const all=states.every(s=>bagOperation?('palletId'in s&&s.palletId===p.targetId):s.batchId===p.targetId);
    const none=states.every(s=>bagOperation?(!('palletId'in s)||!s.palletId):!s.batchId);
    if(all){notice.value=p.kind==='auto-bags'?`已核实:托盘码 ${p.palletCode} 已绑定 ${p.ids.length} 袋。`:'已核实:所有对象绑定成功。';clearPending();binding.value=false;stopAutoPack();detail.value=undefined;}
    else if(none&&p.kind==='auto-bags'){await bindBagCodePallet({ids:p.ids,palletId:p.targetId});notice.value=`已完成托盘码 ${p.palletCode} 的袋码绑定。`;clearPending();stopAutoPack();}
    else if(none){notice.value='已核实:对象仍未绑定。请重新查询对象后采集提交。';clearPending();binding.value=false;bags.value=[];detail.value=undefined;}
    else error.value='对象绑定结果不一致,请联系负责人核对。当前保留待确认操作。';
  }catch(e){error.value=errorText(e);}finally{busy.value=false;}
}
onBeforeRouteLeave(()=>!busy.value&&(!(binding.value||autoPacking.value)||!!pending.value||window.confirm('离开将放弃本次未提交的绑定清单,是否继续?')));
</script>
<template>
  <p v-if="notice" class="m-success" role="status">{{notice}}</p>
  <p v-if="error" class="m-error" role="alert">{{error}}</p>
  <div v-if="pending" class="m-panel"><h2 class="m-title">{{pending.stage==='creating'?'生成结果待确认':'绑定结果待确认'}}</h2><p>本次 {{pending.ids.length}} 个对象<span v-if="pending.targetId">,托盘码 {{pending.palletCode||pending.targetId}}</span>。</p><button class="m-primary" :disabled="busy||!pending.targetId" @click="verifyBind">核实绑定结果</button><p v-if="!pending.targetId" class="m-warning">生成请求的结果尚不明确,请在托盘列表核实并联系负责人处理。</p></div>
  <template v-else>
    <template v-if="binding">
      <h2 class="m-title">{{!target?(key==='bags'?'先扫描目标托盘':'先扫描目标批次'):(key==='bags'?'逐袋采集':'扫描待绑定托盘')}}</h2>
      <div v-if="target" class="m-panel"><p>目标:{{target.code}}</p><p class="m-muted">{{target.data.itemName||'未录入品种'}} · {{target.data.batchCode||'未绑定批次'}}</p></div>
    </template>
    <template v-if="autoPacking"><h2 class="m-title">扫描袋码生成托盘</h2><p class="m-muted">{{palletSize}} 袋型 · 已采集 {{bags.length}} 袋<span v-if="bags.length<palletSize">,还需 {{palletSize-bags.length}} 袋</span>。生成后将自动绑定本次采集的袋码。</p></template>
    <Scanner :disabled="busy||thresholdPrompt" :continuous="autoPacking" @code="identify"/>
    <section v-if="autoPacking&&thresholdPrompt" class="m-panel" role="alert" aria-labelledby="pallet-threshold-title" aria-describedby="pallet-threshold-description">
      <h2 id="pallet-threshold-title" class="m-title">已扫描 {{palletSize}} 袋</h2>
      <p id="pallet-threshold-description">现在生成并绑定托盘码,或继续扫描更多袋码加入本托盘。</p>
      <div class="m-action-bar"><button class="m-primary" :disabled="busy" @click="createAndBindPallet">生成并绑定托盘码</button><button class="m-secondary" :disabled="busy" @click="thresholdPrompt=false;thresholdPassed=true">继续扫码绑定</button></div>
    </section>
    <div v-if="detail&&!binding" class="m-panel">
      <h2 class="m-title">{{detail.data.itemName||'对象详情'}}</h2>
      <p v-if="detail.type === 'batch'" class="m-status">{{detail.data.status === 30 ? '已完成' : detail.data.status === 20 ? '生产中' : detail.data.status === 40 ? '已作废' : '草稿'}} · 已入库 {{detail.data.inboundBagQuantity || 0}} 袋</p>
      <p v-else class="m-status">{{detail.data.inboundTime ? '已入库' : '未入库'}}<span v-if="detail.type === 'pallet'"> · 待入库 {{detail.data.pendingBagQuantity ?? '—'}} 袋</span></p>
      <p v-if="writeBlockReason(detail)" class="m-warning">{{writeBlockReason(detail)}}</p>
      <dl><div class="m-row"><dt>码值</dt><dd>{{detail.code}}</dd></div><div class="m-row"><dt>批次</dt><dd>{{detail.data.batchCode||detail.data.tempCode||'未绑定'}}</dd></div><div class="m-row"><dt>托盘</dt><dd>{{detail.data.palletCode||'—'}}</dd></div><div class="m-row"><dt>生产日期</dt><dd>{{detail.data.produceDate||'未录入'}}</dd></div><div class="m-row"><dt>产线</dt><dd>{{detail.data.lineName||'未录入'}}</dd></div></dl>
      <dl v-if="detail.type==='bag'"><div class="m-row"><dt>追溯码</dt><dd>{{detail.data.code}}</dd></div><div class="m-row"><dt>业务码</dt><dd>{{detail.data.businessCode||'未生成'}}</dd></div></dl>
      <dl v-else><div class="m-row"><dt>业务码</dt><dd>{{detail.data.businessCode||'未生成'}}</dd></div></dl>
    </div>
    <template v-if="binding||autoPacking">
      <p v-if="key==='bags'" class="m-muted">已采集 {{bags.length}} 袋,尚未提交绑定</p>
      <article v-for="(bag,i) in bags" :key="bag.data.id" class="m-record"><p>{{bag.code}}</p><button class="m-detail-toggle" :disabled="busy" @click="removeBag(i)">移除</button></article>
      <p v-if="binding&&key==='pallets'&&detail">待绑定托盘:{{detail.code}}</p>
      <div v-if="autoPacking&&!thresholdPrompt" class="m-action-bar"><button class="m-primary" :disabled="busy||bags.length<palletSize" @click="createAndBindPallet">{{busy?'正在生成…':'生成并绑定托盘码'}}</button><button class="m-secondary" :disabled="busy" @click="stopAutoPack">取消本次扫码</button></div>
      <div v-else-if="binding" class="m-action-bar"><button class="m-primary" :disabled="busy||!target||(key==='bags'?!bags.length:!detail)" @click="submitBind">{{busy?'正在绑定…':'确认绑定'}}</button><button class="m-secondary" :disabled="busy" @click="binding=false;bags=[];target=undefined;detail=undefined">取消本次绑定</button></div>
    </template>
    <template v-else>
      <div v-if="canAutoPack" class="m-panel"><h2 class="m-title">扫描袋码自动生成托盘</h2><label class="m-field"><span>托盘码类型</span><select v-model.number="palletSize" :disabled="busy"><option :value="48">48 袋</option><option :value="100">100 袋</option></select></label><button class="m-primary" :disabled="busy" @click="startAutoPack">开始扫码生成托盘</button></div>
      <button v-if="canBind" class="m-primary" :disabled="busy" @click="startBind">{{key==='bags'?'开始袋码绑定托盘':'开始托盘绑定批次'}}</button>
      <h2 class="m-section-title">对象列表</h2>
      <form class="m-stack" @submit.prevent="load(true)"><label class="m-field"><span>按码值查询</span><input v-model="filter" placeholder="输入码值" /></label><button class="m-secondary" :disabled="busy">查询列表</button></form>
      <p v-if="!rows.length&&!busy" class="m-empty">暂无匹配对象</p>
      <button v-for="row in rows" :key="row.id" class="m-feature" style="width:100%;background:transparent;border-top:0;border-left:0;border-right:0;text-align:left" :disabled="busy" @click="identify(row.code||row.tempCode||'')"><div><strong>{{row.itemName||row.code||row.tempCode}}</strong><small>{{row.code||row.tempCode}} · {{row.batchCode||'点击查看详情'}}</small></div></button>
      <button v-if="rows.length<total" class="m-secondary" :disabled="busy" @click="load()">{{busy?'正在加载…':'加载更多'}}</button>
    </template>
  </template>
</template>