fix: convert apps/website from submodule to regular directory

This commit is contained in:
yeuimu
2026-07-16 01:42:45 +08:00
parent f12ae12ad3
commit 9595030625
341 changed files with 50808 additions and 1 deletions
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="w-full">
<AppHeader />
<HeroBanner />
<TrustSection />
<StepProcess />
<PodProducts />
<FeatureCards />
<WhyInkReach />
<CompanyProfile />
<CustomerCases />
<CtaBanner />
<AppFooter />
</div>
</template>
+387
View File
@@ -0,0 +1,387 @@
<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>