Files
gao-customer-h5/src/pages/customer/CustomerDataCharts.vue
T
2026-08-24 16:16:43 +08:00

330 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-can="permissionCodes.stat.read" class="customer-data-charts-page">
<div class="header bg-white">
<div class="buttons">
<t-button class="selector" size="small" theme="primary" variant="outline" @click="selectorVisible = true">
<template #icon>
<t-icon name="user-search" />
</template>
<span>选择客户</span>
</t-button>
<t-button size="small" theme="primary" @click="toggleScanner">
<template #icon>
<t-icon name="scan" />
</template>
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
</t-button>
</div>
<div v-if="scannerVisible" class="line scanner-wrapper">
<code-scanner
visible
:status-text="scanStatus"
active-text="正在识别客户编码,请将二维码移入取景框"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
</div>
</div>
<customer-profile-card v-entrance-transition class="customer" :customer="selectedCustomer" />
<div class="charts">
<customer-chart-summary
v-entrance-transition="70"
v-if="selectedCustomer"
:customer="selectedCustomer"
:month-cursor="monthCursor"
:selected-event-id-list="selectedEventIdList"
/>
<customer-event-record-calendar-section
v-entrance-transition="140"
v-if="selectedCustomer"
v-model="monthCursor"
v-model:selected-event-id-list="selectedEventIdList"
:customer-id="selectedCustomer.id || ''"
:customer-name="selectedCustomer.name"
:store-id="selectedCustomer.storeId"
@list="toEventRecordList"
/>
<customer-record-trend-chart
v-entrance-transition="210"
v-if="selectedCustomer"
:customer-id="selectedCustomer.id || ''"
:date-text-list="chartDateTextList"
:selected-event-id-list="selectedEventIdList"
/>
<customer-event-structure-chart
v-entrance-transition="280"
v-if="selectedCustomer"
:customer-id="selectedCustomer.id || ''"
:date-text-list="monthDateTextList"
:selected-event-id-list="selectedEventIdList"
/>
<customer-selector
v-entrance-transition="350"
v-model="selectedCustomerId"
v-model:visible="selectorVisible"
@change="handleCustomerSelected"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import CustomerAPI from "@/api/CustomerAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
import CustomerChartSummary from "@/pages/customer/component/CustomerChartSummary.vue";
import CustomerEventRecordCalendarSection from "@/pages/customer/component/CustomerEventRecordCalendarSection.vue";
import CustomerEventStructureChart from "@/pages/customer/component/CustomerEventStructureChart.vue";
import CustomerRecordTrendChart from "@/pages/customer/component/CustomerRecordTrendChart.vue";
import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer";
import { permissionCodes } from "@/utils/permission";
defineOptions({
name: "CustomerDataCharts"
});
const route = useRoute();
const router = useRouter();
const settingStore = useSettingStore();
// ---------- 路由 ----------
const routeCustomerId = computed(() => String(route.params.id || ""));
const routeCustomerCode = computed(() => String(route.query.customerCode || ""));
const customerCode = ref(routeCustomerCode.value);
const selectedCustomerId = ref(routeCustomerId.value);
watch(routeCustomerCode, (value) => {
if (value) {
customerCode.value = value;
}
});
watch(routeCustomerId, async (value) => {
if (value) {
await loadCustomerById(value);
}
});
onMounted(async () => {
if (routeCustomerId.value) {
await loadCustomerById(routeCustomerId.value);
} else if (customerCode.value) {
await loadCustomer(customerCode.value);
}
});
// ---------- 数据 ----------
const selectorVisible = ref(false);
const selectedCustomer = ref<Customer>();
const selectedEventIdList = ref<string[]>([]);
watch(customerCode, async (value) => {
await loadCustomer(value);
});
async function loadCustomer(code: string): Promise<void> {
const normalizedCode = code.trim();
if (!normalizedCode) {
clearLoadedCustomer();
return;
}
const customer = await CustomerAPI.findByCode(normalizedCode);
if (!customer) {
clearLoadedCustomer();
return;
}
selectedCustomer.value = customer;
selectedCustomerId.value = customer.id || "";
}
async function loadCustomerById(id: string): Promise<void> {
const normalizedId = id.trim();
if (!normalizedId) {
clearLoadedCustomer();
return;
}
try {
selectedCustomer.value = await CustomerAPI.detail(normalizedId);
selectedCustomerId.value = selectedCustomer.value.id || "";
} catch (err) {
clearLoadedCustomer();
console.warn("客户详情加载失败", err);
}
}
async function handleCustomerSelected(id: string): Promise<void> {
await loadCustomerById(id);
}
function clearLoadedCustomer(): void {
selectedCustomer.value = undefined;
selectedCustomerId.value = "";
}
async function toEventRecordList(payload: EventRecordCalendarListPayload): Promise<void> {
if (!selectedCustomer.value?.id) {
return;
}
await router.push({
name: "EventRecordList",
query: buildEventRecordListQuery(payload.monthCursor, payload.selectedEventIdList, selectedCustomer.value)
});
}
function buildEventRecordListQuery(monthCursor: Date, eventIdList: string[], customer: Customer): Record<string, string> {
const monthStart = getMonthStart(monthCursor);
const monthEnd = getDateEnd(addDays(addMonths(monthStart, 1), -1));
return {
customerId: customer.id || "",
customerName: customer.name || "",
dateType: "custom",
beginAt: `${Time.formatDate(monthStart)} 00:00`,
endAt: `${Time.formatDate(monthEnd)} 23:59`,
transitionDepth: String(getCurrentRouteDepth() + 1),
eventIds: eventIdList.join(",")
};
}
function getCurrentRouteDepth(): number {
return Number(route.meta.depth ?? 0);
}
// ---------- 扫码 ----------
const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别");
let latestDecodedText = "";
async function toggleScanner(): Promise<void> {
scannerVisible.value = !scannerVisible.value;
if (scannerVisible.value) {
latestDecodedText = "";
scanStatus.value = "正在启动扫码预览";
}
}
async function handleDecodedText(rawText: string): Promise<void> {
const normalizedText = rawText.trim();
if (!normalizedText || normalizedText === latestDecodedText) {
return;
}
latestDecodedText = normalizedText;
await settingStore.playScanSuccessSound();
customerCode.value = normalizedText;
scanStatus.value = `已识别 ${normalizedText},正在加载客户档案`;
scannerVisible.value = false;
}
function handleScanStatusChange(value: string): void {
scanStatus.value = value;
}
// ---------- 图表日期 ----------
const monthCursor = ref(getMonthStart(new Date()));
const chartBeginDate = getDateStart(addMonths(new Date(), -1));
const chartEndDate = getDateEnd(new Date());
const chartDateTextList = buildDateTextList(chartBeginDate, chartEndDate);
const monthDateTextList = computed(() => buildMonthDateTextList(monthCursor.value));
function getMonthStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
}
function getDateStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
}
function buildMonthDateTextList(date: Date): string[] {
const monthStart = getMonthStart(date);
const nextMonthStart = addMonths(monthStart, 1);
const list: string[] = [];
for (let item = monthStart; item < nextMonthStart; item = addDays(item, 1)) {
list.push(Time.formatDate(item));
}
return list;
}
function buildDateTextList(begin: Date, end: Date): string[] {
const list: string[] = [];
for (let item = getDateStart(begin); item <= end; item = addDays(item, 1)) {
list.push(Time.formatDate(item));
}
return list;
}
function addDays(date: Date, days: number): Date {
const value = new Date(date);
value.setDate(value.getDate() + days);
return value;
}
function addMonths(date: Date, months: number): Date {
return new Date(date.getFullYear(), date.getMonth() + months, 1, 0, 0, 0, 0);
}
function getDateEnd(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
}
</script>
<style scoped lang="less">
.customer-data-charts-page {
gap: 1rem;
padding: var(--app-nav-height) 0 1rem 0;
display: flex;
flex-direction: column;
.header {
top: var(--app-nav-height);
z-index: 10;
padding: .5rem 1rem;
position: sticky;
box-shadow: var(--td-shadow-1);
.buttons {
gap: 1rem;
display: flex;
.selector {
flex: 1;
min-width: 0;
}
}
.scanner-wrapper {
margin-top: 1rem;
}
}
.customer {
margin: 0 1rem;
}
.charts {
gap: 1rem;
padding: 0 1rem;
display: flex;
flex-direction: column;
}
}
:global(html.theme-dark) {
.customer-data-charts-page {
.selector {
background: #10161f;
}
}
}
</style>