update customer chart

This commit is contained in:
Timi
2026-09-11 17:33:28 +08:00
parent 537eb5ac7a
commit 62add33884
25 changed files with 150 additions and 135 deletions
+13 -2
View File
@@ -1,9 +1,14 @@
import { axios } from "timi-web-api";
import type { CustomerChartReq } from "@/types/Customer";
import type { CustomerChartReq, CustomerDiseaseWordView } from "@/types/Customer";
import type { StoreEventRecordCalendarView, StoreEventRecordDailyStatView } from "@/types/Store";
import { getStoreChartCache } from "@/utils/storeChartCache";
import { withCurrentStoreId } from "@/utils/store";
const BASE_URI = "/gao/customer/chart";
type CustomerDiseaseWordReq = {
storeId?: string;
topN: number;
};
async function eventRecordDailyStat(req: CustomerChartReq): Promise<StoreEventRecordDailyStatView[]> {
return await axios.post(`${BASE_URI}/event/record/daily`, withCurrentStoreId(req));
@@ -13,7 +18,13 @@ async function eventRecordCalendar(req: CustomerChartReq): Promise<StoreEventRec
return await axios.post(`${BASE_URI}/event/record/calendar`, withCurrentStoreId(req));
}
async function diseaseWord(topN = 60): Promise<CustomerDiseaseWordView[]> {
const request = withCurrentStoreId<CustomerDiseaseWordReq>({ topN });
return await getStoreChartCache(`customer-disease-word:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}/customer/disease/word`, request));
}
export default {
eventRecordDailyStat,
eventRecordCalendar
eventRecordCalendar,
diseaseWord
};
@@ -1,16 +1,13 @@
<template>
<section class="chart-section">
<div class="title-row">
<section class="date-range-chart-card">
<div class="title">
<h2 v-text="title" />
<span v-if="caption" class="caption" v-text="caption" />
</div>
<div ref="chartElement" class="chart" :style="{ height }" />
<div v-if="loading" class="state">
<div v-if="loading" class="center-tips">
<t-loading size="1.5rem" text="加载中" />
</div>
<div v-else-if="!hasData" class="state">
<t-empty description="暂无数据" />
</div>
<chart-range-slider
v-if="chartShowSlider"
:model-value="sliderValue"
@@ -41,7 +38,6 @@ const props = withDefaults(defineProps<{
caption: "",
dateTextList: () => [],
height: "16rem",
hasData: true,
loading: false
});
@@ -61,43 +57,35 @@ const { chartElement } = useStoreChart(option);
</script>
<style scoped lang="less">
.chart-section {
.date-range-chart-card {
padding: 1rem;
position: relative;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.title-row {
.title {
gap: .5rem;
display: flex;
align-items: baseline;
justify-content: space-between;
}
h2 {
color: #0f172a;
margin: 0 0 .875rem 0;
font-size: 1rem;
font-weight: 700;
}
h2 {
color: #0f172a;
margin: 0 0 .875rem 0;
font-size: 1rem;
font-weight: 700;
}
.caption {
color: #94a3b8;
font-size: .6875rem;
.caption {
color: #94a3b8;
font-size: .6875rem;
}
}
.chart {
width: 100%;
}
.state {
inset: 3.5rem 0 0 0;
display: flex;
position: absolute;
align-items: center;
justify-content: center;
}
}
:global(html.theme-dark) {
+54 -34
View File
@@ -2,10 +2,10 @@
<div v-can="permissionCodes.stat.read" class="customer-data-charts-page">
<div class="header bg-white">
<t-input
v-model.trim="customerCode"
v-model="customerCode"
class="customer-search"
type="search"
borderless
clearable
placeholder="输入客户编码或姓名"
@clear="applyCustomerSearch"
@enter="applyCustomerSearch"
@@ -23,44 +23,50 @@
</div>
<customer-profile-card v-entrance-transition class="customer" :customer="selectedCustomer" />
<div class="charts">
<t-segmented
v-if="selectedCustomer"
v-model="chartMode"
class="chart-mode"
block
:options="chartModeOptions"
v-entrance-transition="20"
/>
<customer-order-charts
v-entrance-transition="35"
v-if="selectedCustomer"
v-if="selectedCustomer && chartMode === 'order'"
: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"
/>
<template v-else-if="selectedCustomer">
<customer-chart-summary
v-entrance-transition="70"
:customer="selectedCustomer"
:month-cursor="monthCursor"
:selected-event-id-list="selectedEventIdList"
/>
<customer-event-record-calendar-section
v-entrance-transition="140"
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"
:customer-id="selectedCustomer.id || ''"
:date-text-list="chartDateTextList"
:selected-event-id-list="selectedEventIdList"
/>
<customer-event-structure-chart
v-entrance-transition="280"
:customer-id="selectedCustomer.id || ''"
:date-text-list="monthDateTextList"
:selected-event-id-list="selectedEventIdList"
/>
</template>
<customer-selector
v-entrance-transition="350"
v-model="selectedCustomerId"
@@ -123,6 +129,16 @@ const selectorVisible = ref(false);
const selectedCustomer = ref<Customer>();
const selectedEventIdList = ref<string[]>([]);
// ---------- 图表切换 ----------
type ChartMode = "order" | "customer";
const chartMode = ref<ChartMode>("customer");
const chartModeOptions = [
{ value: "customer" as ChartMode, label: "客户分析" },
{ value: "order" as ChartMode, label: "订单分析" }
];
async function loadCustomer(code: string): Promise<void> {
const keyword = code.trim();
if (!keyword) {
@@ -284,6 +300,10 @@ function getDateEnd(date: Date): Date {
padding: 0 1rem;
display: flex;
flex-direction: column;
.chart-mode {
width: 100%;
}
}
}
@@ -1,18 +1,17 @@
<template>
<gao-chart-card
<date-range-chart-card
title="有效消费趋势"
caption="金额已换算为元"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="18rem"
height="14rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoSalesOrderConsumptionDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, formatChartYuan, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,18 +1,17 @@
<template>
<gao-chart-card
<date-range-chart-card
title="订单日历"
caption="按下单日期统计"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="18rem"
height="14rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoSalesOrderCalendarStatView } from "@/types/GaoChart";
import { buildAxisChartOption, formatChartYuan, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,12 +1,5 @@
<template>
<section class="order-charts">
<div class="section-heading">
<div>
<div class="eyebrow" v-text="'订单经营分析'" />
<h2 v-text="'客户订单看板'" />
</div>
<span v-text="'金额单位:元,数量单位:基础单位'" />
</div>
<order-daily-chart
:date-text-list="dateTextList"
:list="dailyList"
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
title="客户产品购买排行"
<date-range-chart-card
title="产品购买排行"
caption="基础单位购买及履约进度,前 10"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="20rem"
height="14rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoCustomerOrderProductRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
title="客户 SKU 购买排行"
<date-range-chart-card
title="SKU 购买排行"
caption="基础单位购买及履约进度,前 10"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="20rem"
height="14rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoCustomerOrderSkuRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,18 +1,17 @@
<template>
<gao-chart-card
<date-range-chart-card
title="复购趋势"
caption="缺失日期按 0 展示"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="18rem"
height="14rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoCustomerRepeatDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
<date-range-chart-card
title="复购间隔"
caption="按间隔天数统计订单数"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="18rem"
height="14rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoCustomerRepeatIntervalStatView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
<date-range-chart-card
title="客户净收款排行"
caption="排除已取消订单,前 10"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoCustomerOrderRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, formatChartYuan, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,18 +1,17 @@
<template>
<gao-chart-card
<date-range-chart-card
title="订单每日趋势"
caption="金额已换算为元"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="18rem"
height="12rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoSalesOrderDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, formatChartYuan, formatQuantity, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
@@ -80,5 +79,4 @@ function buildOption(visibleDateTextList: string[]): EChartsOption {
]
};
}
</script>
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
<date-range-chart-card
title="付款状态分布"
caption="当前订单快照"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoSalesOrderPaymentStatView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
<date-range-chart-card
title="订单状态分布"
caption="当前订单快照"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoSalesOrderStatusStatView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
<date-range-chart-card
title="订单类型分布"
caption="按订单数量"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoSalesOrderTypeStatView } from "@/types/GaoChart";
import { buildPieChartOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
<date-range-chart-card
title="履约进度排行"
caption="基础单位购买数量的当前履约快照"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="20rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoProductFulfillmentRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,18 +1,17 @@
<template>
<gao-chart-card
<date-range-chart-card
title="每日履约趋势"
caption="基础单位,冲正数量允许为负值"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoProductFulfillmentDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,17 +1,16 @@
<template>
<gao-chart-card
<date-range-chart-card
title="产品销售排行"
caption="按基础单位销量取前 10"
:option="chartOption"
:loading="loading"
:has-data="list.length > 0"
height="20rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoProductSalesRankView } from "@/types/GaoChart";
import { buildHorizontalBarOption, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,18 +1,17 @@
<template>
<gao-chart-card
<date-range-chart-card
title="产品销售趋势"
caption="基础单位,直接产品与套餐权益分开展示"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoProductSalesDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
@@ -1,18 +1,17 @@
<template>
<gao-chart-card
<date-range-chart-card
title="库存变动"
caption="预占与释放不计入实际出入库"
:date-text-list="dateTextList"
:option="buildOption"
:loading="loading"
:has-data="list.length > 0"
height="17rem"
/>
</template>
<script setup lang="ts">
import type { EChartsOption } from "echarts";
import GaoChartCard from "@/pages/product/component/GaoChartCard.vue";
import DateRangeChartCard from "@/components/DateRangeChartCard.vue";
import type { GaoProductStockMovementDailyStatView } from "@/types/GaoChart";
import { buildAxisChartOption, getDailyItem, getNumber } from "@/pages/product/component/chartUtils";
+4 -4
View File
@@ -7,10 +7,10 @@ export function buildAxisChartOption(labelList: string[]): EChartsOption {
return {
color: chartColorList,
grid: {
top: 48,
left: 12,
right: 12,
bottom: 12,
top: 58,
left: 0,
right: 0,
bottom: 0,
containLabel: true
},
legend: {
+4 -2
View File
@@ -5,8 +5,9 @@
<store-event-record-trend-chart v-entrance-transition="140" :date-text-list="dateTextList" />
<store-event-record-number-value-chart v-entrance-transition="210" :date-text-list="dateTextList" />
<store-event-structure-chart v-entrance-transition="280" :date-text-list="dateTextList" />
<store-customer-growth-chart v-entrance-transition="350" :date-text-list="dateTextList" />
<store-customer-gender-chart v-entrance-transition="420" />
<store-customer-disease-word-cloud-chart v-entrance-transition="350" />
<store-customer-growth-chart v-entrance-transition="420" :date-text-list="dateTextList" />
<store-customer-gender-chart v-entrance-transition="490" />
</div>
</template>
@@ -15,6 +16,7 @@ import { Time } from "timi-web";
import StoreChartSummary from "@/pages/store/component/StoreChartSummary.vue";
import StoreCustomerGenderChart from "@/pages/store/component/StoreCustomerGenderChart.vue";
import StoreCustomerGrowthChart from "@/pages/store/component/StoreCustomerGrowthChart.vue";
import StoreCustomerDiseaseWordCloudChart from "@/pages/store/component/StoreCustomerDiseaseWordCloudChart.vue";
import StoreEventRecordCalendarSection from "@/pages/store/component/StoreEventRecordCalendarSection.vue";
import StoreEventRecordTrendChart from "@/pages/store/component/StoreEventRecordTrendChart.vue";
import StoreEventRecordNumberValueChart from "@/pages/store/component/StoreEventRecordNumberValueChart.vue";
+5
View File
@@ -64,6 +64,11 @@ export type CustomerGenderStatView = {
count?: number;
};
export type CustomerDiseaseWordView = {
word?: string;
value?: number;
};
export type CustomerChartReq = {
storeId?: string;
customerId: string;