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
+164 -45
View File
@@ -74,48 +74,73 @@
<h2 v-text="item.title" />
<div :ref="element => setChartRef(item.id, element)" class="chart" />
</section>
<div v-if="!selectedCustomer" class="rank-summary" v-text="rankSummaryText"></div>
<t-cell-group
v-if="!selectedCustomer && customerRegistrationRankList.length"
class="rank-list"
theme="card"
<pull-down-refresh
v-if="!selectedCustomer"
v-model="rankRefreshing"
class="rank-refresh"
:refresh-timeout="30000"
@refresh="refreshCustomerRegistrationRank"
>
<t-cell
v-for="item in customerRegistrationRankList"
:key="item.customerId"
class="item"
arrow
hover
@click="selectRankCustomer(item.customerId || '')"
>
<template #leftIcon>
<span class="left">
<t-image
v-if="getRankCustomerPhotoURL(item)"
:src="getRankCustomerPhotoURL(item)"
:alt="item.customerName || '客户头像'"
fit="cover"
class="photo"
/>
<span v-else class="photo fallback" v-text="getRankCustomerInitial(item)" />
</span>
</template>
<template #title>
<t-tag theme="primary" v-text="`No.${item.rank}`" />
<span class="word-space" v-text="item.customerName || '未知客户'" />
</template>
<template #description>
<div>
<span v-text="`累计登记 ${item.totalRegistrationCount || 0} 次,`" />
<span v-text="`累计登记 ${item.totalRegistrationDays || 0} 天`" />
</div>
<div>
<span v-text="`最近 ${item.recentContinuousRegistrationDays || 0} 天连续登记`" />
</div>
</template>
</t-cell>
</t-cell-group>
<div v-else class="empty-block" v-text="rankLoading ? '正在加载排行榜' : '暂无登记排行数据'" />
<div class="rank-content">
<div class="rank-summary" v-text="rankSummaryText"></div>
<t-cell-group
v-if="customerRegistrationRankList.length"
class="rank-list"
theme="card"
>
<t-cell
v-for="item in paginatedCustomerRegistrationRankList"
:key="item.customerId"
class="item"
arrow
hover
@click="selectRankCustomer(item.customerId || '')"
>
<template #leftIcon>
<span class="left">
<t-image
v-if="getRankCustomerPhotoURL(item)"
:src="getRankCustomerPhotoURL(item)"
:alt="item.customerName || '客户头像'"
fit="cover"
class="photo"
/>
<span v-else class="photo fallback" v-text="getRankCustomerInitial(item)" />
</span>
</template>
<template #title>
<t-tag theme="primary" :content="`No.${item.rank}`" />
<span class="word-space" v-text="item.customerName || '未知客户'" />
</template>
<template #description>
<div>
<span v-text="`累计登记 ${item.totalRegistrationCount || 0} 次,`" />
<span v-text="`累计登记 ${item.totalRegistrationDays || 0} 天`" />
</div>
<div>
<span v-text="`最近 ${item.recentContinuousRegistrationDays || 0} 天连续登记`" />
</div>
</template>
</t-cell>
</t-cell-group>
<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
v-model="selectedCustomerId"
v-model:visible="selectorVisible"
@@ -146,7 +171,7 @@
<script setup lang="ts">
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 {GridComponent, LegendComponent, TooltipComponent} from "echarts/components";
import {init, use} from "echarts/core";
@@ -215,12 +240,18 @@ const monthCursor = ref(getMonthStart(new Date()));
const monthRecords = ref<RegistrationEventRecord[]>([]);
const calendarLoading = ref(false);
const customerRegistrationRankList = ref<CustomerRegistrationRankView[]>([]);
const rankPageSize = 10;
const rankPageIndex = ref(1);
const rankDateFilterType = ref<RankDateFilterType>("all");
const rankDateRange = ref(buildRankPresetRange("all"));
const rankRegistrationEventIdList = ref<string[]>([]);
const rankLoading = ref(false);
const rankRefreshing = ref(false);
const rankStartPickerVisible = ref(false);
const rankEndPickerVisible = ref(false);
const rankCache = new Map<string, CustomerRegistrationRankView[]>();
const rankPendingRequestMap = new Map<string, Promise<CustomerRegistrationRankView[]>>();
let rankRequestToken = 0;
let latestDecodedText = "";
const registrationEventNameMap = computed(() => {
@@ -305,6 +336,12 @@ const rankSummaryText = computed(() => {
}
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 () => {
await nextTick();
@@ -472,12 +509,44 @@ 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;
let request = force ? undefined : rankPendingRequestMap.get(cacheKey);
if (!request) {
request = RegistrationEventRecordAPI.customerRegistrationRank(buildRankStatReq());
rankPendingRequestMap.set(cacheKey, request);
}
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 {
rankLoading.value = false;
if (rankPendingRequestMap.get(cacheKey) === request) {
rankPendingRequestMap.delete(cacheKey);
}
if (token === rankRequestToken) {
rankLoading.value = false;
}
}
}
@@ -485,6 +554,23 @@ async function reloadCustomerRegistrationRank(): Promise<void> {
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> {
if (Array.isArray(value)) {
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] {
if (type === "all") {
return ["", ""];
@@ -894,6 +989,12 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
text-align: center;
}
.rank-content {
gap: 1rem;
display: flex;
flex-direction: column;
}
.rank-list {
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 {
h2 {
margin-bottom: .875rem;
@@ -1034,6 +1147,12 @@ function parseRankDateTime(value: string, isEnd: boolean): Date {
background: rgba(30, 41, 59, .9);
}
.rank-pagination {
.page {
color: #94a3b8;
}
}
}
}
</style>
+1 -1
View File
@@ -154,7 +154,7 @@ const menuGroupList = computed<MenuGroup[]>(() => [
visible: userStore.hasPermissions(gaoPermissionCodes.qrCode.create)
}
]
},
}
]);
function buildFallbackAvatar(name: string): string {