移动端:增加测试批次模式隔离

This commit is contained in:
焦龙言
2026-09-24 09:45:08 +08:00
parent 886a4479c4
commit d15b8e2f6c
29 changed files with 3561 additions and 9 deletions
@@ -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>