移动端:增加测试批次模式隔离
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<view class="w-full">
|
||||
<view class="flex items-center rounded-xl bg-white p-2 shadow-sm">
|
||||
<input
|
||||
v-model="scanValue"
|
||||
class="h-11 min-w-0 flex-1 px-3 text-sm text-gray-700"
|
||||
type="text"
|
||||
confirm-type="done"
|
||||
maxlength="128"
|
||||
:focus="focusRequested"
|
||||
:disabled="disabled || loading"
|
||||
placeholder="请扫描或输入托盘二维码后按回车"
|
||||
@confirm="handleSubmit"
|
||||
@input="clearError"
|
||||
/>
|
||||
<button
|
||||
class="ml-2 h-10 rounded-lg bg-blue-500 px-3 text-xs text-white active:opacity-80 disabled:bg-gray-300"
|
||||
:disabled="disabled || loading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ loading ? "校验中" : "提交扫描" }}
|
||||
</button>
|
||||
</view>
|
||||
<text v-if="errorMessage" class="mt-2 block text-xs text-red-500">
|
||||
{{ errorMessage }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, ref, watch } from "vue";
|
||||
import { isDuplicateScan, normalizeScanValue } from "@/utils/palletScan";
|
||||
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
duplicateWindowMs: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["scan", "invalid", "duplicate"]);
|
||||
const scanValue = ref("");
|
||||
const errorMessage = ref("");
|
||||
const focusRequested = ref(false);
|
||||
let lastScanValue = "";
|
||||
let lastScanAt = 0;
|
||||
|
||||
function focusInput() {
|
||||
if (props.disabled || props.loading) return;
|
||||
focusRequested.value = false;
|
||||
nextTick(() => {
|
||||
if (!props.disabled && !props.loading) {
|
||||
focusRequested.value = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
errorMessage.value = "";
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (props.disabled || props.loading) return;
|
||||
|
||||
let normalizedValue;
|
||||
try {
|
||||
normalizedValue = normalizeScanValue(scanValue.value);
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message;
|
||||
emit("invalid", error.message);
|
||||
focusInput();
|
||||
return;
|
||||
}
|
||||
|
||||
const scannedAt = Date.now();
|
||||
if (
|
||||
isDuplicateScan(
|
||||
normalizedValue,
|
||||
lastScanValue,
|
||||
scannedAt,
|
||||
lastScanAt,
|
||||
props.duplicateWindowMs,
|
||||
)
|
||||
) {
|
||||
scanValue.value = "";
|
||||
emit("duplicate", normalizedValue);
|
||||
focusInput();
|
||||
return;
|
||||
}
|
||||
|
||||
lastScanValue = normalizedValue;
|
||||
lastScanAt = scannedAt;
|
||||
scanValue.value = "";
|
||||
errorMessage.value = "";
|
||||
emit("scan", normalizedValue);
|
||||
focusInput();
|
||||
}
|
||||
|
||||
onMounted(focusInput);
|
||||
|
||||
watch(
|
||||
() => [props.disabled, props.loading],
|
||||
([disabled, loading]) => {
|
||||
if (!disabled && !loading) focusInput();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<view
|
||||
v-if="testBatchStore.isTestMode"
|
||||
class="flex items-start gap-3 rounded-xl border border-red-300 bg-red-50 px-3 py-3"
|
||||
>
|
||||
<view class="mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-red-500 text-white">
|
||||
<view class="i-mdi-flask-outline text-sm"></view>
|
||||
</view>
|
||||
<view class="min-w-0">
|
||||
<text class="block text-sm font-bold text-red-700">测试数据</text>
|
||||
<text class="mt-0.5 block break-all text-xs leading-5 text-red-600">
|
||||
当前批次:{{ testBatchStore.currentBatchName || "已选测试批次" }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useTestBatchStore } from "@/store";
|
||||
|
||||
const testBatchStore = useTestBatchStore();
|
||||
</script>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<view class="space-y-3 rounded-2xl bg-white p-4 shadow-sm">
|
||||
<view class="flex items-center justify-between gap-3">
|
||||
<view class="min-w-0">
|
||||
<text class="block text-sm font-bold text-gray-800">数据模式</text>
|
||||
<text class="mt-1 block text-xs text-gray-500">
|
||||
{{ testBatchStore.isTestMode ? "当前操作隔离在测试批次" : "当前使用正式数据" }}
|
||||
</text>
|
||||
</view>
|
||||
<text
|
||||
:class="testBatchStore.isTestMode ? 'border-red-300 bg-red-50 text-red-700' : 'border-gray-200 bg-gray-50 text-gray-600'"
|
||||
class="shrink-0 rounded-full border px-2.5 py-1 text-xs font-semibold"
|
||||
>
|
||||
{{ testBatchStore.isTestMode ? "测试数据" : "正式数据" }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex rounded-xl bg-gray-100 p-1">
|
||||
<button
|
||||
class="flex-1 rounded-lg py-2 text-xs transition-colors"
|
||||
:class="!testBatchStore.isTestMode ? 'bg-white font-semibold text-gray-800 shadow-sm' : 'text-gray-500'"
|
||||
:disabled="loading"
|
||||
size="mini"
|
||||
@click="handleModeClick('formal')"
|
||||
>
|
||||
正式
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 rounded-lg py-2 text-xs transition-colors"
|
||||
:class="testBatchStore.isTestMode ? 'bg-red-500 font-semibold text-white shadow-sm' : 'text-gray-500'"
|
||||
:disabled="loading || !activeOptions.length"
|
||||
size="mini"
|
||||
@click="handleModeClick('test')"
|
||||
>
|
||||
测试
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="testBatchStore.isTestMode || activeOptions.length" class="space-y-1">
|
||||
<text class="block text-xs text-gray-500">测试批次</text>
|
||||
<picker
|
||||
:range="activeOptions"
|
||||
range-key="batchName"
|
||||
:value="selectedBatchIndex"
|
||||
:disabled="loading || !activeOptions.length"
|
||||
@change="handleBatchChange"
|
||||
>
|
||||
<view class="rounded-xl bg-gray-50 px-3 py-3 text-sm text-gray-700">
|
||||
{{ selectedBatch?.batchName || (loading ? "加载中..." : "暂无可用测试批次") }}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view v-if="loadError" class="flex items-center justify-between gap-2 rounded-lg bg-orange-50 px-3 py-2">
|
||||
<text class="min-w-0 text-xs leading-5 text-orange-700">{{ loadError }}</text>
|
||||
<button class="shrink-0 text-xs text-orange-700" size="mini" :disabled="loading" @click="loadOptions">
|
||||
重试
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useTestBatchStore } from "@/store";
|
||||
|
||||
const testBatchStore = useTestBatchStore();
|
||||
const loading = ref(false);
|
||||
const loadError = ref("");
|
||||
const activeOptions = computed(() =>
|
||||
testBatchStore.options.filter((item) => item.status === "ACTIVE"),
|
||||
);
|
||||
const selectedBatch = computed(() =>
|
||||
activeOptions.value.find(
|
||||
(item) => String(item.batchId) === String(testBatchStore.currentBatchId),
|
||||
),
|
||||
);
|
||||
const selectedBatchIndex = computed(() => {
|
||||
const index = activeOptions.value.findIndex(
|
||||
(item) => String(item.batchId) === String(testBatchStore.currentBatchId),
|
||||
);
|
||||
return index >= 0 ? index : 0;
|
||||
});
|
||||
|
||||
function errorMessage(error) {
|
||||
return error?.response?.data?.msg || error?.message || "测试批次加载失败,请稍后重试";
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
loading.value = true;
|
||||
loadError.value = "";
|
||||
try {
|
||||
await testBatchStore.refreshOptions();
|
||||
} catch (error) {
|
||||
if (error?.reason !== "TEST_BATCH_UNAVAILABLE") {
|
||||
loadError.value = errorMessage(error);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmModeSwitch() {
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: "切换数据模式",
|
||||
content: "切换后会清空当前页面未提交的扫码和表单内容,确定继续吗?",
|
||||
cancelText: "取消",
|
||||
confirmText: "确定",
|
||||
success: (result) => resolve(result.confirm === true),
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleModeClick(mode) {
|
||||
if (loading.value) return;
|
||||
if (mode === "formal") {
|
||||
if (!testBatchStore.isTestMode || !(await confirmModeSwitch())) return;
|
||||
testBatchStore.leaveTestMode();
|
||||
uni.reLaunch({ url: "/pages/work/index" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (testBatchStore.isTestMode) return;
|
||||
const batch = activeOptions.value[0];
|
||||
if (!batch) {
|
||||
uni.showToast({ title: "暂无可用测试批次", icon: "none" });
|
||||
return;
|
||||
}
|
||||
if (!(await confirmModeSwitch())) return;
|
||||
try {
|
||||
testBatchStore.selectBatch(batch);
|
||||
uni.reLaunch({ url: "/pages/work/index" });
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || "测试批次不可用", icon: "none" });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchChange(event) {
|
||||
const batch = activeOptions.value[Number(event.detail.value)];
|
||||
if (!batch || String(batch.batchId) === String(testBatchStore.currentBatchId)) return;
|
||||
if (!(await confirmModeSwitch())) return;
|
||||
try {
|
||||
testBatchStore.selectBatch(batch);
|
||||
uni.reLaunch({ url: "/pages/work/index" });
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || "测试批次不可用", icon: "none" });
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
Reference in New Issue
Block a user