297 lines
8.2 KiB
Vue
297 lines
8.2 KiB
Vue
<template>
|
|
<div v-can="permissionCodes.stat.read" class="customer-data-charts-page">
|
|
<div class="header bg-white">
|
|
<t-input
|
|
v-model.trim="customerCode"
|
|
class="customer-search"
|
|
borderless
|
|
clearable
|
|
placeholder="输入客户编码或姓名"
|
|
@clear="applyCustomerSearch"
|
|
@enter="applyCustomerSearch"
|
|
>
|
|
<template #suffix-icon>
|
|
<t-button
|
|
size="extra-small"
|
|
theme="primary"
|
|
variant="outline"
|
|
content="选择客户"
|
|
@click.stop="selectorVisible = true"
|
|
/>
|
|
</template>
|
|
</t-input>
|
|
</div>
|
|
<customer-profile-card v-entrance-transition class="customer" :customer="selectedCustomer" />
|
|
<div class="charts">
|
|
<customer-order-charts
|
|
v-entrance-transition="35"
|
|
v-if="selectedCustomer"
|
|
:customer-id="selectedCustomer.id || ''"
|
|
:date-text-list="chartDateTextList"
|
|
:store-id="selectedCustomer.storeId"
|
|
/>
|
|
<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 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 CustomerOrderCharts from "@/pages/customer/component/CustomerOrderCharts.vue";
|
|
import CustomerRecordTrendChart from "@/pages/customer/component/CustomerRecordTrendChart.vue";
|
|
import type { Customer } from "@/types/Customer";
|
|
import { permissionCodes } from "@/utils/permission";
|
|
|
|
defineOptions({
|
|
name: "CustomerDataCharts"
|
|
});
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
// ---------- 路由 ----------
|
|
|
|
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, async (value) => {
|
|
customerCode.value = value;
|
|
await loadCustomer(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[]>([]);
|
|
|
|
async function loadCustomer(code: string): Promise<void> {
|
|
const keyword = code.trim();
|
|
if (!keyword) {
|
|
clearLoadedCustomer();
|
|
return;
|
|
}
|
|
|
|
const customer = await CustomerAPI.findByCode(keyword) || await CustomerAPI.findByName(keyword);
|
|
if (!customer) {
|
|
clearLoadedCustomer();
|
|
return;
|
|
}
|
|
|
|
selectedCustomer.value = customer;
|
|
selectedCustomerId.value = customer.id || "";
|
|
}
|
|
|
|
async function applyCustomerSearch(): Promise<void> {
|
|
await loadCustomer(customerCode.value);
|
|
}
|
|
|
|
async function loadCustomerById(id: string): Promise<void> {
|
|
const normalizedId = id.trim();
|
|
if (!normalizedId) {
|
|
clearLoadedCustomer();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
selectedCustomer.value = await CustomerAPI.detail(normalizedId);
|
|
selectedCustomerId.value = selectedCustomer.value.id || "";
|
|
customerCode.value = selectedCustomer.value.code || selectedCustomer.value.name || "";
|
|
} catch (err) {
|
|
clearLoadedCustomer();
|
|
console.warn("客户详情加载失败", err);
|
|
}
|
|
}
|
|
|
|
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 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);
|
|
|
|
.customer-search {
|
|
--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;
|
|
}
|
|
}
|
|
|
|
.customer {
|
|
margin: 0 1rem;
|
|
}
|
|
|
|
.charts {
|
|
gap: 1rem;
|
|
padding: 0 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
|
|
:global(html.theme-dark) {
|
|
.customer-data-charts-page {
|
|
.customer-search {
|
|
background: #10161f;
|
|
}
|
|
}
|
|
}
|
|
</style>
|