add CustomerInviteRank.vue
This commit is contained in:
@@ -18,7 +18,8 @@ async function list(page: CustomerPage): Promise<PageResult<Customer>> {
|
|||||||
size: req.size,
|
size: req.size,
|
||||||
keyword: page.keyword || page.likesExample?.code || page.likesExample?.name || page.likesExample?.telephone,
|
keyword: page.keyword || page.likesExample?.code || page.likesExample?.name || page.likesExample?.telephone,
|
||||||
storeId: page.storeId || req.equalsExample?.storeId,
|
storeId: page.storeId || req.equalsExample?.storeId,
|
||||||
tagIdList: page.tagIdList || []
|
tagIdList: page.tagIdList || [],
|
||||||
|
orderMap: page.orderMap || {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -130,6 +130,12 @@ const menuGroupList = computed<MenuGroup[]>(() => [
|
|||||||
title: "客户管理",
|
title: "客户管理",
|
||||||
visible: userStore.hasPermissions(permissionCodes.customer.read)
|
visible: userStore.hasPermissions(permissionCodes.customer.read)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: "order-list",
|
||||||
|
path: "/management/customer-invite-rank",
|
||||||
|
title: "客户邀请排行",
|
||||||
|
visible: userStore.hasPermissions(permissionCodes.customer.read)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: "chart-combo",
|
icon: "chart-combo",
|
||||||
path: "/management/customer-data-charts",
|
path: "/management/customer-data-charts",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RouteRecordRaw } from "vue-router";
|
|||||||
import QuickSignInPage from "@/pages/home/QuickSignInPage.vue";
|
import QuickSignInPage from "@/pages/home/QuickSignInPage.vue";
|
||||||
import EventRecordEdit from "@/pages/event/EventRecordEdit.vue";
|
import EventRecordEdit from "@/pages/event/EventRecordEdit.vue";
|
||||||
import CustomerList from "@/pages/customer/CustomerList.vue";
|
import CustomerList from "@/pages/customer/CustomerList.vue";
|
||||||
|
import CustomerInviteRank from "@/pages/customer/CustomerInviteRank.vue";
|
||||||
import CustomerDetail from "@/pages/customer/CustomerDetail.vue";
|
import CustomerDetail from "@/pages/customer/CustomerDetail.vue";
|
||||||
import CustomerInviteeList from "@/pages/customer/CustomerInviteeList.vue";
|
import CustomerInviteeList from "@/pages/customer/CustomerInviteeList.vue";
|
||||||
import CustomerEdit from "@/pages/customer/CustomerEdit.vue";
|
import CustomerEdit from "@/pages/customer/CustomerEdit.vue";
|
||||||
@@ -49,6 +50,20 @@ const customerRoutes: RouteRecordRaw[] = [
|
|||||||
darkBodyBackground: "#10161f"
|
darkBodyBackground: "#10161f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/management/customer-invite-rank",
|
||||||
|
name: "CustomerInviteRank",
|
||||||
|
component: CustomerInviteRank,
|
||||||
|
meta: {
|
||||||
|
depth: 3,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "客户邀请排行",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F5F5F8",
|
||||||
|
darkBodyBackground: "#10161f"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/management/customers/create",
|
path: "/management/customers/create",
|
||||||
name: "CustomerCreate",
|
name: "CustomerCreate",
|
||||||
|
|||||||
Reference in New Issue
Block a user