Files
yeuimu 6c61a4e871 feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
2026-08-26 14:23:09 +08:00

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>