From 171aee160297a3cd614b5b74d49d17d7352a6b0a Mon Sep 17 00:00:00 2001 From: Timi Date: Wed, 12 Aug 2026 00:24:12 +0800 Subject: [PATCH] update chart --- src/api/CustomerChartAPI.ts | 19 + src/api/StoreChartAPI.ts | 31 ++ src/components/ChartRangeSlider.vue | 247 ++++++++++ src/pages/customer/CustomerDataCharts.vue | 417 +++++----------- .../CustomerEventRecordCalendarSection.vue | 58 +++ .../component/CustomerEventStructureChart.vue | 91 ++++ .../component/CustomerRecordTrendChart.vue | 88 ++++ .../customer/component/useCustomerChart.ts | 164 +++++++ src/pages/store/StoreDataCharts.vue | 458 +++--------------- .../component/StoreCustomerGenderChart.vue | 108 +++++ .../component/StoreCustomerGrowthChart.vue | 91 ++++ .../StoreEventRecordCalendarSection.vue | 104 ++++ .../component/StoreEventRecordTrendChart.vue | 93 ++++ .../component/StoreEventStructureChart.vue | 88 ++++ src/pages/store/component/useStoreChart.ts | 164 +++++++ src/types/Customer.ts | 8 + src/types/Store.ts | 22 + 17 files changed, 1554 insertions(+), 697 deletions(-) create mode 100644 src/api/CustomerChartAPI.ts create mode 100644 src/api/StoreChartAPI.ts create mode 100644 src/components/ChartRangeSlider.vue create mode 100644 src/pages/customer/component/CustomerEventRecordCalendarSection.vue create mode 100644 src/pages/customer/component/CustomerEventStructureChart.vue create mode 100644 src/pages/customer/component/CustomerRecordTrendChart.vue create mode 100644 src/pages/customer/component/useCustomerChart.ts create mode 100644 src/pages/store/component/StoreCustomerGenderChart.vue create mode 100644 src/pages/store/component/StoreCustomerGrowthChart.vue create mode 100644 src/pages/store/component/StoreEventRecordCalendarSection.vue create mode 100644 src/pages/store/component/StoreEventRecordTrendChart.vue create mode 100644 src/pages/store/component/StoreEventStructureChart.vue create mode 100644 src/pages/store/component/useStoreChart.ts diff --git a/src/api/CustomerChartAPI.ts b/src/api/CustomerChartAPI.ts new file mode 100644 index 0000000..2cdb47e --- /dev/null +++ b/src/api/CustomerChartAPI.ts @@ -0,0 +1,19 @@ +import { axios } from "timi-web"; +import type { CustomerChartReq } from "@/types/Customer"; +import type { StoreEventRecordCalendarView, StoreEventRecordDailyStatView } from "@/types/Store"; +import { withCurrentStoreId } from "@/utils/store"; + +const BASE_URI = "/gao/customer/chart"; + +async function eventRecordDailyStat(req: CustomerChartReq): Promise { + return await axios.post(`${BASE_URI}/event/record/daily`, withCurrentStoreId(req)); +} + +async function eventRecordCalendar(req: CustomerChartReq): Promise { + return await axios.post(`${BASE_URI}/event/record/calendar`, withCurrentStoreId(req)); +} + +export default { + eventRecordDailyStat, + eventRecordCalendar +}; diff --git a/src/api/StoreChartAPI.ts b/src/api/StoreChartAPI.ts new file mode 100644 index 0000000..1a65d03 --- /dev/null +++ b/src/api/StoreChartAPI.ts @@ -0,0 +1,31 @@ +import { axios } from "timi-web"; +import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer"; +import type { StoreChartReq, StoreEventRecordCalendarView, StoreEventRecordDailyStatView } from "@/types/Store"; +import { getCurrentStoreId, withCurrentStoreId } from "@/utils/store"; + +const BASE_URI = "/gao/store/chart"; + +async function customerDailyStat(req: StoreChartReq): Promise { + return await axios.post(`${BASE_URI}/customer/daily`, withCurrentStoreId(req)); +} + +async function customerGenderStat(): Promise { + return await axios.post(`${BASE_URI}/customer/gender`, undefined, { + params: { storeId: getCurrentStoreId() } + }); +} + +async function eventRecordDailyStat(req: StoreChartReq): Promise { + return await axios.post(`${BASE_URI}/event/record/daily`, withCurrentStoreId(req)); +} + +async function eventRecordCalendar(req: StoreChartReq): Promise { + return await axios.post(`${BASE_URI}/event/record/calendar`, withCurrentStoreId(req)); +} + +export default { + customerDailyStat, + customerGenderStat, + eventRecordDailyStat, + eventRecordCalendar +}; diff --git a/src/components/ChartRangeSlider.vue b/src/components/ChartRangeSlider.vue new file mode 100644 index 0000000..e2ff111 --- /dev/null +++ b/src/components/ChartRangeSlider.vue @@ -0,0 +1,247 @@ + + + + + diff --git a/src/pages/customer/CustomerDataCharts.vue b/src/pages/customer/CustomerDataCharts.vue index 89d6a4e..e607ff3 100644 --- a/src/pages/customer/CustomerDataCharts.vue +++ b/src/pages/customer/CustomerDataCharts.vue @@ -38,24 +38,28 @@ - -
-

-
-

+ + diff --git a/src/pages/customer/component/CustomerEventRecordCalendarSection.vue b/src/pages/customer/component/CustomerEventRecordCalendarSection.vue new file mode 100644 index 0000000..b71bbb6 --- /dev/null +++ b/src/pages/customer/component/CustomerEventRecordCalendarSection.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/src/pages/customer/component/CustomerEventStructureChart.vue b/src/pages/customer/component/CustomerEventStructureChart.vue new file mode 100644 index 0000000..e50fbb7 --- /dev/null +++ b/src/pages/customer/component/CustomerEventStructureChart.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/pages/customer/component/CustomerRecordTrendChart.vue b/src/pages/customer/component/CustomerRecordTrendChart.vue new file mode 100644 index 0000000..cb5e758 --- /dev/null +++ b/src/pages/customer/component/CustomerRecordTrendChart.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/pages/customer/component/useCustomerChart.ts b/src/pages/customer/component/useCustomerChart.ts new file mode 100644 index 0000000..764bc0f --- /dev/null +++ b/src/pages/customer/component/useCustomerChart.ts @@ -0,0 +1,164 @@ +import type { ECharts, EChartsOption } from "echarts"; +import { BarChart, LineChart } from "echarts/charts"; +import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components"; +import { init, use } from "echarts/core"; +import { CanvasRenderer } from "echarts/renderers"; +import type { ComputedRef, Ref } from "vue"; + +use([ + BarChart, + LineChart, + GridComponent, + LegendComponent, + TooltipComponent, + CanvasRenderer +]); + +export function buildCustomerBaseChartOption(dateTextList: string[]): EChartsOption { + return { + color: ["#2563eb", "#0f766e", "#d97706", "#db2777", "#7c3aed"], + grid: { + top: 48, + left: 12, + right: 12, + bottom: 12, + containLabel: true + }, + legend: { + top: 0, + icon: "roundRect", + textStyle: { + color: "#52607a" + } + }, + tooltip: { + trigger: "axis" + }, + xAxis: { + type: "category", + data: dateTextList.map(item => item.slice(5)), + axisTick: { + show: false + }, + axisLabel: { + color: "#52607a" + } + }, + yAxis: { + type: "value", + axisLabel: { + color: "#52607a" + }, + splitLine: { + lineStyle: { + color: "rgba(148, 163, 184, .2)" + } + } + } + }; +} + +export function useCustomerChart(option: ComputedRef | Ref) { + const chartElement = ref(); + let chart: ECharts | undefined; + + watch(option, async () => { + await nextTick(); + renderChart(); + }, { + deep: true + }); + + onMounted(async () => { + await nextTick(); + renderChart(); + window.addEventListener("resize", resizeChart); + }); + + onBeforeUnmount(() => { + window.removeEventListener("resize", resizeChart); + chart?.dispose(); + chart = undefined; + }); + + function renderChart(): void { + if (!chartElement.value) { + return; + } + chart = chart || init(chartElement.value); + chart.setOption(option.value, true); + } + + function resizeChart(): void { + chart?.resize(); + } + + return { + chartElement + }; +} + +export function useCustomerChartWindow(dateTextList: ComputedRef) { + const sliderValue = ref([0, 0]); + const viewportWidth = ref(window.innerWidth); + let isSliderInitialized = false; + const visibleDayCount = computed(() => Math.max(6, Math.floor(viewportWidth.value / 64))); + const maxSliderValue = computed(() => Math.max(0, dateTextList.value.length - 1)); + const visibleDateTextList = computed(() => dateTextList.value.slice(sliderValue.value[0], sliderValue.value[1] + 1)); + const showSlider = computed(() => visibleDayCount.value < dateTextList.value.length); + const rangeText = computed(() => { + const list = visibleDateTextList.value; + const beginText = list[0]?.slice(5) || ""; + const endText = list[list.length - 1]?.slice(5) || ""; + return beginText && endText ? `${beginText} 至 ${endText}` : ""; + }); + + watch(maxSliderValue, value => { + if (!isSliderInitialized) { + sliderValue.value = getLatestSliderRange(); + isSliderInitialized = true; + return; + } + const [start, end] = sliderValue.value; + if (value < end) { + sliderValue.value = [Math.max(0, value - Math.max(0, end - start)), value]; + } + }, { + immediate: true + }); + + onMounted(() => { + window.addEventListener("resize", updateViewportWidth); + }); + + onBeforeUnmount(() => { + window.removeEventListener("resize", updateViewportWidth); + }); + + function handleSliderChange(value: number | number[]): void { + if (!Array.isArray(value)) { + return; + } + const [start = 0, end = start] = value; + sliderValue.value = [start, Math.max(start, end)]; + } + + function updateViewportWidth(): void { + viewportWidth.value = window.innerWidth; + } + + function getLatestSliderRange(): number[] { + const end = maxSliderValue.value; + const start = Math.max(0, end - visibleDayCount.value + 1); + return [start, end]; + } + + return { + rangeText, + showSlider, + sliderValue, + maxSliderValue, + visibleDateTextList, + handleSliderChange + }; +} diff --git a/src/pages/store/StoreDataCharts.vue b/src/pages/store/StoreDataCharts.vue index a26e581..62323fd 100644 --- a/src/pages/store/StoreDataCharts.vue +++ b/src/pages/store/StoreDataCharts.vue @@ -7,38 +7,25 @@ - - -
-

-
-

+ + + + + + + diff --git a/src/pages/store/component/StoreCustomerGrowthChart.vue b/src/pages/store/component/StoreCustomerGrowthChart.vue new file mode 100644 index 0000000..a66fce8 --- /dev/null +++ b/src/pages/store/component/StoreCustomerGrowthChart.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/pages/store/component/StoreEventRecordCalendarSection.vue b/src/pages/store/component/StoreEventRecordCalendarSection.vue new file mode 100644 index 0000000..d18f83a --- /dev/null +++ b/src/pages/store/component/StoreEventRecordCalendarSection.vue @@ -0,0 +1,104 @@ + + + diff --git a/src/pages/store/component/StoreEventRecordTrendChart.vue b/src/pages/store/component/StoreEventRecordTrendChart.vue new file mode 100644 index 0000000..c21fd8a --- /dev/null +++ b/src/pages/store/component/StoreEventRecordTrendChart.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/pages/store/component/StoreEventStructureChart.vue b/src/pages/store/component/StoreEventStructureChart.vue new file mode 100644 index 0000000..8bfa267 --- /dev/null +++ b/src/pages/store/component/StoreEventStructureChart.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/src/pages/store/component/useStoreChart.ts b/src/pages/store/component/useStoreChart.ts new file mode 100644 index 0000000..25c8bfb --- /dev/null +++ b/src/pages/store/component/useStoreChart.ts @@ -0,0 +1,164 @@ +import type { ECharts, EChartsOption } from "echarts"; +import { BarChart, LineChart } from "echarts/charts"; +import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components"; +import { init, use } from "echarts/core"; +import { CanvasRenderer } from "echarts/renderers"; +import type { ComputedRef, Ref } from "vue"; + +use([ + BarChart, + LineChart, + GridComponent, + LegendComponent, + TooltipComponent, + CanvasRenderer +]); + +export function buildStoreBaseChartOption(dateTextList: string[]): EChartsOption { + return { + color: ["#2563eb", "#0f766e", "#d97706", "#db2777", "#7c3aed"], + grid: { + top: 48, + left: 12, + right: 12, + bottom: 12, + containLabel: true + }, + legend: { + top: 0, + icon: "roundRect", + textStyle: { + color: "#52607a" + } + }, + tooltip: { + trigger: "axis" + }, + xAxis: { + type: "category", + data: dateTextList.map(item => item.slice(5)), + axisTick: { + show: false + }, + axisLabel: { + color: "#52607a" + } + }, + yAxis: { + type: "value", + axisLabel: { + color: "#52607a" + }, + splitLine: { + lineStyle: { + color: "rgba(148, 163, 184, .2)" + } + } + } + }; +} + +export function useStoreChart(option: ComputedRef | Ref) { + const chartElement = ref(); + let chart: ECharts | undefined; + + watch(option, async () => { + await nextTick(); + renderChart(); + }, { + deep: true + }); + + onMounted(async () => { + await nextTick(); + renderChart(); + window.addEventListener("resize", resizeChart); + }); + + onBeforeUnmount(() => { + window.removeEventListener("resize", resizeChart); + chart?.dispose(); + chart = undefined; + }); + + function renderChart(): void { + if (!chartElement.value) { + return; + } + chart = chart || init(chartElement.value); + chart.setOption(option.value, true); + } + + function resizeChart(): void { + chart?.resize(); + } + + return { + chartElement + }; +} + +export function useStoreChartWindow(dateTextList: ComputedRef) { + const sliderValue = ref([0, 0]); + const viewportWidth = ref(window.innerWidth); + let isSliderInitialized = false; + const visibleDayCount = computed(() => Math.max(6, Math.floor(viewportWidth.value / 64))); + const maxSliderValue = computed(() => Math.max(0, dateTextList.value.length - 1)); + const visibleDateTextList = computed(() => dateTextList.value.slice(sliderValue.value[0], sliderValue.value[1] + 1)); + const showSlider = computed(() => visibleDayCount.value < dateTextList.value.length); + const rangeText = computed(() => { + const list = visibleDateTextList.value; + const beginText = list[0]?.slice(5) || ""; + const endText = list[list.length - 1]?.slice(5) || ""; + return beginText && endText ? `${beginText} 至 ${endText}` : ""; + }); + + watch(maxSliderValue, value => { + if (!isSliderInitialized) { + sliderValue.value = getLatestSliderRange(); + isSliderInitialized = true; + return; + } + const [start, end] = sliderValue.value; + if (value < end) { + sliderValue.value = [Math.max(0, value - Math.max(0, end - start)), value]; + } + }, { + immediate: true + }); + + onMounted(() => { + window.addEventListener("resize", updateViewportWidth); + }); + + onBeforeUnmount(() => { + window.removeEventListener("resize", updateViewportWidth); + }); + + function handleSliderChange(value: number | number[]): void { + if (!Array.isArray(value)) { + return; + } + const [start = 0, end = start] = value; + sliderValue.value = [start, Math.max(start, end)]; + } + + function updateViewportWidth(): void { + viewportWidth.value = window.innerWidth; + } + + function getLatestSliderRange(): number[] { + const end = maxSliderValue.value; + const start = Math.max(0, end - visibleDayCount.value + 1); + return [start, end]; + } + + return { + rangeText, + showSlider, + sliderValue, + maxSliderValue, + visibleDateTextList, + handleSliderChange + }; +} diff --git a/src/types/Customer.ts b/src/types/Customer.ts index e62e34a..edb2322 100644 --- a/src/types/Customer.ts +++ b/src/types/Customer.ts @@ -53,3 +53,11 @@ export type CustomerGenderStatView = { genderCode?: CustomerGender | "UNKNOWN"; count?: number; }; + +export type CustomerChartReq = { + storeId?: string; + customerId: string; + beginAt: number; + endAt: number; + eventIdList?: string[]; +}; diff --git a/src/types/Store.ts b/src/types/Store.ts index 9da6abf..df8b21e 100644 --- a/src/types/Store.ts +++ b/src/types/Store.ts @@ -27,3 +27,25 @@ export type StoreBusinessDayPage = { endDateValue?: number; } & BasePage; +export type StoreChartReq = { + storeId?: string; + beginAt: number; + endAt: number; + eventIdList?: string[]; +}; + +export type StoreEventRecordDailyStatView = { + eventId?: string; + dateValue?: number; + dailyCount?: number; +}; + +export type StoreEventRecordCalendarView = { + id?: string; + customerId?: string; + customerCode?: string; + customerName?: string; + eventId?: string; + registeredAt?: number; +}; +