<template>
|
<view class="page-container">
|
<scroll-view scroll-y class="page-scroll">
|
|
<!-- 1. Pallet Barcode Section -->
|
<view class="card-box">
|
<view class="card-header">
|
<view class="header-indicator"></view>
|
<text class="header-title">Pallet Barcode</text>
|
</view>
|
<view class="card-body">
|
<view class="input-wrapper">
|
<input
|
v-model="barcode"
|
type="text"
|
placeholder="Scan Pallet Barcode"
|
maxlength="20"
|
:focus="barcodeFocus"
|
@confirm="barcodeInput"
|
class="custom-input"
|
/>
|
<view class="icon-scan" @click="barcodeInput">
|
<uni-icons type="scan" size="24" color="#007aff" />
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<!-- 2. Scanning Options -->
|
<view class="card-box">
|
<view class="card-header">
|
<text class="header-title">Carton Scanning Option</text>
|
</view>
|
<view class="card-body">
|
<radio-group @change="modeChange" class="radio-group-vertical">
|
<label class="radio-row">
|
<radio value="one" :checked="scanMode === 'one'" color="#007aff" style="transform:scale(0.8)" />
|
<text class="radio-label">One Label per Carton</text>
|
</label>
|
<label class="radio-row">
|
<radio value="multi" :checked="scanMode === 'multi'" color="#007aff" style="transform:scale(0.8)" />
|
<text class="radio-label">Multiple Labels per Carton</text>
|
</label>
|
</radio-group>
|
</view>
|
</view>
|
|
<!-- 3. Scan Carton Label -->
|
<view class="card-box">
|
<view class="card-header">
|
<view class="header-indicator"></view>
|
<text class="header-title">Scan Carton Label</text>
|
</view>
|
<view class="card-body">
|
<view class="input-row">
|
<view class="input-wrapper flex-1">
|
<input
|
v-model="code"
|
type="text"
|
placeholder="Scan Carton Label"
|
:focus="codeFocus"
|
@confirm="codeInput"
|
class="custom-input"
|
/>
|
</view>
|
<button class="cu-btn bg-blue shadow-blur sm margin-left-sm" @click="codeInput">Add</button>
|
</view>
|
</view>
|
</view>
|
|
<!-- 4. Zone Selection & Queue Action -->
|
<view class="card-box">
|
<view class="card-body">
|
<!-- Zone Selector -->
|
<view class="form-row">
|
<text class="form-label">Zone:</text>
|
<picker @change="zoneChange" :value="zoneIndex" :range="zoneList" range-key="areaId" class="flex-1">
|
<view class="picker-box">
|
<text class="picker-text">{{ zoneList[zoneIndex] ? zoneList[zoneIndex].areaId : 'Select Zone' }}</text>
|
<uni-icons type="arrowdown" size="14" color="#666"></uni-icons>
|
</view>
|
</picker>
|
</view>
|
|
<!-- Add Queue Button (Full Width) -->
|
<button class="cu-btn bg-blue block margin-top-sm lg shadow" @click="addToQueue">
|
<text class="text-bold">ADD TO QUEUE</text>
|
</button>
|
</view>
|
</view>
|
|
<!-- 5. Data Tables -->
|
<view class="tables-section">
|
|
<!-- Left Table: Current Scanned -->
|
<view class="card-box no-padding">
|
<view class="table-header-row">
|
<text class="th col-2">Carton Label</text>
|
<text class="th col-1 text-center">Qty</text>
|
<text class="th col-05 text-center">Op</text>
|
</view>
|
<view class="table-body-scroll">
|
<view v-for="(item, index) in matList" :key="index" class="table-row">
|
<text class="td col-2 text-cut">{{ item.barcode }}</text>
|
<text class="td col-1 text-center">{{ item.anfme }}</text>
|
<view class="td col-05 text-center">
|
<uni-icons type="trash" size="18" color="#dd524d" @click="removeCurrentItem(index)" />
|
</view>
|
</view>
|
<view v-if="matList.length === 0" class="empty-state">No data</view>
|
</view>
|
<view class="table-footer-info">
|
<text>Total Quantity: <text class="text-blue text-bold">{{ totalQty }}</text></text>
|
</view>
|
</view>
|
|
<!-- Right Table: Queue List -->
|
<view class="card-box no-padding margin-top-sm">
|
<view class="card-header small-header">
|
<text class="header-title text-sm">Queue List</text>
|
</view>
|
<scroll-view scroll-y class="queue-list-scroll">
|
<view v-for="(qItem, qIndex) in queueList" :key="qIndex" class="queue-card">
|
<view class="queue-header">
|
<view class="flex align-center">
|
<uni-icons type="box" size="16" color="#007aff" class="margin-right-xs"/>
|
<text class="text-bold text-dark">{{ qItem.barcode }}</text>
|
</view>
|
<view class="flex align-center">
|
<text class="cu-tag bg-cyan light sm margin-right-xs">{{ qItem.zoneName }}</text>
|
<uni-icons type="closeempty" size="18" color="#999" @click="removeFromQueue(qIndex)" />
|
</view>
|
</view>
|
<view class="queue-content">
|
<view v-for="(cItem, cIndex) in qItem.cartons" :key="cIndex" class="queue-detail-row">
|
<text class="text-grey">Label:{{ cItem.barcode }}</text>
|
<text class="text-grey">ItemNo:{{ cItem.matnr }}</text>
|
<text class="text-grey">x{{ cItem.anfme }}</text>
|
</view>
|
</view>
|
</view>
|
<view v-if="queueList.length === 0" class="empty-state">Queue is empty</view>
|
</scroll-view>
|
</view>
|
|
</view>
|
|
<view style="height: 140rpx;"></view>
|
</scroll-view>
|
|
<!-- Bottom Footer -->
|
<view class="footer-actions">
|
<button class="cu-btn bg-blue shadow-blur lg flex-1 margin-right-sm" @click="moveToASRS">MOVE TO ASRS</button>
|
<button class="cu-btn line-grey lg flex-1" @click="cancelAll">X CANCEL</button>
|
</view>
|
</view>
|
</template>
|
|
<script>
|
import permision from "@/common/permission.js";
|
|
export default {
|
data() {
|
return {
|
commonUrl: null,
|
|
// Pallet
|
barcode: "",
|
barcodeFocus: true,
|
|
// Scan Mode
|
scanMode: "multi", // 'one' | 'multi'
|
|
// Carton Input
|
code: "",
|
codeFocus: false,
|
|
// Zone
|
zoneList: [],
|
zoneIndex: -1,
|
|
// Current List (Left Table)
|
matList: [], // { matnr: string, qty: number }
|
|
// Queue List (Right Table)
|
queueList: [], // { barcode: string, zoneId: string, zoneName: string, cartons: [] }
|
|
// System
|
baseIP: "",
|
basePORT: "",
|
baseUrl: "",
|
};
|
},
|
computed: {
|
totalQty() {
|
return this.matList.reduce((sum, item) => sum + (Number(item.qty) || 0), 0);
|
}
|
},
|
mounted() {
|
const UIP = uni.getStorageSync("UIP");
|
this.baseIP = UIP;
|
const UPORT = uni.getStorageSync("UPORT");
|
this.basePORT = UPORT;
|
const PROJ = uni.getStorageSync("UPROJ");
|
this.baseUrl = PROJ;
|
this.getUrl();
|
|
// Load Zones
|
this.getZoneList();
|
},
|
methods: {
|
getUrl() {
|
this.commonUrl = this.baseHttp + this.baseIP + ":" + this.basePORT + "/" + this.baseUrl;
|
},
|
|
// 1. Get Zone List
|
getZoneList() {
|
uni.showLoading({ title: "Loading Zones..." });
|
uni.request({
|
url: this.commonUrl + "/area/list/auth",
|
method: "POST",
|
data: {
|
},
|
header: { token: uni.getStorageSync("token") },
|
success: res => {
|
uni.hideLoading();
|
const r = res.data;
|
if (r.code === 200) {
|
this.zoneList = (r.data && r.data.records) ? r.data.records : [];
|
// Default select first if available
|
if (this.zoneList.length > 0) {
|
this.zoneIndex = 0;
|
}
|
} else if (r.code === 403) {
|
uni.showToast({ title: r.msg || "Re-login required", icon: "none" });
|
setTimeout(() => {
|
uni.reLaunch({ url: "/pages/login/login" });
|
}, 1000);
|
} else {
|
uni.showToast({ title: r.msg || "Fetch failed", icon: "none" });
|
}
|
},
|
fail() {
|
uni.hideLoading();
|
uni.showToast({ title: "Request failed", icon: "none" });
|
},
|
});
|
},
|
|
// 2. Zone Change
|
zoneChange(e) {
|
this.zoneIndex = e.detail.value;
|
},
|
|
// 3. Mode Change
|
modeChange(e) {
|
this.scanMode = e.detail.value;
|
// If switching to 'one' and we have > 1 items, warn or clear?
|
// Requirement says: "One label per carton... only allows one data"
|
if (this.scanMode === 'one' && this.matList.length > 1) {
|
uni.showToast({ title: "Switched to One Label mode. Clearing extra items.", icon: "none" });
|
this.matList = [this.matList[0]];
|
}
|
},
|
|
// 4. Pallet Barcode Input
|
barcodeInput() {
|
if (!this.barcode) return;
|
// Validate if needed
|
this.barcodeFocus = false;
|
this.$nextTick(() => {
|
this.codeFocus = true; // Jump to carton scan
|
});
|
},
|
|
// 5. Carton Code Input
|
codeInput() {
|
if (!this.code) {
|
this.codeFocus = false;
|
this.$nextTick(() => this.codeFocus = true);
|
return;
|
}
|
|
// Check Mode Constraints
|
if (this.scanMode === 'one') {
|
if (this.matList.length >= 1) {
|
uni.showToast({ title: "One Label Mode: Only 1 carton allowed per pallet.", icon: "none" });
|
this.code = "";
|
this.codeFocus = false;
|
this.$nextTick(() => this.codeFocus = true);
|
return;
|
}
|
}
|
|
if (this.queueList.some(q => q.barcode === this.barcode)) {
|
uni.showToast({ title: "Pallet Barcode has been in queue", icon: "none" });
|
this.code = "";
|
this.codeFocus = false;
|
this.$nextTick(() => this.codeFocus = true);
|
return;
|
}
|
|
const cartonLabel = this.code;
|
if (this.queueList.some(q => (q.cartons || []).some(c => c.barcode === cartonLabel))) {
|
uni.showToast({ title: "Label has been in queue", icon: "none" });
|
this.code = "";
|
this.codeFocus = false;
|
this.$nextTick(() => this.codeFocus = true);
|
return;
|
}
|
uni.showLoading({ title: "Scanning..." });
|
|
uni.request({
|
url: this.commonUrl + "/mobile/cartonScan/auth",
|
method: "POST",
|
data: {
|
cartonLabel: cartonLabel
|
},
|
header: { token: uni.getStorageSync("token") },
|
success: (res) => {
|
uni.hideLoading();
|
const r = res.data;
|
if (r.code === 200) {
|
const data = r.data || {};
|
const newItem = {
|
matnr: data.matnr || data.cartonLabel || cartonLabel,
|
barcode: data.barcode || cartonLabel,
|
cartonLabel:data.barcode || cartonLabel,
|
anfme: data.anfme || 0,
|
orderNo: data.orderNo, // Store orderNo from API
|
...data // Store other fields
|
};
|
|
// Check if exists in current list
|
const exist = this.matList.find(m => m.barcode === newItem.barcode);
|
if (exist) {
|
exist.anfme = newItem.anfme;
|
} else {
|
this.matList.push(newItem);
|
}
|
|
uni.vibrateShort();
|
} else if (r.code === 403) {
|
uni.showToast({ title: r.msg || "Re-login required", icon: "none" });
|
setTimeout(() => {
|
uni.reLaunch({ url: "/pages/login/login" });
|
}, 1000);
|
} else {
|
uni.showToast({ title: r.msg || "Scan failed", icon: "none" });
|
}
|
|
// Clear and Refocus
|
this.code = "";
|
this.codeFocus = false;
|
this.$nextTick(() => this.codeFocus = true);
|
},
|
fail: () => {
|
uni.hideLoading();
|
uni.showToast({ title: "Request failed", icon: "none" });
|
this.code = "";
|
this.codeFocus = false;
|
this.$nextTick(() => this.codeFocus = true);
|
}
|
});
|
},
|
|
removeCurrentItem(index) {
|
this.matList.splice(index, 1);
|
},
|
|
// 6. Add to Queue
|
addToQueue() {
|
if (!this.barcode) {
|
uni.showToast({ title: "Please scan Pallet Barcode", icon: "none" });
|
return;
|
}
|
if (this.matList.length === 0) {
|
uni.showToast({ title: "Please scan Carton Labels", icon: "none" });
|
return;
|
}
|
if (this.zoneIndex < 0 || !this.zoneList[this.zoneIndex]) {
|
uni.showToast({ title: "Please select a Zone", icon: "none" });
|
return;
|
}
|
|
const zone = this.zoneList[this.zoneIndex];
|
|
// Extract orderNo from first carton if available (assuming same order per pallet or taking first)
|
const firstCarton = this.matList[0] || {};
|
const orderNo = firstCarton.orderNo || "";
|
|
this.queueList.push({
|
barcode: this.barcode,
|
zoneId: zone.id, // Submit ID
|
zoneName: zone.areaId, // Display areaId
|
orderNo: orderNo, // Add orderNo
|
cartons: JSON.parse(JSON.stringify(this.matList)) // Deep copy
|
});
|
|
// Clear Inputs
|
this.barcode = "";
|
this.matList = [];
|
this.barcodeFocus = true; // Focus back to pallet
|
|
uni.showToast({ title: "Added to Queue", icon: "success" });
|
},
|
|
removeFromQueue(index) {
|
this.queueList.splice(index, 1);
|
},
|
|
// 7. Move to ASRS (Submit)
|
moveToASRS() {
|
if (this.queueList.length === 0) {
|
uni.showToast({ title: "Queue is empty", icon: "none" });
|
return;
|
}
|
|
const that = this;
|
uni.showLoading({ title: "Submitting..." });
|
|
// Submit to API
|
uni.request({
|
url: that.commonUrl + "/mobile/cartonComb/auth",
|
method: "POST",
|
data: that.queueList,
|
header: {
|
token: uni.getStorageSync("token"),
|
'content-type': 'application/json'
|
},
|
success: (res) => {
|
uni.hideLoading();
|
const r = res.data;
|
if (r.code === 200) {
|
uni.showToast({ title: "Moved to ASRS Successfully", icon: "success" });
|
that.queueList = []; // Clear Queue
|
that.cancelAll(); // Reset form
|
} else if (r.code === 403) {
|
uni.showToast({ title: r.msg || "Re-login required", icon: "none" });
|
setTimeout(() => {
|
uni.reLaunch({ url: "/pages/login/login" });
|
}, 1000);
|
} else {
|
uni.showToast({ title: r.msg || "Submission failed", icon: "none" });
|
}
|
},
|
fail: () => {
|
uni.hideLoading();
|
uni.showToast({ title: "Network request failed", icon: "none" });
|
}
|
});
|
},
|
|
// 8. Cancel
|
cancelAll() {
|
this.barcode = "";
|
this.matList = [];
|
this.queueList = []; // User said "Cancel ... clear queue list"
|
this.code = "";
|
this.scanMode = "multi"; // Reset default?
|
this.barcodeFocus = true;
|
}
|
}
|
};
|
</script>
|
|
<style scoped>
|
.page-container {
|
background-color: #f1f1f1;
|
height: 100vh;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.page-scroll {
|
flex: 1;
|
padding: 20rpx;
|
box-sizing: border-box;
|
}
|
|
/* Card Styling */
|
.card-box {
|
background-color: #ffffff;
|
border-radius: 12rpx;
|
margin-bottom: 20rpx;
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.03);
|
overflow: hidden;
|
}
|
|
.card-header {
|
padding: 20rpx;
|
border-bottom: 1rpx solid #f0f0f0;
|
display: flex;
|
align-items: center;
|
}
|
|
.header-indicator {
|
width: 8rpx;
|
height: 28rpx;
|
background-color: #007aff;
|
border-radius: 4rpx;
|
margin-right: 16rpx;
|
}
|
|
.header-title {
|
font-size: 30rpx;
|
font-weight: bold;
|
color: #333;
|
}
|
|
.small-header {
|
padding: 15rpx 20rpx;
|
background-color: #fafafa;
|
}
|
|
.card-body {
|
padding: 20rpx;
|
}
|
|
/* Inputs */
|
.input-wrapper {
|
background-color: #f5f7fa;
|
border-radius: 8rpx;
|
padding: 0 20rpx;
|
height: 80rpx;
|
display: flex;
|
align-items: center;
|
border: 1rpx solid #e0e0e0;
|
}
|
|
.custom-input {
|
flex: 1;
|
height: 100%;
|
font-size: 28rpx;
|
color: #333;
|
}
|
|
.icon-scan {
|
padding: 10rpx;
|
}
|
|
.input-row {
|
display: flex;
|
align-items: center;
|
}
|
|
/* Radio Group */
|
.radio-group-vertical {
|
display: flex;
|
flex-direction: column;
|
}
|
|
.radio-row {
|
display: flex;
|
align-items: center;
|
padding: 10rpx 0;
|
}
|
|
.radio-label {
|
font-size: 28rpx;
|
margin-left: 10rpx;
|
color: #333;
|
}
|
|
/* Zone & Form */
|
.form-row {
|
display: flex;
|
align-items: center;
|
}
|
|
.form-label {
|
font-weight: bold;
|
font-size: 28rpx;
|
margin-right: 20rpx;
|
color: #333;
|
}
|
|
.picker-box {
|
background-color: #f5f7fa;
|
border: 1rpx solid #e0e0e0;
|
border-radius: 8rpx;
|
height: 70rpx;
|
padding: 0 20rpx;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.picker-text {
|
font-size: 28rpx;
|
color: #333;
|
}
|
|
/* Tables */
|
.tables-section {
|
display: flex;
|
flex-direction: column;
|
}
|
|
.no-padding {
|
padding: 0;
|
}
|
|
.table-header-row {
|
display: flex;
|
background-color: #f0f2f5;
|
padding: 15rpx 20rpx;
|
font-weight: bold;
|
font-size: 26rpx;
|
color: #666;
|
}
|
|
.table-body-scroll {
|
max-height: 300rpx;
|
overflow-y: auto;
|
}
|
|
.table-row {
|
display: flex;
|
padding: 20rpx;
|
border-bottom: 1rpx solid #eee;
|
align-items: center;
|
font-size: 26rpx;
|
}
|
|
.col-2 { flex: 2; }
|
.col-1 { flex: 1; }
|
.col-05 { flex: 0.5; }
|
|
.text-center { text-align: center; }
|
.text-cut {
|
overflow: hidden;
|
white-space: nowrap;
|
text-overflow: ellipsis;
|
}
|
|
.empty-state {
|
padding: 40rpx;
|
text-align: center;
|
color: #999;
|
font-size: 26rpx;
|
}
|
|
.table-footer-info {
|
padding: 15rpx 20rpx;
|
text-align: right;
|
font-size: 26rpx;
|
background-color: #fff;
|
border-top: 1rpx solid #f0f0f0;
|
}
|
|
/* Queue List */
|
.queue-list-scroll {
|
max-height: 400rpx;
|
padding: 10rpx;
|
background-color: #f8faff;
|
}
|
|
.queue-card {
|
background-color: #fff;
|
border: 1rpx solid #eee;
|
border-radius: 8rpx;
|
margin-bottom: 10rpx;
|
overflow: hidden;
|
}
|
|
.queue-header {
|
padding: 15rpx;
|
background-color: #eef6ff;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
font-size: 26rpx;
|
}
|
|
.queue-content {
|
padding: 10rpx 15rpx;
|
}
|
|
.queue-detail-row {
|
display: flex;
|
justify-content: space-between;
|
font-size: 24rpx;
|
padding: 5rpx 0;
|
}
|
|
/* Footer Actions */
|
.footer-actions {
|
position: fixed;
|
bottom: 0;
|
left: 0;
|
right: 0;
|
height: 120rpx;
|
background-color: #fff;
|
display: flex;
|
align-items: center;
|
padding: 0 20rpx;
|
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.05);
|
z-index: 99;
|
}
|
|
/* Utilities */
|
.flex-1 { flex: 1; }
|
.margin-top-sm { margin-top: 20rpx; }
|
.margin-left-sm { margin-left: 20rpx; }
|
.margin-right-sm { margin-right: 20rpx; }
|
.margin-right-xs { margin-right: 10rpx; }
|
.text-bold { font-weight: bold; }
|
.text-blue { color: #007aff; }
|
.text-dark { color: #333; }
|
.text-grey { color: #888; }
|
.text-sm { font-size: 24rpx; }
|
|
</style>
|