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:
@@ -1,127 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { Product } from '~/composables/useProductCenter';
|
||||
|
||||
const props = defineProps<{
|
||||
products: Product[];
|
||||
loading: boolean;
|
||||
showCountry?: boolean;
|
||||
}>();
|
||||
|
||||
const skeletonCount = 12;
|
||||
defineProps<{ products: Product[]; loading: boolean; showCountry?: boolean }>();
|
||||
const emit = defineEmits<{ 'clear-filters': []; 'view-all': [] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loading" class="product-grid">
|
||||
<ProductCardSkeleton
|
||||
v-for="n in skeletonCount"
|
||||
:key="`skeleton-${n}`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="products.length === 0" class="empty-state">
|
||||
<div class="empty-art">
|
||||
<div class="empty-box"></div>
|
||||
<div class="empty-lid"></div>
|
||||
<div class="empty-spark spark-1"></div>
|
||||
<div class="empty-spark spark-2"></div>
|
||||
</div>
|
||||
<h2>暂无匹配商品</h2>
|
||||
<p>试试清空筛选条件,或切换其他分类看看。</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="product-grid">
|
||||
<ProductCard
|
||||
v-for="product in products"
|
||||
:key="product.id"
|
||||
:product="product"
|
||||
:show-country="showCountry"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="loading" class="product-grid"><ProductCardSkeleton v-for="index in 8" :key="index" /></div>
|
||||
<div v-else-if="!products.length" class="empty">
|
||||
<img src="/product-empty.svg" alt="" />
|
||||
<h2>未找到相关商品</h2>
|
||||
<p>当前筛选条件下暂无匹配产品,可尝试调整关键词或减少筛选条件。</p>
|
||||
<div class="empty-actions"><button type="button" class="view-all" @click="emit('view-all')">查看全部商品</button><button type="button" class="clear" @click="emit('clear-filters')">清空筛选</button></div>
|
||||
</div>
|
||||
<div v-else class="product-grid"><ProductCard v-for="product in products" :key="product.id" :product="product" :show-country="showCountry" /></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 480px;
|
||||
padding: 80px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-art {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
height: 176px;
|
||||
margin: 0 auto 28px;
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
position: absolute;
|
||||
left: 35px;
|
||||
top: 42px;
|
||||
width: 150px;
|
||||
height: 94px;
|
||||
border-radius: 18px;
|
||||
background: #f2f2f2;
|
||||
transform: perspective(400px) rotateX(58deg) rotateZ(-8deg);
|
||||
}
|
||||
|
||||
.empty-lid {
|
||||
position: absolute;
|
||||
left: 48px;
|
||||
top: 20px;
|
||||
width: 124px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
background: #e8e8e8;
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
|
||||
.empty-spark {
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
background: #ff6a00;
|
||||
}
|
||||
|
||||
.spark-1 {
|
||||
left: 21px;
|
||||
top: 52px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.spark-2 {
|
||||
right: 26px;
|
||||
top: 18px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
color: #777;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.product-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px 24px; }
|
||||
.empty { display: flex; flex-direction: column; align-items: center; min-height: 600px; padding-top: 86px; color: rgba(0,0,0,.6); text-align: center; }.empty img { width: 260px; height: 260px; object-fit: contain; }.empty h2 { margin: 12px 0 10px; color: #000; font-size: 22px; font-weight: 600; }.empty p { margin: 0; font-size: 14px; }.empty-actions { display: flex; gap: 24px; margin-top: 34px; }.empty-actions button { height: 38px; padding: 0 16px; border: 0; border-radius: 60px; font-size: 16px; cursor: pointer; }.empty-actions .view-all { color: #ff6800; background: rgba(255,104,0,.06); }.empty-actions .clear { min-width: 128px; color: #fff; background: #ff6800; }
|
||||
@media (max-width: 1180px) { .product-grid { gap: 16px; } }
|
||||
@media (max-width: 760px) { .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 430px) { .product-grid { grid-template-columns: minmax(0, 1fr); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user