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" /> <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
<t-cell-group v-if="!selectedCustomer"
v-if="!selectedCustomer && customerRegistrationRankList.length" v-model="rankRefreshing"
class="rank-list" class="rank-refresh"
theme="card" :refresh-timeout="30000"
@refresh="refreshCustomerRegistrationRank"
> >
<t-cell <div class="rank-content">
v-for="item in customerRegistrationRankList" <div class="rank-summary" v-text="rankSummaryText"></div>
:key="item.customerId" <t-cell-group
class="item" v-if="customerRegistrationRankList.length"
arrow class="rank-list"
hover theme="card"
@click="selectRankCustomer(item.customerId || '')" >
> <t-cell
<template #leftIcon> v-for="item in paginatedCustomerRegistrationRankList"
<span class="left"> :key="item.customerId"
<t-image class="item"
v-if="getRankCustomerPhotoURL(item)" arrow
:src="getRankCustomerPhotoURL(item)" hover
:alt="item.customerName || '客户头像'" @click="selectRankCustomer(item.customerId || '')"
fit="cover" >
class="photo" <template #leftIcon>
/> <span class="left">
<span v-else class="photo fallback" v-text="getRankCustomerInitial(item)" /> <t-image
</span> v-if="getRankCustomerPhotoURL(item)"
</template> :src="getRankCustomerPhotoURL(item)"
<template #title> :alt="item.customerName || '客户头像'"
<t-tag theme="primary" v-text="`No.${item.rank}`" /> fit="cover"
<span class="word-space" v-text="item.customerName || '未知客户'" /> class="photo"
</template> />
<template #description> <span v-else class="photo fallback" v-text="getRankCustomerInitial(item)" />
<div> </span>
<span v-text="`累计登记 ${item.totalRegistrationCount || 0} 次,`" /> </template>
<span v-text="`累计登记 ${item.totalRegistrationDays || 0} 天`" /> <template #title>
</div> <t-tag theme="primary" :content="`No.${item.rank}`" />
<div> <span class="word-space" v-text="item.customerName || '未知客户'" />
<span v-text="`最近 ${item.recentContinuousRegistrationDays || 0} 天连续登记`" /> </template>
</div> <template #description>
</template> <div>
</t-cell> <span v-text="`累计登记 ${item.totalRegistrationCount || 0} 次,`" />
</t-cell-group> <span v-text="`累计登记 ${item.totalRegistrationDays || 0} 天`" />
<div v-else class="empty-block" v-text="rankLoading ? '正在加载排行榜' : '暂无登记排行数据'" /> </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 <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,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; 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 {
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(); 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 {