scanner in CustomerSelector.vue
This commit is contained in:
@@ -48,10 +48,10 @@ async function findById(id: string): Promise<Customer | undefined> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findByCode(code: string): Promise<Customer | undefined> {
|
async function findByCode(code: string, storeId = getCurrentStoreId()): Promise<Customer | undefined> {
|
||||||
return await axios.post(`${BASE_URI}/find/code`, null, {
|
return await axios.post(`${BASE_URI}/find/code`, null, {
|
||||||
params: {
|
params: {
|
||||||
storeId: getCurrentStoreId(),
|
storeId,
|
||||||
code
|
code
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,6 +92,23 @@ html.theme-dark body {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.t-dropdown-menu {
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu__item) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu__title) {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.center-tips {
|
.center-tips {
|
||||||
color: var(--tui-gray);
|
color: var(--tui-gray);
|
||||||
padding: .75rem;
|
padding: .75rem;
|
||||||
|
|||||||
@@ -9,12 +9,34 @@
|
|||||||
>
|
>
|
||||||
<div class="customer-selector">
|
<div class="customer-selector">
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
<t-input
|
<div class="search-row">
|
||||||
v-model.trim="keyword"
|
<t-input
|
||||||
class="search"
|
v-model.trim="keyword"
|
||||||
borderless
|
class="search"
|
||||||
clearable
|
borderless
|
||||||
placeholder="搜索编码、姓名(拼音)、电话"
|
clearable
|
||||||
|
placeholder="搜索编码、姓名(拼音)、电话"
|
||||||
|
/>
|
||||||
|
<t-button
|
||||||
|
v-if="props.enableScan"
|
||||||
|
class="scan-button"
|
||||||
|
theme="primary"
|
||||||
|
@click="toggleScanner"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<t-icon name="scan" />
|
||||||
|
</template>
|
||||||
|
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="scannerVisible" class="scanner-wrapper">
|
||||||
|
<code-scanner
|
||||||
|
:visible="scannerVisible"
|
||||||
|
:status-text="scanStatus"
|
||||||
|
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
|
||||||
|
@decode="handleDecodedText"
|
||||||
|
@status-change="handleScanStatusChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div ref="resultListRef" class="result-list">
|
<div ref="resultListRef" class="result-list">
|
||||||
@@ -34,12 +56,15 @@
|
|||||||
import { Scroller } from "timi-web";
|
import { Scroller } from "timi-web";
|
||||||
import { BottomPopup } from "timi-tdesign-mobile";
|
import { BottomPopup } from "timi-tdesign-mobile";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
|
import CodeScanner from "@/components/CodeScanner.vue";
|
||||||
import CustomerList from "@/components/CustomerList.vue";
|
import CustomerList from "@/components/CustomerList.vue";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
|
|
||||||
type SelectionMode = "single" | "multiple";
|
type SelectionMode = "single" | "multiple";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
enableScan?: boolean;
|
||||||
modelValue: string | string[];
|
modelValue: string | string[];
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
selectionMode?: SelectionMode;
|
selectionMode?: SelectionMode;
|
||||||
@@ -47,10 +72,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
enableScan: true,
|
||||||
selectionMode: "single",
|
selectionMode: "single",
|
||||||
storeId: ""
|
storeId: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [value: string | string[]];
|
"update:modelValue": [value: string | string[]];
|
||||||
"update:visible": [value: boolean];
|
"update:visible": [value: boolean];
|
||||||
@@ -70,6 +98,10 @@ const draftSelectedIdList = ref<string[]>([]);
|
|||||||
const selectedCustomerCache = new Map<string, Customer>();
|
const selectedCustomerCache = new Map<string, Customer>();
|
||||||
let searchToken = 0;
|
let searchToken = 0;
|
||||||
let keywordTimer: number | undefined;
|
let keywordTimer: number | undefined;
|
||||||
|
const scannerVisible = ref(false);
|
||||||
|
const scanStatus = ref("点击扫码开始识别");
|
||||||
|
let latestDecodedText = "";
|
||||||
|
let scanToken = 0;
|
||||||
|
|
||||||
const isMultiple = computed(() => props.selectionMode === "multiple");
|
const isMultiple = computed(() => props.selectionMode === "multiple");
|
||||||
|
|
||||||
@@ -159,8 +191,8 @@ function toggleCustomer(item: Customer): void {
|
|||||||
|
|
||||||
selectedCustomerCache.set(id, item);
|
selectedCustomerCache.set(id, item);
|
||||||
if (!isMultiple.value) {
|
if (!isMultiple.value) {
|
||||||
emit("update:modelValue", id);
|
emit("update:modelValue", id);
|
||||||
emit("change", id);
|
emit("change", id);
|
||||||
emit("update:visible", false);
|
emit("update:visible", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -173,6 +205,64 @@ function toggleCustomer(item: Customer): void {
|
|||||||
draftSelectedIdList.value = [...draftSelectedIdList.value, id];
|
draftSelectedIdList.value = [...draftSelectedIdList.value, id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleScanner(): void {
|
||||||
|
if (!props.enableScan) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scannerVisible.value = !scannerVisible.value;
|
||||||
|
if (scannerVisible.value) {
|
||||||
|
latestDecodedText = "";
|
||||||
|
scanStatus.value = "正在启动扫码预览";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scanToken++;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDecodedText(rawText: string): Promise<void> {
|
||||||
|
const normalizedText = rawText.trim();
|
||||||
|
if (!normalizedText || normalizedText === latestDecodedText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
latestDecodedText = normalizedText;
|
||||||
|
const token = ++scanToken;
|
||||||
|
await settingStore.playScanSuccessSound();
|
||||||
|
try {
|
||||||
|
const customer = await CustomerAPI.findByCode(normalizedText, props.storeId || undefined);
|
||||||
|
if (token !== scanToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!customer?.id) {
|
||||||
|
scanStatus.value = `客户 ${normalizedText} 不存在,请先建档。`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCustomerCache.set(customer.id, customer);
|
||||||
|
if (!customerOptions.value.some(item => item.id === customer.id)) {
|
||||||
|
customerOptions.value = [customer, ...customerOptions.value];
|
||||||
|
}
|
||||||
|
if (isMultiple.value) {
|
||||||
|
if (!draftSelectedIdList.value.includes(customer.id)) {
|
||||||
|
draftSelectedIdList.value = [...draftSelectedIdList.value, customer.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit("update:modelValue", customer.id);
|
||||||
|
emit("change", customer.id);
|
||||||
|
emit("update:visible", false);
|
||||||
|
}
|
||||||
|
scanStatus.value = `已识别 ${customer.code || normalizedText},已选中 ${customer.name || "未命名客户"}。`;
|
||||||
|
scannerVisible.value = false;
|
||||||
|
} catch {
|
||||||
|
if (token === scanToken) {
|
||||||
|
scanStatus.value = "客户查询失败,请重试。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScanStatusChange(value: string): void {
|
||||||
|
scanStatus.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
function cancelSelection(): void {
|
function cancelSelection(): void {
|
||||||
syncDraftSelection();
|
syncDraftSelection();
|
||||||
emit("update:visible", false);
|
emit("update:visible", false);
|
||||||
@@ -189,6 +279,10 @@ watch(() => props.visible, async (value) => {
|
|||||||
if (!value) {
|
if (!value) {
|
||||||
syncDraftSelection();
|
syncDraftSelection();
|
||||||
keyword.value = "";
|
keyword.value = "";
|
||||||
|
scannerVisible.value = false;
|
||||||
|
latestDecodedText = "";
|
||||||
|
scanToken++;
|
||||||
|
scanStatus.value = "点击扫码开始识别";
|
||||||
window.clearTimeout(keywordTimer);
|
window.clearTimeout(keywordTimer);
|
||||||
Scroller.removeListener(scrollListenerName);
|
Scroller.removeListener(scrollListenerName);
|
||||||
return;
|
return;
|
||||||
@@ -198,6 +292,14 @@ watch(() => props.visible, async (value) => {
|
|||||||
await bindScrollListener();
|
await bindScrollListener();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => props.enableScan, (value) => {
|
||||||
|
if (value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scannerVisible.value = false;
|
||||||
|
scanToken++;
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.storeId, async () => {
|
watch(() => props.storeId, async () => {
|
||||||
if (!props.visible) {
|
if (!props.visible) {
|
||||||
return;
|
return;
|
||||||
@@ -229,23 +331,39 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.customer-selector {
|
.customer-selector {
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.search-wrapper {
|
.search-wrapper {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: 1rem 0 .5rem 0;
|
padding: 1rem 0 .5rem;
|
||||||
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
box-shadow: 0 4px 4px rgba(0, 0, 0, .05);
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
padding: 0 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
--td-input-vertical-padding: 8px 14px;
|
--td-input-vertical-padding: 8px 14px;
|
||||||
|
|
||||||
flex: 0;
|
flex: 1;
|
||||||
margin: 0 1rem;
|
|
||||||
border: 1px solid var(--td-component-border);
|
border: 1px solid var(--td-component-border);
|
||||||
|
min-width: 0;
|
||||||
border-radius: .875rem;
|
border-radius: .875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scan-button {
|
||||||
|
--td-button-border-radius: .75rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanner-wrapper {
|
||||||
|
padding: .5rem 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-list {
|
.result-list {
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.stat.read" class="customer-data-charts-page">
|
<div v-can="permissionCodes.stat.read" class="customer-data-charts-page">
|
||||||
<div class="header bg-white">
|
<div class="header bg-white">
|
||||||
<div class="buttons">
|
<t-input
|
||||||
<t-button class="selector" size="small" theme="primary" variant="outline" @click="selectorVisible = true">
|
v-model.trim="customerCode"
|
||||||
<template #icon>
|
class="customer-search"
|
||||||
<t-icon name="user-search" />
|
borderless
|
||||||
</template>
|
clearable
|
||||||
<span>选择客户</span>
|
placeholder="输入客户编码或姓名"
|
||||||
</t-button>
|
@clear="applyCustomerSearch"
|
||||||
<t-button size="small" theme="primary" @click="toggleScanner">
|
@enter="applyCustomerSearch"
|
||||||
<template #icon>
|
>
|
||||||
<t-icon name="scan" />
|
<template #suffix-icon>
|
||||||
</template>
|
<t-button
|
||||||
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
size="extra-small"
|
||||||
</t-button>
|
theme="primary"
|
||||||
</div>
|
variant="outline"
|
||||||
<div v-if="scannerVisible" class="line scanner-wrapper">
|
content="选择客户"
|
||||||
<code-scanner
|
@click.stop="selectorVisible = true"
|
||||||
visible
|
/>
|
||||||
:status-text="scanStatus"
|
</template>
|
||||||
active-text="正在识别客户编码,请将二维码移入取景框"
|
</t-input>
|
||||||
@decode="handleDecodedText"
|
|
||||||
@status-change="handleScanStatusChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<customer-profile-card v-entrance-transition class="customer" :customer="selectedCustomer" />
|
<customer-profile-card v-entrance-transition class="customer" :customer="selectedCustomer" />
|
||||||
<div class="charts">
|
<div class="charts">
|
||||||
@@ -78,7 +74,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Time } from "timi-web";
|
import { Time } from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
|
||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
||||||
import CustomerChartSummary from "@/pages/customer/component/CustomerChartSummary.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 CustomerEventStructureChart from "@/pages/customer/component/CustomerEventStructureChart.vue";
|
||||||
import CustomerOrderCharts from "@/pages/customer/component/CustomerOrderCharts.vue";
|
import CustomerOrderCharts from "@/pages/customer/component/CustomerOrderCharts.vue";
|
||||||
import CustomerRecordTrendChart from "@/pages/customer/component/CustomerRecordTrendChart.vue";
|
import CustomerRecordTrendChart from "@/pages/customer/component/CustomerRecordTrendChart.vue";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
@@ -96,7 +90,6 @@ defineOptions({
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
// ---------- 路由 ----------
|
// ---------- 路由 ----------
|
||||||
|
|
||||||
@@ -105,10 +98,9 @@ const routeCustomerCode = computed(() => String(route.query.customerCode || ""))
|
|||||||
const customerCode = ref(routeCustomerCode.value);
|
const customerCode = ref(routeCustomerCode.value);
|
||||||
const selectedCustomerId = ref(routeCustomerId.value);
|
const selectedCustomerId = ref(routeCustomerId.value);
|
||||||
|
|
||||||
watch(routeCustomerCode, (value) => {
|
watch(routeCustomerCode, async (value) => {
|
||||||
if (value) {
|
customerCode.value = value;
|
||||||
customerCode.value = value;
|
await loadCustomer(value);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(routeCustomerId, async (value) => {
|
watch(routeCustomerId, async (value) => {
|
||||||
@@ -131,18 +123,14 @@ const selectorVisible = ref(false);
|
|||||||
const selectedCustomer = ref<Customer>();
|
const selectedCustomer = ref<Customer>();
|
||||||
const selectedEventIdList = ref<string[]>([]);
|
const selectedEventIdList = ref<string[]>([]);
|
||||||
|
|
||||||
watch(customerCode, async (value) => {
|
|
||||||
await loadCustomer(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadCustomer(code: string): Promise<void> {
|
async function loadCustomer(code: string): Promise<void> {
|
||||||
const normalizedCode = code.trim();
|
const keyword = code.trim();
|
||||||
if (!normalizedCode) {
|
if (!keyword) {
|
||||||
clearLoadedCustomer();
|
clearLoadedCustomer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const customer = await CustomerAPI.findByCode(normalizedCode);
|
const customer = await CustomerAPI.findByCode(keyword) || await CustomerAPI.findByName(keyword);
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
clearLoadedCustomer();
|
clearLoadedCustomer();
|
||||||
return;
|
return;
|
||||||
@@ -152,6 +140,10 @@ async function loadCustomer(code: string): Promise<void> {
|
|||||||
selectedCustomerId.value = customer.id || "";
|
selectedCustomerId.value = customer.id || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyCustomerSearch(): Promise<void> {
|
||||||
|
await loadCustomer(customerCode.value);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCustomerById(id: string): Promise<void> {
|
async function loadCustomerById(id: string): Promise<void> {
|
||||||
const normalizedId = id.trim();
|
const normalizedId = id.trim();
|
||||||
if (!normalizedId) {
|
if (!normalizedId) {
|
||||||
@@ -162,6 +154,7 @@ async function loadCustomerById(id: string): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
selectedCustomer.value = await CustomerAPI.detail(normalizedId);
|
selectedCustomer.value = await CustomerAPI.detail(normalizedId);
|
||||||
selectedCustomerId.value = selectedCustomer.value.id || "";
|
selectedCustomerId.value = selectedCustomer.value.id || "";
|
||||||
|
customerCode.value = selectedCustomer.value.code || selectedCustomer.value.name || "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearLoadedCustomer();
|
clearLoadedCustomer();
|
||||||
console.warn("客户详情加载失败", err);
|
console.warn("客户详情加载失败", err);
|
||||||
@@ -206,36 +199,6 @@ function getCurrentRouteDepth(): number {
|
|||||||
return Number(route.meta.depth ?? 0);
|
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()));
|
const monthCursor = ref(getMonthStart(new Date()));
|
||||||
@@ -300,18 +263,14 @@ function getDateEnd(date: Date): Date {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
box-shadow: var(--td-shadow-1);
|
box-shadow: var(--td-shadow-1);
|
||||||
|
|
||||||
.buttons {
|
.customer-search {
|
||||||
gap: 1rem;
|
--td-input-vertical-padding: .25rem 1rem;
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.selector {
|
width: 100%;
|
||||||
flex: 1;
|
border: 1px solid var(--td-component-border);
|
||||||
min-width: 0;
|
background: var(--td-bg-color-container);
|
||||||
}
|
padding-right: .3rem;
|
||||||
}
|
border-radius: 1rem;
|
||||||
|
|
||||||
.scanner-wrapper {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +288,7 @@ function getDateEnd(date: Date): Date {
|
|||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
.customer-data-charts-page {
|
.customer-data-charts-page {
|
||||||
.selector {
|
.customer-search {
|
||||||
background: #10161f;
|
background: #10161f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="savePermissionCode" class="customer-edit">
|
<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-form-item label="客户编码">
|
||||||
<t-input
|
<t-input
|
||||||
v-model.trim="formData.code"
|
v-model.trim="formData.code"
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
theme="primary"
|
theme="primary"
|
||||||
size="extra-small"
|
size="extra-small"
|
||||||
:disabled="false"
|
:disabled="false"
|
||||||
@click="toggleScanner('code')"
|
@click="toggleScanner"
|
||||||
>
|
>
|
||||||
<span v-text="scannerVisible && scannerTarget === 'code' ? '关闭扫码' : '扫码'" />
|
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
||||||
</t-button>
|
</t-button>
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</t-input>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<div v-if="scannerVisible && scannerTarget === 'code'" class="code-scanner-wrapper">
|
<div v-if="scannerVisible" class="code-scanner-wrapper">
|
||||||
<code-scanner
|
<code-scanner
|
||||||
visible
|
visible
|
||||||
:status-text="scanStatus"
|
:status-text="scanStatus"
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
<t-input
|
<t-input
|
||||||
:value="formData.introducerCustomer?.name"
|
:value="formData.introducerCustomer?.name"
|
||||||
readonly
|
readonly
|
||||||
placeholder="选择或扫码"
|
placeholder="选择介绍人"
|
||||||
borderless
|
borderless
|
||||||
:disabled="isCustomerBound"
|
:disabled="isCustomerBound"
|
||||||
/>
|
/>
|
||||||
@@ -113,22 +113,10 @@
|
|||||||
size="extra-small"
|
size="extra-small"
|
||||||
:disabled="isCustomerBound"
|
:disabled="isCustomerBound"
|
||||||
@click="selectorVisible = true"
|
@click="selectorVisible = true"
|
||||||
>选择</t-button>
|
>选择客户</t-button>
|
||||||
<t-button theme="primary" size="extra-small" :disabled="isCustomerBound" @click="toggleScanner('introducer')">
|
|
||||||
<span v-text="scannerVisible && scannerTarget === 'introducer' ? '关闭扫码' : '扫码'" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-form-item>
|
</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">
|
<t-form-item label="调理部位" :arrow="!isCustomerBound" @click="openBodyAreaPicker">
|
||||||
<div class="picker-text" v-text="selectedAreaText" />
|
<div class="picker-text" v-text="selectedAreaText" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
@@ -244,9 +232,7 @@ let latestDecodedText = "";
|
|||||||
|
|
||||||
// ---------- 扫码 ----------
|
// ---------- 扫码 ----------
|
||||||
|
|
||||||
type ScanTarget = "code" | "introducer";
|
|
||||||
const scannerVisible = ref(false);
|
const scannerVisible = ref(false);
|
||||||
const scannerTarget = ref<ScanTarget>("code");
|
|
||||||
const scanStatus = ref("点击扫码开始识别");
|
const scanStatus = ref("点击扫码开始识别");
|
||||||
const introducerCustomerId = ref("");
|
const introducerCustomerId = ref("");
|
||||||
const boundCodeCustomer = ref<Customer>();
|
const boundCodeCustomer = ref<Customer>();
|
||||||
@@ -259,15 +245,11 @@ const isCustomerBound = computed(() => {
|
|||||||
return !!boundCustomerId && boundCustomerId !== formData.id;
|
return !!boundCustomerId && boundCustomerId !== formData.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function toggleScanner(target: ScanTarget): Promise<void> {
|
async function toggleScanner(): Promise<void> {
|
||||||
if (isCustomerBound.value && target !== "code") {
|
if (scannerVisible.value) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (scannerVisible.value && scannerTarget.value === target) {
|
|
||||||
scannerVisible.value = false;
|
scannerVisible.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scannerTarget.value = target;
|
|
||||||
scannerVisible.value = true;
|
scannerVisible.value = true;
|
||||||
latestDecodedText = "";
|
latestDecodedText = "";
|
||||||
}
|
}
|
||||||
@@ -279,17 +261,10 @@ async function handleDecodedText(rawText: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
latestDecodedText = normalizedText;
|
latestDecodedText = normalizedText;
|
||||||
await settingStore.playScanSuccessSound();
|
await settingStore.playScanSuccessSound();
|
||||||
if (scannerTarget.value === "code") {
|
formData.code = normalizedText;
|
||||||
// 选择客户扫码
|
scannerVisible.value = false;
|
||||||
formData.code = normalizedText;
|
await checkBoundCustomerCode();
|
||||||
scannerVisible.value = false;
|
scanStatus.value = `已识别客户编码 ${normalizedText}`;
|
||||||
await checkBoundCustomerCode();
|
|
||||||
scanStatus.value = `已识别客户编码 ${normalizedText}`;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// 选择介绍人扫码
|
|
||||||
await applyIntroducer(normalizedText);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleScanStatusChange(value: string): void {
|
function handleScanStatusChange(value: string): void {
|
||||||
@@ -343,28 +318,6 @@ async function checkBoundCustomerCode(): Promise<void> {
|
|||||||
|
|
||||||
const selectorVisible = ref(false);
|
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> {
|
async function handleIntroducerChange(id: string): Promise<void> {
|
||||||
const customer = id ? await CustomerAPI.findById(id) : undefined;
|
const customer = id ? await CustomerAPI.findById(id) : undefined;
|
||||||
introducerCustomerId.value = customer?.id || "";
|
introducerCustomerId.value = customer?.id || "";
|
||||||
@@ -743,7 +696,7 @@ function buildAttachmentList(): Attachment[] {
|
|||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.customer-edit {
|
.customer-edit {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: calc(var(--app-nav-height) + 1rem) 0 4rem 0;
|
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@@ -795,7 +748,7 @@ function buildAttachmentList(): Attachment[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sex {
|
.sex {
|
||||||
gap: 1rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<customer-selector
|
<customer-selector
|
||||||
v-model="selectedInviteeId"
|
v-model="selectedInviteeId"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
|
:enable-scan="false"
|
||||||
@change="handleInviteeSelected"
|
@change="handleInviteeSelected"
|
||||||
/>
|
/>
|
||||||
<bottom-popup
|
<bottom-popup
|
||||||
|
|||||||
@@ -6,7 +6,21 @@
|
|||||||
class="search"
|
class="search"
|
||||||
placeholder="搜索编码、姓名(拼音)、电话"
|
placeholder="搜索编码、姓名(拼音)、电话"
|
||||||
borderless
|
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
|
<t-button
|
||||||
v-if="canReadTag"
|
v-if="canReadTag"
|
||||||
class="tag-filter"
|
class="tag-filter"
|
||||||
@@ -20,6 +34,15 @@
|
|||||||
<span v-text="tagFilterText" />
|
<span v-text="tagFilterText" />
|
||||||
</t-button>
|
</t-button>
|
||||||
</div>
|
</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">
|
<div v-if="canReadTag && selectedTagList.length" class="selected-tags bg-white">
|
||||||
<t-tag
|
<t-tag
|
||||||
v-for="item in selectedTagList"
|
v-for="item in selectedTagList"
|
||||||
@@ -89,10 +112,12 @@ import { Message } from "@/utils/message";
|
|||||||
import { Time, Toolkit, WebToolkit } from "timi-web";
|
import { Time, Toolkit, WebToolkit } from "timi-web";
|
||||||
import { type Page, type PageResult, useUserStore } from "timi-web-api";
|
import { type Page, type PageResult, useUserStore } from "timi-web-api";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
|
import CodeScanner from "@/components/CodeScanner.vue";
|
||||||
import CustomerRemoveDialog from "@/pages/customer/component/CustomerRemoveDialog.vue";
|
import CustomerRemoveDialog from "@/pages/customer/component/CustomerRemoveDialog.vue";
|
||||||
import TagSelector from "@/components/TagSelector.vue";
|
import TagSelector from "@/components/TagSelector.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { NavBarAction } from "@/types/NavBarAction";
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
import type { Tag } from "@/types/Tag";
|
import type { Tag } from "@/types/Tag";
|
||||||
@@ -105,6 +130,7 @@ defineOptions({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
|
const settingStore = useSettingStore();
|
||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const isRefreshing = ref(false);
|
const isRefreshing = ref(false);
|
||||||
const isExporting = 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 canDeleteCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.delete));
|
||||||
const canExportCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.export));
|
const canExportCustomer = computed(() => userStore.hasPermissions(permissionCodes.customer.export));
|
||||||
const canShowActionMenu = computed(() => canCreateCustomer.value || canExportCustomer.value);
|
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 actionList = computed<NavBarAction[]>(() => {
|
||||||
const list: NavBarAction[] = [];
|
const list: NavBarAction[] = [];
|
||||||
@@ -232,6 +294,7 @@ watch(keyword, async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
window.clearTimeout(keywordTimer);
|
window.clearTimeout(keywordTimer);
|
||||||
|
scannerVisible.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- 导航 ----------
|
// ---------- 导航 ----------
|
||||||
@@ -315,9 +378,11 @@ async function doExport(): Promise<void> {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
|
--td-input-vertical-padding: .25rem 1rem;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--td-component-border);
|
border: 1px solid var(--td-component-border);
|
||||||
padding: .5rem 1rem;
|
padding-right: .3rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,6 +393,10 @@ async function doExport(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scanner-wrapper {
|
||||||
|
padding: .5rem 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.selected-tags {
|
.selected-tags {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
v-model="selectedCustomerIdList"
|
v-model="selectedCustomerIdList"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
:selection-mode="isEditMode ? 'single' : 'multiple'"
|
:selection-mode="isEditMode ? 'single' : 'multiple'"
|
||||||
|
:enable-scan="false"
|
||||||
@change="handleCustomerChange"
|
@change="handleCustomerChange"
|
||||||
/>
|
/>
|
||||||
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
<t-popup v-model="selectedCustomerListVisible" placement="bottom">
|
||||||
|
|||||||
@@ -8,7 +8,17 @@
|
|||||||
type="search"
|
type="search"
|
||||||
borderless
|
borderless
|
||||||
placeholder="搜索客户、姓名拼音、编码、事件、备注、登记人"
|
placeholder="搜索客户、姓名拼音、编码、事件、备注、登记人"
|
||||||
/>
|
>
|
||||||
|
<template #suffix-icon>
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
content="选择客户"
|
||||||
|
@click.stop="customerSelectorVisible = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
<t-dropdown-menu class="date-filter">
|
<t-dropdown-menu class="date-filter">
|
||||||
<t-dropdown-item
|
<t-dropdown-item
|
||||||
v-model="dateFilterType"
|
v-model="dateFilterType"
|
||||||
@@ -93,6 +103,11 @@
|
|||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
|
<nav-bar-action-menu v-if="canShowActionMenu" :actions="actionList" />
|
||||||
|
<customer-selector
|
||||||
|
v-model="customerId"
|
||||||
|
v-model:visible="customerSelectorVisible"
|
||||||
|
@change="handleCustomerChange"
|
||||||
|
/>
|
||||||
<t-popup v-model="startVisible" placement="bottom">
|
<t-popup v-model="startVisible" placement="bottom">
|
||||||
<t-date-time-picker
|
<t-date-time-picker
|
||||||
v-if="startVisible"
|
v-if="startVisible"
|
||||||
@@ -125,6 +140,7 @@ import { Time, WebToolkit } from "timi-web";
|
|||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import EventAPI from "@/api/EventAPI";
|
import EventAPI from "@/api/EventAPI";
|
||||||
import EventRecordAPI from "@/api/EventRecordAPI";
|
import EventRecordAPI from "@/api/EventRecordAPI";
|
||||||
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import EventRecordCell from "@/pages/event/component/EventRecordCell.vue";
|
import EventRecordCell from "@/pages/event/component/EventRecordCell.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { EventItem, EventStatus } from "@/types/EventItem";
|
import type { EventItem, EventStatus } from "@/types/EventItem";
|
||||||
@@ -161,6 +177,7 @@ const dateOptions: Array<{ label: string; value: DateFilterType }> = [
|
|||||||
|
|
||||||
const keyword = ref(getInitialKeyword());
|
const keyword = ref(getInitialKeyword());
|
||||||
const customerId = ref(getQueryText("customerId"));
|
const customerId = ref(getQueryText("customerId"));
|
||||||
|
const customerSelectorVisible = ref(false);
|
||||||
const dateFilterType = ref<DateFilterType>(parseDateFilterType(route.query.dateType));
|
const dateFilterType = ref<DateFilterType>(parseDateFilterType(route.query.dateType));
|
||||||
const dateRange = ref(parseDateRange(dateFilterType.value));
|
const dateRange = ref(parseDateRange(dateFilterType.value));
|
||||||
const startDraft = ref("");
|
const startDraft = ref("");
|
||||||
@@ -270,6 +287,12 @@ const actionList = computed<NavBarAction[]>(() => {
|
|||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
|
customerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
||||||
|
await replaceFilterQuery();
|
||||||
|
await reloadRecords();
|
||||||
|
}
|
||||||
|
|
||||||
watch(keyword, () => {
|
watch(keyword, () => {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return;
|
return;
|
||||||
@@ -731,12 +754,14 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
|
--td-button-border-radius: .75rem;
|
||||||
--td-input-vertical-padding: .25rem 1rem;
|
--td-input-vertical-padding: .25rem 1rem;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: 1px solid rgba(148, 163, 184, .28);
|
border: 1px solid rgba(148, 163, 184, .28);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background: #f8fbff;
|
background: #f8fbff;
|
||||||
|
padding-right: .3rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,20 +794,6 @@ onUnmounted(() => {
|
|||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 0 0 1rem 1rem;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-empty-tip {
|
.option-empty-tip {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
padding: .75rem 1rem;
|
padding: .75rem 1rem;
|
||||||
|
|||||||
@@ -1,7 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.orderFulfillment.read" class="fulfillment-list">
|
<div v-can="permissionCodes.orderFulfillment.read" class="fulfillment-list">
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
|
<t-input
|
||||||
|
class="customer"
|
||||||
|
v-model.trim="customerSearchKeyword"
|
||||||
|
borderless
|
||||||
|
placeholder="输入客户编码或姓名"
|
||||||
|
@enter="applyCustomerSearch"
|
||||||
|
>
|
||||||
|
<template #suffix-icon>
|
||||||
|
<div class="actions">
|
||||||
|
<t-button
|
||||||
|
v-if="customerId"
|
||||||
|
shape="circle"
|
||||||
|
theme="danger"
|
||||||
|
variant="text"
|
||||||
|
size="extra-small"
|
||||||
|
aria-label="清除客户选择"
|
||||||
|
@click.stop="clearCustomer"
|
||||||
|
>
|
||||||
|
<t-icon name="close" size="1rem" />
|
||||||
|
</t-button>
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
content="选择客户"
|
||||||
|
@click.stop="customerSelectorVisible = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
<t-dropdown-menu class="menus">
|
<t-dropdown-menu class="menus">
|
||||||
|
<t-dropdown-item
|
||||||
|
v-model="dateFilterType"
|
||||||
|
:label="dateFilterLabel"
|
||||||
|
:options="dateOptions"
|
||||||
|
@change="selectDateFilter"
|
||||||
|
/>
|
||||||
<t-dropdown-item
|
<t-dropdown-item
|
||||||
v-model="status"
|
v-model="status"
|
||||||
label="履约状态"
|
label="履约状态"
|
||||||
@@ -11,38 +47,86 @@
|
|||||||
</t-dropdown-menu>
|
</t-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
<t-pull-down-refresh
|
<t-pull-down-refresh
|
||||||
class="list-wrap"
|
|
||||||
v-model="refreshing"
|
v-model="refreshing"
|
||||||
:refresh-timeout="30000"
|
:refresh-timeout="30000"
|
||||||
@refresh="refresh"
|
@refresh="refresh"
|
||||||
@scrolltolower="loadMore"
|
@scrolltolower="loadMore"
|
||||||
>
|
>
|
||||||
<div v-if="list.length" class="items">
|
<div class="list-wrap">
|
||||||
<order-fulfillment-list-cell
|
<t-cell-group v-if="isCustomDate" class="date-summary" theme="card">
|
||||||
v-for="(item, index) in list"
|
<t-cell
|
||||||
:key="item.id"
|
title="开始时间"
|
||||||
class="item"
|
:note="dateRange[0] || '-'"
|
||||||
:fulfillment="item"
|
arrow
|
||||||
v-entrance-transition="index * 30"
|
@click="openStartTime"
|
||||||
@detail="toDetail(item)"
|
/>
|
||||||
/>
|
<t-cell
|
||||||
|
title="结束时间"
|
||||||
|
:note="dateRange[1] || '-'"
|
||||||
|
arrow
|
||||||
|
@click="openEndTime"
|
||||||
|
/>
|
||||||
|
</t-cell-group>
|
||||||
|
<div v-if="list.length" class="items">
|
||||||
|
<order-fulfillment-list-cell
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
:key="item.id"
|
||||||
|
class="item"
|
||||||
|
:fulfillment="item"
|
||||||
|
v-entrance-transition="index * 30"
|
||||||
|
@detail="toDetail(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="center-tips" v-text="loading ? '正在加载履约单' : '暂无履约单'" />
|
||||||
|
<div class="center-tips" v-text="loading ? '正在加载' : noMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="center-tips" v-text="loading ? '正在加载履约单' : '暂无履约单'" />
|
|
||||||
<div class="center-tips" v-text="loading ? '正在加载' : noMore ? '已经到底了' : '上拉加载更多'" />
|
|
||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
<nav-bar-action-menu v-if="canCreate" :actions="actions" />
|
<nav-bar-action-menu v-if="canCreate" :actions="actions" />
|
||||||
|
<customer-selector
|
||||||
|
v-model="customerId"
|
||||||
|
v-model:visible="customerSelectorVisible"
|
||||||
|
@change="handleCustomerChange"
|
||||||
|
/>
|
||||||
|
<t-popup v-model="startVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-if="startVisible"
|
||||||
|
v-model="startDraft"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="开始时间"
|
||||||
|
:default-value="dateRange[0]"
|
||||||
|
@cancel="cancelStartTime"
|
||||||
|
@confirm="confirmStartTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
|
<t-popup v-model="endVisible" placement="bottom">
|
||||||
|
<t-date-time-picker
|
||||||
|
v-if="endVisible"
|
||||||
|
v-model="endDraft"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:mode="['date', 'minute']"
|
||||||
|
title="结束时间"
|
||||||
|
:default-value="dateRange[1]"
|
||||||
|
@cancel="cancelEndTime"
|
||||||
|
@confirm="confirmEndTime"
|
||||||
|
/>
|
||||||
|
</t-popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type DropdownValue } from "tdesign-mobile-vue";
|
import { type DropdownValue } from "tdesign-mobile-vue";
|
||||||
|
import { Time } from "timi-web";
|
||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import FulfillmentAPI from "@/api/FulfillmentAPI";
|
import FulfillmentAPI from "@/api/FulfillmentAPI";
|
||||||
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
||||||
import OrderFulfillmentListCell from "@/pages/fulfillment/components/OrderFulfillmentListCell.vue";
|
import OrderFulfillmentListCell from "@/pages/fulfillment/components/OrderFulfillmentListCell.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { GaoFulfillmentStatus, OrderFulfillment } from "@/types/Fulfillment";
|
import type { GaoFulfillmentStatus, OrderFulfillment } from "@/types/Fulfillment";
|
||||||
import type { NavBarAction } from "@/types/NavBarAction";
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
|
import type { Customer } from "@/types/Customer";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
@@ -70,6 +154,27 @@ const actions = computed<NavBarAction[]>(() => [
|
|||||||
action: toPackageOrderList
|
action: toPackageOrderList
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
type DateFilterType = "today" | "yesterday" | "lastWeek" | "thisMonth" | "lastMonth" | "all" | "custom";
|
||||||
|
const dateOptions: Array<{ label: string; value: DateFilterType }> = [
|
||||||
|
{ label: "今天", value: "today" },
|
||||||
|
{ label: "昨天", value: "yesterday" },
|
||||||
|
{ label: "上周", value: "lastWeek" },
|
||||||
|
{ label: "本月", value: "thisMonth" },
|
||||||
|
{ label: "上个月", value: "lastMonth" },
|
||||||
|
{ label: "全部", value: "all" },
|
||||||
|
{ label: "自定义范围", value: "custom" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const customerId = ref(getQueryText(route.query.customerId));
|
||||||
|
const selectedCustomer = ref<Customer>();
|
||||||
|
const customerSelectorVisible = ref(false);
|
||||||
|
const customerSearchKeyword = ref("");
|
||||||
|
const dateFilterType = ref<DateFilterType>(parseDateFilterType(route.query.dateType));
|
||||||
|
const dateRange = ref<[string, string]>(parseDateRange(dateFilterType.value));
|
||||||
|
const startDraft = ref("");
|
||||||
|
const endDraft = ref("");
|
||||||
|
const startVisible = ref(false);
|
||||||
|
const endVisible = ref(false);
|
||||||
const status = ref<"ALL" | GaoFulfillmentStatus>(getInitialStatus(route.query.status));
|
const status = ref<"ALL" | GaoFulfillmentStatus>(getInitialStatus(route.query.status));
|
||||||
const list = ref<OrderFulfillment[]>([]);
|
const list = ref<OrderFulfillment[]>([]);
|
||||||
const pageIndex = ref(0);
|
const pageIndex = ref(0);
|
||||||
@@ -87,6 +192,8 @@ const statusOptions = [
|
|||||||
{ label: "已撤销", value: "CANCELLED" },
|
{ label: "已撤销", value: "CANCELLED" },
|
||||||
{ label: "已冲正", value: "REVOKED" }
|
{ label: "已冲正", value: "REVOKED" }
|
||||||
];
|
];
|
||||||
|
const dateFilterLabel = computed(() => dateOptions.find(item => item.value === dateFilterType.value)?.label || "时间范围");
|
||||||
|
const isCustomDate = computed(() => dateFilterType.value === "custom");
|
||||||
|
|
||||||
function getQueryText(value: unknown): string {
|
function getQueryText(value: unknown): string {
|
||||||
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
|
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
|
||||||
@@ -100,6 +207,18 @@ function getInitialStatus(value: unknown): "ALL" | GaoFulfillmentStatus {
|
|||||||
return "ALL";
|
return "ALL";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDateFilterType(value: unknown): DateFilterType {
|
||||||
|
const dateFilter = getQueryText(value);
|
||||||
|
return dateOptions.some(item => item.value === dateFilter) ? dateFilter as DateFilterType : "today";
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateRange(type: DateFilterType): [string, string] {
|
||||||
|
if (type !== "custom") {
|
||||||
|
return buildPresetRange(type);
|
||||||
|
}
|
||||||
|
return [getQueryText(route.query.beginAt), getQueryText(route.query.endAt)];
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchList(reset: boolean): Promise<void> {
|
async function fetchList(reset: boolean): Promise<void> {
|
||||||
if (loading.value || (!reset && noMore.value)) {
|
if (loading.value || (!reset && noMore.value)) {
|
||||||
return;
|
return;
|
||||||
@@ -112,10 +231,14 @@ async function fetchList(reset: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
|
const [beginAt, endAt] = buildUnixRange();
|
||||||
const result = await FulfillmentAPI.list({
|
const result = await FulfillmentAPI.list({
|
||||||
index: pageIndex.value,
|
index: pageIndex.value,
|
||||||
size: pageSize,
|
size: pageSize,
|
||||||
salesOrderId: getQueryText(route.query.salesOrderId) || undefined,
|
salesOrderId: getQueryText(route.query.salesOrderId) || undefined,
|
||||||
|
customerId: customerId.value || undefined,
|
||||||
|
beginAt,
|
||||||
|
endAt,
|
||||||
status: status.value === "ALL" ? undefined : status.value
|
status: status.value === "ALL" ? undefined : status.value
|
||||||
});
|
});
|
||||||
const rows = result.list || [];
|
const rows = result.list || [];
|
||||||
@@ -128,6 +251,14 @@ async function fetchList(reset: boolean): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reloadList(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fetchList(true);
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "履约单加载失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refresh(): Promise<void> {
|
async function refresh(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await fetchList(true);
|
await fetchList(true);
|
||||||
@@ -149,10 +280,198 @@ async function loadMore(): Promise<void> {
|
|||||||
async function selectStatus(value: DropdownValue): Promise<void> {
|
async function selectStatus(value: DropdownValue): Promise<void> {
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
status.value = String(value) as "ALL" | GaoFulfillmentStatus;
|
status.value = String(value) as "ALL" | GaoFulfillmentStatus;
|
||||||
await fetchList(true);
|
await reloadList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
|
customerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
||||||
|
try {
|
||||||
|
selectedCustomer.value = customerId.value ? await CustomerAPI.detail(customerId.value) : undefined;
|
||||||
|
customerSearchKeyword.value = getCustomerSearchKeyword(selectedCustomer.value);
|
||||||
|
await fetchList(true);
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "客户履约单加载失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyCustomerSearch(): Promise<void> {
|
||||||
|
const keyword = customerSearchKeyword.value.trim();
|
||||||
|
if (!keyword) {
|
||||||
|
await clearCustomer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keyword === getCustomerSearchKeyword(selectedCustomer.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const customer = await CustomerAPI.findByCode(keyword) || await CustomerAPI.findByName(keyword);
|
||||||
|
if (!customer?.id) {
|
||||||
|
Message.warning("未找到客户");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customerId.value = customer.id;
|
||||||
|
selectedCustomer.value = customer;
|
||||||
|
customerSearchKeyword.value = getCustomerSearchKeyword(customer);
|
||||||
|
await fetchList(true);
|
||||||
|
} catch (error) {
|
||||||
|
Message.error(getErrorMessage(error, "客户信息加载失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCustomerSearchKeyword(customer?: Customer): string {
|
||||||
|
return customer?.name?.trim() || customer?.code?.trim() || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearCustomer(): Promise<void> {
|
||||||
|
customerId.value = "";
|
||||||
|
selectedCustomer.value = undefined;
|
||||||
|
customerSearchKeyword.value = "";
|
||||||
|
await reloadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectDateFilter(value: DropdownValue): Promise<void> {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dateFilterType.value = String(value) as DateFilterType;
|
||||||
|
if (dateFilterType.value !== "custom") {
|
||||||
|
dateRange.value = buildPresetRange(dateFilterType.value);
|
||||||
|
await replaceFilterQuery();
|
||||||
|
await reloadList();
|
||||||
|
} else {
|
||||||
|
await replaceFilterQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStartTime(): void {
|
||||||
|
startDraft.value = dateRange.value[0];
|
||||||
|
startVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEndTime(): void {
|
||||||
|
endDraft.value = dateRange.value[1];
|
||||||
|
endVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelStartTime(): void {
|
||||||
|
startVisible.value = false;
|
||||||
|
startDraft.value = dateRange.value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEndTime(): void {
|
||||||
|
endVisible.value = false;
|
||||||
|
endDraft.value = dateRange.value[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmStartTime(value?: string | number): Promise<void> {
|
||||||
|
startVisible.value = false;
|
||||||
|
dateRange.value = [String(value || startDraft.value), dateRange.value[1]];
|
||||||
|
await confirmCustomRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmEndTime(value?: string | number): Promise<void> {
|
||||||
|
endVisible.value = false;
|
||||||
|
dateRange.value = [dateRange.value[0], String(value || endDraft.value)];
|
||||||
|
await confirmCustomRange();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmCustomRange(): Promise<void> {
|
||||||
|
if (dateRange.value[0] && dateRange.value[1] && dateRange.value[1] < dateRange.value[0]) {
|
||||||
|
dateRange.value = [dateRange.value[1], dateRange.value[0]];
|
||||||
|
}
|
||||||
|
dateFilterType.value = "custom";
|
||||||
|
await replaceFilterQuery();
|
||||||
|
await reloadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilterQuery(transitionDepth = getQueryText(route.query.transitionDepth)): Record<string, string> {
|
||||||
|
const query = {
|
||||||
|
salesOrderId: getQueryText(route.query.salesOrderId),
|
||||||
|
customerId: customerId.value,
|
||||||
|
dateType: dateFilterType.value,
|
||||||
|
beginAt: dateRange.value[0] || "",
|
||||||
|
endAt: dateRange.value[1] || "",
|
||||||
|
status: status.value === "ALL" ? "" : status.value
|
||||||
|
};
|
||||||
|
if (isValidTransitionDepth(transitionDepth)) {
|
||||||
|
return { ...query, transitionDepth };
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function replaceFilterQuery(): Promise<void> {
|
||||||
|
await router.replace({
|
||||||
|
name: "OrderFulfillmentList",
|
||||||
|
query: buildFilterQuery()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidTransitionDepth(value: string): boolean {
|
||||||
|
const depth = Number(value);
|
||||||
|
return value !== "" && Number.isInteger(depth) && 0 <= depth && depth <= 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPresetRange(type: Exclude<DateFilterType, "custom">): [string, string] {
|
||||||
|
if (type === "all") {
|
||||||
|
return ["", ""];
|
||||||
|
}
|
||||||
|
if (type === "lastWeek") {
|
||||||
|
const current = new Date();
|
||||||
|
current.setHours(0, 0, 0, 0);
|
||||||
|
const day = current.getDay() || 7;
|
||||||
|
const thisWeekMonday = new Date(current);
|
||||||
|
thisWeekMonday.setDate(current.getDate() - day + 1);
|
||||||
|
const lastWeekMonday = new Date(thisWeekMonday);
|
||||||
|
lastWeekMonday.setDate(thisWeekMonday.getDate() - 7);
|
||||||
|
const lastWeekSunday = new Date(thisWeekMonday);
|
||||||
|
lastWeekSunday.setDate(thisWeekMonday.getDate() - 1);
|
||||||
|
return [`${Time.formatDate(lastWeekMonday)} 00:00`, `${Time.formatDate(lastWeekSunday)} 23:59`];
|
||||||
|
}
|
||||||
|
if (type === "thisMonth") {
|
||||||
|
const current = new Date();
|
||||||
|
const begin = new Date(current.getFullYear(), current.getMonth(), 1);
|
||||||
|
return [`${Time.formatDate(begin)} 00:00`, `${Time.formatDate(current)} 23:59`];
|
||||||
|
}
|
||||||
|
if (type === "lastMonth") {
|
||||||
|
const current = new Date();
|
||||||
|
const begin = new Date(current.getFullYear(), current.getMonth() - 1, 1);
|
||||||
|
const end = new Date(current.getFullYear(), current.getMonth(), 0);
|
||||||
|
return [`${Time.formatDate(begin)} 00:00`, `${Time.formatDate(end)} 23:59`];
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetMap: Record<Exclude<DateFilterType, "all" | "custom" | "lastWeek" | "thisMonth" | "lastMonth">, number> = {
|
||||||
|
today: 0,
|
||||||
|
yesterday: 1
|
||||||
|
};
|
||||||
|
const date = new Date();
|
||||||
|
date.setHours(0, 0, 0, 0);
|
||||||
|
date.setDate(date.getDate() - offsetMap[type]);
|
||||||
|
const value = `${Time.formatDate(date)} 00:00`;
|
||||||
|
return [value, `${Time.formatDate(date)} 23:59`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUnixRange(): [number | undefined, number | undefined] {
|
||||||
|
if (dateFilterType.value === "all") {
|
||||||
|
return [0, Time.toTimestamp(new Date())];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
Time.toTimestamp(parseDateTime(dateRange.value[0], false)),
|
||||||
|
Time.toTimestamp(parseDateTime(dateRange.value[1], true))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateTime(value: string, isEnd: boolean): Date {
|
||||||
|
if (!value) {
|
||||||
|
return isEnd ? new Date() : new Date(0);
|
||||||
|
}
|
||||||
|
const [dateText, timeText = isEnd ? "23:59" : "00:00"] = value.split(" ");
|
||||||
|
const [year, month, day] = dateText.split("-").map(Number);
|
||||||
|
const [hour, minute] = timeText.split(":").map(Number);
|
||||||
|
return new Date(year, month - 1, day, hour || 0, minute || 0, isEnd ? 59 : 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
async function toDetail(item: OrderFulfillment): Promise<void> {
|
async function toDetail(item: OrderFulfillment): Promise<void> {
|
||||||
if (item.id) {
|
if (item.id) {
|
||||||
await router.push({
|
await router.push({
|
||||||
@@ -194,6 +513,10 @@ onMounted(async () => {
|
|||||||
navBarStore.clearRight();
|
navBarStore.clearRight();
|
||||||
navBarStore.setCustomRightVisible(canCreate.value);
|
navBarStore.setCustomRightVisible(canCreate.value);
|
||||||
try {
|
try {
|
||||||
|
if (customerId.value) {
|
||||||
|
selectedCustomer.value = await CustomerAPI.detail(customerId.value);
|
||||||
|
customerSearchKeyword.value = getCustomerSearchKeyword(selectedCustomer.value);
|
||||||
|
}
|
||||||
await fetchList(true);
|
await fetchList(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Message.error(getErrorMessage(error, "履约单加载失败"));
|
Message.error(getErrorMessage(error, "履约单加载失败"));
|
||||||
@@ -215,38 +538,65 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding: .75rem 1rem;
|
padding: .75rem 1rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
background: var(--td-bg-color-page);
|
background: #F5F5F8;
|
||||||
box-shadow: var(--td-shadow-1);
|
box-shadow: var(--td-shadow-1);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.customer {
|
||||||
|
--td-input-vertical-padding: .25rem 1rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
background: var(--td-bg-color-container);
|
||||||
|
padding-right: .3rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
--td-button-border-radius: .75rem;
|
||||||
|
|
||||||
|
gap: .25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.menus {
|
.menus {
|
||||||
--td-spacer-1: 6px;
|
--td-spacer-1: 6px;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid rgba(148, 163, 184, .18);
|
border: 1px solid rgba(148, 163, 184, .18);
|
||||||
|
z-index: -1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--td-bg-color-container);
|
margin-top: -1rem;
|
||||||
|
background: #fff;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 1rem;
|
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 {
|
.list-wrap {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.date-summary {
|
||||||
|
|
||||||
|
:deep(.t-cell__title) {
|
||||||
|
flex: 0 0 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__note) {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: right;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -180,6 +180,7 @@
|
|||||||
v-model="selectedCustomerId"
|
v-model="selectedCustomerId"
|
||||||
v-model:visible="selectorVisible"
|
v-model:visible="selectorVisible"
|
||||||
selection-mode="single"
|
selection-mode="single"
|
||||||
|
:enable-scan="false"
|
||||||
@change="handleCustomerChange"
|
@change="handleCustomerChange"
|
||||||
/>
|
/>
|
||||||
<t-popup v-model="configSelectorVisible" placement="bottom">
|
<t-popup v-model="configSelectorVisible" placement="bottom">
|
||||||
|
|||||||
@@ -6,7 +6,17 @@
|
|||||||
class="search"
|
class="search"
|
||||||
placeholder="搜索客户编码、姓名、拼音或电话"
|
placeholder="搜索客户编码、姓名、拼音或电话"
|
||||||
borderless
|
borderless
|
||||||
/>
|
>
|
||||||
|
<template #suffix-icon>
|
||||||
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
content="选择客户"
|
||||||
|
@click.stop="customerSelectorVisible = true"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
</div>
|
</div>
|
||||||
<t-pull-down-refresh
|
<t-pull-down-refresh
|
||||||
class="list-wrapper"
|
class="list-wrapper"
|
||||||
@@ -37,12 +47,19 @@
|
|||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div class="center-tips" v-text="loadStateText" />
|
<div class="center-tips" v-text="loadStateText" />
|
||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
|
<customer-selector
|
||||||
|
v-model="selectedCustomerId"
|
||||||
|
v-model:visible="customerSelectorVisible"
|
||||||
|
@change="handleCustomerChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type PageResult, useUserStore } from "timi-web-api";
|
import { type PageResult, useUserStore } from "timi-web-api";
|
||||||
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import PointAPI from "@/api/PointAPI";
|
import PointAPI from "@/api/PointAPI";
|
||||||
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import type { PointAccount, PointAccountPage } from "@/types/Point";
|
import type { PointAccount, PointAccountPage } from "@/types/Point";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
@@ -71,8 +88,31 @@ async function prepareStoreScope(): Promise<void> {
|
|||||||
// ---------- 筛选 ----------
|
// ---------- 筛选 ----------
|
||||||
|
|
||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
|
const selectedCustomerId = ref("");
|
||||||
|
const customerSelectorVisible = ref(false);
|
||||||
|
let selectedCustomerKeyword = "";
|
||||||
let keywordTimer: number | undefined;
|
let keywordTimer: number | undefined;
|
||||||
|
|
||||||
|
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
||||||
|
selectedCustomerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
||||||
|
if (!selectedCustomerId.value) {
|
||||||
|
selectedCustomerKeyword = "";
|
||||||
|
keyword.value = "";
|
||||||
|
await reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const customer = await CustomerAPI.detail(selectedCustomerId.value);
|
||||||
|
selectedCustomerKeyword = customer.name?.trim() || customer.code?.trim() || "";
|
||||||
|
keyword.value = selectedCustomerKeyword;
|
||||||
|
await reload();
|
||||||
|
} catch {
|
||||||
|
selectedCustomerKeyword = "";
|
||||||
|
Message.error("客户信息加载失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 分页 ----------
|
// ---------- 分页 ----------
|
||||||
|
|
||||||
const page = reactive<PointAccountPage>({
|
const page = reactive<PointAccountPage>({
|
||||||
@@ -179,6 +219,11 @@ async function toDetail(item: PointAccount): Promise<void> {
|
|||||||
|
|
||||||
watch(keyword, () => {
|
watch(keyword, () => {
|
||||||
window.clearTimeout(keywordTimer);
|
window.clearTimeout(keywordTimer);
|
||||||
|
if (selectedCustomerKeyword && keyword.value.trim() === selectedCustomerKeyword) {
|
||||||
|
selectedCustomerKeyword = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedCustomerKeyword = "";
|
||||||
keywordTimer = window.setTimeout(async () => {
|
keywordTimer = window.setTimeout(async () => {
|
||||||
await reload();
|
await reload();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<t-notice-bar
|
<t-notice-bar
|
||||||
theme="warning"
|
theme="warning"
|
||||||
visible
|
visible
|
||||||
content="人工调整会追加一条不可修改的 ADJUST 流水,请填写真实、明确的调整原因。"
|
content="人工调整会追加一条不可修改的积分流水,请填写真实、明确的调整原因。"
|
||||||
/>
|
/>
|
||||||
<t-form class="form" label-align="left" label-width="7rem" v-entrance-transition>
|
<t-form class="form" label-align="left" label-width="7rem" v-entrance-transition>
|
||||||
<t-form-item label="客户" required-mark>
|
<t-form-item label="客户" required-mark>
|
||||||
@@ -13,24 +13,9 @@
|
|||||||
size="extra-small"
|
size="extra-small"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@click="selectorVisible = true"
|
@click="selectorVisible = true"
|
||||||
>选择</t-button>
|
>选择客户</t-button>
|
||||||
<t-button theme="primary" size="extra-small" variant="outline" @click="toggleScanner">
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="scan" />
|
|
||||||
</template>
|
|
||||||
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<div v-if="scannerVisible" class="scanner-wrapper">
|
|
||||||
<code-scanner
|
|
||||||
visible
|
|
||||||
:status-text="scanStatus"
|
|
||||||
active-text="正在识别客户编码,请将二维码移入取景框"
|
|
||||||
@decode="handleDecodedText"
|
|
||||||
@status-change="handleScanStatusChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<section v-if="selectedCustomer" class="selected-customer">
|
<section v-if="selectedCustomer" class="selected-customer">
|
||||||
<customer-profile-card :customer="selectedCustomer" />
|
<customer-profile-card :customer="selectedCustomer" />
|
||||||
<div class="balance" v-text="balanceText" />
|
<div class="balance" v-text="balanceText" />
|
||||||
@@ -92,11 +77,9 @@ import { useUserStore } from "timi-web-api";
|
|||||||
import type { PointAction } from "@/types/Point";
|
import type { PointAction } from "@/types/Point";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import PointAPI from "@/api/PointAPI";
|
import PointAPI from "@/api/PointAPI";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
|
||||||
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { PointAccount } from "@/types/Point";
|
import type { PointAccount } from "@/types/Point";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
@@ -110,7 +93,6 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
// ---------- 权限与门店 ----------
|
// ---------- 权限与门店 ----------
|
||||||
|
|
||||||
@@ -138,28 +120,6 @@ async function handleCustomerChange(value: string | string[]): Promise<void> {
|
|||||||
await loadCustomerAccount();
|
await loadCustomerAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCustomerByCode(code: string): Promise<void> {
|
|
||||||
const normalizedCode = code.trim();
|
|
||||||
if (!normalizedCode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const matched = await CustomerAPI.findByCode(normalizedCode);
|
|
||||||
if (!matched?.id) {
|
|
||||||
Message.warning("未找到客户");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedCustomerId.value = matched.id;
|
|
||||||
selectedCustomer.value = matched;
|
|
||||||
await loadCustomerAccount();
|
|
||||||
} catch {
|
|
||||||
selectedCustomer.value = undefined;
|
|
||||||
account.value = undefined;
|
|
||||||
Message.error("客户信息加载失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCustomerAccount(): Promise<void> {
|
async function loadCustomerAccount(): Promise<void> {
|
||||||
if (!selectedCustomerId.value) {
|
if (!selectedCustomerId.value) {
|
||||||
selectedCustomer.value = undefined;
|
selectedCustomer.value = undefined;
|
||||||
@@ -183,36 +143,6 @@ async function loadCustomerAccount(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
|
||||||
|
|
||||||
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();
|
|
||||||
scanStatus.value = `已识别 ${normalizedText},正在加载客户档案`;
|
|
||||||
scannerVisible.value = false;
|
|
||||||
await loadCustomerByCode(normalizedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleScanStatusChange(value: string): void {
|
|
||||||
scanStatus.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 调整表单 ----------
|
// ---------- 调整表单 ----------
|
||||||
|
|
||||||
const pointDeltaText = ref("");
|
const pointDeltaText = ref("");
|
||||||
@@ -322,10 +252,6 @@ onMounted(async () => {
|
|||||||
justify-content: end;
|
justify-content: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scanner-wrapper {
|
|
||||||
margin: 1rem .75rem .75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-customer {
|
.selected-customer {
|
||||||
margin: 1rem .75rem .5rem;
|
margin: 1rem .75rem .5rem;
|
||||||
|
|
||||||
|
|||||||
@@ -2,33 +2,25 @@
|
|||||||
<div v-can="permissionCodes.point.ledgerRead" class="point-ledger-list">
|
<div v-can="permissionCodes.point.ledgerRead" class="point-ledger-list">
|
||||||
<div class="header bg-white">
|
<div class="header bg-white">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<t-button
|
<t-input
|
||||||
class="selector"
|
v-model.trim="customerKeyword"
|
||||||
size="small"
|
class="search"
|
||||||
theme="primary"
|
borderless
|
||||||
variant="outline"
|
clearable
|
||||||
@click="selectorVisible = true"
|
placeholder="输入客户编码或姓名"
|
||||||
|
@clear="applyCustomerSearch"
|
||||||
|
@enter="applyCustomerSearch"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #suffix-icon>
|
||||||
<t-icon name="user-search" />
|
<t-button
|
||||||
|
size="extra-small"
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
content="选择客户"
|
||||||
|
@click.stop="selectorVisible = true"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<span v-text="customerFilterText" />
|
</t-input>
|
||||||
</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>
|
</div>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<t-dropdown-menu>
|
<t-dropdown-menu>
|
||||||
@@ -160,10 +152,8 @@ import { Time } from "timi-web";
|
|||||||
import { type PageResult, useUserStore } from "timi-web-api";
|
import { type PageResult, useUserStore } from "timi-web-api";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import PointAPI from "@/api/PointAPI";
|
import PointAPI from "@/api/PointAPI";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
|
||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { PointLedger, PointLedgerChangeType, PointLedgerPage } from "@/types/Point";
|
import type { PointLedger, PointLedgerChangeType, PointLedgerPage } from "@/types/Point";
|
||||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||||
@@ -180,7 +170,6 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
// ---------- 筛选 ----------
|
// ---------- 筛选 ----------
|
||||||
|
|
||||||
@@ -188,6 +177,7 @@ const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalMan
|
|||||||
const selectedCustomerId = ref(String(route.query.customerId || ""));
|
const selectedCustomerId = ref(String(route.query.customerId || ""));
|
||||||
const selectedAccountId = ref(String(route.query.accountId || ""));
|
const selectedAccountId = ref(String(route.query.accountId || ""));
|
||||||
const customer = ref<Customer>();
|
const customer = ref<Customer>();
|
||||||
|
const customerKeyword = ref("");
|
||||||
const selectorVisible = ref(false);
|
const selectorVisible = ref(false);
|
||||||
const changeTypeFilter = ref("ALL");
|
const changeTypeFilter = ref("ALL");
|
||||||
const dateFilter = ref<DateFilter>("last30");
|
const dateFilter = ref<DateFilter>("last30");
|
||||||
@@ -208,7 +198,6 @@ const dateFilterOptions = [
|
|||||||
{ label: "近 30 天", value: "last30" },
|
{ label: "近 30 天", value: "last30" },
|
||||||
{ label: "自定义", value: "custom" }
|
{ label: "自定义", value: "custom" }
|
||||||
];
|
];
|
||||||
const customerFilterText = computed(() => customer.value?.name || customer.value?.code || "全部客户");
|
|
||||||
const dateFilterLabel = computed(() => dateFilterOptions.find(item => item.value === dateFilter.value)?.label || "时间范围");
|
const dateFilterLabel = computed(() => dateFilterOptions.find(item => item.value === dateFilter.value)?.label || "时间范围");
|
||||||
const isCustomDate = computed(() => dateFilter.value === "custom");
|
const isCustomDate = computed(() => dateFilter.value === "custom");
|
||||||
|
|
||||||
@@ -216,32 +205,46 @@ async function handleCustomerChange(value: string | string[]): Promise<void> {
|
|||||||
selectedCustomerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
selectedCustomerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
||||||
if (selectedCustomerId.value) {
|
if (selectedCustomerId.value) {
|
||||||
customer.value = await CustomerAPI.detail(selectedCustomerId.value);
|
customer.value = await CustomerAPI.detail(selectedCustomerId.value);
|
||||||
|
customerKeyword.value = getCustomerSearchKeyword(customer.value);
|
||||||
} else {
|
} else {
|
||||||
customer.value = undefined;
|
customer.value = undefined;
|
||||||
|
customerKeyword.value = "";
|
||||||
}
|
}
|
||||||
await reload();
|
await reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCustomerByCode(code: string): Promise<void> {
|
async function applyCustomerSearch(): Promise<void> {
|
||||||
const normalizedCode = code.trim();
|
const keyword = customerKeyword.value.trim();
|
||||||
if (!normalizedCode) {
|
if (!keyword) {
|
||||||
|
selectedCustomerId.value = "";
|
||||||
|
customer.value = undefined;
|
||||||
|
await reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (keyword === getCustomerSearchKeyword(customer.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const matched = await CustomerAPI.findByCode(normalizedCode);
|
const matched = await CustomerAPI.findByCode(keyword, currentStoreStore.currentStoreId || undefined)
|
||||||
|
|| await CustomerAPI.findByName(keyword);
|
||||||
if (!matched?.id) {
|
if (!matched?.id) {
|
||||||
Message.warning("未找到客户");
|
Message.warning("未找到客户");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedCustomerId.value = matched.id;
|
selectedCustomerId.value = matched.id;
|
||||||
customer.value = matched;
|
customer.value = matched;
|
||||||
|
customerKeyword.value = getCustomerSearchKeyword(matched);
|
||||||
await reload();
|
await reload();
|
||||||
} catch {
|
} catch {
|
||||||
Message.error("客户信息加载失败");
|
Message.error("客户信息加载失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCustomerSearchKeyword(value?: Customer): string {
|
||||||
|
return value?.name?.trim() || value?.code?.trim() || "";
|
||||||
|
}
|
||||||
|
|
||||||
async function selectChangeType(value: DropdownValue): Promise<void> {
|
async function selectChangeType(value: DropdownValue): Promise<void> {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return;
|
return;
|
||||||
@@ -270,36 +273,6 @@ async function confirmDateRange(): Promise<void> {
|
|||||||
await reload();
|
await reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
|
||||||
|
|
||||||
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();
|
|
||||||
scanStatus.value = `已识别 ${normalizedText},正在加载积分流水`;
|
|
||||||
scannerVisible.value = false;
|
|
||||||
await loadCustomerByCode(normalizedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleScanStatusChange(value: string): void {
|
|
||||||
scanStatus.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 分页 ----------
|
// ---------- 分页 ----------
|
||||||
|
|
||||||
const page = reactive<PointLedgerPage>({
|
const page = reactive<PointLedgerPage>({
|
||||||
@@ -490,6 +463,7 @@ async function preparePage(): Promise<void> {
|
|||||||
}
|
}
|
||||||
if (selectedCustomerId.value) {
|
if (selectedCustomerId.value) {
|
||||||
customer.value = await CustomerAPI.detail(selectedCustomerId.value);
|
customer.value = await CustomerAPI.detail(selectedCustomerId.value);
|
||||||
|
customerKeyword.value = getCustomerSearchKeyword(customer.value);
|
||||||
}
|
}
|
||||||
await reload();
|
await reload();
|
||||||
}
|
}
|
||||||
@@ -518,12 +492,15 @@ onMounted(async () => {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
gap: 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.selector {
|
.search {
|
||||||
flex: 1;
|
--td-input-vertical-padding: .25rem 1rem;
|
||||||
min-width: 0;
|
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
padding-right: .3rem;
|
||||||
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,10 +508,6 @@ onMounted(async () => {
|
|||||||
margin: .5rem -1rem 0;
|
margin: .5rem -1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scanner-wrapper {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.t-dropdown-menu) {
|
:deep(.t-dropdown-menu) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
@@ -610,7 +583,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
.point-ledger-list {
|
.point-ledger-list {
|
||||||
.selector {
|
.search {
|
||||||
background: #10161f;
|
background: #10161f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
@click="customerSelectorVisible = true"
|
@click="customerSelectorVisible = true"
|
||||||
>
|
>
|
||||||
<template #suffix-icon>
|
<template #suffix-icon>
|
||||||
<div class="filter-actions">
|
<div class="actions">
|
||||||
<t-button
|
<t-button
|
||||||
v-if="customerId"
|
v-if="customerId"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
@@ -141,6 +141,7 @@
|
|||||||
<customer-selector
|
<customer-selector
|
||||||
v-model="customerId"
|
v-model="customerId"
|
||||||
v-model:visible="customerSelectorVisible"
|
v-model:visible="customerSelectorVisible"
|
||||||
|
:enable-scan="false"
|
||||||
@change="handleCustomerChange"
|
@change="handleCustomerChange"
|
||||||
/>
|
/>
|
||||||
<t-popup v-model="startVisible" placement="bottom">
|
<t-popup v-model="startVisible" placement="bottom">
|
||||||
@@ -531,12 +532,14 @@ onMounted(async () => {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: var(--td-bg-color-container);
|
background: var(--td-bg-color-container);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
|
||||||
|
|
||||||
.filter-actions {
|
.actions {
|
||||||
gap: .25rem;
|
--td-button-extra-small-height: 24px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
gap: .25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menus {
|
.menus {
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
@click="save"
|
@click="save"
|
||||||
>提交订单</t-button>
|
>提交订单</t-button>
|
||||||
</div>
|
</div>
|
||||||
<customer-selector v-model="customerIdList" v-model:visible="customerSelectorVisible" selection-mode="single" @change="loadCustomer" />
|
<customer-selector v-model="customerIdList" v-model:visible="customerSelectorVisible" selection-mode="single" :enable-scan="false" @change="loadCustomer" />
|
||||||
<t-popup v-model="pickerVisible" placement="bottom">
|
<t-popup v-model="pickerVisible" placement="bottom">
|
||||||
<t-date-time-picker v-if="pickerVisible" v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="下单时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
<t-date-time-picker v-if="pickerVisible" v-model="draftTime" format="YYYY-MM-DD HH:mm" :mode="['date', 'minute']" title="下单时间" @cancel="pickerVisible = false" @confirm="confirmTime" />
|
||||||
</t-popup>
|
</t-popup>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class="keyword"
|
class="keyword"
|
||||||
type="search"
|
type="search"
|
||||||
borderless
|
borderless
|
||||||
placeholder="搜索订单号或客户"
|
placeholder="订单号或客户"
|
||||||
>
|
>
|
||||||
<template #suffix-icon>
|
<template #suffix-icon>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
size="extra-small"
|
size="extra-small"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:content="scannerVisible ? '关闭扫码' : '扫码'"
|
content="选择客户"
|
||||||
@click.stop="toggleScanner"
|
@click.stop="customerSelectorVisible = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,15 +58,6 @@
|
|||||||
@refresh="refresh"
|
@refresh="refresh"
|
||||||
@scrolltolower="loadMore"
|
@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">
|
<div v-if="list.length" class="items">
|
||||||
<sales-order-list-cell
|
<sales-order-list-cell
|
||||||
class="item"
|
class="item"
|
||||||
@@ -92,6 +83,11 @@
|
|||||||
</t-pull-down-refresh>
|
</t-pull-down-refresh>
|
||||||
</div>
|
</div>
|
||||||
<nav-bar-action-menu v-if="actions.length" :actions="actions" />
|
<nav-bar-action-menu v-if="actions.length" :actions="actions" />
|
||||||
|
<customer-selector
|
||||||
|
v-model="customerId"
|
||||||
|
v-model:visible="customerSelectorVisible"
|
||||||
|
@change="handleCustomerChange"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -100,7 +96,7 @@ import { Toolkit } from "timi-web";
|
|||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
||||||
import SalesOrderListCell from "@/pages/salesOrder/components/SalesOrderListCell.vue";
|
import SalesOrderListCell from "@/pages/salesOrder/components/SalesOrderListCell.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
@@ -141,9 +137,7 @@ const actions = computed(() => [
|
|||||||
const keyword = ref("");
|
const keyword = ref("");
|
||||||
const customerId = ref("");
|
const customerId = ref("");
|
||||||
const selectedCustomer = ref<Customer>();
|
const selectedCustomer = ref<Customer>();
|
||||||
const scannerVisible = ref(false);
|
const customerSelectorVisible = ref(false);
|
||||||
const scanStatus = ref("点击扫码开始识别。");
|
|
||||||
let latestDecodedText = "";
|
|
||||||
let customerKeyword = "";
|
let customerKeyword = "";
|
||||||
const orderType = ref("ALL");
|
const orderType = ref("ALL");
|
||||||
type PaymentFilter = "ALL" | GaoSalesPaymentStatus | "PENDING_RECEIVE";
|
type PaymentFilter = "ALL" | GaoSalesPaymentStatus | "PENDING_RECEIVE";
|
||||||
@@ -204,6 +198,16 @@ async function loadRouteCustomer(): Promise<void> {
|
|||||||
setCustomerKeyword(selectedCustomer.value);
|
setCustomerKeyword(selectedCustomer.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCustomerChange(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await loadRouteCustomer();
|
||||||
|
await fetchList(true);
|
||||||
|
} catch (error) {
|
||||||
|
selectedCustomer.value = undefined;
|
||||||
|
Message.error(getErrorMessage(error, "客户加载失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildPageFilter(): SalesOrderPage {
|
function buildPageFilter(): SalesOrderPage {
|
||||||
const pendingReceive = paymentStatus.value === "PENDING_RECEIVE";
|
const pendingReceive = paymentStatus.value === "PENDING_RECEIVE";
|
||||||
return {
|
return {
|
||||||
@@ -279,39 +283,6 @@ 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> {
|
async function toDetail(item: SalesOrder): Promise<void> {
|
||||||
if (item.id) await router.push(`/management/sales-orders/${item.id}`);
|
if (item.id) await router.push(`/management/sales-orders/${item.id}`);
|
||||||
}
|
}
|
||||||
@@ -455,10 +426,6 @@ onUnmounted(() => { navBarStore.clearRight(); search.cancel(); });
|
|||||||
.list-wrap {
|
.list-wrap {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
|
|
||||||
.scanner-wrap {
|
|
||||||
margin: 0 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user