scanner in CustomerSelector.vue
This commit is contained in:
@@ -1,29 +1,25 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.stat.read" class="customer-data-charts-page">
|
||||
<div class="header bg-white">
|
||||
<div class="buttons">
|
||||
<t-button class="selector" size="small" theme="primary" variant="outline" @click="selectorVisible = true">
|
||||
<template #icon>
|
||||
<t-icon name="user-search" />
|
||||
</template>
|
||||
<span>选择客户</span>
|
||||
</t-button>
|
||||
<t-button size="small" theme="primary" @click="toggleScanner">
|
||||
<template #icon>
|
||||
<t-icon name="scan" />
|
||||
</template>
|
||||
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||
</t-button>
|
||||
</div>
|
||||
<div v-if="scannerVisible" class="line scanner-wrapper">
|
||||
<code-scanner
|
||||
visible
|
||||
:status-text="scanStatus"
|
||||
active-text="正在识别客户编码,请将二维码移入取景框"
|
||||
@decode="handleDecodedText"
|
||||
@status-change="handleScanStatusChange"
|
||||
/>
|
||||
</div>
|
||||
<t-input
|
||||
v-model.trim="customerCode"
|
||||
class="customer-search"
|
||||
borderless
|
||||
clearable
|
||||
placeholder="输入客户编码或姓名"
|
||||
@clear="applyCustomerSearch"
|
||||
@enter="applyCustomerSearch"
|
||||
>
|
||||
<template #suffix-icon>
|
||||
<t-button
|
||||
size="extra-small"
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
content="选择客户"
|
||||
@click.stop="selectorVisible = true"
|
||||
/>
|
||||
</template>
|
||||
</t-input>
|
||||
</div>
|
||||
<customer-profile-card v-entrance-transition class="customer" :customer="selectedCustomer" />
|
||||
<div class="charts">
|
||||
@@ -78,7 +74,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import CustomerAPI from "@/api/CustomerAPI";
|
||||
import CodeScanner from "@/components/CodeScanner.vue";
|
||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
||||
import CustomerChartSummary from "@/pages/customer/component/CustomerChartSummary.vue";
|
||||
@@ -86,7 +81,6 @@ import CustomerEventRecordCalendarSection from "@/pages/customer/component/Custo
|
||||
import CustomerEventStructureChart from "@/pages/customer/component/CustomerEventStructureChart.vue";
|
||||
import CustomerOrderCharts from "@/pages/customer/component/CustomerOrderCharts.vue";
|
||||
import CustomerRecordTrendChart from "@/pages/customer/component/CustomerRecordTrendChart.vue";
|
||||
import { useSettingStore } from "@/store/settingStore";
|
||||
import type { Customer } from "@/types/Customer";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
@@ -96,7 +90,6 @@ defineOptions({
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const settingStore = useSettingStore();
|
||||
|
||||
// ---------- 路由 ----------
|
||||
|
||||
@@ -105,10 +98,9 @@ const routeCustomerCode = computed(() => String(route.query.customerCode || ""))
|
||||
const customerCode = ref(routeCustomerCode.value);
|
||||
const selectedCustomerId = ref(routeCustomerId.value);
|
||||
|
||||
watch(routeCustomerCode, (value) => {
|
||||
if (value) {
|
||||
customerCode.value = value;
|
||||
}
|
||||
watch(routeCustomerCode, async (value) => {
|
||||
customerCode.value = value;
|
||||
await loadCustomer(value);
|
||||
});
|
||||
|
||||
watch(routeCustomerId, async (value) => {
|
||||
@@ -131,18 +123,14 @@ const selectorVisible = ref(false);
|
||||
const selectedCustomer = ref<Customer>();
|
||||
const selectedEventIdList = ref<string[]>([]);
|
||||
|
||||
watch(customerCode, async (value) => {
|
||||
await loadCustomer(value);
|
||||
});
|
||||
|
||||
async function loadCustomer(code: string): Promise<void> {
|
||||
const normalizedCode = code.trim();
|
||||
if (!normalizedCode) {
|
||||
const keyword = code.trim();
|
||||
if (!keyword) {
|
||||
clearLoadedCustomer();
|
||||
return;
|
||||
}
|
||||
|
||||
const customer = await CustomerAPI.findByCode(normalizedCode);
|
||||
const customer = await CustomerAPI.findByCode(keyword) || await CustomerAPI.findByName(keyword);
|
||||
if (!customer) {
|
||||
clearLoadedCustomer();
|
||||
return;
|
||||
@@ -152,6 +140,10 @@ async function loadCustomer(code: string): Promise<void> {
|
||||
selectedCustomerId.value = customer.id || "";
|
||||
}
|
||||
|
||||
async function applyCustomerSearch(): Promise<void> {
|
||||
await loadCustomer(customerCode.value);
|
||||
}
|
||||
|
||||
async function loadCustomerById(id: string): Promise<void> {
|
||||
const normalizedId = id.trim();
|
||||
if (!normalizedId) {
|
||||
@@ -162,6 +154,7 @@ async function loadCustomerById(id: string): Promise<void> {
|
||||
try {
|
||||
selectedCustomer.value = await CustomerAPI.detail(normalizedId);
|
||||
selectedCustomerId.value = selectedCustomer.value.id || "";
|
||||
customerCode.value = selectedCustomer.value.code || selectedCustomer.value.name || "";
|
||||
} catch (err) {
|
||||
clearLoadedCustomer();
|
||||
console.warn("客户详情加载失败", err);
|
||||
@@ -206,36 +199,6 @@ function getCurrentRouteDepth(): number {
|
||||
return Number(route.meta.depth ?? 0);
|
||||
}
|
||||
|
||||
// ---------- 扫码 ----------
|
||||
|
||||
const scannerVisible = ref(false);
|
||||
const scanStatus = ref("点击扫码开始识别");
|
||||
let latestDecodedText = "";
|
||||
|
||||
async function toggleScanner(): Promise<void> {
|
||||
scannerVisible.value = !scannerVisible.value;
|
||||
if (scannerVisible.value) {
|
||||
latestDecodedText = "";
|
||||
scanStatus.value = "正在启动扫码预览";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDecodedText(rawText: string): Promise<void> {
|
||||
const normalizedText = rawText.trim();
|
||||
if (!normalizedText || normalizedText === latestDecodedText) {
|
||||
return;
|
||||
}
|
||||
latestDecodedText = normalizedText;
|
||||
await settingStore.playScanSuccessSound();
|
||||
customerCode.value = normalizedText;
|
||||
scanStatus.value = `已识别 ${normalizedText},正在加载客户档案`;
|
||||
scannerVisible.value = false;
|
||||
}
|
||||
|
||||
function handleScanStatusChange(value: string): void {
|
||||
scanStatus.value = value;
|
||||
}
|
||||
|
||||
// ---------- 图表日期 ----------
|
||||
|
||||
const monthCursor = ref(getMonthStart(new Date()));
|
||||
@@ -300,18 +263,14 @@ function getDateEnd(date: Date): Date {
|
||||
position: sticky;
|
||||
box-shadow: var(--td-shadow-1);
|
||||
|
||||
.buttons {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
.customer-search {
|
||||
--td-input-vertical-padding: .25rem 1rem;
|
||||
|
||||
.selector {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.scanner-wrapper {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
border: 1px solid var(--td-component-border);
|
||||
background: var(--td-bg-color-container);
|
||||
padding-right: .3rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +288,7 @@ function getDateEnd(date: Date): Date {
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.customer-data-charts-page {
|
||||
.selector {
|
||||
.customer-search {
|
||||
background: #10161f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-can="savePermissionCode" class="customer-edit">
|
||||
<t-form class="form" label-align="left" label-width="6rem" :disabled="isCustomerBound" v-entrance-transition>
|
||||
<t-form class="form" label-align="left" label-width="5rem" :disabled="isCustomerBound" v-entrance-transition>
|
||||
<t-form-item label="客户编码">
|
||||
<t-input
|
||||
v-model.trim="formData.code"
|
||||
@@ -14,14 +14,14 @@
|
||||
theme="primary"
|
||||
size="extra-small"
|
||||
:disabled="false"
|
||||
@click="toggleScanner('code')"
|
||||
@click="toggleScanner"
|
||||
>
|
||||
<span v-text="scannerVisible && scannerTarget === 'code' ? '关闭扫码' : '扫码'" />
|
||||
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||
</t-button>
|
||||
</template>
|
||||
</t-input>
|
||||
</t-form-item>
|
||||
<div v-if="scannerVisible && scannerTarget === 'code'" class="code-scanner-wrapper">
|
||||
<div v-if="scannerVisible" class="code-scanner-wrapper">
|
||||
<code-scanner
|
||||
visible
|
||||
:status-text="scanStatus"
|
||||
@@ -102,7 +102,7 @@
|
||||
<t-input
|
||||
:value="formData.introducerCustomer?.name"
|
||||
readonly
|
||||
placeholder="选择或扫码"
|
||||
placeholder="选择介绍人"
|
||||
borderless
|
||||
:disabled="isCustomerBound"
|
||||
/>
|
||||
@@ -113,22 +113,10 @@
|
||||
size="extra-small"
|
||||
:disabled="isCustomerBound"
|
||||
@click="selectorVisible = true"
|
||||
>选择</t-button>
|
||||
<t-button theme="primary" size="extra-small" :disabled="isCustomerBound" @click="toggleScanner('introducer')">
|
||||
<span v-text="scannerVisible && scannerTarget === 'introducer' ? '关闭扫码' : '扫码'" />
|
||||
</t-button>
|
||||
>选择客户</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</t-form-item>
|
||||
<div v-if="scannerVisible && scannerTarget === 'introducer'" class="code-scanner-wrapper">
|
||||
<code-scanner
|
||||
visible
|
||||
:status-text="scanStatus"
|
||||
active-text="正在识别介绍人编码"
|
||||
@decode="handleDecodedText"
|
||||
@status-change="handleScanStatusChange"
|
||||
/>
|
||||
</div>
|
||||
<t-form-item label="调理部位" :arrow="!isCustomerBound" @click="openBodyAreaPicker">
|
||||
<div class="picker-text" v-text="selectedAreaText" />
|
||||
</t-form-item>
|
||||
@@ -244,9 +232,7 @@ let latestDecodedText = "";
|
||||
|
||||
// ---------- 扫码 ----------
|
||||
|
||||
type ScanTarget = "code" | "introducer";
|
||||
const scannerVisible = ref(false);
|
||||
const scannerTarget = ref<ScanTarget>("code");
|
||||
const scanStatus = ref("点击扫码开始识别");
|
||||
const introducerCustomerId = ref("");
|
||||
const boundCodeCustomer = ref<Customer>();
|
||||
@@ -259,15 +245,11 @@ const isCustomerBound = computed(() => {
|
||||
return !!boundCustomerId && boundCustomerId !== formData.id;
|
||||
});
|
||||
|
||||
async function toggleScanner(target: ScanTarget): Promise<void> {
|
||||
if (isCustomerBound.value && target !== "code") {
|
||||
return;
|
||||
}
|
||||
if (scannerVisible.value && scannerTarget.value === target) {
|
||||
async function toggleScanner(): Promise<void> {
|
||||
if (scannerVisible.value) {
|
||||
scannerVisible.value = false;
|
||||
return;
|
||||
}
|
||||
scannerTarget.value = target;
|
||||
scannerVisible.value = true;
|
||||
latestDecodedText = "";
|
||||
}
|
||||
@@ -279,17 +261,10 @@ async function handleDecodedText(rawText: string): Promise<void> {
|
||||
}
|
||||
latestDecodedText = normalizedText;
|
||||
await settingStore.playScanSuccessSound();
|
||||
if (scannerTarget.value === "code") {
|
||||
// 选择客户扫码
|
||||
formData.code = normalizedText;
|
||||
scannerVisible.value = false;
|
||||
await checkBoundCustomerCode();
|
||||
scanStatus.value = `已识别客户编码 ${normalizedText}`;
|
||||
return;
|
||||
} else {
|
||||
// 选择介绍人扫码
|
||||
await applyIntroducer(normalizedText);
|
||||
}
|
||||
formData.code = normalizedText;
|
||||
scannerVisible.value = false;
|
||||
await checkBoundCustomerCode();
|
||||
scanStatus.value = `已识别客户编码 ${normalizedText}`;
|
||||
}
|
||||
|
||||
function handleScanStatusChange(value: string): void {
|
||||
@@ -343,28 +318,6 @@ async function checkBoundCustomerCode(): Promise<void> {
|
||||
|
||||
const selectorVisible = ref(false);
|
||||
|
||||
async function applyIntroducer(code: string): Promise<void> {
|
||||
if (!code) {
|
||||
formData.introducerCustomerId = "";
|
||||
introducerCustomerId.value = "";
|
||||
scannerVisible.value = false;
|
||||
return;
|
||||
}
|
||||
const matched = await CustomerAPI.findByCode(code);
|
||||
if (!matched?.id) {
|
||||
formData.introducerCustomerId = "";
|
||||
formData.introducerCustomer = undefined;
|
||||
introducerCustomerId.value = "";
|
||||
scannerVisible.value = false;
|
||||
Message.warning(`未匹配到 ${code} 编码介绍人,请选择或扫码已有客户`);
|
||||
return;
|
||||
}
|
||||
formData.introducerCustomerId = matched.id;
|
||||
formData.introducerCustomer = matched;
|
||||
introducerCustomerId.value = matched.id;
|
||||
scannerVisible.value = false;
|
||||
}
|
||||
|
||||
async function handleIntroducerChange(id: string): Promise<void> {
|
||||
const customer = id ? await CustomerAPI.findById(id) : undefined;
|
||||
introducerCustomerId.value = customer?.id || "";
|
||||
@@ -743,7 +696,7 @@ function buildAttachmentList(): Attachment[] {
|
||||
<style scoped lang="less">
|
||||
.customer-edit {
|
||||
gap: 1rem;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 4rem 0;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -795,7 +748,7 @@ function buildAttachmentList(): Attachment[] {
|
||||
}
|
||||
|
||||
.sex {
|
||||
gap: 1rem;
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<customer-selector
|
||||
v-model="selectedInviteeId"
|
||||
v-model:visible="selectorVisible"
|
||||
:enable-scan="false"
|
||||
@change="handleInviteeSelected"
|
||||
/>
|
||||
<bottom-popup
|
||||
|
||||
@@ -6,7 +6,21 @@
|
||||
class="search"
|
||||
placeholder="搜索编码、姓名(拼音)、电话"
|
||||
borderless
|
||||
/>
|
||||
>
|
||||
<template #suffix-icon>
|
||||
<t-button
|
||||
size="extra-small"
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
@click.stop="toggleScanner"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="scan" />
|
||||
</template>
|
||||
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||
</t-button>
|
||||
</template>
|
||||
</t-input>
|
||||
<t-button
|
||||
v-if="canReadTag"
|
||||
class="tag-filter"
|
||||
@@ -20,6 +34,15 @@
|
||||
<span v-text="tagFilterText" />
|
||||
</t-button>
|
||||
</div>
|
||||
<div v-if="scannerVisible" class="scanner-wrapper">
|
||||
<code-scanner
|
||||
:visible="scannerVisible"
|
||||
:status-text="scanStatus"
|
||||
active-text="正在识别客户编码,请将条码或二维码移入取景框"
|
||||
@decode="handleDecodedText"
|
||||
@status-change="handleScanStatusChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="canReadTag && selectedTagList.length" class="selected-tags bg-white">
|
||||
<t-tag
|
||||
v-for="item in selectedTagList"
|
||||
@@ -89,10 +112,12 @@ import { Message } from "@/utils/message";
|
||||
import { Time, Toolkit, WebToolkit } from "timi-web";
|
||||
import { type Page, type PageResult, useUserStore } from "timi-web-api";
|
||||
import CustomerAPI from "@/api/CustomerAPI";
|
||||
import CodeScanner from "@/components/CodeScanner.vue";
|
||||
import CustomerRemoveDialog from "@/pages/customer/component/CustomerRemoveDialog.vue";
|
||||
import TagSelector from "@/components/TagSelector.vue";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import { useSettingStore } from "@/store/settingStore";
|
||||
import type { Customer } from "@/types/Customer";
|
||||
import type { NavBarAction } from "@/types/NavBarAction";
|
||||
import type { Tag } from "@/types/Tag";
|
||||
@@ -105,6 +130,7 @@ defineOptions({
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const settingStore = useSettingStore();
|
||||
const navBarStore = useNavBarStore();
|
||||
const isRefreshing = ref(false);
|
||||
const isExporting = ref(false);
|
||||
@@ -118,6 +144,42 @@ const canCreateCustomer = computed(() => userStore.hasPermissions(permissionCode
|
||||
const canDeleteCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.delete));
|
||||
const canExportCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.export));
|
||||
const canShowActionMenu = computed(() => canCreateCustomer.value || canExportCustomer.value);
|
||||
const scannerVisible = ref(false);
|
||||
const scanStatus = ref("点击扫码开始识别");
|
||||
let latestDecodedText = "";
|
||||
|
||||
async function toggleScanner(): Promise<void> {
|
||||
scannerVisible.value = !scannerVisible.value;
|
||||
if (scannerVisible.value) {
|
||||
latestDecodedText = "";
|
||||
scanStatus.value = "正在启动扫码预览";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDecodedText(rawText: string): Promise<void> {
|
||||
const normalizedText = rawText.trim();
|
||||
if (!normalizedText || normalizedText === latestDecodedText) {
|
||||
return;
|
||||
}
|
||||
latestDecodedText = normalizedText;
|
||||
await settingStore.playScanSuccessSound();
|
||||
try {
|
||||
const customer = await CustomerAPI.findByCode(normalizedText, currentStoreStore.currentStoreId || undefined);
|
||||
if (!customer?.id) {
|
||||
scanStatus.value = `客户 ${normalizedText} 不存在,请先建档。`;
|
||||
return;
|
||||
}
|
||||
keyword.value = customer.code || normalizedText;
|
||||
scannerVisible.value = false;
|
||||
scanStatus.value = `已识别 ${customer.code || normalizedText}`;
|
||||
} catch {
|
||||
scanStatus.value = "客户查询失败,请重试。";
|
||||
}
|
||||
}
|
||||
|
||||
function handleScanStatusChange(value: string): void {
|
||||
scanStatus.value = value;
|
||||
}
|
||||
|
||||
const actionList = computed<NavBarAction[]>(() => {
|
||||
const list: NavBarAction[] = [];
|
||||
@@ -232,6 +294,7 @@ watch(keyword, async () => {
|
||||
onUnmounted(() => {
|
||||
navBarStore.clearRight();
|
||||
window.clearTimeout(keywordTimer);
|
||||
scannerVisible.value = false;
|
||||
});
|
||||
|
||||
// ---------- 导航 ----------
|
||||
@@ -315,9 +378,11 @@ async function doExport(): Promise<void> {
|
||||
align-items: center;
|
||||
|
||||
.search {
|
||||
--td-input-vertical-padding: .25rem 1rem;
|
||||
|
||||
width: 100%;
|
||||
border: 1px solid var(--td-component-border);
|
||||
padding: .5rem 1rem;
|
||||
padding-right: .3rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
@@ -328,6 +393,10 @@ async function doExport(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
.scanner-wrapper {
|
||||
padding: .5rem 1rem 0;
|
||||
}
|
||||
|
||||
.selected-tags {
|
||||
gap: .5rem;
|
||||
z-index: 9;
|
||||
|
||||
Reference in New Issue
Block a user