refactor file tree
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.customer.read" class="customer-list">
|
||||
<div class="filter-bar">
|
||||
<t-dropdown-menu v-if="isGlobalManager">
|
||||
<t-dropdown-item
|
||||
v-model="selectedStoreId"
|
||||
:label="storeFilterLabel"
|
||||
:options="storeOptions"
|
||||
@change="selectStoreFilter"
|
||||
/>
|
||||
</t-dropdown-menu>
|
||||
<t-input
|
||||
v-model.trim="keyword"
|
||||
class="text-input"
|
||||
placeholder="搜索编码、姓名、电话"
|
||||
clearable
|
||||
borderless
|
||||
/>
|
||||
</div>
|
||||
<t-pull-down-refresh
|
||||
class="list-wrapper"
|
||||
v-model="isRefreshing"
|
||||
:refresh-timeout="30000"
|
||||
@refresh="refresh"
|
||||
@scrolltolower="fetch"
|
||||
>
|
||||
<t-cell-group class="items" theme="card">
|
||||
<t-cell
|
||||
class="item"
|
||||
v-for="(item, index) in pageResult.list"
|
||||
:key="item.id || item.code || index"
|
||||
arrow
|
||||
@click="router.push(`/management/customers/${item.id}`)"
|
||||
>
|
||||
<template #left-icon>
|
||||
<t-image
|
||||
v-if="getCustomerPhotoURL(item)"
|
||||
class="avatar"
|
||||
:src="getCustomerPhotoURL(item)"
|
||||
:alt="item.name"
|
||||
fit="cover"
|
||||
/>
|
||||
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="header">
|
||||
<div class="left">
|
||||
<div class="text-bold" v-text="item.name" />
|
||||
<gender-icon :value="item.gender" size="1.125rem" />
|
||||
</div>
|
||||
<span class="gray" v-text="item.code || ''" />
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<telephone-link :value="item.telephone" />
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
<div v-if="!pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载客户' : '暂无客户数据'" />
|
||||
<div v-if="pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载更多' : isNoMore ? '已经到底了' : '上拉加载更多'" />
|
||||
</t-pull-down-refresh>
|
||||
<div class="bottom-actions">
|
||||
<t-button
|
||||
v-can="permissionCodes.customer.export"
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
class="export bg-white"
|
||||
:loading="isExporting"
|
||||
:disabled="isExporting"
|
||||
@click="doExport"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="download" />
|
||||
</template>
|
||||
导出
|
||||
</t-button>
|
||||
<t-button
|
||||
v-can="permissionCodes.customer.create"
|
||||
theme="primary"
|
||||
class="create"
|
||||
@click="toCreate"
|
||||
>
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
添加
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { type DropdownValue } from "tdesign-mobile-vue";
|
||||
import { type Page, type PageResult, Time, Toolkit, useUserStore } from "timi-web";
|
||||
import CustomerAPI from "@/api/CustomerAPI";
|
||||
import GenderIcon from "@/components/GenderIcon.vue";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import type { Customer } from "@/types/Customer";
|
||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
|
||||
defineOptions({
|
||||
name: "CustomerList"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const isRefreshing = ref(false);
|
||||
const isExporting = ref(false);
|
||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||
|
||||
// ---------- 门店筛选 ----------
|
||||
|
||||
const selectedStoreId = ref(currentStoreStore.currentStoreId);
|
||||
const storeOptions = computed(() => currentStoreStore.storeList
|
||||
.map(item => ({
|
||||
label: item.name || item.code || "未命名门店",
|
||||
value: item.id || ""
|
||||
}))
|
||||
.filter(item => item.value));
|
||||
const storeFilterLabel = computed(() => storeOptions.value.find(item => item.value === selectedStoreId.value)?.label || "选择门店");
|
||||
|
||||
async function selectStoreFilter(value: DropdownValue): Promise<void> {
|
||||
if (Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storeId = String(value);
|
||||
selectedStoreId.value = storeId;
|
||||
currentStoreStore.selectStore(storeId);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
// ---------- 分页 ----------
|
||||
|
||||
const keyword = ref();
|
||||
const isLoading = ref(false);
|
||||
const isNoMore = ref(false);
|
||||
const pageResult = reactive<PageResult<Customer>>({
|
||||
total: 0,
|
||||
list: []
|
||||
});
|
||||
const page = reactive<Page<Customer>>({
|
||||
index: 0,
|
||||
size: 15
|
||||
});
|
||||
|
||||
const fetch = Toolkit.debounce(async () => {
|
||||
const result = await CustomerAPI.list({
|
||||
...page,
|
||||
likesExample: {
|
||||
code: keyword.value,
|
||||
name: keyword.value,
|
||||
telephone: keyword.value
|
||||
}
|
||||
});
|
||||
pageResult.total = result.total;
|
||||
pageResult.list = pageResult.list.concat(result.list);
|
||||
page.index += 1;
|
||||
isNoMore.value = (result.list || []).length < page.size || pageResult.total <= pageResult.list.length;
|
||||
});
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
isRefreshing.value = true;
|
||||
try {
|
||||
page.index = 0;
|
||||
page.size = 15;
|
||||
pageResult.total = 0;
|
||||
pageResult.list = [];
|
||||
fetch();
|
||||
} finally {
|
||||
isRefreshing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
if (isGlobalManager.value) {
|
||||
await currentStoreStore.loadStoreList();
|
||||
selectedStoreId.value = currentStoreStore.currentStoreId;
|
||||
} else {
|
||||
await currentStoreStore.loadLoginStore();
|
||||
}
|
||||
await fetch();
|
||||
}
|
||||
|
||||
onMounted(loadData);
|
||||
|
||||
// ---------- 导航 ----------
|
||||
|
||||
async function toCreate(): Promise<void> {
|
||||
await router.push({
|
||||
path: "/management/customers/create",
|
||||
query: isGlobalManager.value && selectedStoreId.value ? { storeId: selectedStoreId.value } : undefined
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 导出 ----------
|
||||
|
||||
async function doExport(): Promise<void> {
|
||||
if (isExporting.value) {
|
||||
return;
|
||||
}
|
||||
isExporting.value = true;
|
||||
try {
|
||||
const blob = await CustomerAPI.exportXSSF(page);
|
||||
Toolkit.downloadBlob(blob, `客户列表-${Time.formatDate(new Date())}.xlsx`);
|
||||
Toast.success("导出已开始");
|
||||
} finally {
|
||||
isExporting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.customer-list {
|
||||
padding: calc(var(--app-nav-height)) 0 5.5rem 0;
|
||||
|
||||
.filter-bar {
|
||||
top: var(--app-nav-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
padding: .75rem 1rem;
|
||||
position: sticky;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
padding: 1rem 0 4rem 0;
|
||||
|
||||
.items {
|
||||
|
||||
.item {
|
||||
padding: 10px;
|
||||
|
||||
.avatar {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
gap: .5rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.left {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
.filter-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