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