388 lines
7.7 KiB
Vue
388 lines
7.7 KiB
Vue
<script setup lang="ts">
|
|
import type { Tag, Country } from '~/composables/useProductCenter';
|
|
import { sortTagsByGroup } from '~/composables/useProductCenter';
|
|
|
|
useSeoMeta({
|
|
title: '产品中心 | InkReach',
|
|
description:
|
|
'InkReach 产品中心:按品类和国家筛选全球 POD 定制商品,支持关键词搜索、分页浏览与多维筛选。',
|
|
});
|
|
|
|
const stickyTopRef = ref<HTMLElement | null>(null);
|
|
const stickySpacerHeight = ref(152);
|
|
|
|
let resizeObserver: ResizeObserver | null = null;
|
|
|
|
onMounted(() => {
|
|
if (!stickyTopRef.value) return;
|
|
const updateHeight = () => {
|
|
if (stickyTopRef.value) {
|
|
stickySpacerHeight.value = stickyTopRef.value.offsetHeight;
|
|
}
|
|
};
|
|
updateHeight();
|
|
resizeObserver = new ResizeObserver(updateHeight);
|
|
resizeObserver.observe(stickyTopRef.value);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
resizeObserver?.disconnect();
|
|
});
|
|
|
|
const {
|
|
categories,
|
|
countries,
|
|
tags,
|
|
products,
|
|
total,
|
|
loading,
|
|
query,
|
|
loadFilters,
|
|
fetchProducts,
|
|
setCategory,
|
|
setCountry,
|
|
setTag,
|
|
clearTags,
|
|
setKeyword,
|
|
setPage,
|
|
setPageSize,
|
|
} = useProductCenter();
|
|
|
|
const searchKeyword = ref('');
|
|
|
|
await loadFilters();
|
|
await fetchProducts();
|
|
|
|
watch(
|
|
() => [
|
|
query.countryId,
|
|
query.categoryId,
|
|
[...query.tagIds],
|
|
query.keyword,
|
|
query.page,
|
|
query.pageSize,
|
|
],
|
|
() => {
|
|
fetchProducts();
|
|
},
|
|
);
|
|
|
|
function onSidebarSelect(id: string | null): void {
|
|
setCategory(id);
|
|
}
|
|
|
|
function onCountrySelect(id: string | null): void {
|
|
setCountry(id);
|
|
}
|
|
|
|
function onToggleTag(id: string): void {
|
|
setTag(id);
|
|
}
|
|
|
|
function onSearch(): void {
|
|
setKeyword(searchKeyword.value);
|
|
}
|
|
|
|
function onPageChange(page: number): void {
|
|
setPage(page);
|
|
if (typeof window !== 'undefined') {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
}
|
|
|
|
function onPageSizeChange(size: number): void {
|
|
setPageSize(size);
|
|
}
|
|
|
|
function clearAllFilters(): void {
|
|
setCategory(null);
|
|
setCountry(null);
|
|
clearTags();
|
|
searchKeyword.value = '';
|
|
setKeyword('');
|
|
}
|
|
|
|
const activeTags = computed(() => {
|
|
const list = query.tagIds
|
|
.map((id) => tags.value.find((t: Tag) => t.id === id))
|
|
.filter(Boolean) as Tag[];
|
|
return sortTagsByGroup(list);
|
|
});
|
|
|
|
const activeCountryName = computed(() => {
|
|
if (!query.countryId) return null;
|
|
return countries.value.find((c: Country) => c.id === query.countryId)?.name ?? null;
|
|
});
|
|
|
|
const showCountry = computed(() => !query.countryId);
|
|
|
|
const hasActiveFilters = computed(() => {
|
|
return query.tagIds.length > 0 || Boolean(query.countryId || query.keyword);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="product-page">
|
|
<AppHeader />
|
|
|
|
<main class="content">
|
|
<aside class="sidebar-area">
|
|
<ProductSidebar
|
|
:categories="categories"
|
|
:active-category-id="query.categoryId ?? null"
|
|
@update:active-category-id="onSidebarSelect"
|
|
/>
|
|
</aside>
|
|
|
|
<section class="main-area">
|
|
<div ref="stickyTopRef" class="sticky-top">
|
|
<div class="country-section">
|
|
<ProductCountryFilter
|
|
:countries="countries"
|
|
:active-country-id="query.countryId ?? null"
|
|
@update:active-country-id="onCountrySelect"
|
|
/>
|
|
</div>
|
|
|
|
<div class="filter-section">
|
|
<div class="filter-left">
|
|
<ProductTagFilter
|
|
:tags="tags"
|
|
:selected-tag-ids="query.tagIds"
|
|
@toggle-tag="onToggleTag"
|
|
/>
|
|
</div>
|
|
|
|
<div class="selected-tags">
|
|
<span
|
|
v-for="tag in activeTags"
|
|
:key="'tag-' + tag.id"
|
|
class="sel-tag"
|
|
:style="{ color: tag.fontColor || tag.color || '#ff6a00', background: (tag.color || '#ff6a00') + '4d', borderColor: (tag.color || '#ff6a00') + '99' }"
|
|
>
|
|
{{ tag.name }}
|
|
<button type="button" class="tag-close" @click="onToggleTag(tag.id)"></button>
|
|
</span>
|
|
<span v-if="query.keyword" class="sel-tag">
|
|
"{{ query.keyword }}"
|
|
<button type="button" class="tag-close" @click="searchKeyword = ''; onSearch()"></button>
|
|
</span>
|
|
<span v-if="activeCountryName" class="sel-tag">
|
|
{{ activeCountryName }}
|
|
<button type="button" class="tag-close" @click="onCountrySelect(null)"></button>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="filter-right">
|
|
<ProductFilterBar
|
|
v-model:keyword="searchKeyword"
|
|
@search="onSearch"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sticky-spacer" :style="{ height: stickySpacerHeight + 'px' }"></div>
|
|
|
|
<ProductGrid :products="products" :loading="loading" :show-country="showCountry" />
|
|
|
|
<div v-if="!loading && products.length === 0 && hasActiveFilters" class="clear-all-area">
|
|
<button type="button" class="clear-all-btn" @click="clearAllFilters">
|
|
清空筛选
|
|
</button>
|
|
</div>
|
|
|
|
<ProductPagination
|
|
v-if="!loading && total > 0"
|
|
:total="total"
|
|
:page="query.page"
|
|
:page-size="query.pageSize"
|
|
@update:page="onPageChange"
|
|
@update:page-size="onPageSizeChange"
|
|
/>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.product-page {
|
|
width: 100%;
|
|
min-height: 100vh;
|
|
background: #fff;
|
|
padding-top: 80px;
|
|
}
|
|
|
|
.content {
|
|
display: flex;
|
|
}
|
|
|
|
.sidebar-area {
|
|
position: fixed;
|
|
left: 0;
|
|
top: 80px;
|
|
bottom: 0;
|
|
width: 240px;
|
|
border-right: 1px solid #eee;
|
|
overflow-y: auto;
|
|
scrollbar-width: none;
|
|
-ms-overflow-style: none;
|
|
}
|
|
|
|
.sidebar-area::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.main-area {
|
|
min-width: 0;
|
|
background: #fff;
|
|
width: 100%;
|
|
margin-left: 240px;
|
|
padding: 0 32px 24px;
|
|
}
|
|
|
|
.sticky-spacer {
|
|
height: 152px;
|
|
}
|
|
|
|
.sticky-top {
|
|
position: fixed;
|
|
left: 240px;
|
|
right: 0;
|
|
top: 80px;
|
|
z-index: 10;
|
|
background: #fff;
|
|
padding: 20px 32px 0;
|
|
}
|
|
|
|
.country-section {
|
|
padding: 0 0 14px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.filter-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 14px 0;
|
|
}
|
|
|
|
.filter-left {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.selected-tags {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.sel-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
height: 32px;
|
|
padding: 0 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid;
|
|
font-size: 13px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tag-close {
|
|
position: relative;
|
|
width: 14px;
|
|
height: 14px;
|
|
border: 0;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
opacity: 0.6;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.tag-close:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.tag-close::before,
|
|
.tag-close::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 6px;
|
|
top: 1px;
|
|
width: 1.5px;
|
|
height: 12px;
|
|
background: currentColor;
|
|
}
|
|
|
|
.tag-close::before {
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.tag-close::after {
|
|
transform: rotate(-45deg);
|
|
}
|
|
|
|
.filter-right {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.clear-all-area {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 40px 0;
|
|
}
|
|
|
|
.clear-all-btn {
|
|
height: 44px;
|
|
padding: 0 26px;
|
|
border: 0;
|
|
border-radius: 10px;
|
|
background: #ff6a00;
|
|
color: #fff;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.clear-all-btn:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.sidebar-area {
|
|
position: relative;
|
|
width: 100%;
|
|
top: 0;
|
|
border-right: 0;
|
|
border-bottom: 1px solid #eee;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.main-area {
|
|
margin-left: 0;
|
|
}
|
|
|
|
.sticky-top {
|
|
position: relative;
|
|
left: 0;
|
|
top: 0;
|
|
}
|
|
|
|
.sticky-spacer {
|
|
display: none;
|
|
}
|
|
|
|
.filter-section {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-right {
|
|
width: 100%;
|
|
}
|
|
}
|
|
</style>
|