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
+154
View File
@@ -0,0 +1,154 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus, Loading } from '@element-plus/icons-vue'
import type { UploadProps } from 'element-plus'
import { uploadApi } from '@/api/upload'
const model = defineModel<string>({ default: '' })
const props = withDefaults(defineProps<{
width?: number
height?: number
shape?: 'square' | 'circle'
label?: string
}>(), {
width: 80,
height: 80,
shape: 'square',
label: '点击上传',
})
const uploading = ref(false)
const previewVisible = ref(false)
const customUpload: UploadProps['httpRequest'] = async (options) => {
const file = options.file as File
uploading.value = true
try {
const result = await uploadApi.uploadImage(file)
model.value = result.url
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || '上传失败')
} finally {
uploading.value = false
}
}
function removeImage() {
model.value = ''
}
function onImgError() {
console.warn('Image failed to load:', model.value)
}
</script>
<template>
<div class="img-upload">
<el-upload
class="img-uploader"
:show-file-list="false"
:http-request="customUpload"
accept="image/*"
>
<div v-if="model" class="img-preview-wrap" :style="{ width: width + 'px', height: height + 'px', borderRadius: shape === 'circle' ? '50%' : '8px' }">
<img :src="model" class="img-preview-img" @click.stop="previewVisible = true" @error="onImgError" />
<div class="img-overlay">
<span @click.stop="previewVisible = true">预览</span>
<span @click.stop="removeImage">删除</span>
</div>
</div>
<div v-else class="img-placeholder" :style="{ width: width + 'px', height: height + 'px', borderRadius: shape === 'circle' ? '50%' : '8px' }">
<el-icon v-if="!uploading" :size="20"><Plus /></el-icon>
<el-icon v-else :size="20" class="is-loading"><Loading /></el-icon>
<span class="img-placeholder-text">{{ uploading ? '上传中' : label }}</span>
</div>
</el-upload>
<el-image-viewer v-if="previewVisible" :url-list="[model]" @close="previewVisible = false" />
</div>
</template>
<style scoped>
.img-upload {
display: inline-block;
}
.img-uploader {
display: inline-block;
}
.img-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
border: 1px dashed #d9d9d9;
background: #fafafa;
color: #a8abb2;
cursor: pointer;
transition: border-color 0.2s;
overflow: hidden;
}
.img-placeholder:hover {
border-color: var(--brand-color, #ff6800);
color: var(--brand-color, #ff6800);
}
.img-placeholder-text {
font-size: 12px;
}
.img-preview-wrap {
position: relative;
overflow: hidden;
cursor: pointer;
}
.img-preview-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.img-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
opacity: 0;
transition: opacity 0.2s;
}
.img-preview-wrap:hover .img-overlay {
opacity: 1;
}
.img-overlay span {
color: #fff;
font-size: 13px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.15);
}
.img-overlay span:hover {
background: rgba(255, 255, 255, 0.3);
}
.is-loading {
animation: rotating 1.5s linear infinite;
}
@keyframes rotating {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>