update charts

This commit is contained in:
Timi
2026-08-12 16:24:23 +08:00
parent 2a64f2ad72
commit 826f1b2dd0
14 changed files with 820 additions and 319 deletions
+2 -2
View File
@@ -47,7 +47,7 @@
</div> </div>
<div v-if="loading" class="empty-block">正在加载登记日历</div> <div v-if="loading" class="empty-block">正在加载登记日历</div>
<div v-else-if="!currentMonthFilteredRecords.length" class="empty-block" v-text="emptyText" /> <div v-else-if="!currentMonthFilteredRecords.length" class="empty-block" v-text="emptyText" />
<t-popup v-model="dayRecordVisible" placement="bottom"> <t-popup v-model="dayRecordVisible" placement="bottom" destroy-on-close>
<div class="day-record-panel"> <div class="day-record-panel">
<div class="popup-head"> <div class="popup-head">
<h3 v-text="selectedDayTitle" /> <h3 v-text="selectedDayTitle" />
@@ -69,7 +69,7 @@
</div> </div>
</div> </div>
</t-popup> </t-popup>
<t-popup v-model="eventFilterVisible" placement="bottom"> <t-popup v-model="eventFilterVisible" placement="bottom" destroy-on-close>
<div class="filter-popup"> <div class="filter-popup">
<div class="popup-head"> <div class="popup-head">
<div class="title">登记事件筛选</div> <div class="title">登记事件筛选</div>
+10 -166
View File
@@ -33,32 +33,30 @@
</div> </div>
</div> </div>
<section v-if="selectedCustomer" class="summary"> <section v-if="selectedCustomer" class="summary">
<div v-for="item in summaryList" :key="item.label" class="summary-item"> <customer-chart-summary
<span v-text="item.label" /> :customer="selectedCustomer"
<strong v-text="item.value" /> :month-cursor="monthCursor"
</div> :selected-event-id-list="selectedEventIdList"
/>
</section> </section>
<customer-event-record-calendar-section <customer-event-record-calendar-section
v-if="selectedCustomer" v-if="selectedCustomer"
v-model="monthCursor" v-model="monthCursor"
v-model:selected-event-id-list="selectedEventIdList" v-model:selected-event-id-list="selectedEventIdList"
:customer-id="selectedCustomer.id || ''"
:customer-name="selectedCustomer.name" :customer-name="selectedCustomer.name"
:event-list="eventList"
:loading="calendarLoading"
:records="monthRecords"
/> />
<customer-record-trend-chart <customer-record-trend-chart
v-if="selectedCustomer" v-if="selectedCustomer"
:customer-id="selectedCustomer.id || ''"
:date-text-list="chartDateTextList" :date-text-list="chartDateTextList"
:selected-event-id-list="selectedEventIdList" :selected-event-id-list="selectedEventIdList"
:stat-map="recordDailyStatMap"
/> />
<customer-event-structure-chart <customer-event-structure-chart
v-if="selectedCustomer" v-if="selectedCustomer"
:customer-id="selectedCustomer.id || ''"
:date-text-list="monthDateTextList" :date-text-list="monthDateTextList"
:event-list="eventList"
:selected-event-id-list="selectedEventIdList" :selected-event-id-list="selectedEventIdList"
:stat-map="recordDailyStatMap"
/> />
<customer-selector <customer-selector
v-model="selectedCustomerId" v-model="selectedCustomerId"
@@ -71,25 +69,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web"; import { Time } from "timi-web";
import CustomerAPI from "@/api/CustomerAPI"; import CustomerAPI from "@/api/CustomerAPI";
import CustomerChartAPI from "@/api/CustomerChartAPI";
import EventAPI from "@/api/EventAPI";
import CodeScanner from "@/components/CodeScanner.vue"; import CodeScanner from "@/components/CodeScanner.vue";
import CustomerSelector from "@/components/CustomerSelector.vue"; import CustomerSelector from "@/components/CustomerSelector.vue";
import CustomerChartSummary from "@/pages/customer/component/CustomerChartSummary.vue";
import CustomerEventRecordCalendarSection from "@/pages/customer/component/CustomerEventRecordCalendarSection.vue"; import CustomerEventRecordCalendarSection from "@/pages/customer/component/CustomerEventRecordCalendarSection.vue";
import CustomerEventStructureChart from "@/pages/customer/component/CustomerEventStructureChart.vue"; import CustomerEventStructureChart from "@/pages/customer/component/CustomerEventStructureChart.vue";
import CustomerRecordTrendChart from "@/pages/customer/component/CustomerRecordTrendChart.vue"; import CustomerRecordTrendChart from "@/pages/customer/component/CustomerRecordTrendChart.vue";
import { useSettingStore } from "@/store/settingStore"; import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer"; import type { Customer } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem";
import type { EventRecord } from "@/types/EventRecord";
import type { StoreEventRecordCalendarView, StoreEventRecordDailyStatView } from "@/types/Store";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
interface SummaryItem {
label: string;
value: string;
}
defineOptions({ defineOptions({
name: "CustomerDataCharts" name: "CustomerDataCharts"
}); });
@@ -105,56 +94,14 @@ const selectorVisible = ref(false);
const scannerVisible = ref(false); const scannerVisible = ref(false);
const scanStatus = ref("点击扫码开始识别。"); const scanStatus = ref("点击扫码开始识别。");
const selectedCustomer = ref<Customer>(); const selectedCustomer = ref<Customer>();
const eventList = ref<EventItem[]>([]);
const selectedEventIdList = ref<string[]>([]); const selectedEventIdList = ref<string[]>([]);
const monthCursor = ref(getMonthStart(new Date())); const monthCursor = ref(getMonthStart(new Date()));
const monthRecords = ref<EventRecord[]>([]);
const calendarLoading = ref(false);
const recordDailyStatMap = ref<Record<string, StoreEventRecordDailyStatView[]>>({});
const chartBeginDate = getDateStart(addMonths(new Date(), -1)); const chartBeginDate = getDateStart(addMonths(new Date(), -1));
const chartEndDate = getDateEnd(new Date()); const chartEndDate = getDateEnd(new Date());
const chartDateTextList = buildDateTextList(chartBeginDate, chartEndDate); const chartDateTextList = buildDateTextList(chartBeginDate, chartEndDate);
let latestDecodedText = ""; let latestDecodedText = "";
let monthDataRequestId = 0;
const eventNameMap = computed(() => {
const map = new Map<string, string>();
for (const item of eventList.value) {
if (item.id) {
map.set(item.id, item.name || "未命名事件");
}
}
return map;
});
const selectedEventIdSet = computed(() => new Set(selectedEventIdList.value));
const filteredMonthRecords = computed(() => {
if (!selectedEventIdList.value.length) {
return [];
}
return monthRecords.value.filter(item => {
if (!item.eventId || !selectedEventIdSet.value.has(item.eventId)) {
return false;
}
const recordDate = Time.toDateObject(item.registeredAt);
return !!recordDate && recordDate.getFullYear() === monthCursor.value.getFullYear() && recordDate.getMonth() === monthCursor.value.getMonth();
});
});
const monthDateTextList = computed(() => buildMonthDateTextList(monthCursor.value)); const monthDateTextList = computed(() => buildMonthDateTextList(monthCursor.value));
const summaryList = computed<SummaryItem[]>(() => [
{
label: "本月登记",
value: `${filteredMonthRecords.value.length} 次`
},
{
label: "转介绍",
value: `${selectedCustomer.value?.invitedCount || 0} 人`
},
{
label: "最近登记",
value: getRecordEventName(filteredMonthRecords.value[0]) || "暂无"
}
]);
watch(routeCustomerCode, (value) => { watch(routeCustomerCode, (value) => {
if (value) { if (value) {
@@ -172,12 +119,7 @@ watch(customerCode, async (value) => {
await loadCustomer(value); await loadCustomer(value);
}); });
watch([selectedCustomer, monthCursor], async () => {
await loadMonthData();
});
onMounted(async () => { onMounted(async () => {
await loadEvents();
if (routeCustomerId.value) { if (routeCustomerId.value) {
await loadCustomerById(routeCustomerId.value); await loadCustomerById(routeCustomerId.value);
} else if (customerCode.value) { } else if (customerCode.value) {
@@ -187,11 +129,6 @@ onMounted(async () => {
// ---------- 数据 ---------- // ---------- 数据 ----------
async function loadEvents(): Promise<void> {
eventList.value = await EventAPI.listValid();
selectedEventIdList.value = getEventIdList();
}
async function loadCustomer(code: string): Promise<void> { async function loadCustomer(code: string): Promise<void> {
const normalizedCode = code.trim(); const normalizedCode = code.trim();
if (!normalizedCode) { if (!normalizedCode) {
@@ -241,53 +178,6 @@ async function clearCustomerSelection(): Promise<void> {
function clearLoadedCustomer(): void { function clearLoadedCustomer(): void {
selectedCustomer.value = undefined; selectedCustomer.value = undefined;
selectedCustomerId.value = ""; selectedCustomerId.value = "";
monthRecords.value = [];
recordDailyStatMap.value = {};
}
async function loadMonthData(): Promise<void> {
const customerId = selectedCustomer.value?.id;
if (!customerId) {
monthRecords.value = [];
recordDailyStatMap.value = {};
return;
}
const eventIdList = getEventIdList();
if (!eventIdList.length) {
monthRecords.value = [];
recordDailyStatMap.value = {};
return;
}
const requestId = ++monthDataRequestId;
const calendarBeginDate = getCalendarBeginDate(monthCursor.value);
const calendarEndDate = getDateEnd(addDays(calendarBeginDate, 41));
calendarLoading.value = true;
try {
const [dailyStatList, calendarRecordList] = await Promise.all([
CustomerChartAPI.eventRecordDailyStat({
customerId,
beginAt: Time.toTimestamp(chartBeginDate),
endAt: Time.toTimestamp(chartEndDate),
eventIdList
}),
CustomerChartAPI.eventRecordCalendar({
customerId,
beginAt: Time.toTimestamp(calendarBeginDate),
endAt: Time.toTimestamp(calendarEndDate),
eventIdList
})
]);
if (requestId === monthDataRequestId) {
recordDailyStatMap.value = groupRecordDailyStatByEventId(dailyStatList);
monthRecords.value = calendarRecordList.map(buildCalendarRecord);
}
} finally {
if (requestId === monthDataRequestId) {
calendarLoading.value = false;
}
}
} }
// ---------- 扫码 ---------- // ---------- 扫码 ----------
@@ -317,48 +207,6 @@ function handleScanStatusChange(value: string): void {
scanStatus.value = value; scanStatus.value = value;
} }
// ---------- 统计 ----------
function groupRecordDailyStatByEventId(list: StoreEventRecordDailyStatView[]): Record<string, StoreEventRecordDailyStatView[]> {
const map: Record<string, StoreEventRecordDailyStatView[]> = {};
for (const item of list) {
if (!item.eventId) {
continue;
}
if (!map[item.eventId]) {
map[item.eventId] = [];
}
map[item.eventId].push(item);
}
return map;
}
function getRecordEventName(record?: EventRecord): string {
if (!record) {
return "";
}
return record.event?.name || (record.eventId ? eventNameMap.value.get(record.eventId) : "") || "未知事件";
}
function getEventIdList(): string[] {
return eventList.value.map(item => item.id).filter((item): item is string => Boolean(item));
}
function buildCalendarRecord(record: StoreEventRecordCalendarView): EventRecord {
return {
id: record.id,
customerId: record.customerId,
eventId: record.eventId,
registeredAt: record.registeredAt,
customer: record.customerId || record.customerCode || record.customerName ? {
id: record.customerId,
code: record.customerCode,
name: record.customerName
} : undefined
};
}
// ---------- 日期 ---------- // ---------- 日期 ----------
function getMonthStart(date: Date): Date { function getMonthStart(date: Date): Date {
@@ -369,11 +217,6 @@ function getDateStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0); return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
} }
function getCalendarBeginDate(date: Date): Date {
const monthStart = getMonthStart(date);
return addDays(monthStart, -monthStart.getDay());
}
function buildMonthDateTextList(date: Date): string[] { function buildMonthDateTextList(date: Date): string[] {
const monthStart = getMonthStart(date); const monthStart = getMonthStart(date);
const nextMonthStart = addMonths(monthStart, 1); const nextMonthStart = addMonths(monthStart, 1);
@@ -405,6 +248,7 @@ function addMonths(date: Date, months: number): Date {
function getDateEnd(date: Date): Date { function getDateEnd(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
} }
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -0,0 +1,124 @@
<template>
<div v-for="item in summaryList" :key="item.label" class="summary-item">
<span v-text="item.label" />
<strong v-text="item.value" />
</div>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import CustomerChartAPI from "@/api/CustomerChartAPI";
import EventAPI from "@/api/EventAPI";
import type { Customer } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem";
import type { StoreEventRecordCalendarView } from "@/types/Store";
interface SummaryItem {
label: string;
value: string;
}
const props = defineProps<{
customer: Customer;
monthCursor: Date;
selectedEventIdList: string[];
}>();
// ---------- 摘要数据 ----------
const eventList = ref<EventItem[]>([]);
const monthRecordList = ref<StoreEventRecordCalendarView[]>([]);
let summaryRequestId = 0;
const eventNameMap = computed(() => {
const map = new Map<string, string>();
for (const item of eventList.value) {
if (item.id) {
map.set(item.id, item.name || "未命名事件");
}
}
return map;
});
const summaryList = computed<SummaryItem[]>(() => [
{
label: "本月登记",
value: `${monthRecordList.value.length} 次`
},
{
label: "转介绍",
value: `${props.customer.invitedCount || 0} 人`
},
{
label: "最近登记",
value: getRecordEventName(monthRecordList.value[0]) || "暂无"
}
]);
watch([
() => props.customer.id,
() => props.monthCursor,
() => props.selectedEventIdList
], async () => {
await loadMonthRecords();
}, {
deep: true
});
onMounted(async () => {
await loadEvents();
await loadMonthRecords();
});
async function loadEvents(): Promise<void> {
eventList.value = await EventAPI.listValid();
}
async function loadMonthRecords(): Promise<void> {
const customerId = props.customer.id;
if (!customerId || !props.selectedEventIdList.length) {
monthRecordList.value = [];
return;
}
const requestId = ++summaryRequestId;
const begin = getMonthStart(props.monthCursor);
const end = getDateEnd(addDays(addMonths(begin, 1), -1));
const recordList = await CustomerChartAPI.eventRecordCalendar({
customerId,
beginAt: Time.toTimestamp(begin),
endAt: Time.toTimestamp(end),
eventIdList: props.selectedEventIdList
});
if (requestId === summaryRequestId) {
monthRecordList.value = recordList;
}
}
function getRecordEventName(record?: StoreEventRecordCalendarView): string {
if (!record) {
return "";
}
return record.eventId ? eventNameMap.value.get(record.eventId) || "未知事件" : "未知事件";
}
// ---------- 日期 ----------
function getMonthStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
}
function getDateEnd(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
}
function addMonths(date: Date, months: number): Date {
return new Date(date.getFullYear(), date.getMonth() + months, date.getDate(), 0, 0, 0, 0);
}
function addDays(date: Date, days: number): Date {
const value = new Date(date);
value.setDate(value.getDate() + days);
return value;
}
</script>
@@ -5,22 +5,24 @@
class="calendar-section" class="calendar-section"
:event-list="eventList" :event-list="eventList"
:customer-name="customerName" :customer-name="customerName"
:loading="loading" :loading="isCalendarLoading"
:records="records" :records="records"
/> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web";
import CustomerChartAPI from "@/api/CustomerChartAPI";
import EventAPI from "@/api/EventAPI";
import EventRecordCalendar from "@/components/EventRecordCalendar.vue"; import EventRecordCalendar from "@/components/EventRecordCalendar.vue";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import type { EventRecord } from "@/types/EventRecord"; import type { EventRecord } from "@/types/EventRecord";
import type { StoreEventRecordCalendarView } from "@/types/Store";
const props = defineProps<{ const props = defineProps<{
customerId: string;
customerName?: string; customerName?: string;
eventList: EventItem[];
loading?: boolean;
modelValue: Date; modelValue: Date;
records: EventRecord[];
selectedEventIdList: string[]; selectedEventIdList: string[];
}>(); }>();
@@ -38,6 +40,100 @@ const selectedEventIdListProxy = computed({
get: () => props.selectedEventIdList, get: () => props.selectedEventIdList,
set: value => emit("update:selectedEventIdList", value) set: value => emit("update:selectedEventIdList", value)
}); });
// ---------- 日历数据 ----------
const eventList = ref<EventItem[]>([]);
const records = ref<EventRecord[]>([]);
const isCalendarLoading = ref(false);
let calendarRequestId = 0;
watch([
() => props.customerId,
() => props.modelValue,
() => props.selectedEventIdList
], async () => {
await loadMonthRecords();
}, {
deep: true
});
onMounted(async () => {
await loadEvents();
await loadMonthRecords();
});
async function loadEvents(): Promise<void> {
eventList.value = await EventAPI.listValid();
const eventIdList = getEventIdList();
emit("update:selectedEventIdList", props.selectedEventIdList.length ? props.selectedEventIdList.filter(item => eventIdList.includes(item)) : eventIdList);
}
async function loadMonthRecords(): Promise<void> {
if (!props.customerId || !props.selectedEventIdList.length) {
records.value = [];
return;
}
const requestId = ++calendarRequestId;
const begin = getCalendarBeginDate(props.modelValue);
const end = getDateEnd(addDays(begin, 41));
isCalendarLoading.value = true;
try {
const calendarRecordList = await CustomerChartAPI.eventRecordCalendar({
customerId: props.customerId,
beginAt: Time.toTimestamp(begin),
endAt: Time.toTimestamp(end),
eventIdList: props.selectedEventIdList
});
if (requestId === calendarRequestId) {
records.value = calendarRecordList.map(buildCalendarRecord);
}
} finally {
if (requestId === calendarRequestId) {
isCalendarLoading.value = false;
}
}
}
function getEventIdList(): string[] {
return eventList.value.map(item => item.id).filter((id): id is string => Boolean(id));
}
function buildCalendarRecord(record: StoreEventRecordCalendarView): EventRecord {
return {
id: record.id,
customerId: record.customerId,
eventId: record.eventId,
registeredAt: record.registeredAt,
customer: record.customerId || record.customerCode || record.customerName ? {
id: record.customerId,
code: record.customerCode,
name: record.customerName
} : undefined
};
}
// ---------- 日期 ----------
function getMonthStart(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0);
}
function getCalendarBeginDate(date: Date): Date {
const monthStart = getMonthStart(date);
return addDays(monthStart, -monthStart.getDay());
}
function getDateEnd(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999);
}
function addDays(date: Date, days: number): Date {
const value = new Date(date);
value.setDate(value.getDate() + days);
return value;
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -6,20 +6,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts"; import type { EChartsOption } from "echarts";
import CustomerChartAPI from "@/api/CustomerChartAPI";
import EventAPI from "@/api/EventAPI";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import type { StoreEventRecordDailyStatView } from "@/types/Store"; import type { StoreEventRecordDailyStatView } from "@/types/Store";
import { buildCustomerBaseChartOption, useCustomerChart } from "@/pages/customer/component/useCustomerChart"; import { buildCustomerBaseChartOption, useCustomerChart } from "@/pages/customer/component/useCustomerChart";
const props = defineProps<{ const props = defineProps<{
customerId: string;
dateTextList: string[]; dateTextList: string[];
eventList: EventItem[];
selectedEventIdList: string[]; selectedEventIdList: string[];
statMap: Record<string, StoreEventRecordDailyStatView[]>;
}>(); }>();
// ---------- 图表数据 ----------
const eventList = ref<EventItem[]>([]);
const statMap = ref<Record<string, StoreEventRecordDailyStatView[]>>({});
let statRequestId = 0;
const selectedEventIdSet = computed(() => new Set(props.selectedEventIdList)); const selectedEventIdSet = computed(() => new Set(props.selectedEventIdList));
const visibleEventList = computed(() => props.eventList.filter(item => item.id && selectedEventIdSet.value.has(item.id))); const visibleEventList = computed(() => eventList.value.filter(item => item.id && selectedEventIdSet.value.has(item.id)));
const chartOption = computed<EChartsOption>(() => ({ const chartOption = computed<EChartsOption>(() => ({
...buildCustomerBaseChartOption(props.dateTextList), ...buildCustomerBaseChartOption(props.dateTextList),
xAxis: { xAxis: {
@@ -48,13 +56,76 @@ const chartOption = computed<EChartsOption>(() => ({
const { chartElement } = useCustomerChart(chartOption); const { chartElement } = useCustomerChart(chartOption);
watch([
() => props.customerId,
() => props.dateTextList,
() => props.selectedEventIdList
], async () => {
await loadDailyStat();
}, {
deep: true
});
onMounted(async () => {
await loadEvents();
await loadDailyStat();
});
async function loadEvents(): Promise<void> {
eventList.value = await EventAPI.listValid();
}
async function loadDailyStat(): Promise<void> {
if (!props.customerId || !props.dateTextList.length || !props.selectedEventIdList.length) {
statMap.value = {};
return;
}
const requestId = ++statRequestId;
const dailyStatList = await CustomerChartAPI.eventRecordDailyStat({
customerId: props.customerId,
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1])),
eventIdList: props.selectedEventIdList
});
if (requestId === statRequestId) {
statMap.value = groupRecordDailyStatByEventId(dailyStatList);
}
}
function countRecordByEvent(eventId: string): number { function countRecordByEvent(eventId: string): number {
return (props.statMap[eventId] || []).reduce((sum, item) => sum + (item.dailyCount || 0), 0); return (statMap.value[eventId] || []).reduce((sum, item) => sum + (item.dailyCount || 0), 0);
} }
function getEventColor(event: EventItem): string | undefined { function getEventColor(event: EventItem): string | undefined {
return event.color || undefined; return event.color || undefined;
} }
function groupRecordDailyStatByEventId(list: StoreEventRecordDailyStatView[]): Record<string, StoreEventRecordDailyStatView[]> {
const map: Record<string, StoreEventRecordDailyStatView[]> = {};
for (const item of list) {
if (!item.eventId) {
continue;
}
if (!map[item.eventId]) {
map[item.eventId] = [];
}
map[item.eventId].push(item);
}
return map;
}
// ---------- 日期 ----------
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> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -14,17 +14,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts"; import type { EChartsOption } from "echarts";
import CustomerChartAPI from "@/api/CustomerChartAPI";
import ChartRangeSlider from "@/components/ChartRangeSlider.vue"; import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
import type { StoreEventRecordDailyStatView } from "@/types/Store"; import type { StoreEventRecordDailyStatView } from "@/types/Store";
import { buildCustomerBaseChartOption, useCustomerChart, useCustomerChartWindow } from "@/pages/customer/component/useCustomerChart"; import { buildCustomerBaseChartOption, useCustomerChart, useCustomerChartWindow } from "@/pages/customer/component/useCustomerChart";
const props = defineProps<{ const props = defineProps<{
customerId: string;
dateTextList: string[]; dateTextList: string[];
selectedEventIdList: string[]; selectedEventIdList: string[];
statMap: Record<string, StoreEventRecordDailyStatView[]>;
}>(); }>();
// ---------- 图表数据 ----------
const statMap = ref<Record<string, StoreEventRecordDailyStatView[]>>({});
let statRequestId = 0;
// ---------- 图表窗口 ----------
const dateTextList = computed(() => props.dateTextList); const dateTextList = computed(() => props.dateTextList);
const { const {
rangeText, rangeText,
@@ -48,9 +57,64 @@ const chartOption = computed<EChartsOption>(() => ({
const { chartElement } = useCustomerChart(chartOption); const { chartElement } = useCustomerChart(chartOption);
watch([
() => props.customerId,
() => props.dateTextList,
() => props.selectedEventIdList
], async () => {
await loadDailyStat();
}, {
deep: true,
immediate: true
});
async function loadDailyStat(): Promise<void> {
if (!props.customerId || !props.dateTextList.length || !props.selectedEventIdList.length) {
statMap.value = {};
return;
}
const requestId = ++statRequestId;
const dailyStatList = await CustomerChartAPI.eventRecordDailyStat({
customerId: props.customerId,
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1])),
eventIdList: props.selectedEventIdList
});
if (requestId === statRequestId) {
statMap.value = groupRecordDailyStatByEventId(dailyStatList);
}
}
function countRecordByDate(dateText: string): number { function countRecordByDate(dateText: string): number {
const dateValue = Number(dateText.replaceAll("-", "")); const dateValue = Number(dateText.replaceAll("-", ""));
return props.selectedEventIdList.reduce((sum, eventId) => sum + (props.statMap[eventId]?.find(item => item.dateValue === dateValue)?.dailyCount || 0), 0); return props.selectedEventIdList.reduce((sum, eventId) => sum + (statMap.value[eventId]?.find(item => item.dateValue === dateValue)?.dailyCount || 0), 0);
}
function groupRecordDailyStatByEventId(list: StoreEventRecordDailyStatView[]): Record<string, StoreEventRecordDailyStatView[]> {
const map: Record<string, StoreEventRecordDailyStatView[]> = {};
for (const item of list) {
if (!item.eventId) {
continue;
}
if (!map[item.eventId]) {
map[item.eventId] = [];
}
map[item.eventId].push(item);
}
return map;
}
// ---------- 日期 ----------
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> </script>
+8 -116
View File
@@ -1,138 +1,34 @@
<template> <template>
<div v-can="permissionCodes.stat.read" class="data-charts-page"> <div v-can="permissionCodes.stat.read" class="data-charts-page">
<section class="summary"> <store-chart-summary :date-text-list="dateTextList" />
<div v-for="item in summaryList" :key="item.label" class="summary-item">
<span v-text="item.label" />
<strong v-text="item.value" />
</div>
</section>
<store-event-record-calendar-section :event-list="eventList" /> <store-event-record-calendar-section />
<store-customer-growth-chart :date-text-list="dateTextList" :stat-list="customerDailyStatList" /> <store-customer-growth-chart :date-text-list="dateTextList" />
<store-event-record-trend-chart :date-text-list="dateTextList" :event-list="eventList" :stat-map="recordDailyStatMap" /> <store-event-record-trend-chart :date-text-list="dateTextList" />
<store-event-structure-chart :date-text-list="dateTextList" :event-list="eventList" :stat-map="recordDailyStatMap" /> <store-event-structure-chart :date-text-list="dateTextList" />
<store-customer-gender-chart :date-text-list="dateTextList" :stat-list="customerGenderStatList" /> <store-customer-gender-chart :date-text-list="dateTextList" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import EventAPI from "@/api/EventAPI"; import { Time } from "timi-web";
import StoreChartAPI from "@/api/StoreChartAPI"; import StoreChartSummary from "@/pages/store/component/StoreChartSummary.vue";
import StoreCustomerGenderChart from "@/pages/store/component/StoreCustomerGenderChart.vue"; import StoreCustomerGenderChart from "@/pages/store/component/StoreCustomerGenderChart.vue";
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 StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.vue"; import StoreEventStructureChart from "@/pages/store/component/StoreEventStructureChart.vue";
import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem";
import type { StoreEventRecordDailyStatView } from "@/types/Store";
import { Time } from "timi-web";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
interface SummaryItem {
label: string;
value: string;
}
defineOptions({ defineOptions({
name: "StoreDataCharts" name: "StoreDataCharts"
}); });
// ---------- 数据源 ---------- // ---------- 数据源 ----------
const eventList = ref<EventItem[]>([]);
const customerDailyStatList = ref<CustomerDailyStatView[]>([]);
const customerGenderStatList = ref<CustomerGenderStatView[]>([]);
const recordDailyStatMap = ref<Record<string, StoreEventRecordDailyStatView[]>>({});
const weekBeginDate = getDateStart(addDays(new Date(), -6));
const beginDate = getDateStart(addMonths(new Date(), -1)); const beginDate = getDateStart(addMonths(new Date(), -1));
const endDate = getDateEnd(new Date()); const endDate = getDateEnd(new Date());
const dateTextList = buildDateTextList(beginDate, endDate); const dateTextList = buildDateTextList(beginDate, endDate);
const timestampRange = {
beginAt: Time.toTimestamp(beginDate),
endAt: Time.toTimestamp(endDate)
};
// ---------- 摘要 ----------
const summaryList = computed<SummaryItem[]>(() => [
{
label: "当前客户",
value: `${getLatestTotalCustomerCount()} 人`
},
{
label: "本周新增",
value: `${sumCustomerNewCount()} 人`
},
{
label: "本周登记",
value: `${sumActionDailyCount()} 次`
}
]);
onMounted(async () => {
await loadChartsData();
});
// ---------- 页面数据 ----------
async function loadChartsData(): Promise<void> {
eventList.value = await EventAPI.listValid();
const valideventList = eventList.value.filter((item): item is EventItem & { id: string } => Boolean(item.id));
const eventIdList = valideventList.map(item => item.id);
const [customerStatList, genderStatList, recordDailyStatList] = await Promise.all([
StoreChartAPI.customerDailyStat({
beginAt: timestampRange.beginAt,
endAt: timestampRange.endAt
}),
StoreChartAPI.customerGenderStat(),
eventIdList.length ? StoreChartAPI.eventRecordDailyStat({
beginAt: timestampRange.beginAt,
endAt: timestampRange.endAt,
eventIdList
}) : Promise.resolve([])
]);
customerDailyStatList.value = customerStatList;
customerGenderStatList.value = genderStatList;
recordDailyStatMap.value = groupRecordDailyStatByEventId(recordDailyStatList);
}
// ---------- 统计计算 ----------
function getLatestTotalCustomerCount(): number {
const latest = customerDailyStatList.value[customerDailyStatList.value.length - 1];
return latest?.totalCustomerCount || 0;
}
function sumCustomerNewCount(): number {
return customerDailyStatList.value.filter(isRecentWeekStat).reduce((sum, item) => sum + (item.newCustomerCount || 0), 0);
}
function sumActionDailyCount(): number {
return eventList.value.reduce((sum, item) => sum + countRecentWeekRecordByEvent(item.id || ""), 0);
}
function countRecentWeekRecordByEvent(eventId: string): number {
return (recordDailyStatMap.value[eventId] || []).filter(isRecentWeekStat).reduce((sum, item) => sum + (item.dailyCount || 0), 0);
}
function isRecentWeekStat(item: { dateValue?: number }): boolean {
return toDateValue(weekBeginDate) <= (item.dateValue || 0);
}
function groupRecordDailyStatByEventId(list: StoreEventRecordDailyStatView[]): Record<string, StoreEventRecordDailyStatView[]> {
const map: Record<string, StoreEventRecordDailyStatView[]> = {};
for (const item of list) {
if (!item.eventId) {
continue;
}
if (!map[item.eventId]) {
map[item.eventId] = [];
}
map[item.eventId].push(item);
}
return map;
}
// ---------- 日期工具 ---------- // ---------- 日期工具 ----------
@@ -154,10 +50,6 @@ function addMonths(date: Date, months: number): Date {
return new Date(date.getFullYear(), date.getMonth() + months, date.getDate(), 0, 0, 0, 0); return new Date(date.getFullYear(), date.getMonth() + months, date.getDate(), 0, 0, 0, 0);
} }
function toDateValue(date: Date): number {
return Number(Time.formatDate(date).replaceAll("-", ""));
}
function buildDateTextList(begin: Date, end: Date): string[] { function buildDateTextList(begin: Date, end: Date): string[] {
const list: string[] = []; const list: string[] = [];
for (let item = getDateStart(begin); item <= end; item = addDays(item, 1)) { for (let item = getDateStart(begin); item <= end; item = addDays(item, 1)) {
@@ -0,0 +1,202 @@
<template>
<section class="summary">
<div v-for="item in summaryList" :key="item.label" class="summary-item">
<span v-text="item.label" />
<strong v-text="item.value" />
</div>
</section>
</template>
<script setup lang="ts">
import { Time } from "timi-web";
import EventAPI from "@/api/EventAPI";
import StoreChartAPI from "@/api/StoreChartAPI";
import type { CustomerDailyStatView } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem";
import type { StoreEventRecordDailyStatView } from "@/types/Store";
interface SummaryItem {
label: string;
value: string;
}
const props = defineProps<{
dateTextList: string[];
}>();
// ---------- 摘要数据 ----------
const eventList = ref<EventItem[]>([]);
const customerDailyStatList = ref<CustomerDailyStatView[]>([]);
const recordDailyStatMap = ref<Record<string, StoreEventRecordDailyStatView[]>>({});
const todayDateValue = toDateValue(new Date());
const weekBeginDateValue = toDateValue(getWeekBeginDate(new Date()));
let summaryRequestId = 0;
const summaryList = computed<SummaryItem[]>(() => [
{
label: "当前客户",
value: `${getLatestTotalCustomerCount()} 人`
},
{
label: "本周新增",
value: `${sumCustomerNewCount()} 人`
},
{
label: "本周登记",
value: `${sumActionDailyCount()} 次`
}
]);
watch(() => props.dateTextList, async () => {
await loadSummaryData();
}, {
deep: true,
immediate: true
});
async function loadSummaryData(): Promise<void> {
if (!props.dateTextList.length) {
customerDailyStatList.value = [];
recordDailyStatMap.value = {};
return;
}
const requestId = ++summaryRequestId;
eventList.value = await EventAPI.listValid();
const eventIdList = getEventIdList();
const [customerStatList, recordDailyStatList] = await Promise.all([
StoreChartAPI.customerDailyStat({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1]))
}),
eventIdList.length ? StoreChartAPI.eventRecordDailyStat({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1])),
eventIdList
}) : Promise.resolve([])
]);
if (requestId === summaryRequestId) {
customerDailyStatList.value = customerStatList;
recordDailyStatMap.value = groupRecordDailyStatByEventId(recordDailyStatList);
}
}
// ---------- 统计 ----------
function getLatestTotalCustomerCount(): number {
const latest = customerDailyStatList.value[customerDailyStatList.value.length - 1];
return latest?.totalCustomerCount || 0;
}
function sumCustomerNewCount(): number {
return customerDailyStatList.value.filter(isRecentWeekStat).reduce((sum, item) => sum + (item.newCustomerCount || 0), 0);
}
function sumActionDailyCount(): number {
return eventList.value.reduce((sum, item) => sum + countRecentWeekRecordByEvent(item.id || ""), 0);
}
function countRecentWeekRecordByEvent(eventId: string): number {
return (recordDailyStatMap.value[eventId] || []).filter(isRecentWeekStat).reduce((sum, item) => sum + (item.dailyCount || 0), 0);
}
function isRecentWeekStat(item: { dateValue?: number }): boolean {
const dateValue = item.dateValue || 0;
return weekBeginDateValue <= dateValue && dateValue <= todayDateValue;
}
function getEventIdList(): string[] {
return eventList.value.map(item => item.id).filter((id): id is string => Boolean(id));
}
function groupRecordDailyStatByEventId(list: StoreEventRecordDailyStatView[]): Record<string, StoreEventRecordDailyStatView[]> {
const map: Record<string, StoreEventRecordDailyStatView[]> = {};
for (const item of list) {
if (!item.eventId) {
continue;
}
if (!map[item.eventId]) {
map[item.eventId] = [];
}
map[item.eventId].push(item);
}
return map;
}
// ---------- 日期 ----------
function getDateStart(dateText: string): Date;
function getDateStart(date: Date): Date;
function getDateStart(value: string | Date): Date {
if (typeof value !== "string") {
return new Date(value.getFullYear(), value.getMonth(), value.getDate(), 0, 0, 0, 0);
}
const [year, month, day] = value.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);
}
function getWeekBeginDate(date: Date): Date {
const value = getDateStart(date);
const day = value.getDay() || 7;
value.setDate(value.getDate() - day + 1);
return value;
}
function toDateValue(date: Date): number {
return Number(Time.formatDate(date).replaceAll("-", ""));
}
</script>
<style scoped lang="less">
.summary {
gap: .75rem;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
.summary-item {
padding: .875rem .625rem;
display: flex;
min-width: 0;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
align-items: center;
border-radius: 1rem;
flex-direction: column;
span {
color: #52607a;
font-size: .75rem;
}
strong {
margin-top: .375rem;
color: #0f172a;
font-size: 1.125rem;
font-weight: 700;
}
}
}
:global(html.theme-dark) {
.summary {
.summary-item {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
span {
color: #94a3b8;
}
strong {
color: #e2e8f0;
}
}
}
}
</style>
@@ -7,12 +7,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { EChartsOption } from "echarts"; import type { EChartsOption } from "echarts";
import StoreChartAPI from "@/api/StoreChartAPI";
import type { CustomerGenderStatView } from "@/types/Customer"; import type { CustomerGenderStatView } from "@/types/Customer";
import { buildStoreBaseChartOption, useStoreChart } from "@/pages/store/component/useStoreChart"; import { buildStoreBaseChartOption, useStoreChart } from "@/pages/store/component/useStoreChart";
const props = defineProps<{ const props = defineProps<{
dateTextList: string[]; dateTextList: string[];
statList: CustomerGenderStatView[];
}>(); }>();
const genderColorMap = { const genderColorMap = {
@@ -21,6 +21,10 @@ const genderColorMap = {
UNKNOWN: "#64748b" UNKNOWN: "#64748b"
} as const; } as const;
// ---------- 图表数据 ----------
const statList = ref<CustomerGenderStatView[]>([]);
const chartOption = computed<EChartsOption>(() => ({ const chartOption = computed<EChartsOption>(() => ({
...buildStoreBaseChartOption(props.dateTextList), ...buildStoreBaseChartOption(props.dateTextList),
tooltip: { tooltip: {
@@ -70,8 +74,16 @@ const chartOption = computed<EChartsOption>(() => ({
const { chartElement } = useStoreChart(chartOption); const { chartElement } = useStoreChart(chartOption);
onMounted(async () => {
await loadCustomerGenderStat();
});
async function loadCustomerGenderStat(): Promise<void> {
statList.value = await StoreChartAPI.customerGenderStat();
}
function getGenderCount(genderCode: "MALE" | "FEMALE" | "UNKNOWN"): number { function getGenderCount(genderCode: "MALE" | "FEMALE" | "UNKNOWN"): number {
return props.statList.find(item => item.genderCode === genderCode)?.count || 0; return statList.value.find(item => item.genderCode === genderCode)?.count || 0;
} }
</script> </script>
@@ -14,16 +14,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts"; import type { EChartsOption } from "echarts";
import StoreChartAPI from "@/api/StoreChartAPI";
import ChartRangeSlider from "@/components/ChartRangeSlider.vue"; import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
import type { CustomerDailyStatView } from "@/types/Customer"; import type { CustomerDailyStatView } from "@/types/Customer";
import { buildStoreBaseChartOption, useStoreChart, useStoreChartWindow } from "@/pages/store/component/useStoreChart"; import { buildStoreBaseChartOption, useStoreChart, useStoreChartWindow } from "@/pages/store/component/useStoreChart";
const props = defineProps<{ const props = defineProps<{
dateTextList: string[]; dateTextList: string[];
statList: CustomerDailyStatView[];
}>(); }>();
// ---------- 图表数据 ----------
const statList = ref<CustomerDailyStatView[]>([]);
let statRequestId = 0;
// ---------- 图表窗口 ----------
const dateTextList = computed(() => props.dateTextList); const dateTextList = computed(() => props.dateTextList);
const { const {
rangeText, rangeText,
@@ -52,9 +60,44 @@ const chartOption = computed<EChartsOption>(() => ({
const { chartElement } = useStoreChart(chartOption); const { chartElement } = useStoreChart(chartOption);
watch(() => props.dateTextList, async () => {
await loadCustomerDailyStat();
}, {
deep: true,
immediate: true
});
async function loadCustomerDailyStat(): Promise<void> {
if (!props.dateTextList.length) {
statList.value = [];
return;
}
const requestId = ++statRequestId;
const list = await StoreChartAPI.customerDailyStat({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1]))
});
if (requestId === statRequestId) {
statList.value = list;
}
}
function getDailyStat(dateText: string): CustomerDailyStatView | undefined { function getDailyStat(dateText: string): CustomerDailyStatView | undefined {
const dateValue = Number(dateText.replaceAll("-", "")); const dateValue = Number(dateText.replaceAll("-", ""));
return props.statList.find(item => item.dateValue === dateValue); return statList.value.find(item => item.dateValue === dateValue);
}
// ---------- 日期 ----------
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> </script>
@@ -10,6 +10,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import EventAPI from "@/api/EventAPI";
import StoreChartAPI from "@/api/StoreChartAPI"; import StoreChartAPI from "@/api/StoreChartAPI";
import EventRecordCalendar from "@/components/EventRecordCalendar.vue"; import EventRecordCalendar from "@/components/EventRecordCalendar.vue";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
@@ -17,10 +18,7 @@ import type { EventRecord } from "@/types/EventRecord";
import type { StoreEventRecordCalendarView } from "@/types/Store"; import type { StoreEventRecordCalendarView } from "@/types/Store";
import { Time } from "timi-web"; import { Time } from "timi-web";
const props = defineProps<{ const eventList = ref<EventItem[]>([]);
eventList: EventItem[];
}>();
const selectedEventIdList = ref<string[]>([]); const selectedEventIdList = ref<string[]>([]);
const monthRecords = ref<EventRecord[]>([]); const monthRecords = ref<EventRecord[]>([]);
const isCalendarLoading = ref(false); const isCalendarLoading = ref(false);
@@ -31,13 +29,16 @@ watch(monthCursor, async () => {
await loadMonthRecords(); await loadMonthRecords();
}); });
watch(() => props.eventList, async () => { onMounted(async () => {
await loadEvents();
await loadMonthRecords();
});
async function loadEvents(): Promise<void> {
eventList.value = await EventAPI.listValid();
const eventIdList = getEventIdList(); const eventIdList = getEventIdList();
selectedEventIdList.value = selectedEventIdList.value.length ? selectedEventIdList.value.filter(item => eventIdList.includes(item)) : eventIdList; selectedEventIdList.value = selectedEventIdList.value.length ? selectedEventIdList.value.filter(item => eventIdList.includes(item)) : eventIdList;
await loadMonthRecords(); }
}, {
immediate: true
});
async function loadMonthRecords(): Promise<void> { async function loadMonthRecords(): Promise<void> {
const requestId = ++calendarRequestId; const requestId = ++calendarRequestId;
@@ -66,7 +67,7 @@ async function loadMonthRecords(): Promise<void> {
} }
function getEventIdList(): string[] { function getEventIdList(): string[] {
return props.eventList.map(item => item.id).filter((id): id is string => Boolean(id)); return eventList.value.map(item => item.id).filter((id): id is string => Boolean(id));
} }
function buildCalendarRecord(record: StoreEventRecordCalendarView): EventRecord { function buildCalendarRecord(record: StoreEventRecordCalendarView): EventRecord {
@@ -14,7 +14,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts"; import type { EChartsOption } from "echarts";
import EventAPI from "@/api/EventAPI";
import StoreChartAPI from "@/api/StoreChartAPI";
import ChartRangeSlider from "@/components/ChartRangeSlider.vue"; import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import type { StoreEventRecordDailyStatView } from "@/types/Store"; import type { StoreEventRecordDailyStatView } from "@/types/Store";
@@ -22,10 +25,16 @@ import { buildStoreBaseChartOption, useStoreChart, useStoreChartWindow } from "@
const props = defineProps<{ const props = defineProps<{
dateTextList: string[]; dateTextList: string[];
eventList: EventItem[];
statMap: Record<string, StoreEventRecordDailyStatView[]>;
}>(); }>();
// ---------- 图表数据 ----------
const eventList = ref<EventItem[]>([]);
const statMap = ref<Record<string, StoreEventRecordDailyStatView[]>>({});
let statRequestId = 0;
// ---------- 图表窗口 ----------
const dateTextList = computed(() => props.dateTextList); const dateTextList = computed(() => props.dateTextList);
const { const {
rangeText, rangeText,
@@ -37,7 +46,7 @@ const {
} = useStoreChartWindow(dateTextList); } = useStoreChartWindow(dateTextList);
const chartOption = computed<EChartsOption>(() => ({ const chartOption = computed<EChartsOption>(() => ({
...buildStoreBaseChartOption(visibleDateTextList.value), ...buildStoreBaseChartOption(visibleDateTextList.value),
series: props.eventList.map(event => ({ series: eventList.value.map(event => ({
name: event.name || "未命名事件", name: event.name || "未命名事件",
type: "bar", type: "bar",
stack: "record", stack: "record",
@@ -50,14 +59,82 @@ const chartOption = computed<EChartsOption>(() => ({
const { chartElement } = useStoreChart(chartOption); const { chartElement } = useStoreChart(chartOption);
watch(() => props.dateTextList, async () => {
await loadDailyStat();
}, {
deep: true
});
onMounted(async () => {
await loadEvents();
await loadDailyStat();
});
async function loadEvents(): Promise<void> {
eventList.value = await EventAPI.listValid();
}
async function loadDailyStat(): Promise<void> {
if (!props.dateTextList.length) {
statMap.value = {};
return;
}
const eventIdList = getEventIdList();
if (!eventIdList.length) {
statMap.value = {};
return;
}
const requestId = ++statRequestId;
const dailyStatList = await StoreChartAPI.eventRecordDailyStat({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1])),
eventIdList
});
if (requestId === statRequestId) {
statMap.value = groupRecordDailyStatByEventId(dailyStatList);
}
}
function countRecordByEventAndDate(eventId: string, dateText: string): number { function countRecordByEventAndDate(eventId: string, dateText: string): number {
const dateValue = Number(dateText.replaceAll("-", "")); const dateValue = Number(dateText.replaceAll("-", ""));
return props.statMap[eventId]?.find(item => item.dateValue === dateValue)?.dailyCount || 0; return statMap.value[eventId]?.find(item => item.dateValue === dateValue)?.dailyCount || 0;
} }
function getEventColor(event: EventItem): string | undefined { function getEventColor(event: EventItem): string | undefined {
return event.color || undefined; return event.color || undefined;
} }
function getEventIdList(): string[] {
return eventList.value.map(item => item.id).filter((id): id is string => Boolean(id));
}
function groupRecordDailyStatByEventId(list: StoreEventRecordDailyStatView[]): Record<string, StoreEventRecordDailyStatView[]> {
const map: Record<string, StoreEventRecordDailyStatView[]> = {};
for (const item of list) {
if (!item.eventId) {
continue;
}
if (!map[item.eventId]) {
map[item.eventId] = [];
}
map[item.eventId].push(item);
}
return map;
}
// ---------- 日期 ----------
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> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -6,17 +6,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Time } from "timi-web";
import type { EChartsOption } from "echarts"; import type { EChartsOption } from "echarts";
import EventAPI from "@/api/EventAPI";
import StoreChartAPI from "@/api/StoreChartAPI";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import type { StoreEventRecordDailyStatView } from "@/types/Store"; import type { StoreEventRecordDailyStatView } from "@/types/Store";
import { buildStoreBaseChartOption, useStoreChart } from "@/pages/store/component/useStoreChart"; import { buildStoreBaseChartOption, useStoreChart } from "@/pages/store/component/useStoreChart";
const props = defineProps<{ const props = defineProps<{
dateTextList: string[]; dateTextList: string[];
eventList: EventItem[];
statMap: Record<string, StoreEventRecordDailyStatView[]>;
}>(); }>();
// ---------- 图表数据 ----------
const eventList = ref<EventItem[]>([]);
const statMap = ref<Record<string, StoreEventRecordDailyStatView[]>>({});
let statRequestId = 0;
const chartOption = computed<EChartsOption>(() => ({ const chartOption = computed<EChartsOption>(() => ({
...buildStoreBaseChartOption(props.dateTextList), ...buildStoreBaseChartOption(props.dateTextList),
legend: { legend: {
@@ -24,7 +31,7 @@ const chartOption = computed<EChartsOption>(() => ({
}, },
xAxis: { xAxis: {
type: "category", type: "category",
data: props.eventList.map(item => item.name || "未命名事件"), data: eventList.value.map(item => item.name || "未命名事件"),
axisTick: { axisTick: {
show: false show: false
}, },
@@ -35,7 +42,7 @@ const chartOption = computed<EChartsOption>(() => ({
series: [{ series: [{
name: "登记次数", name: "登记次数",
type: "bar", type: "bar",
data: props.eventList.map(item => ({ data: eventList.value.map(item => ({
value: countRecordByEvent(item.id || ""), value: countRecordByEvent(item.id || ""),
itemStyle: getEventColor(item) ? { itemStyle: getEventColor(item) ? {
color: getEventColor(item) color: getEventColor(item)
@@ -46,13 +53,81 @@ const chartOption = computed<EChartsOption>(() => ({
const { chartElement } = useStoreChart(chartOption); const { chartElement } = useStoreChart(chartOption);
watch(() => props.dateTextList, async () => {
await loadDailyStat();
}, {
deep: true
});
onMounted(async () => {
await loadEvents();
await loadDailyStat();
});
async function loadEvents(): Promise<void> {
eventList.value = await EventAPI.listValid();
}
async function loadDailyStat(): Promise<void> {
if (!props.dateTextList.length) {
statMap.value = {};
return;
}
const eventIdList = getEventIdList();
if (!eventIdList.length) {
statMap.value = {};
return;
}
const requestId = ++statRequestId;
const dailyStatList = await StoreChartAPI.eventRecordDailyStat({
beginAt: Time.toTimestamp(getDateStart(props.dateTextList[0])),
endAt: Time.toTimestamp(getDateEnd(props.dateTextList[props.dateTextList.length - 1])),
eventIdList
});
if (requestId === statRequestId) {
statMap.value = groupRecordDailyStatByEventId(dailyStatList);
}
}
function countRecordByEvent(eventId: string): number { function countRecordByEvent(eventId: string): number {
return (props.statMap[eventId] || []).reduce((sum, item) => sum + (item.dailyCount || 0), 0); return (statMap.value[eventId] || []).reduce((sum, item) => sum + (item.dailyCount || 0), 0);
} }
function getEventColor(event: EventItem): string | undefined { function getEventColor(event: EventItem): string | undefined {
return event.color || undefined; return event.color || undefined;
} }
function getEventIdList(): string[] {
return eventList.value.map(item => item.id).filter((id): id is string => Boolean(id));
}
function groupRecordDailyStatByEventId(list: StoreEventRecordDailyStatView[]): Record<string, StoreEventRecordDailyStatView[]> {
const map: Record<string, StoreEventRecordDailyStatView[]> = {};
for (const item of list) {
if (!item.eventId) {
continue;
}
if (!map[item.eventId]) {
map[item.eventId] = [];
}
map[item.eventId].push(item);
}
return map;
}
// ---------- 日期 ----------
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> </script>
<style scoped lang="less"> <style scoped lang="less">
+2 -2
View File
@@ -116,7 +116,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
path: "/management/customer-data-charts", path: "/management/customer-data-charts",
title: "客户统计数据", title: "客户统计数据",
visible: userStore.hasPermissions(permissionCodes.stat.read) visible: userStore.hasPermissions(permissionCodes.stat.read)
}, }
] ]
}, },
{ {
@@ -145,7 +145,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
path: "/management/points/adjust", path: "/management/points/adjust",
title: "人工调整积分", title: "人工调整积分",
visible: userStore.hasPermissions(permissionCodes.point.adjust) visible: userStore.hasPermissions(permissionCodes.point.adjust)
}, }
] ]
}, },
{ {