From 4f8b9923505fec987b576cea328f438a7018ceaf Mon Sep 17 00:00:00 2001 From: Timi Date: Thu, 13 Aug 2026 20:49:50 +0800 Subject: [PATCH] fix tag style --- src/api/CustomerAPI.ts | 17 +++- src/components/TagSelector.vue | 5 +- src/layout/MainLayout.vue | 31 +++++- src/pages/customer/CustomerList.vue | 96 +++++++++++++++++-- .../customer/component/CustomerTagCard.vue | 2 +- src/pages/tag/TagList.vue | 6 +- 6 files changed, 140 insertions(+), 17 deletions(-) diff --git a/src/api/CustomerAPI.ts b/src/api/CustomerAPI.ts index b895d30..b0b020d 100644 --- a/src/api/CustomerAPI.ts +++ b/src/api/CustomerAPI.ts @@ -4,8 +4,21 @@ import { getCurrentStoreId, withCurrentStoreId, withCurrentStorePage } from "@/u const BASE_URI = "/gao/customer"; -async function list(page: Page): Promise> { - return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(page)); +export type CustomerPage = Page & { + keyword?: string; + storeId?: string; + tagIdList?: string[]; +}; + +async function list(page: CustomerPage): Promise> { + const req = withCurrentStorePage(page); + return await axios.post(`${BASE_URI}/list`, { + index: req.index, + size: req.size, + keyword: page.keyword || page.likesExample?.code || page.likesExample?.name || page.likesExample?.telephone, + storeId: page.storeId || req.equalsExample?.storeId, + tagIdList: page.tagIdList || [] + }); } async function detail(id: string): Promise { diff --git a/src/components/TagSelector.vue b/src/components/TagSelector.vue index 4c890bd..9b04e83 100644 --- a/src/components/TagSelector.vue +++ b/src/components/TagSelector.vue @@ -39,7 +39,7 @@ :key="item.id" class="item" :theme="isSelected(item) ? 'primary' : 'default'" - size="extra-large" + size="large" :variant="isSelected(item) ? 'dark' : 'light-outline'" @click="toggleTag(item)" > @@ -219,7 +219,7 @@ watch(() => props.visible, async (value) => { .search-row { gap: .5rem; display: flex; - padding: .5rem 1rem; + padding: 1rem; align-items: center; .input { @@ -245,7 +245,6 @@ watch(() => props.visible, async (value) => { margin: 0; min-width: 5rem; max-width: 100%; - border-radius: .75rem; justify-content: center; } diff --git a/src/layout/MainLayout.vue b/src/layout/MainLayout.vue index aaa4fe3..7002603 100644 --- a/src/layout/MainLayout.vue +++ b/src/layout/MainLayout.vue @@ -59,11 +59,14 @@ import PageTransition from "@/components/PageTransition.vue"; import {useNavBarStore} from "@/store/navBarStore"; import {useTabBarStore} from "@/store/tabBarStore"; +import { permissionCodes } from "@/utils/permission"; +import { useUserStore } from "timi-web"; const route = useRoute(); const router = useRouter(); const navBarStore = useNavBarStore(); const tabBarStore = useTabBarStore(); +const userStore = useUserStore(); // ---------- 导航高度 ---------- @@ -113,17 +116,32 @@ const navBarHeight = computed(() => { // ---------- Tab 切换 ---------- +type TabItem = { + value: string; + icon: string; + text: string; + permission?: string; +}; + const tabVal = ref(route.path); -const list = [ +const tabList: TabItem[] = [ { value: "/customers", icon: "usergroup", text: "客户" }, - { value: "/store", icon: "shop-3", text: "门店" }, + { value: "/store", icon: "shop-3", text: "门店", permission: permissionCodes.stat.read }, { value: "/management", icon: "setting-1", text: "管理" } ]; +const list = computed(() => tabList.filter(item => !item.permission || userStore.hasPermissions(item.permission))); watch(() => route.path, (newPath: string) => { tabVal.value = resolveTabValue(newPath); }, { immediate: true }); +watch(list, () => { + const visiblePath = resolveVisibleTabValue(route.path); + if (tabVal.value !== visiblePath) { + tabVal.value = visiblePath; + } +}, { immediate: true }); + function resolveTabValue(path: string): string { if (path.startsWith("/customers")) { return "/customers"; @@ -140,6 +158,15 @@ function resolveTabValue(path: string): string { return path; } +function resolveVisibleTabValue(path: string): string { + const tabPath = resolveTabValue(path); + if (list.value.some(item => item.value === tabPath)) { + return tabPath; + } + + return list.value[0]?.value || tabPath; +} + function onChangeTab(value: string | number): void { const path = String(value); if (resolveTabValue(route.path) === path) { diff --git a/src/pages/customer/CustomerList.vue b/src/pages/customer/CustomerList.vue index da7dc88..1123a14 100644 --- a/src/pages/customer/CustomerList.vue +++ b/src/pages/customer/CustomerList.vue @@ -16,6 +16,28 @@ clearable borderless /> + + + + + +
+ + 清空
+ @@ -95,10 +122,12 @@ 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 TagSelector from "@/components/TagSelector.vue"; import { useCurrentStoreStore } from "@/store/currentStoreStore"; import type { Customer } from "@/types/Customer"; +import type { Tag } from "@/types/Tag"; import { getCustomerPhotoURL } from "@/utils/customer"; -import { gaoRoleCodes, permissionCodes } from "@/utils/permission"; +import { corePermissionCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission"; defineOptions({ name: "CustomerList" @@ -110,6 +139,7 @@ const currentStoreStore = useCurrentStoreStore(); const isRefreshing = ref(false); const isExporting = ref(false); const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager)); +const canReadTag = computed(() => userStore.hasPermissions(corePermissionCodes.tag.read)); // ---------- 门店筛选 ---------- @@ -133,11 +163,35 @@ async function selectStoreFilter(value: DropdownValue): Promise { await refresh(); } +// ---------- 标签筛选 ---------- + +const selectedTagList = ref([]); +const selectedTagIdList = ref([]); +const tagSelectorVisible = ref(false); +const tagFilterText = computed(() => selectedTagList.value.length ? `${selectedTagList.value.length} 个标签` : "标签"); + +function getTagName(tag: Tag): string { + return tag.name || tag.zhCN || "未命名标签"; +} + +async function handleTagFilterChanged(tagList: Tag[]): Promise { + selectedTagList.value = tagList; + selectedTagIdList.value = tagList.map(item => item.id || "").filter(Boolean); + await refresh(); +} + +async function clearTagFilter(): Promise { + selectedTagList.value = []; + selectedTagIdList.value = []; + await refresh(); +} + // ---------- 分页 ---------- const keyword = ref(); const isLoading = ref(false); const isNoMore = ref(false); +let keywordTimer: number | undefined; const pageResult = reactive>({ total: 0, list: [] @@ -150,11 +204,9 @@ const page = reactive>({ const fetch = Toolkit.debounce(async () => { const result = await CustomerAPI.list({ ...page, - likesExample: { - code: keyword.value, - name: keyword.value, - telephone: keyword.value - } + keyword: keyword.value, + storeId: selectedStoreId.value, + tagIdList: selectedTagIdList.value }); pageResult.total = result.total; pageResult.list = pageResult.list.concat(result.list); @@ -187,6 +239,17 @@ async function loadData(): Promise { onMounted(loadData); +watch(keyword, async () => { + window.clearTimeout(keywordTimer); + keywordTimer = window.setTimeout(async () => { + await refresh(); + }, 300); +}); + +onUnmounted(() => { + window.clearTimeout(keywordTimer); +}); + // ---------- 导航 ---------- async function toCreate(): Promise { @@ -242,6 +305,27 @@ async function doExport(): Promise { padding: .5rem 1rem; border-radius: 1rem; } + + .tag-filter { + --td-button-border-radius: .875rem; + + flex: 0 0 auto; + } + } + + .selected-tags { + gap: .5rem; + z-index: 9; + display: flex; + padding: .5rem 1rem; + overflow: auto; + position: sticky; + top: calc(var(--app-nav-height) + 3.5rem); + box-shadow: var(--td-shadow-1); + + :deep(.t-tag) { + flex: 0 0 auto; + } } .list-wrapper { diff --git a/src/pages/customer/component/CustomerTagCard.vue b/src/pages/customer/component/CustomerTagCard.vue index c47b298..c0d03c5 100644 --- a/src/pages/customer/component/CustomerTagCard.vue +++ b/src/pages/customer/component/CustomerTagCard.vue @@ -38,7 +38,7 @@ const emit = defineEmits<{ edit: []; }>(); -const maxRowCount = 5; +const maxRowCount = 3; const tagRowList = computed(() => { const rowCount = Math.min(maxRowCount, props.tagList.length); const rowList = Array.from({ length: rowCount }, () => [] as Tag[]); diff --git a/src/pages/tag/TagList.vue b/src/pages/tag/TagList.vue index bdd6806..6f1345f 100644 --- a/src/pages/tag/TagList.vue +++ b/src/pages/tag/TagList.vue @@ -216,12 +216,12 @@ onMounted(loadTags); display: flex; min-width: 5rem; max-width: 100%; - padding: .5rem .75rem; - font-size: .875rem; + padding: .25rem .75rem; + font-size: 1rem; min-height: 2rem; border: 1px solid var(--td-brand-color); align-items: center; - border-radius: .75rem; + border-radius: .25rem; justify-content: center; .name {