add CustomerInviteRank.vue

This commit is contained in:
Timi
2026-09-10 14:25:53 +08:00
parent 10c4c5ffbc
commit 6e2b97fc8f
4 changed files with 304 additions and 1 deletions
+281
View File
@@ -0,0 +1,281 @@
<template>
<t-pull-down-refresh
v-can="permissionCodes.customer.read"
class="customer-invite-rank"
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="loadRanks"
>
<t-cell-group v-if="rankedCustomerList.length" class="list" theme="card">
<customer-cell
v-for="(item, index) in rankedCustomerList"
:key="item.id || item.code || index"
v-entrance-transition="index * 30"
class="item"
:class="{ 'in-top3': index < 3 }"
:customer="item"
arrow
hover
@click="toCustomerDetail(item)"
>
<template #note>
<div
class="rank"
:class="{
top1: index === 0,
top2: index === 1,
top3: index === 2
}"
v-text="String(index + 1)"
/>
</template>
<template #description>
<div class="description">
<t-tag
theme="primary"
variant="light-outline"
:content="getInvitedCountText(item)"
/>
</div>
</template>
</customer-cell>
</t-cell-group>
<div v-else class="center-tips" v-text="emptyText" />
<div v-if="rankedCustomerList.length" class="center-tips" v-text="loadStateText" />
</t-pull-down-refresh>
</template>
<script setup lang="ts">
import { type PageResult, useUserStore } from "timi-web-api";
import CustomerAPI, { type CustomerPage } from "@/api/CustomerAPI";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { Customer } from "@/types/Customer";
import { getErrorMessage } from "@/utils/error";
import { Message } from "@/utils/message";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
defineOptions({
name: "CustomerInviteRank"
});
const pageSize = 20;
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
// ---------- 列表数据 ----------
const page = reactive<CustomerPage>({
index: 0,
size: pageSize,
orderMap: {
invitedCount: "DESC"
}
});
const pageResult = reactive<PageResult<Customer>>({
total: 0,
list: []
});
const isLoading = ref(false);
const isRefreshing = ref(false);
const isNoMore = ref(false);
let loadedCustomerCount = 0;
const rankedCustomerList = computed(() => pageResult.list);
const summaryText = computed(() => isNoMore.value
? `${rankedCustomerList.value.length} 位客户有邀请记录`
: `已加载 ${rankedCustomerList.value.length} 位客户`);
const emptyText = computed(() => isLoading.value ? "正在加载客户邀请排行" : "暂无客户邀请记录");
const loadStateText = computed(() => {
if (isLoading.value) {
return "正在加载更多";
}
return isNoMore.value ? "没有更多排行" : "上拉加载更多";
});
async function refresh(): Promise<void> {
if (isLoading.value) {
return;
}
page.index = 0;
pageResult.total = 0;
pageResult.list = [];
loadedCustomerCount = 0;
isNoMore.value = false;
isRefreshing.value = true;
await loadRanks();
}
async function loadRanks(): Promise<void> {
if (isLoading.value) {
return;
}
if (isNoMore.value) {
return;
}
isLoading.value = true;
try {
const result: PageResult<Customer> = await CustomerAPI.list({
...page,
storeId: currentStoreStore.currentStoreId
});
const list = result.list || [];
const rankedList = list.filter(item => 0 < getInvitedCount(item));
pageResult.total = result.total || 0;
pageResult.list = pageResult.list.concat(rankedList);
loadedCustomerCount += list.length;
page.index += 1;
isNoMore.value = !list.length
|| list.length < page.size
|| pageResult.total <= loadedCustomerCount
|| list.some(item => 0 >= getInvitedCount(item));
} catch (error) {
Message.error(getErrorMessage(error, "客户邀请排行加载失败"));
} finally {
isLoading.value = false;
isRefreshing.value = false;
}
}
// ---------- 导航和格式化 ----------
async function toCustomerDetail(customer: Customer): Promise<void> {
if (!customer.id) {
return;
}
await router.push(`/management/customers/${customer.id}`);
}
function getInvitedCount(customer: Customer): number {
return customer.invitedCount ?? 0;
}
function getInvitedCountText(customer: Customer): string {
return `邀请 ${getInvitedCount(customer)} 人`;
}
// ---------- 生命周期 ----------
onMounted(async () => {
if (isGlobalManager.value) {
await currentStoreStore.loadStoreList();
} else {
await currentStoreStore.loadLoginStore();
}
await refresh();
});
</script>
<style scoped lang="less">
.customer-invite-rank {
gap: 1rem;
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
display: flex;
flex-direction: column;
.summary {
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
.title {
color: #0f172a;
font-size: 1rem;
font-weight: 700;
}
.count {
color: #0f766e;
font-size: .8125rem;
}
}
.list-wrapper {
.list {
.item {
.rank {
flex: 0 0 1.5rem;
width: 1.5rem;
color: #52607a;
height: 1.5rem;
display: flex;
font-size: .76rem;
background: #f0f0f0;
font-weight: 700;
align-items: center;
border-radius: .5rem;
justify-content: center;
&.top1 {
color: #5c3a00;
background: #ffd700;
}
&.top2 {
color: #374151;
background: #c0c0c0;
}
&.top3 {
color: #fff8f0;
background: #cd7f32;
}
}
.description {
margin-top: .5rem;
}
}
}
}
}
:global(html.theme-dark) {
.customer-invite-rank {
.summary {
background: #182231;
.title {
color: #e2e8f0;
}
.count {
color: #2dd4bf;
}
}
.list {
.item {
background: #182231;
.rank {
color: #cbd5e1;
background: #243244;
&.top1 {
color: #facc15;
background: #3f3415;
}
&.top2 {
color: #e2e8f0;
background: #334155;
}
&.top3 {
color: #fdba74;
background: #3f2a1b;
}
}
}
}
}
}
</style>