add event value chart
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { axios } from "timi-web";
|
||||
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";
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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[]> {
|
||||
return await axios.post(`${BASE_URI}/event/record/calendar`, withCurrentStoreId(req));
|
||||
}
|
||||
@@ -27,5 +31,6 @@ export default {
|
||||
customerDailyStat,
|
||||
customerGenderStat,
|
||||
eventRecordDailyStat,
|
||||
eventRecordNumberValueDailyStat,
|
||||
eventRecordCalendar
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<store-event-record-calendar-section @list="toEventRecordList" />
|
||||
<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-customer-growth-chart :date-text-list="dateTextList" />
|
||||
<store-customer-gender-chart />
|
||||
@@ -17,6 +18,7 @@ import StoreCustomerGenderChart from "@/pages/store/component/StoreCustomerGende
|
||||
import StoreCustomerGrowthChart from "@/pages/store/component/StoreCustomerGrowthChart.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";
|
||||
import StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.vue";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
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>
|
||||
@@ -41,6 +41,12 @@ export type StoreEventRecordDailyStatView = {
|
||||
dailyCount?: number;
|
||||
};
|
||||
|
||||
export type StoreEventRecordNumberValueDailyStatView = {
|
||||
eventId?: string;
|
||||
dateValue?: number;
|
||||
dailyNumberValue?: number;
|
||||
};
|
||||
|
||||
export type StoreEventRecordCalendarView = {
|
||||
id?: string;
|
||||
customerId?: string;
|
||||
|
||||
Reference in New Issue
Block a user