云
3 天以前 2f9c84cf0e0c03a8e3bf07d3924ccdf791070b77
src/views/mobile/query.vue
@@ -3,7 +3,7 @@
import { onBeforeRouteLeave, useRoute } from 'vue-router';
import { useUserStore } from '@vben/stores';
import { bindBagCodePallet, getBagCode, getBagCodePage } from '#/api/mes/pro/bagCode';
import { bindPalletBatch, getPallet, getPalletPage } from '#/api/mes/pro/pallet';
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';
@@ -13,10 +13,12 @@
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[]>([]);
type PendingBind={kind:string;ids:number[];targetId:number};
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='';
@@ -31,10 +33,22 @@
}
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)return;busy.value=true;error.value='';notice.value='';
  if(busy.value||pending.value||thresholdPrompt.value)return;busy.value=true;error.value='';notice.value='';
  try{
    const r=await lookup(code);
    if(binding.value&&writeBlockReason(r))throw new Error(writeBlockReason(r));
    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;
@@ -56,7 +70,34 @@
  }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]:[];
@@ -74,26 +115,35 @@
  if(!pending.value||busy.value)return;busy.value=true;error.value='';
  try{
    const p=pending.value;
    const states=await Promise.all(p.ids.map(id=>p.kind==='bags'?getBagCode(id):getPallet(id)));
    const all=states.every(s=>p.kind==='bags'?('palletId'in s&&s.palletId===p.targetId):s.batchId===p.targetId);
    const none=states.every(s=>p.kind==='bags'?(!('palletId'in s)||!s.palletId):!s.batchId);
    if(all){notice.value='已核实:所有对象绑定成功。';clearPending();binding.value=false;bags.value=[];detail.value=undefined;}
    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||!!pending.value||window.confirm('离开将放弃本次未提交的绑定清单,是否继续?')));
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">绑定结果待确认</h2><p>本次 {{pending.ids.length}} 个对象,目标编号 {{pending.targetId}}。</p><button class="m-primary" :disabled="busy" @click="verifyBind">核实绑定结果</button></div>
  <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>
    <Scanner :disabled="busy" @code="identify"/>
    <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>
@@ -103,13 +153,15 @@
      <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">
    <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="bags.splice(i,1)">移除</button></article>
      <p v-if="key==='pallets'&&detail">待绑定托盘:{{detail.code}}</p>
      <div 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>
      <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>