add scanner

This commit is contained in:
Timi
2026-09-09 16:55:41 +08:00
parent 5a3ae314ef
commit 288ea7b80d
+120 -17
View File
@@ -2,7 +2,25 @@
<div v-can="permissionCodes.salesOrder.read" class="sales-order-list">
<div class="filters">
<div class="search-status">
<t-input v-model.trim="keyword" class="keyword" type="search" borderless clearable placeholder="搜索订单号、客户" />
<t-input
v-model.trim="keyword"
class="keyword"
type="search"
borderless
placeholder="搜索订单号或客户"
>
<template #suffix-icon>
<div class="actions">
<t-button
size="extra-small"
theme="primary"
variant="outline"
:content="scannerVisible ? '关闭扫码' : '扫码'"
@click.stop="toggleScanner"
/>
</div>
</template>
</t-input>
<t-dropdown-menu class="status">
<t-dropdown-item
v-model="status"
@@ -40,6 +58,15 @@
@refresh="refresh"
@scrolltolower="loadMore"
>
<div v-if="scannerVisible" class="scanner-wrap">
<code-scanner
visible
:status-text="scanStatus"
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
</div>
<div v-if="list.length" class="items">
<sales-order-list-cell
class="item"
@@ -71,10 +98,13 @@
import { type DropdownValue } from "tdesign-mobile-vue";
import { Toolkit } from "timi-web";
import { useUserStore } from "timi-web-api";
import CustomerAPI from "@/api/CustomerAPI";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
import SalesOrderListCell from "@/pages/salesOrder/components/SalesOrderListCell.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { Customer } from "@/types/Customer";
import type { GaoSalesOrderStatus, GaoSalesOrderType, GaoSalesPaymentStatus, GaoSalesRefundStatus, SalesOrder, SalesOrderPage } from "@/types/SalesOrder";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
@@ -110,6 +140,11 @@ const actions = computed(() => [
]);
const keyword = ref("");
const customerId = ref("");
const selectedCustomer = ref<Customer>();
const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别。");
let latestDecodedText = "";
let customerKeyword = "";
const orderType = ref("ALL");
type PaymentFilter = "ALL" | GaoSalesPaymentStatus | "PENDING_RECEIVE";
type RefundFilter = "ALL" | GaoSalesRefundStatus;
@@ -151,6 +186,24 @@ function applyRouteFilters(): void {
refundStatus.value = queryValue(route.query.refundStatus) as RefundFilter || "ALL";
}
function setCustomerKeyword(customer: Customer): void {
const value = customer.name?.trim() || customer.code?.trim() || "";
customerId.value = customer.id || "";
selectedCustomer.value = customer;
customerKeyword = value;
keyword.value = value;
}
async function loadRouteCustomer(): Promise<void> {
if (!customerId.value) {
selectedCustomer.value = undefined;
customerKeyword = "";
return;
}
selectedCustomer.value = await CustomerAPI.detail(customerId.value);
setCustomerKeyword(selectedCustomer.value);
}
function buildPageFilter(): SalesOrderPage {
const pendingReceive = paymentStatus.value === "PENDING_RECEIVE";
return {
@@ -226,6 +279,39 @@ async function selectRefundStatus(value: DropdownValue): Promise<void> {
}
}
async function toggleScanner(): Promise<void> {
scannerVisible.value = !scannerVisible.value;
if (scannerVisible.value) {
latestDecodedText = "";
scanStatus.value = "正在打开相机。";
}
}
async function handleDecodedText(rawText: string): Promise<void> {
const code = rawText.trim();
if (!code || code === latestDecodedText) {
return;
}
latestDecodedText = code;
try {
const customer = await CustomerAPI.findByCode(code);
if (!customer?.id) {
scanStatus.value = `客户 ${code} 不存在。`;
return;
}
setCustomerKeyword(customer);
scanStatus.value = `已识别 ${customer.name || customer.code || code},已填入搜索框。`;
scannerVisible.value = false;
await fetchList(true);
} catch (error) {
scanStatus.value = getErrorMessage(error, "客户查询失败,请重试。");
}
}
function handleScanStatusChange(value: string): void {
scanStatus.value = value;
}
async function toDetail(item: SalesOrder): Promise<void> {
if (item.id) await router.push(`/management/sales-orders/${item.id}`);
}
@@ -265,16 +351,35 @@ const search = Toolkit.debounce(async () => {
Message.error(getErrorMessage(error, "销售订单加载失败"));
}
}, false);
watch(keyword, () => search());
watch(keyword, async () => {
const isCustomerKeyword = !!customerId.value && keyword.value.trim() === customerKeyword;
customerKeyword = "";
if (isCustomerKeyword) {
return;
}
customerId.value = "";
selectedCustomer.value = undefined;
await search();
});
watch(routeFilterKey, async () => {
applyRouteFilters();
try {
await loadRouteCustomer();
} catch {
selectedCustomer.value = undefined;
}
await fetchList(true);
});
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setCustomRightVisible(!!actions.value.length);
applyRouteFilters();
try { await fetchList(true); } catch (error) { Message.error(getErrorMessage(error, "销售订单加载失败")); }
try {
await loadRouteCustomer();
await fetchList(true);
} catch (error) {
Message.error(getErrorMessage(error, "销售订单加载失败"));
}
});
onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
</script>
@@ -299,13 +404,21 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
display: flex;
.keyword {
--td-button-border-radius: .75rem;
--td-input-vertical-padding: .25rem 1rem;
flex: 1;
border: 1px solid var(--td-component-border);
z-index: 2;
background: var(--td-bg-color-container);
padding-right: .3rem;
border-radius: 1rem;
.actions {
gap: .25rem;
display: flex;
align-items: center;
}
}
.status {
@@ -336,26 +449,16 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
border-top: none;
padding-top: .8rem;
border-radius: 0 0 1rem 1rem;
&:after {
display: none;
}
:deep(.t-dropdown-menu__item) {
min-width: 0;
}
:deep(.t-dropdown-menu__title) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.list-wrap {
padding-top: 1rem;
.scanner-wrap {
margin: 0 1rem 1rem;
}
.items {
gap: 1rem;
display: flex;