add event value chart

This commit is contained in:
Timi
2026-08-18 17:26:33 +08:00
parent eb6b8beb6b
commit 43833e6ab0
4 changed files with 228 additions and 1 deletions
+6 -1
View File
@@ -1,6 +1,6 @@
import { axios } from "timi-web"; import { axios } from "timi-web";
import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer"; import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
import type { StoreChartReq, StoreEventRecordCalendarView, StoreEventRecordDailyStatView } from "@/types/Store"; import type { StoreChartReq, StoreEventRecordCalendarView, StoreEventRecordDailyStatView, StoreEventRecordNumberValueDailyStatView } from "@/types/Store";
import { getCurrentStoreId, withCurrentStoreId } from "@/utils/store"; import { getCurrentStoreId, withCurrentStoreId } from "@/utils/store";
const BASE_URI = "/gao/store/chart"; const BASE_URI = "/gao/store/chart";
@@ -19,6 +19,10 @@ async function eventRecordDailyStat(req: StoreChartReq): Promise<StoreEventRecor
return await axios.post(`${BASE_URI}/event/record/daily`, withCurrentStoreId(req)); return await axios.post(`${BASE_URI}/event/record/daily`, withCurrentStoreId(req));
} }
async function eventRecordNumberValueDailyStat(req: StoreChartReq): Promise<StoreEventRecordNumberValueDailyStatView[]> {
return await axios.post(`${BASE_URI}/event/record/number-value/daily`, withCurrentStoreId(req));
}
async function eventRecordCalendar(req: StoreChartReq): Promise<StoreEventRecordCalendarView[]> { async function eventRecordCalendar(req: StoreChartReq): Promise<StoreEventRecordCalendarView[]> {
return await axios.post(`${BASE_URI}/event/record/calendar`, withCurrentStoreId(req)); return await axios.post(`${BASE_URI}/event/record/calendar`, withCurrentStoreId(req));
} }
@@ -27,5 +31,6 @@ export default {
customerDailyStat, customerDailyStat,
customerGenderStat, customerGenderStat,
eventRecordDailyStat, eventRecordDailyStat,
eventRecordNumberValueDailyStat,
eventRecordCalendar eventRecordCalendar
}; };
+2
View File
@@ -4,6 +4,7 @@
<store-event-record-calendar-section @list="toEventRecordList" /> <store-event-record-calendar-section @list="toEventRecordList" />
<store-event-record-trend-chart :date-text-list="dateTextList" /> <store-event-record-trend-chart :date-text-list="dateTextList" />
<store-event-record-number-value-chart :date-text-list="dateTextList" />
<store-event-structure-chart :date-text-list="dateTextList" /> <store-event-structure-chart :date-text-list="dateTextList" />
<store-customer-growth-chart :date-text-list="dateTextList" /> <store-customer-growth-chart :date-text-list="dateTextList" />
<store-customer-gender-chart /> <store-customer-gender-chart />
@@ -17,6 +18,7 @@ import StoreCustomerGenderChart from "@/pages/store/component/StoreCustomerGende
import StoreCustomerGrowthChart from "@/pages/store/component/StoreCustomerGrowthChart.vue"; import StoreCustomerGrowthChart from "@/pages/store/component/StoreCustomerGrowthChart.vue";
import StoreEventRecordCalendarSection from "@/pages/store/component/StoreEventRecordCalendarSection.vue"; import StoreEventRecordCalendarSection from "@/pages/store/component/StoreEventRecordCalendarSection.vue";
import StoreEventRecordTrendChart from "@/pages/store/component/StoreEventRecordTrendChart.vue"; import StoreEventRecordTrendChart from "@/pages/store/component/StoreEventRecordTrendChart.vue";
import StoreEventRecordNumberValueChart from "@/pages/store/component/StoreEventRecordNumberValueChart.vue";
import StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.vue"; import StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.vue";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue"; import type { EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
@@ -0,0 +1,214 @@
<template>
<section class="store-event-record-number-value-chart">
<div class="header">
<h2>事件值变化</h2>
<t-button
class="event-button"
theme="primary"
size="extra-small"
variant="outline"
:disabled="!numberEventList.length"
@click="openEventPicker"
>
<template #icon>
<t-icon name="filter" />
</template>
<span v-text="selectedEvent?.name || '选择事件'" />
</t-button>
</div>
<div v-if="selectedEvent" ref="chartElement" class="chart" :style="chartStyle" />
<t-empty v-else description="暂无携带数值的事件" />
<chart-range-slider
v-if="selectedEvent && showSlider"
:model-value="sliderValue"
:max="maxSliderValue"
:min-range="5"
:label="rangeText"
@change="handleSliderChange"
/>
<t-popup v-model="eventPickerVisible" placement="bottom">
<t-picker
v-model="eventPickerDraft"
:columns="eventPickerColumns"
title="选择统计事件"
@cancel="eventPickerVisible = false"
@confirm="confirmEventPicker"
/>
</t-popup>
</section>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts";
import EventAPI from "@/api/EventAPI";
import StoreChartAPI from "@/api/StoreChartAPI";
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
import type { EventItem } from "@/types/EventItem";
import type { StoreEventRecordNumberValueDailyStatView } from "@/types/Store";
import { formatEventNumberValue, getEventValueUnitText } from "@/utils/eventValue";
import { buildStoreBaseChartOption, useStoreChart, useStoreChartWindow } from "@/pages/store/component/useStoreChart";
const props = defineProps<{ dateTextList: string[] }>();
// ---------- 事件选择 ----------
const numberEventList = ref<EventItem[]>([]);
const selectedEventId = ref<string>();
const selectedEvent = computed(() => numberEventList.value.find(item => item.id === selectedEventId.value));
const eventPickerVisible = ref(false);
const eventPickerDraft = ref<Array<string | number>>([]);
const eventPickerColumns = computed(() => [[
...numberEventList.value.map(item => ({ label: item.name || "未命名事件", value: item.id || "" }))
]]);
// ---------- 图表数据 ----------
const statList = ref<StoreEventRecordNumberValueDailyStatView[]>([]);
let statRequestId = 0;
// ---------- 图表窗口 ----------
const dateTextList = computed(() => props.dateTextList);
const { rangeText, showSlider, sliderValue, maxSliderValue, visibleDateTextList, handleSliderChange } = useStoreChartWindow(dateTextList);
const chartStyle = { height: "16rem" };
const chartOption = computed<EChartsOption>(() => ({
...buildStoreBaseChartOption(visibleDateTextList.value),
legend: { show: false },
series: [{
name: selectedEvent.value?.name || "事件值",
type: "line",
smooth: true,
symbol: "circle",
connectNulls: true,
data: visibleDateTextList.value.map(getDailyValue)
}],
tooltip: {
trigger: "axis",
valueFormatter: value => formatChartValue(Number(value))
},
yAxis: {
type: "value",
name: selectedEvent.value ? getEventValueUnitText(selectedEvent.value.valueUnit) : "",
axisLabel: { color: "#52607a" },
splitLine: { lineStyle: { color: "rgba(148, 163, 184, .2)" } }
}
}));
const { chartElement } = useStoreChart(chartOption);
watch([() => props.dateTextList, selectedEventId], async () => {
await loadNumberValueStat();
}, { deep: true });
onMounted(async () => {
await loadEvents();
await loadNumberValueStat();
});
async function loadEvents(): Promise<void> {
numberEventList.value = (await EventAPI.listValid()).filter(item => item.valueType === "NUMBER");
if (!selectedEventId.value) {
selectedEventId.value = numberEventList.value[0]?.id;
}
}
async function loadNumberValueStat(): Promise<void> {
const eventId = selectedEventId.value;
if (!eventId || !props.dateTextList.length) {
statList.value = [];
return;
}
const requestId = ++statRequestId;
const list = await StoreChartAPI.eventRecordNumberValueDailyStat({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1])),
eventIdList: [eventId]
});
if (requestId === statRequestId) {
statList.value = list;
}
}
function openEventPicker(): void {
if (!numberEventList.value.length) {
return;
}
eventPickerDraft.value = [selectedEventId.value || numberEventList.value[0].id || ""];
eventPickerVisible.value = true;
}
function confirmEventPicker(value: Array<string | number>): void {
const eventId = String(value[0] || "");
if (numberEventList.value.some(item => item.id === eventId)) {
selectedEventId.value = eventId;
}
eventPickerVisible.value = false;
}
function getDailyValue(dateText: string): number {
const dateValue = Number(dateText.replaceAll("-", ""));
const stat = statList.value.find(item => item.dateValue === dateValue);
if (stat?.dailyNumberValue === undefined || stat.dailyNumberValue === null) {
return 0;
}
return toDisplayValue(stat.dailyNumberValue);
}
function toDisplayValue(value: number): number {
return selectedEvent.value?.valueUnit === "CENT" ? value / 100 : value;
}
function formatChartValue(value: number): string {
if (selectedEvent.value?.valueUnit === "CENT") {
return `${value.toFixed(2).replace(/\.00$/, "").replace(/(\.\d)0$/, "$1")} 元`;
}
return formatEventNumberValue(selectedEvent.value, value);
}
function getDateStart(dateText: string): Date {
const [year, month, day] = dateText.split("-").map(Number);
return new Date(year, month - 1, day, 0, 0, 0, 0);
}
function getDateEnd(dateText: string): Date {
const [year, month, day] = dateText.split("-").map(Number);
return new Date(year, month - 1, day, 23, 59, 59, 999);
}
</script>
<style scoped lang="less">
.store-event-record-number-value-chart {
padding: 1rem;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.header {
display: flex;
justify-content: space-between;
h2 {
color: #0f172a;
margin: 0 0 .5rem 0;
font-size: 1rem;
font-weight: 700;
}
}
.chart {
width: 100%;
}
}
:global(html.theme-dark) {
.store-event-record-number-value-chart {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
h2 {
color: #e2e8f0;
}
}
}
</style>
+6
View File
@@ -41,6 +41,12 @@ export type StoreEventRecordDailyStatView = {
dailyCount?: number; dailyCount?: number;
}; };
export type StoreEventRecordNumberValueDailyStatView = {
eventId?: string;
dateValue?: number;
dailyNumberValue?: number;
};
export type StoreEventRecordCalendarView = { export type StoreEventRecordCalendarView = {
id?: string; id?: string;
customerId?: string; customerId?: string;