page for customer event rank

This commit is contained in:
Timi
2026-08-04 11:38:37 +08:00
parent 7f0632c148
commit 089d356b77
2 changed files with 165 additions and 46 deletions
+126 -7
View File
@@ -74,14 +74,22 @@
<h2 v-text="item.title" /> <h2 v-text="item.title" />
<div :ref="element => setChartRef(item.id, element)" class="chart" /> <div :ref="element => setChartRef(item.id, element)" class="chart" />
</section> </section>
<div v-if="!selectedCustomer" class="rank-summary" v-text="rankSummaryText"></div> <pull-down-refresh
v-if="!selectedCustomer"
v-model="rankRefreshing"
class="rank-refresh"
:refresh-timeout="30000"
@refresh="refreshCustomerRegistrationRank"
>
<div class="rank-content">
<div class="rank-summary" v-text="rankSummaryText"></div>
<t-cell-group <t-cell-group
v-if="!selectedCustomer && customerRegistrationRankList.length" v-if="customerRegistrationRankList.length"
class="rank-list" class="rank-list"
theme="card" theme="card"
> >
<t-cell <t-cell
v-for="item in customerRegistrationRankList" v-for="item in paginatedCustomerRegistrationRankList"
:key="item.customerId" :key="item.customerId"
class="item" class="item"
arrow arrow
@@ -101,7 +109,7 @@
</span> </span>
</template> </template>
<template #title> <template #title>
<t-tag theme="primary" v-text="`No.${item.rank}`" /> <t-tag theme="primary" :content="`No.${item.rank}`" />
<span class="word-space" v-text="item.customerName || '未知客户'" /> <span class="word-space" v-text="item.customerName || '未知客户'" />
</template> </template>
<template #description> <template #description>
@@ -116,6 +124,23 @@
</t-cell> </t-cell>
</t-cell-group> </t-cell-group>
<div v-else class="empty-block" v-text="rankLoading ? '正在加载排行榜' : '暂无登记排行数据'" /> <div v-else class="empty-block" v-text="rankLoading ? '正在加载排行榜' : '暂无登记排行数据'" />
<div v-if="rankPaginationVisible" class="rank-pagination">
<t-button
variant="outline"
size="small"
:disabled="rankPageIndex < 2"
@click="changeRankPage(-1)"
>上一页</t-button>
<span class="page" v-text="`第 ${rankPageIndex} / ${rankPageCount} 页`" />
<t-button
variant="outline"
size="small"
:disabled="rankPageCount < rankPageIndex + 1"
@click="changeRankPage(1)"
>下一页</t-button>
</div>
</div>
</pull-down-refresh>
<customer-selector <customer-selector
v-model="selectedCustomerId" v-model="selectedCustomerId"
v-model:visible="selectorVisible" v-model:visible="selectorVisible"
@@ -146,7 +171,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type {ECharts, EChartsOption} from "echarts"; import type {ECharts, EChartsOption} from "echarts";
import type {DropdownValue} from "tdesign-mobile-vue"; import {PullDownRefresh, type DropdownValue} from "tdesign-mobile-vue";
import {BarChart, LineChart} from "echarts/charts"; import {BarChart, LineChart} from "echarts/charts";
import {GridComponent, LegendComponent, TooltipComponent} from "echarts/components"; import {GridComponent, LegendComponent, TooltipComponent} from "echarts/components";
import {init, use} from "echarts/core"; import {init, use} from "echarts/core";
@@ -215,12 +240,18 @@ const monthCursor = ref(getMonthStart(new Date()));
const monthRecords = ref<RegistrationEventRecord[]>([]); const monthRecords = ref<RegistrationEventRecord[]>([]);
const calendarLoading = ref(false); const calendarLoading = ref(false);
const customerRegistrationRankList = ref<CustomerRegistrationRankView[]>([]); const customerRegistrationRankList = ref<CustomerRegistrationRankView[]>([]);
const rankPageSize = 10;
const rankPageIndex = ref(1);
const rankDateFilterType = ref<RankDateFilterType>("all"); const rankDateFilterType = ref<RankDateFilterType>("all");
const rankDateRange = ref(buildRankPresetRange("all")); const rankDateRange = ref(buildRankPresetRange("all"));
const rankRegistrationEventIdList = ref<string[]>([]); const rankRegistrationEventIdList = ref<string[]>([]);
const rankLoading = ref(false); const rankLoading = ref(false);
const rankRefreshing = ref(false);
const rankStartPickerVisible = ref(false); const rankStartPickerVisible = ref(false);
const rankEndPickerVisible = ref(false); const rankEndPickerVisible = ref(false);
const rankCache = new Map<string, CustomerRegistrationRankView[]>();
const rankPendingRequestMap = new Map<string, Promise<CustomerRegistrationRankView[]>>();
let rankRequestToken = 0;
let latestDecodedText = ""; let latestDecodedText = "";
const registrationEventNameMap = computed(() => { const registrationEventNameMap = computed(() => {
@@ -305,6 +336,12 @@ const rankSummaryText = computed(() => {
} }
return `范围:${rankDateFilterLabel.value}|${rankRegistrationEventFilterLabel.value}`; return `范围:${rankDateFilterLabel.value}|${rankRegistrationEventFilterLabel.value}`;
}); });
const paginatedCustomerRegistrationRankList = computed(() => {
const startIndex = (rankPageIndex.value - 1) * rankPageSize;
return customerRegistrationRankList.value.slice(startIndex, startIndex + rankPageSize);
});
const rankPageCount = computed(() => Math.max(1, Math.ceil(customerRegistrationRankList.value.length / rankPageSize)));
const rankPaginationVisible = computed(() => 1 < rankPageCount.value);
watch(chartList, async () => { watch(chartList, async () => {
await nextTick(); await nextTick();
@@ -472,19 +509,68 @@ async function loadMonthRecords(): Promise<void> {
} }
} }
async function loadCustomerRegistrationRank(): Promise<void> { async function loadCustomerRegistrationRank(force = false): Promise<void> {
const cacheKey = buildRankCacheKey();
if (!force) {
const cachedList = rankCache.get(cacheKey);
if (cachedList) {
customerRegistrationRankList.value = cachedList;
rankPageIndex.value = 1;
return;
}
} else {
rankCache.delete(cacheKey);
}
const token = rankRequestToken + 1;
rankRequestToken = token;
rankLoading.value = true; rankLoading.value = true;
let request = force ? undefined : rankPendingRequestMap.get(cacheKey);
if (!request) {
request = RegistrationEventRecordAPI.customerRegistrationRank(buildRankStatReq());
rankPendingRequestMap.set(cacheKey, request);
}
try { try {
customerRegistrationRankList.value = await RegistrationEventRecordAPI.customerRegistrationRank(buildRankStatReq()); const result = await request;
if (rankPendingRequestMap.get(cacheKey) === request) {
rankCache.set(cacheKey, result);
}
if (token !== rankRequestToken) {
return;
}
customerRegistrationRankList.value = result;
rankPageIndex.value = 1;
} finally { } finally {
if (rankPendingRequestMap.get(cacheKey) === request) {
rankPendingRequestMap.delete(cacheKey);
}
if (token === rankRequestToken) {
rankLoading.value = false; rankLoading.value = false;
} }
} }
}
async function reloadCustomerRegistrationRank(): Promise<void> { async function reloadCustomerRegistrationRank(): Promise<void> {
await loadCustomerRegistrationRank(); await loadCustomerRegistrationRank();
} }
async function refreshCustomerRegistrationRank(): Promise<void> {
rankRefreshing.value = true;
try {
await loadCustomerRegistrationRank(true);
} finally {
rankRefreshing.value = false;
}
}
function changeRankPage(offset: number): void {
const nextPage = rankPageIndex.value + offset;
if (nextPage < 1 || rankPageCount.value < nextPage) {
return;
}
rankPageIndex.value = nextPage;
}
async function selectRankDateFilter(value: DropdownValue): Promise<void> { async function selectRankDateFilter(value: DropdownValue): Promise<void> {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return; return;
@@ -738,6 +824,15 @@ function buildRankStatReq(): Partial<RegistrationEventRecordRangeReq> | undefine
}; };
} }
function buildRankCacheKey(): string {
const req = buildRankStatReq();
return JSON.stringify({
beginRegisteredAt: req?.beginRegisteredAt,
endRegisteredAt: req?.endRegisteredAt,
registrationEventIdList: req?.registrationEventIdList?.slice().sort()
});
}
function buildRankPresetRange(type: Exclude<RankDateFilterType, "custom">): [string, string] { function buildRankPresetRange(type: Exclude<RankDateFilterType, "custom">): [string, string] {
if (type === "all") { if (type === "all") {
return ["", ""]; return ["", ""];
@@ -894,6 +989,12 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
text-align: center; text-align: center;
} }
.rank-content {
gap: 1rem;
display: flex;
flex-direction: column;
}
.rank-list { .rank-list {
margin: 0; margin: 0;
@@ -934,6 +1035,18 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
} }
} }
.rank-pagination {
gap: .75rem;
display: flex;
align-items: center;
justify-content: center;
.page {
color: #52607a;
font-size: .875rem;
}
}
.chart-section { .chart-section {
h2 { h2 {
margin-bottom: .875rem; margin-bottom: .875rem;
@@ -1034,6 +1147,12 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
background: rgba(30, 41, 59, .9); background: rgba(30, 41, 59, .9);
} }
.rank-pagination {
.page {
color: #94a3b8;
}
}
} }
} }
</style> </style>
+1 -1
View File
@@ -154,7 +154,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
visible: userStore.hasPermissions(gaoPermissionCodes.qrCode.create) visible: userStore.hasPermissions(gaoPermissionCodes.qrCode.create)
} }
] ]
}, }
]); ]);
function buildFallbackAvatar(name: string): string { function buildFallbackAvatar(name: string): string {