init project
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
<template>
|
||||
<div ref="pageRef" v-can="gaoPermissionCodes.customer.read" class="customer-list">
|
||||
<div class="search-bar">
|
||||
<t-input
|
||||
v-model.trim="keyword"
|
||||
class="text-input"
|
||||
placeholder="搜索编码、姓名、电话"
|
||||
clearable
|
||||
borderless
|
||||
/>
|
||||
</div>
|
||||
<section class="total">
|
||||
<span class="gray">客户总数:</span>
|
||||
<span class="bold" v-text="String(totalCount)" />
|
||||
</section>
|
||||
<section class="items">
|
||||
<div
|
||||
class="item"
|
||||
v-for="item in customerList"
|
||||
:key="item.id || item.code"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<t-image v-if="getCustomerPhotoURL(item)" :src="getCustomerPhotoURL(item)" :alt="item.name" fit="cover" class="avatar" />
|
||||
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
||||
<div class="content">
|
||||
<div class="line">
|
||||
<div class="name">
|
||||
<h4 class="value" v-text="item.name" />
|
||||
<gender-icon :value="item.gender" size="1.125rem" />
|
||||
</div>
|
||||
<span class="meta gray" v-text="item.code" />
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="gray">
|
||||
<template v-if="item.telephone">
|
||||
<t-icon name="call" />
|
||||
<span class="word-space" v-text="item.telephone" />
|
||||
</template>
|
||||
</div>
|
||||
<t-tag
|
||||
size="small"
|
||||
variant="outline"
|
||||
theme="success"
|
||||
:content="`${item.invitedCount} 位邀请`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!customerList.length" class="empty-block" v-text="loading ? '正在加载客户' : '暂无客户数据'" />
|
||||
<div v-if="customerList.length" class="load-state" v-text="loading ? '正在加载更多' : noMore ? '已经到底了' : '上拉加载更多'" />
|
||||
</section>
|
||||
<div class="bottom-actions">
|
||||
<t-button
|
||||
v-can="gaoPermissionCodes.customer.read"
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
class="export bg-white"
|
||||
:loading="exporting"
|
||||
:disabled="exporting"
|
||||
@click="exportCustomers"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="download" />
|
||||
</template>
|
||||
导出
|
||||
</t-button>
|
||||
<t-button
|
||||
v-can="gaoPermissionCodes.customer.create"
|
||||
theme="primary"
|
||||
class="create"
|
||||
@click="router.push('/management/customers/create')"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
添加
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { Scroller, Time, Toolkit } from "timi-web";
|
||||
import CustomerAPI from "@/api/CustomerAPI";
|
||||
import GenderIcon from "@/components/GenderIcon.vue";
|
||||
import type { Customer, CustomerSearchReq } from "@/types/Customer";
|
||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||
import { gaoPermissionCodes } from "@/utils/gaoPermission";
|
||||
|
||||
defineOptions({
|
||||
name: "CustomerList"
|
||||
});
|
||||
|
||||
const scrollListenerName = "CustomerList";
|
||||
const router = useRouter();
|
||||
const pageSize = 20;
|
||||
const keyword = ref("");
|
||||
const loading = ref(false);
|
||||
const exporting = ref(false);
|
||||
const noMore = ref(false);
|
||||
const pageIndex = ref(0);
|
||||
const totalCount = ref(0);
|
||||
const customerList = ref<Customer[]>([]);
|
||||
const pageRef = ref<HTMLElement>();
|
||||
let loadToken = 0;
|
||||
let keywordTimer: number | undefined;
|
||||
|
||||
function getPageScrollElement(): HTMLElement | null {
|
||||
return pageRef.value?.closest(".pages") || null;
|
||||
}
|
||||
|
||||
async function reloadCustomers(): Promise<void> {
|
||||
pageIndex.value = 0;
|
||||
totalCount.value = 0;
|
||||
customerList.value = [];
|
||||
noMore.value = false;
|
||||
await loadCustomers();
|
||||
}
|
||||
|
||||
async function loadCustomers(): Promise<void> {
|
||||
if (loading.value || noMore.value) {
|
||||
return;
|
||||
}
|
||||
const token = loadToken + 1;
|
||||
loadToken = token;
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await CustomerAPI.searchPage({
|
||||
index: pageIndex.value,
|
||||
size: pageSize,
|
||||
equalsExample: buildSearchReq()
|
||||
});
|
||||
if (token !== loadToken) {
|
||||
return;
|
||||
}
|
||||
customerList.value = customerList.value.concat(result.list || []);
|
||||
totalCount.value = result.total || 0;
|
||||
pageIndex.value += 1;
|
||||
noMore.value = customerList.value.length >= totalCount.value || (result.list || []).length < pageSize;
|
||||
} finally {
|
||||
if (token === loadToken) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function exportCustomers(): Promise<void> {
|
||||
if (exporting.value) {
|
||||
return;
|
||||
}
|
||||
exporting.value = true;
|
||||
try {
|
||||
const blob = await CustomerAPI.exportExcel(buildSearchReq());
|
||||
Toolkit.downloadBlob(blob, `客户列表-${Time.formatDate(new Date())}.xlsx`);
|
||||
showToast("导出已开始");
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function goDetail(customer: Customer): Promise<void> {
|
||||
if (!customer.id) {
|
||||
showToast("客户 ID 缺失,无法打开详情", "warning");
|
||||
return;
|
||||
}
|
||||
await router.push(`/management/customers/${customer.id}`);
|
||||
}
|
||||
|
||||
function buildSearchReq(): CustomerSearchReq {
|
||||
return {
|
||||
keyword: keyword.value || undefined
|
||||
};
|
||||
}
|
||||
|
||||
function showToast(message: string, theme: "success" | "warning" = "success"): void {
|
||||
Toast({
|
||||
message,
|
||||
theme
|
||||
});
|
||||
}
|
||||
|
||||
watch(keyword, async () => {
|
||||
window.clearTimeout(keywordTimer);
|
||||
keywordTimer = window.setTimeout(async () => {
|
||||
await reloadCustomers();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await reloadCustomers();
|
||||
Scroller.addBottomListener(scrollListenerName, loadCustomers, {
|
||||
canLoad: () => !loading.value && !noMore.value,
|
||||
getElement: getPageScrollElement
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.clearTimeout(keywordTimer);
|
||||
Scroller.removeListener(scrollListenerName);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.customer-list {
|
||||
padding: calc(var(--app-nav-height)) 0 5.5rem 0;
|
||||
|
||||
.search-bar {
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
padding: .75rem 1rem;
|
||||
position: sticky;
|
||||
background: #F5F5F8;
|
||||
box-shadow: var(--td-shadow-1);
|
||||
|
||||
.text-input {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(148, 163, 184, .28);
|
||||
padding: .5rem 1rem;
|
||||
background: #f8fbff;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.total {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.items {
|
||||
gap: .5rem;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
|
||||
.item {
|
||||
gap: .75rem;
|
||||
padding: .5rem;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
background: #f7faff;
|
||||
align-items: center;
|
||||
border: 1px solid var(--td-border-level-1-color);
|
||||
border-radius: .875rem;
|
||||
justify-content: space-between;
|
||||
|
||||
.avatar {
|
||||
flex: none;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: .5rem;
|
||||
|
||||
&.fallback {
|
||||
color: #2563eb;
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
background: #dbeafe;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: .75rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.meta {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
gap: .375rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.value {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-block,
|
||||
.load-state {
|
||||
padding: 1rem;
|
||||
color: #52607a;
|
||||
text-align: center;
|
||||
background: #f8fbff;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
gap: .75rem;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
|
||||
position: fixed;
|
||||
|
||||
.export {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.create {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.customer-list {
|
||||
|
||||
.search-bar {
|
||||
background: #10161f;
|
||||
|
||||
.text-input {
|
||||
color: #e2e8f0;
|
||||
background: rgba(15, 23, 42, .72);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
|
||||
.load-state,
|
||||
.item,
|
||||
.empty-block {
|
||||
color: #e2e8f0;
|
||||
background: rgba(30, 41, 59, .9);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
|
||||
&.fallback {
|
||||
color: #93c5fd;
|
||||
background: rgba(30, 64, 175, .32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats {
|
||||
|
||||
.stat {
|
||||
background: rgba(30, 41, 59, .9);
|
||||
border-color: rgba(148, 163, 184, .16);
|
||||
|
||||
.label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user