feat(product-center): implement Figma-designed UI, upload module, and website tests

- 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
This commit is contained in:
yeuimu
2026-08-20 14:32:03 +08:00
parent b5fc88f3fa
commit 79fabd85f7
107 changed files with 5364 additions and 2601 deletions
+41 -36
View File
@@ -1,5 +1,8 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
const activeStep = ref(0);
const STEP_INTERVAL_MS = 2000;
const steps = [
{ num: '1', title: '选择产品', desc: '海量精选产品,满足多元定制需求', image: '/01步骤-选择产品.png' },
@@ -10,63 +13,50 @@ const steps = [
const selectStep = (idx: number) => {
activeStep.value = idx;
startStepTimer();
};
let timer: ReturnType<typeof setInterval> | null = null;
let stepTimer: ReturnType<typeof setInterval> | null = null;
const startAutoPlay = () => {
stopAutoPlay();
timer = setInterval(() => {
function goToLogin(): void {
window.location.href = 'https://inkpod.vip/user/login';
}
function startStepTimer(): void {
if (stepTimer) clearInterval(stepTimer);
stepTimer = setInterval(() => {
activeStep.value = (activeStep.value + 1) % steps.length;
}, 3000);
};
}, STEP_INTERVAL_MS);
}
const stopAutoPlay = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
};
onMounted(startStepTimer);
const pauseAutoPlay = () => {
stopAutoPlay();
};
const resumeAutoPlay = () => {
startAutoPlay();
};
onMounted(startAutoPlay);
onUnmounted(stopAutoPlay);
onBeforeUnmount(() => {
if (stepTimer) clearInterval(stepTimer);
});
</script>
<template>
<section class="w-full flex flex-col gap-8 lg:gap-10 items-center p-6 lg:p-10">
<div class="text-2xl md:text-4xl lg:text-6xl text-center">4 步开始你的按需定制生意</div>
<section class="step-section w-full flex flex-col gap-8 lg:gap-10 items-center p-6 lg:p-10">
<div class="text-[48px] font-extrabold md:text-4xl lg:text-6xl text-center">4步开始你的按需定制生意</div>
<div class="flex flex-col lg:flex-row gap-5 w-full max-w-7xl items-stretch">
<div class="flex flex-row lg:flex-col gap-3 lg:gap-10 lg:p-5 lg:w-5/12 overflow-x-auto lg:overflow-visible justify-center"
@mouseenter="pauseAutoPlay"
@mouseleave="resumeAutoPlay"
>
<div class="flex flex-row lg:flex-col gap-3 lg:gap-8 lg:p-5 lg:w-5/12 overflow-x-auto lg:overflow-visible justify-center">
<div
v-for="(step, idx) in steps"
:key="idx"
class="flex-shrink-0 flex flex-col gap-2 lg:gap-5 cursor-pointer group min-w-[140px] lg:min-w-0"
@click="selectStep(idx)"
@mouseenter="activeStep = idx"
@mouseenter="selectStep(idx)"
>
<p class="text-xl lg:text-3xl font-bold">{{ step.num }} {{ step.title }}</p>
<p class="step-title text-xl lg:text-4xl">{{ step.num }} {{ step.title }}</p>
<p class="hidden lg:block indent-10 text-gray-500 text-xl">{{ step.desc }}</p>
<div class="h-0.5 w-full rounded-full bg-gray-200 overflow-hidden">
<div
class="h-full rounded-full transition-all duration-500"
:class="activeStep === idx ? 'w-1/2 bg-primary' : 'w-0 bg-primary'"
></div>
<div v-if="activeStep === idx" class="step-progress h-full rounded-full bg-primary"></div>
</div>
</div>
<div class="hidden lg:flex items-center pt-6">
<button class="text-xl lg:text-2xl text-white bg-primary rounded-lg px-10 lg:px-14 py-3 lg:py-4 hover:opacity-90 transition-opacity">
<button type="button" class="text-xl lg:text-2xl text-white bg-primary rounded-lg px-10 lg:px-14 py-3 lg:py-4 hover:opacity-90 transition-opacity" @click="goToLogin">
立即开始
</button>
</div>
@@ -78,6 +68,7 @@ onUnmounted(stopAutoPlay);
:key="'img-' + idx"
:src="step.image"
:alt="step.title"
:data-testid="`step-image-${idx}`"
class="w-full h-full object-contain transition-opacity duration-500 absolute inset-0"
:class="activeStep === idx ? 'opacity-100' : 'opacity-0 pointer-events-none'"
/>
@@ -85,9 +76,23 @@ onUnmounted(stopAutoPlay);
</div>
<div class="flex lg:hidden">
<button class="text-xl text-white bg-primary rounded-lg px-10 py-3 hover:opacity-90 transition-opacity">
<button type="button" class="text-xl text-white bg-primary rounded-lg px-10 py-3 hover:opacity-90 transition-opacity" @click="goToLogin">
立即开始
</button>
</div>
</section>
</template>
<style scoped>
.step-progress { width: 0; animation: fill-step-progress 2s linear forwards; }
@keyframes fill-step-progress {
to { width: 100%; }
}
@media (min-width: 1024px) {
.step-section { height: 935px; padding-top: 54px; }
.step-section > div:first-child { font-size: 48px; }
.step-section > div:nth-child(2) { height: 708px; }
}
</style>