add customer age chart
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { axios } from "timi-web-api";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import StoreAPI from "@/api/StoreAPI";
|
||||
import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
|
||||
import type { CustomerAgeStatView, CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import type { StoreBusinessDay, StoreChartReq, StoreEventRecordCalendarView, StoreEventRecordDailyStatView, StoreEventRecordNumberValueDailyStatView } from "@/types/Store";
|
||||
import { getStoreChartCache } from "@/utils/storeChartCache";
|
||||
@@ -21,6 +21,13 @@ async function customerGenderStat(): Promise<CustomerGenderStatView[]> {
|
||||
}));
|
||||
}
|
||||
|
||||
async function customerAgeStat(): Promise<CustomerAgeStatView[]> {
|
||||
const storeId = getCurrentStoreId();
|
||||
return await getStoreChartCache(`customer-age:${storeId}`, async () => await axios.post(`${BASE_URI}/customer/age`, undefined, {
|
||||
params: { storeId }
|
||||
}));
|
||||
}
|
||||
|
||||
async function eventRecordDailyStat(req: StoreChartReq): Promise<StoreEventRecordDailyStatView[]> {
|
||||
const request = withCurrentStoreId(req);
|
||||
return await getStoreChartCache(`event-record-daily:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}/event/record/daily`, request));
|
||||
@@ -48,6 +55,7 @@ async function businessDayCalendar(storeId: string, beginDateValue: number, endD
|
||||
export default {
|
||||
customerDailyStat,
|
||||
customerGenderStat,
|
||||
customerAgeStat,
|
||||
eventRecordDailyStat,
|
||||
eventRecordNumberValueDailyStat,
|
||||
eventRecordCalendar,
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.stat.read" class="data-charts-page">
|
||||
<store-chart-summary v-entrance-transition :date-text-list="dateTextList" />
|
||||
<t-segmented
|
||||
v-model="chartMode"
|
||||
class="chart-mode"
|
||||
block
|
||||
:options="chartModeOptions"
|
||||
v-entrance-transition="20"
|
||||
/>
|
||||
<template v-if="chartMode === 'event'">
|
||||
<store-event-record-calendar-section v-entrance-transition="70" @list="toEventRecordList" />
|
||||
<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-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" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<store-customer-growth-chart v-entrance-transition="70" :date-text-list="dateTextList" />
|
||||
<store-customer-age-chart v-entrance-transition="140" />
|
||||
<store-customer-disease-word-cloud-chart v-entrance-transition="210" />
|
||||
<store-customer-gender-chart v-entrance-transition="280" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import StoreChartSummary from "@/pages/store/component/StoreChartSummary.vue";
|
||||
import StoreCustomerAgeChart from "@/pages/store/component/StoreCustomerAgeChart.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";
|
||||
@@ -28,6 +41,16 @@ defineOptions({
|
||||
name: "StoreDataCharts"
|
||||
});
|
||||
|
||||
type ChartMode = "event" | "customer";
|
||||
|
||||
// ---------- 图表切换 ----------
|
||||
|
||||
const chartMode = ref<ChartMode>("event");
|
||||
const chartModeOptions = [
|
||||
{ value: "event" as ChartMode, label: "事件登记图表" },
|
||||
{ value: "customer" as ChartMode, label: "客户分析图表" }
|
||||
];
|
||||
|
||||
// ---------- 数据源 ----------
|
||||
|
||||
const beginDate = getDateStart(addMonths(new Date(), -1));
|
||||
@@ -100,6 +123,10 @@ function buildDateTextList(begin: Date, end: Date): string[] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-mode {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary {
|
||||
gap: .75rem;
|
||||
display: grid;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<section class="chart-section">
|
||||
<h2>年龄分布</h2>
|
||||
<div v-if="isLoading" class="center-tips" v-text="'正在分析客户年龄'" />
|
||||
<div v-else-if="!ageDataList.length" class="center-tips" v-text="'暂无可分析的客户年龄'" />
|
||||
<div v-else ref="chartElement" class="chart" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { EChartsOption } from "echarts";
|
||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||
import type { CustomerAgeStatView } from "@/types/Customer";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { Message } from "@/utils/message";
|
||||
import { useStoreChart } from "@/pages/store/component/useStoreChart";
|
||||
|
||||
defineOptions({
|
||||
name: "StoreCustomerAgeChart"
|
||||
});
|
||||
|
||||
// ---------- 图表数据 ----------
|
||||
|
||||
const ageList = ref<CustomerAgeStatView[]>([]);
|
||||
const isLoading = ref(false);
|
||||
let requestId = 0;
|
||||
|
||||
const ageDataList = computed(() => ageList.value
|
||||
.filter(item => item.age !== undefined && Number.isFinite(item.age) && 0 < (item.count || 0))
|
||||
.map(item => [item.age as number, item.count || 0]));
|
||||
const shouldRotateAgeLabel = computed(() => 12 < ageDataList.value.length);
|
||||
const chartOption = computed<EChartsOption>(() => ({
|
||||
grid: {
|
||||
top: 24,
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: 0,
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow"
|
||||
},
|
||||
formatter: "年龄:{b}<br/>客户数:{c} 人"
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: ageDataList.value.map(item => item[0]),
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: "#52607a",
|
||||
hideOverlap: true,
|
||||
interval: 0,
|
||||
rotate: shouldRotateAgeLabel.value ? 45 : 0
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
name: "客户数",
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
color: "#52607a"
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: "rgba(148, 163, 184, .2)"
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
barMaxWidth: 24,
|
||||
itemStyle: {
|
||||
color: "#0f766e",
|
||||
},
|
||||
data: ageDataList.value.map(item => item[1])
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
const { chartElement } = useStoreChart(chartOption);
|
||||
|
||||
// ---------- 图表生命周期 ----------
|
||||
|
||||
onMounted(async () => {
|
||||
await loadAgeList();
|
||||
});
|
||||
|
||||
async function loadAgeList(): Promise<void> {
|
||||
const currentRequestId = ++requestId;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const list = await StoreChartAPI.customerAgeStat();
|
||||
if (currentRequestId === requestId) {
|
||||
ageList.value = list || [];
|
||||
}
|
||||
} catch (error) {
|
||||
if (currentRequestId === requestId) {
|
||||
Message.error(getErrorMessage(error, "客户年龄图表加载失败"));
|
||||
}
|
||||
} finally {
|
||||
if (currentRequestId === requestId) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.chart-section {
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, .92);
|
||||
box-shadow: var(--td-shadow-1);
|
||||
border-radius: 1.25rem;
|
||||
|
||||
h2 {
|
||||
color: #0f172a;
|
||||
margin: 0 0 .875rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.chart-section {
|
||||
background: rgba(15, 23, 42, .88);
|
||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||
|
||||
h2 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="chart-section">
|
||||
<h2>客户病症词云</h2>
|
||||
<h2>病症词云</h2>
|
||||
<div v-if="isLoading" class="center-tips" v-text="'正在分析客户病症'" />
|
||||
<div v-else-if="!wordList.length" class="center-tips" v-text="'暂无可分析的病症描述'" />
|
||||
<div v-else ref="chartElement" class="chart" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="chart-section">
|
||||
<h2>性别统计</h2>
|
||||
<h2>性别</h2>
|
||||
<div ref="chartElement" class="chart" />
|
||||
</section>
|
||||
</template>
|
||||
@@ -44,8 +44,8 @@ const chartOption = computed<EChartsOption>(() => ({
|
||||
{
|
||||
name: "客户数",
|
||||
type: "pie",
|
||||
top: 4,
|
||||
bottom: 24,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
radius: ["0%", "58%"],
|
||||
center: ["50%", "46%"],
|
||||
avoidLabelOverlap: true,
|
||||
@@ -103,7 +103,7 @@ function getGenderPieData(): Array<{ name: string; value: number; itemStyle: { c
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 16rem;
|
||||
height: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="chart-section">
|
||||
<h2>客户增长趋势</h2>
|
||||
<h2>增长趋势</h2>
|
||||
<div ref="chartElement" class="chart" />
|
||||
<chart-range-slider
|
||||
v-if="showSlider"
|
||||
@@ -144,7 +144,7 @@ function getDateEnd(dateText: string): Date {
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 16rem;
|
||||
height: 12rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,11 @@ export type CustomerGenderStatView = {
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export type CustomerAgeStatView = {
|
||||
age?: number;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
export type CustomerDiseaseWordView = {
|
||||
word?: string;
|
||||
value?: number;
|
||||
|
||||
Reference in New Issue
Block a user