- Redesign website homepage & product-center per Figma (fonts, logos, hero/footer/customer cases) - Add API upload module (multer) with static serving for uploads/public assets - Add OriginGood.delisted flag and SDS request retry logic - Add admin ImageUpload component and goods import/upload flows - Add vitest suite for website components and composables (32 tests) - Add skills, docs, plans and PRODUCT.md
349 lines
12 KiB
Vue
349 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import type { RecommendCategory, SolutionColumn, NavBanner } from '~/composables/useNavData';
|
|
|
|
const PORTAL_URL = 'https://inkpod.vip/portal/search';
|
|
const SHOW_EXTENDED_NAV = false;
|
|
|
|
const mobileMenuOpen = ref(false);
|
|
const recommendMenuOpen = ref(false);
|
|
const solutionMenuOpen = ref(false);
|
|
const mobileRecommendOpen = ref(false);
|
|
const mobileSolutionOpen = ref(false);
|
|
const mobileRecommendTab = ref(0);
|
|
|
|
const {
|
|
recommendCategories,
|
|
solutionColumns,
|
|
solutionBanner,
|
|
activeRecommendIdx,
|
|
setActiveRecommend,
|
|
} = useNavData();
|
|
|
|
let recommendTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
let solutionTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
function openRecommendMenu(): void {
|
|
if (recommendTimeout) {
|
|
clearTimeout(recommendTimeout);
|
|
recommendTimeout = null;
|
|
}
|
|
recommendMenuOpen.value = true;
|
|
}
|
|
|
|
function closeRecommendMenu(): void {
|
|
recommendTimeout = setTimeout(() => {
|
|
recommendMenuOpen.value = false;
|
|
}, 150);
|
|
}
|
|
|
|
function openSolutionMenu(): void {
|
|
if (solutionTimeout) {
|
|
clearTimeout(solutionTimeout);
|
|
solutionTimeout = null;
|
|
}
|
|
solutionMenuOpen.value = true;
|
|
}
|
|
|
|
function closeSolutionMenu(): void {
|
|
solutionTimeout = setTimeout(() => {
|
|
solutionMenuOpen.value = false;
|
|
}, 150);
|
|
}
|
|
|
|
function toggleMobileMenu(): void {
|
|
mobileMenuOpen.value = !mobileMenuOpen.value;
|
|
}
|
|
|
|
function closeMobileMenu(): void {
|
|
mobileMenuOpen.value = false;
|
|
mobileRecommendOpen.value = false;
|
|
mobileSolutionOpen.value = false;
|
|
}
|
|
|
|
function goToLogin(): void {
|
|
window.location.href = 'https://inkpod.vip/user/login';
|
|
}
|
|
|
|
function toggleMobileRecommend(): void {
|
|
mobileRecommendOpen.value = !mobileRecommendOpen.value;
|
|
mobileSolutionOpen.value = false;
|
|
}
|
|
|
|
function toggleMobileSolution(): void {
|
|
mobileSolutionOpen.value = !mobileSolutionOpen.value;
|
|
mobileRecommendOpen.value = false;
|
|
}
|
|
|
|
const mobileActiveCategory = computed(
|
|
() => recommendCategories.value[mobileRecommendTab.value] ?? null,
|
|
);
|
|
|
|
const route = useRoute();
|
|
const isProductCenter = computed(() => route.path === '/product-center');
|
|
</script>
|
|
|
|
<template>
|
|
<header class="pl-6 md:pl-10 lg:pl-16 pr-6 md:pr-10 lg:pr-20 flex items-center h-20 fixed top-0 left-0 right-0 z-50 bg-white border-b border-gray-100">
|
|
<NuxtLink to="/" aria-label="返回首页">
|
|
<img src="/logo-unified.svg" alt="Inkreach" class="h-12 w-[83px] lg:h-[56px] lg:w-[97px] object-contain" />
|
|
</NuxtLink>
|
|
|
|
<nav class="desktop-nav hidden lg:flex gap-6 flex-1 ml-8 items-center h-20">
|
|
<NuxtLink to="/product-center"
|
|
class="transition-colors"
|
|
:class="isProductCenter
|
|
? 'text-black font-medium'
|
|
: 'text-gray-400 hover:text-primary hover:underline underline-offset-14 decoration-primary decoration-2'"
|
|
>产品中心</NuxtLink>
|
|
<a :href="PORTAL_URL" target="_blank"
|
|
class="text-gray-400 hover:text-primary hover:underline underline-offset-14 decoration-primary decoration-2 transition-colors"
|
|
>在线设计</a>
|
|
|
|
<div
|
|
v-if="SHOW_EXTENDED_NAV"
|
|
class="relative h-full flex items-center"
|
|
@mouseenter="openRecommendMenu"
|
|
@mouseleave="closeRecommendMenu"
|
|
>
|
|
<button
|
|
class="transition-colors flex items-center gap-1"
|
|
:class="[
|
|
recommendMenuOpen ? 'text-primary' : '',
|
|
!recommendMenuOpen ? 'text-gray-400' : '',
|
|
]"
|
|
>
|
|
<span class="hover:underline underline-offset-14 decoration-primary decoration-2">选品推荐</span>
|
|
<i class="fa-solid fa-chevron-down text-xs transition-transform duration-200" :class="recommendMenuOpen ? 'rotate-180' : ''"></i>
|
|
</button>
|
|
<NavMegaMenu
|
|
:categories="recommendCategories"
|
|
:active-index="activeRecommendIdx"
|
|
:visible="recommendMenuOpen"
|
|
@update:active-index="setActiveRecommend"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="SHOW_EXTENDED_NAV"
|
|
class="relative h-full flex items-center"
|
|
@mouseenter="openSolutionMenu"
|
|
@mouseleave="closeSolutionMenu"
|
|
>
|
|
<button
|
|
class="transition-colors flex items-center gap-1"
|
|
:class="[
|
|
solutionMenuOpen ? 'text-primary' : '',
|
|
!solutionMenuOpen ? 'text-gray-400' : '',
|
|
]"
|
|
>
|
|
<span class="hover:underline underline-offset-14 decoration-primary decoration-2">解决方案</span>
|
|
<i class="fa-solid fa-chevron-down text-xs transition-transform duration-200" :class="solutionMenuOpen ? 'rotate-180' : ''"></i>
|
|
</button>
|
|
<NavColumnMenu
|
|
:columns="solutionColumns"
|
|
:banner="solutionBanner"
|
|
:visible="solutionMenuOpen"
|
|
/>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="desktop-nav hidden lg:flex gap-6 items-center">
|
|
<button type="button" @click="goToLogin"
|
|
class="hover:text-primary hover:underline underline-offset-12 decoration-primary decoration-2 transition-colors"
|
|
>登录</button>
|
|
<button type="button" @click="goToLogin"
|
|
class="text-white rounded-lg px-6 py-1.5 self-center hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1 pulse-animation bg-primary"
|
|
>立即开始</button>
|
|
</div>
|
|
|
|
<button
|
|
class="lg:hidden ml-auto p-2"
|
|
@click="toggleMobileMenu"
|
|
aria-label="Toggle menu"
|
|
>
|
|
<i v-if="!mobileMenuOpen" class="fa-solid fa-bars text-2xl"></i>
|
|
<i v-else class="fa-solid fa-xmark text-2xl"></i>
|
|
</button>
|
|
|
|
<Teleport to="body">
|
|
<Transition name="fade">
|
|
<div
|
|
v-if="mobileMenuOpen"
|
|
class="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
|
@click="closeMobileMenu"
|
|
></div>
|
|
</Transition>
|
|
</Teleport>
|
|
|
|
<Transition name="slide">
|
|
<div
|
|
v-if="mobileMenuOpen"
|
|
class="fixed top-0 right-0 h-full w-80 bg-white shadow-2xl z-50 lg:hidden flex flex-col overflow-y-auto"
|
|
>
|
|
<div class="flex justify-between items-center p-6 pb-4">
|
|
<NuxtLink to="/" aria-label="返回首页" @click="closeMobileMenu">
|
|
<img src="/logo-unified.svg" alt="Inkreach" class="h-[56px] w-[97px] object-contain" />
|
|
</NuxtLink>
|
|
<button @click="closeMobileMenu" class="p-2" aria-label="Close menu">
|
|
<i class="fa-solid fa-xmark text-2xl"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="flex flex-col px-6 gap-1 text-lg">
|
|
<NuxtLink
|
|
to="/product-center"
|
|
class="py-3 hover:text-primary transition-colors"
|
|
@click="closeMobileMenu"
|
|
>产品中心</NuxtLink>
|
|
<a :href="PORTAL_URL" target="_blank"
|
|
class="py-3 hover:text-primary transition-colors"
|
|
>在线设计</a>
|
|
|
|
<!-- Temporarily hidden until recommendation content is ready. -->
|
|
<div v-if="SHOW_EXTENDED_NAV">
|
|
<button
|
|
class="w-full py-3 flex items-center justify-between hover:text-primary transition-colors"
|
|
@click="toggleMobileRecommend"
|
|
>
|
|
<span>选品推荐</span>
|
|
<i
|
|
class="fa-solid fa-chevron-down text-xs transition-transform duration-200"
|
|
:class="mobileRecommendOpen ? 'rotate-180' : ''"
|
|
></i>
|
|
</button>
|
|
<Transition name="accordion">
|
|
<div v-if="mobileRecommendOpen" class="pl-3 pb-2">
|
|
<div class="flex gap-2 mb-3 flex-wrap">
|
|
<button
|
|
v-for="(cat, idx) in recommendCategories"
|
|
:key="cat.id"
|
|
class="px-3 py-1.5 rounded-full text-sm transition-colors"
|
|
:class="mobileRecommendTab === idx
|
|
? 'bg-primary text-white'
|
|
: 'bg-inkreach-homepage-1 text-gray-600'"
|
|
@click="mobileRecommendTab = idx"
|
|
>
|
|
{{ cat.title }}
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<a
|
|
v-for="item in mobileActiveCategory?.items ?? []"
|
|
:key="item.id"
|
|
:href="item.link"
|
|
target="_blank"
|
|
class="group block rounded-lg overflow-hidden"
|
|
>
|
|
<div class="w-full h-[70px] rounded-lg overflow-hidden bg-inkreach-homepage-1">
|
|
<img
|
|
:src="item.image"
|
|
:alt="item.title"
|
|
class="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
<p class="mt-1 text-[13px] text-gray-700 group-hover:text-primary truncate">
|
|
{{ item.title }}
|
|
</p>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
|
|
<!-- Temporarily hidden until solution content is ready. -->
|
|
<div v-if="SHOW_EXTENDED_NAV">
|
|
<button
|
|
class="w-full py-3 flex items-center justify-between hover:text-primary transition-colors"
|
|
@click="toggleMobileSolution"
|
|
>
|
|
<span>解决方案</span>
|
|
<i
|
|
class="fa-solid fa-chevron-down text-xs transition-transform duration-200"
|
|
:class="mobileSolutionOpen ? 'rotate-180' : ''"
|
|
></i>
|
|
</button>
|
|
<Transition name="accordion">
|
|
<div v-if="mobileSolutionOpen" class="pl-3 pb-2">
|
|
<div
|
|
v-for="col in solutionColumns"
|
|
:key="col.id"
|
|
class="mb-3"
|
|
>
|
|
<h4 class="text-[15px] font-medium text-gray-800 mb-2">
|
|
{{ col.title }}
|
|
</h4>
|
|
<ul class="space-y-1.5">
|
|
<li v-for="link in col.links" :key="link.id">
|
|
<a
|
|
:href="link.link"
|
|
target="_blank"
|
|
class="text-[14px] text-gray-600 hover:text-primary transition-colors flex items-center gap-1"
|
|
>
|
|
{{ link.title }}
|
|
<span
|
|
v-if="link.tag === 'hot'"
|
|
class="text-[10px] text-white bg-red-500 px-1 py-0.5 rounded leading-none"
|
|
>HOT</span>
|
|
<span
|
|
v-if="link.tag === 'new'"
|
|
class="text-[10px] text-white bg-green-500 px-1 py-0.5 rounded leading-none"
|
|
>NEW</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="flex flex-col gap-4 px-6 mt-auto pb-6 pt-4">
|
|
<button type="button" @click="goToLogin"
|
|
class="py-2.5 text-lg hover:text-primary transition-colors text-center"
|
|
>登录</button>
|
|
<button type="button" @click="goToLogin"
|
|
class="text-white rounded-lg px-6 py-3 text-center hover:shadow-lg transition-all duration-300 pulse-animation bg-primary"
|
|
>立即开始</button>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</header>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.desktop-nav { font-size: 18px; }
|
|
|
|
.fade-enter-active,
|
|
.fade-leave-active {
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
.fade-enter-from,
|
|
.fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.slide-enter-active,
|
|
.slide-leave-active {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
.slide-enter-from,
|
|
.slide-leave-to {
|
|
transform: translateX(100%);
|
|
}
|
|
|
|
.accordion-enter-active,
|
|
.accordion-leave-active {
|
|
transition: all 0.25s ease;
|
|
overflow: hidden;
|
|
}
|
|
.accordion-enter-from,
|
|
.accordion-leave-to {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
}
|
|
.accordion-enter-to,
|
|
.accordion-leave-from {
|
|
max-height: 600px;
|
|
}
|
|
</style>
|