From 18732f2761a0f5d1d2b9a455fe7c69f0b11ae27d Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 16 十月 2026 18:07:17 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_新疆_塔河种业' into dev_新疆_塔河种业

---
 src/api/mes/pro/pallet/index.ts             |    5 
 src/views/mobile/scanner.vue                |   11 +
 src/views/basicData/mdm/data.ts             |   42 -------
 src/views/basicData/mdm/index.vue           |   35 -----
 src/views/mobile/query.vue                  |   82 +++++++++++--
 src/api/mes/trace-consumer-scan/index.ts    |   44 +++++++
 src/views/mes/trace-consumer-scan/index.vue |   87 ++++++++++++++
 7 files changed, 208 insertions(+), 98 deletions(-)

diff --git a/src/api/mes/pro/pallet/index.ts b/src/api/mes/pro/pallet/index.ts
index ab23f39..f964b2f 100644
--- a/src/api/mes/pro/pallet/index.ts
+++ b/src/api/mes/pro/pallet/index.ts
@@ -17,6 +17,7 @@
     batchId?: number; // 鐢熶骇鎵规缂栧彿
     batchCode?: string; // 鎵规鍙�
     palletNo?: number; // 鎵规鍐呮墭鐩樺簭鍙�
+    capacity?: number; // 鏍囩О姣忔墭琚嬫暟
     autoGenerated?: boolean; // 鏄惁鑷姩鐢熸垚锛堣鐮佹暟鎹寘鐢熸垚鏃惰嚜鍔ㄥ垱寤猴級
     bagCount?: number; // 宸茬粦瀹氳鐮佹暟閲�
     status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_pallet_status锛�
@@ -44,7 +45,7 @@
     code?: string; // 鎵樼洏鐮侊紙缁戝畾鎵规鍚庣殑姝e紡鎵樼洏鐮侊級
     tempCode?: string; // 涓存椂鎵樼洏鐮侊紙浜岀淮鐮佸唴瀹癸紱鏈粦瀹氭壒娆℃椂灞曠ず锛�
     palletNo?: number; // 鎵规鍐呮墭鐩樺簭鍙�
-    capacity?: number; // 鍙琚嬫暟
+    capacity?: number; // 鏍囩О姣忔墭琚嬫暟
     bagCount?: number; // 宸茬粦瀹氳鐮佹暟閲�
     status?: number; // 鐘舵�侊紙瀛楀吀 mes_pro_pallet_status锛�
     batchId?: number; // 鐢熶骇鎵规缂栧彿
@@ -67,6 +68,7 @@
   export interface BatchCreateParams {
     batchId: number; // 鐢熶骇鎵规缂栧彿
     count: number; // 鐢熸垚鏁伴噺
+    capacity?: number; // 鏍囩О姣忔墭琚嬫暟
     remark?: string; // 澶囨敞
   }
 
@@ -86,6 +88,7 @@
 /** 棰勭敓鎴愭墭鐩樹复鏃朵簩缁寸爜锛堜笉缁戝畾鐢熶骇鎵规锛� */
 export function preGeneratePallet(data: {
   count: number;
+  capacity?: number;
   remark?: string;
 }) {
   return requestClient.post<MesProPalletApi.PreGenerateResult[]>(
diff --git a/src/api/mes/trace-consumer-scan/index.ts b/src/api/mes/trace-consumer-scan/index.ts
index 14d738b..6a8064e 100644
--- a/src/api/mes/trace-consumer-scan/index.ts
+++ b/src/api/mes/trace-consumer-scan/index.ts
@@ -1,6 +1,25 @@
 import { requestClient } from '#/api/request';
 
 export namespace MesTraceConsumerScanApi {
+  export interface LocationVisit {
+    id: number;
+    traceCode: string;
+    latitude: number;
+    longitude: number;
+    accuracy: number;
+    visitTime: string;
+  }
+
+  export interface LocationVisitPage {
+    list: LocationVisit[];
+    total: number;
+  }
+
+  export interface LocationDistribution {
+    latitude: number;
+    longitude: number;
+    count: number;
+  }
   /** 娑堣垂鑰呮壂鐮佽闂眹鎬� */
   export interface Summary {
     total: number; // 鎬昏闂鏁�
@@ -24,6 +43,31 @@
   }
 }
 
+/** 鎺堟潈瀹氫綅鐨勮拷婧闂褰曪紱娌跨敤鎵爜缁熻鏌ヨ鏉冮檺 */
+export function getTraceLocationVisits(params: {
+  pageNo: number;
+  pageSize: number;
+  startTime?: string;
+  endTime?: string;
+  traceCode?: string;
+}) {
+  return requestClient.get<MesTraceConsumerScanApi.LocationVisitPage>(
+    '/mes/trace-consumer-scan/location-visits',
+    { params },
+  );
+}
+
+export function getTraceLocationDistribution(params: {
+  startTime?: string;
+  endTime?: string;
+  traceCode?: string;
+}) {
+  return requestClient.get<MesTraceConsumerScanApi.LocationDistribution[]>(
+    '/mes/trace-consumer-scan/location-distribution',
+    { params },
+  );
+}
+
 /** 鑾峰緱娑堣垂鑰呮壂鐮佽闂眹鎬� */
 export function getTraceConsumerScanSummary(
   startTime?: string,
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
index 94837a7..7604cc1 100644
--- a/src/views/basicData/mdm/data.ts
+++ b/src/views/basicData/mdm/data.ts
@@ -503,57 +503,15 @@
       minWidth: 80,
     },
     {
-      field: "unitMeasureName2",
-      title: "杈呭崟浣�1",
-      minWidth: 80,
-    },
-    {
-      field: "unitMeasureRate1",
-      title: "杈呭崟浣�1鎹㈢畻姣旂巼",
-      minWidth: 130,
-    },
-    {
-      field: "unitMeasureName3",
-      title: "杈呭崟浣�2",
-      minWidth: 80,
-    },
-    {
-      field: "unitMeasureRate2",
-      title: "杈呭崟浣�2鎹㈢畻姣旂巼",
-      minWidth: 130,
-    },
-    {
       field: "warehouseName",
       title: "榛樿浠撳簱",
       minWidth: 100,
-    },
-    {
-      field: "salesPrice",
-      title: "閿�鍞环",
-      minWidth: 100,
-      formatter: ({ cellValue }) => {
-        return cellValue != null ? `楼${Number(cellValue).toFixed(2)}` : "";
-      },
     },
     {
       field: "createTime",
       title: "鍒涘缓鏃堕棿",
       minWidth: 180,
       formatter: "formatDateTime",
-    },
-    {
-      field: "isBatchManaged",
-      title: "鎵规绠$悊",
-      width: 80,
-      fixed: "right",
-      slots: { default: "isBatchManaged" },
-    },
-    {
-      field: "syncedMes",
-      title: "鍚屾鐘舵��",
-      width: 100,
-      fixed: "right",
-      slots: { default: "syncedMes" },
     },
     {
       field: "status",
diff --git a/src/views/basicData/mdm/index.vue b/src/views/basicData/mdm/index.vue
index 3a02fe0..5b173a8 100644
--- a/src/views/basicData/mdm/index.vue
+++ b/src/views/basicData/mdm/index.vue
@@ -7,7 +7,7 @@
 import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src';
 import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src';
 
-import { message, Switch, Tag } from 'ant-design-vue';
+import { message } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
@@ -16,7 +16,6 @@
   // exportItem,
   getItemPage,
   updateItemStatus,
-  updateItemSync,
 } from '#/api/mdm/item';
 import { $t } from '#/locales';
 
@@ -99,23 +98,6 @@
   }
 }
 
-const syncingId = ref<number | null>(null);
-
-/** 鍒囨崲鍚屾鐘舵�侊細鍏抽棴寮�鍏充細浠� MES 鍒犻櫎鍝佺锛岄渶纭 */
-async function handleSyncChange(row: MdmItemApi.Item, checked: boolean) {
-  if (!checked) {
-    await confirm('鍏抽棴鍚屾灏嗕粠 MES 鍒犻櫎璇ュ搧绉嶅強鍏宠仈閰嶇疆锛圔OM/SOP/SIP/鎵规閰嶇疆锛夛紝纭缁х画锛�');
-  }
-  syncingId.value = row.id!;
-  try {
-    await updateItemSync(row.id!, checked);
-    message.success(checked ? '宸插悓姝ュ埌 MES' : '宸插彇娑堝悓姝�');
-    handleRefresh();
-  } finally {
-    syncingId.value = null;
-  }
-}
-
 const checkedIds = ref<number[]>([]);
 function handleRowCheckboxChange({
   records,
@@ -193,19 +175,6 @@
           ]"
         />
       </template>
-      <template #isBatchManaged="{ row }">
-        <Tag v-if="row.isBatchManaged" color="success">鏄�</Tag>
-        <Tag v-else>鍚�</Tag>
-      </template>
-      <template #syncedMes="{ row }">
-        <Switch
-          :checked="row.syncedMes"
-          :loading="syncingId === row.id"
-          checkedChildren="宸插悓姝�"
-          unCheckedChildren="鏈悓姝�"
-          @change="handleSyncChange(row, $event)"
-        />
-      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
@@ -232,4 +201,4 @@
       </template>
     </Grid>
   </Page>
-</template>
\ No newline at end of file
+</template>
diff --git a/src/views/mes/trace-consumer-scan/index.vue b/src/views/mes/trace-consumer-scan/index.vue
index 0a6da66..3a675eb 100644
--- a/src/views/mes/trace-consumer-scan/index.vue
+++ b/src/views/mes/trace-consumer-scan/index.vue
@@ -1,12 +1,14 @@
 <script lang="ts" setup>
-import { computed, ref } from 'vue';
+import { computed, ref, watch } from 'vue';
 
 import { Page } from '@vben/common-ui';
 
-import { Card, Col, DatePicker, Row } from 'ant-design-vue';
+import { Card, Col, DatePicker, Input, Row, Table } from 'ant-design-vue';
 import dayjs from 'dayjs';
 
 import { getRangePickerDefaultProps } from '#/utils';
+import { getTraceLocationDistribution, getTraceLocationVisits } from '#/api/mes/trace-consumer-scan';
+import type { MesTraceConsumerScanApi } from '#/api/mes/trace-consumer-scan';
 
 import DistributionChart from './modules/distribution-chart.vue';
 import SummaryCards from './modules/summary-cards.vue';
@@ -22,6 +24,59 @@
 
 const startTime = computed(() => dateRange.value?.[0]);
 const endTime = computed(() => dateRange.value?.[1]);
+
+const traceCode = ref('');
+const pageNo = ref(1);
+const locationLoading = ref(false);
+const locationRows = ref<MesTraceConsumerScanApi.LocationVisit[]>([]);
+const locationDistribution = ref<MesTraceConsumerScanApi.LocationDistribution[]>([]);
+const locationTotal = ref(0);
+const locationColumns = [
+  { title: '璁块棶鏃堕棿', dataIndex: 'visitTime', key: 'visitTime' },
+  { title: '杩芥函鐮�', dataIndex: 'traceCode', key: 'traceCode' },
+  { title: '绾害', dataIndex: 'latitude', key: 'latitude' },
+  { title: '缁忓害', dataIndex: 'longitude', key: 'longitude' },
+  { title: '绮惧害锛堢背锛�', dataIndex: 'accuracy', key: 'accuracy' },
+  { title: '浣嶇疆', key: 'map' },
+];
+
+async function loadLocationVisits() {
+  locationLoading.value = true;
+  try {
+    const page = await getTraceLocationVisits({
+      pageNo: pageNo.value,
+      pageSize: 20,
+      startTime: startTime.value,
+      endTime: endTime.value,
+      traceCode: traceCode.value.trim() || undefined,
+    });
+    locationRows.value = page.list;
+    locationTotal.value = page.total;
+    locationDistribution.value = await getTraceLocationDistribution({
+      startTime: startTime.value,
+      endTime: endTime.value,
+      traceCode: traceCode.value.trim() || undefined,
+    });
+  } finally {
+    locationLoading.value = false;
+  }
+}
+
+watch([startTime, endTime, traceCode], () => {
+  pageNo.value = 1;
+  loadLocationVisits();
+}, { immediate: true });
+
+function mapUrl(row: Record<string, any>) {
+  const lat = Number(row.latitude);
+  const lon = Number(row.longitude);
+  return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=16/${lat}/${lon}`;
+}
+
+function handleLocationPageChange(pagination: { current?: number }) {
+  pageNo.value = pagination.current || 1;
+  loadLocationVisits();
+}
 </script>
 
 <template>
@@ -46,5 +101,33 @@
         <DistributionChart :start-time="startTime" :end-time="endTime" />
       </Col>
     </Row>
+
+    <Card title="瀹氫綅鎺堟潈璁块棶" class="mb-4">
+      <p class="mb-3 text-sm text-muted-foreground">
+        褰撳墠绛涢�夋潯浠朵笅鍏� {{ locationTotal }} 娆℃巿鏉冨畾浣嶈闂紱涓嬭〃鏄剧ず鐢ㄦ埛鎺堟潈鍚庝笂浼犵殑浣嶇疆鍙婂畾浣嶇簿搴︺��
+      </p>
+      <Input v-model:value="traceCode" class="mb-3 max-w-xs" allow-clear placeholder="鎸夎拷婧爜绛涢��" />
+      <div v-if="locationDistribution.length" class="mb-4">
+        <div class="mb-2 font-medium">鐑棬浣嶇疆锛堝潗鏍囦繚鐣欎袱浣嶅皬鏁帮紝鏈�澶� 20 澶勶級</div>
+        <div class="flex flex-wrap gap-2">
+          <span v-for="item in locationDistribution" :key="`${item.latitude},${item.longitude}`" class="rounded bg-green-50 px-3 py-1 text-sm text-green-800">
+            {{ item.latitude }}, {{ item.longitude }}锛歿{ item.count }} 娆�
+          </span>
+        </div>
+      </div>
+      <Table
+        :columns="locationColumns"
+        :data-source="locationRows"
+        :loading="locationLoading"
+        :pagination="{ current: pageNo, pageSize: 20, total: locationTotal, showSizeChanger: false }"
+        :row-key="(row: MesTraceConsumerScanApi.LocationVisit) => row.id"
+        :scroll="{ x: 850 }"
+        @change="handleLocationPageChange"
+      >
+        <template #bodyCell="{ column, record }">
+          <a v-if="column.key === 'map'" :href="mapUrl(record)" target="_blank" rel="noopener noreferrer">鏌ョ湅鍦板浘</a>
+        </template>
+      </Table>
+    </Card>
   </Page>
 </template>
diff --git a/src/views/mobile/query.vue b/src/views/mobile/query.vue
index 207fee3..e6a831e 100644
--- a/src/views/mobile/query.vue
+++ b/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='瀵硅薄缁戝畾缁撴灉涓嶄竴鑷达紝璇疯仈绯昏礋璐d汉鏍稿銆傚綋鍓嶄繚鐣欏緟纭鎿嶄綔銆�';
   }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">鐢熸垚璇锋眰鐨勭粨鏋滃皻涓嶆槑纭紝璇峰湪鎵樼洏鍒楄〃鏍稿疄骞惰仈绯昏礋璐d汉澶勭悊銆�</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?'姝e湪缁戝畾鈥�':'纭缁戝畾'}}</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?'姝e湪鐢熸垚鈥�':'鐢熸垚骞剁粦瀹氭墭鐩樼爜'}}</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?'姝e湪缁戝畾鈥�':'纭缁戝畾'}}</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>
diff --git a/src/views/mobile/scanner.vue b/src/views/mobile/scanner.vue
index e01c066..55ceff2 100644
--- a/src/views/mobile/scanner.vue
+++ b/src/views/mobile/scanner.vue
@@ -1,12 +1,13 @@
 <script setup lang="ts">
-import { onBeforeUnmount, onDeactivated, onMounted, ref } from 'vue';
+import { nextTick, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
 import Icon from '#/mobile/icon.vue';
-const props=defineProps<{disabled?:boolean}>();
+const props=defineProps<{disabled?:boolean;continuous?:boolean}>();
 const emit=defineEmits<{code:[value:string]}>();
-const input=ref(''),error=ref(''),active=ref(false),starting=ref(false),video=ref<HTMLVideoElement>();
+const input=ref(''),error=ref(''),active=ref(false),starting=ref(false),video=ref<HTMLVideoElement>(),codeInput=ref<HTMLInputElement>();
 let stream:MediaStream|undefined,frame=0,generation=0,last=0;
 function stop(){generation++;active.value=false;starting.value=false;cancelAnimationFrame(frame);stream?.getTracks().forEach(t=>t.stop());stream=undefined;if(video.value)video.value.srcObject=null;}
-function accept(value:string){if(props.disabled)return;stop();input.value=value;emit('code',value);}
+function accept(value:string){if(props.disabled)return;stop();input.value=props.continuous?'':value;emit('code',value);}
+watch([()=>props.disabled,()=>props.continuous],async ([disabled,continuous])=>{if(!disabled&&continuous){await nextTick();codeInput.value?.focus();}});
 async function start(){
   error.value='';stop();const ticket=generation;starting.value=true;
   try{
@@ -52,7 +53,7 @@
   <template v-else><video ref="video" class="m-camera" muted playsinline aria-label="浜岀淮鐮佸彇鏅�"/><button type="button" class="m-secondary" @click="stop">鍏抽棴鎽勫儚澶�</button></template>
   <p v-if="error" class="m-error" role="alert">{{error}}</p>
   <form @submit.prevent="accept(input)">
-    <label class="m-field" style="margin-top:16px"><span>鐮佸�� / 婧簮閾炬帴</span><input v-model="input" autocomplete="off" :disabled="disabled" placeholder="鎵嬪姩杈撳叆鎴栦娇鐢ㄦ壂鐮佽澶�" maxlength="2048" /></label>
+    <label class="m-field" style="margin-top:16px"><span>鐮佸�� / 婧簮閾炬帴</span><input ref="codeInput" v-model="input" autocomplete="off" :disabled="disabled" placeholder="鎵嬪姩杈撳叆鎴栦娇鐢ㄦ壂鐮佽澶�" maxlength="2048" /></label>
     <button class="m-secondary" :disabled="disabled||!input.trim()" type="submit">{{disabled?'姝e湪澶勭悊鈥�':'璇嗗埆鐮佸��'}}</button>
   </form>
 </div>

--
Gitblit v1.9.3