优化商品图片显示

This commit is contained in:
Qi 2025-08-10 21:46:20 +08:00
parent 08bdd86007
commit 7e1e542a6c
2 changed files with 407 additions and 33 deletions

View File

@ -20,22 +20,34 @@
<!-- 产品图片 --> <!-- 产品图片 -->
<div class="product-images"> <div class="product-images">
<div class="main-image"> <div class="main-image">
<div class="image-placeholder"> <img
<svg viewBox="0 0 400 400" width="100%" height="400"> v-if="product.mainImage"
<rect width="400" height="400" :fill="product.color" opacity="0.2"/> :src="getFileAccessHttpUrl(product.mainImage)"
<circle cx="200" cy="200" r="80" :fill="product.color" opacity="0.5"/> :alt="product.productName || product.name"
<text x="200" y="210" text-anchor="middle" :fill="product.color" class="product-main-image"
font-size="24" font-weight="bold">{{ (product.productName || product.name || '产品').substring(0, 2) }}</text> @click="openImagePreview(getAllProductImages(), 0)"
@error="handleImageError"
/>
<div v-else class="image-placeholder">
<svg width="120" height="120" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21,15 16,10 5,21"/>
</svg> </svg>
</div> </div>
</div> </div>
<div class="thumbnail-list"> <div v-if="getAllProductImages().length > 1" class="thumbnail-list">
<div v-for="i in 4" :key="i" class="thumbnail" :class="{ active: i === 1 }"> <div
<svg viewBox="0 0 100 100" width="100%" height="80"> v-for="(image, index) in getAllProductImages().slice(0, 5)"
<rect width="100" height="100" :fill="product.color" opacity="0.3"/> :key="index"
<text x="50" y="55" text-anchor="middle" :fill="product.color" class="thumbnail"
font-size="12" font-weight="bold">{{ i }}</text> :class="{ active: index === currentImageIndex }"
</svg> @click="openImagePreview(getAllProductImages(), index)"
>
<img :src="getFileAccessHttpUrl(image)" :alt="`${product.productName || product.name} - 图片${index + 1}`" />
<div class="thumb-overlay">
<el-icon><ZoomIn /></el-icon>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -266,13 +278,71 @@
<el-icon class="is-loading" size="50"><Loading /></el-icon> <el-icon class="is-loading" size="50"><Loading /></el-icon>
<p>加载中...</p> <p>加载中...</p>
</div> </div>
<!-- 图片预览对话框 -->
<el-dialog
v-model="imagePreviewVisible"
title="图片预览"
width="80%"
top="5vh"
class="image-preview-dialog"
@close="closeImagePreview"
>
<div v-if="previewImages.length > 0" class="image-preview-container">
<div class="preview-main">
<img
:src="previewImages[currentImageIndex]"
:alt="`预览图片 ${currentImageIndex + 1}`"
class="preview-image"
/>
</div>
<!-- 图片导航 -->
<div v-if="previewImages.length > 1" class="preview-navigation">
<el-button
:disabled="currentImageIndex === 0"
@click="currentImageIndex--"
circle
size="large"
>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<span class="image-counter">
{{ currentImageIndex + 1 }} / {{ previewImages.length }}
</span>
<el-button
:disabled="currentImageIndex === previewImages.length - 1"
@click="currentImageIndex++"
circle
size="large"
>
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<!-- 缩略图导航 -->
<div v-if="previewImages.length > 1" class="preview-thumbnails">
<div
v-for="(image, index) in previewImages"
:key="index"
class="preview-thumb"
:class="{ active: index === currentImageIndex }"
@click="currentImageIndex = index"
>
<img :src="image" :alt="`缩略图 ${index + 1}`" />
</div>
</div>
</div>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useMainStore } from '../stores' import { useMainStore } from '../stores'
import { User, StarFilled, Loading } from '@element-plus/icons-vue' import { User, StarFilled, Loading, ZoomIn, ArrowLeft, ArrowRight, Picture } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { productApi } from '../api/product' import { productApi } from '../api/product'
@ -285,6 +355,11 @@ const product = ref<any>(null)
const quantity = ref(1) const quantity = ref(1)
const activeTab = ref('details') const activeTab = ref('details')
//
const imagePreviewVisible = ref(false)
const previewImages = ref<string[]>([])
const currentImageIndex = ref(0)
// //
const categories = ref([ const categories = ref([
{ id: 'handicrafts', name: '手工艺品' }, { id: 'handicrafts', name: '手工艺品' },
@ -464,6 +539,88 @@ const getRandomColor = () => {
const colors = ['#8b4513', '#2d5016'] const colors = ['#8b4513', '#2d5016']
return colors[Math.floor(Math.random() * colors.length)] return colors[Math.floor(Math.random() * colors.length)]
} }
// +
const getAllProductImages = () => {
const images = []
//
if (product.value?.mainImage) {
images.push(product.value.mainImage)
}
//
if (product.value?.images) {
const productImages = getProductImages(product.value.images)
images.push(...productImages)
}
return images
}
//
const getProductImages = (imagesStr: string) => {
if (!imagesStr) return []
return imagesStr.split(',').map(img => img.trim()).filter(img => img)
}
// 访URL
const getFileAccessHttpUrl = (filePath: string) => {
if (!filePath) {
console.warn('getFileAccessHttpUrl: 文件路径为空')
return ''
}
console.log('getFileAccessHttpUrl: 原始路径 =', filePath)
// HTTP URL
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
console.log('getFileAccessHttpUrl: 完整URL直接返回 =', filePath)
return filePath
}
// blob URL
if (filePath.startsWith('blob:')) {
console.log('getFileAccessHttpUrl: blob URL直接返回 =', filePath)
return filePath
}
// 访
const baseUrl = 'http://localhost:8080/jeecg-boot/sys/common/static/'
//
let cleanPath = filePath.startsWith('/') ? filePath.substring(1) : filePath
//
if (!cleanPath.includes('/')) {
cleanPath = 'product/' + cleanPath
}
const finalUrl = baseUrl + cleanPath
console.log('getFileAccessHttpUrl: 最终URL =', finalUrl)
return finalUrl
}
//
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
console.error('图片加载失败:', img.src)
//
}
//
const openImagePreview = (images: string[], index: number = 0) => {
previewImages.value = images.map(img => getFileAccessHttpUrl(img))
currentImageIndex.value = index
imagePreviewVisible.value = true
}
//
const closeImagePreview = () => {
imagePreviewVisible.value = false
previewImages.value = []
currentImageIndex.value = 0
}
</script> </script>
<style scoped> <style scoped>
@ -516,38 +673,122 @@ const getRandomColor = () => {
.main-image { .main-image {
width: 100%; width: 100%;
margin-bottom: 1rem; margin-bottom: 1rem;
border-radius: 8px; border-radius: 16px;
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
cursor: pointer;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
position: relative;
}
.main-image::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 49%, rgba(255,255,255,0.1) 50%, transparent 51%);
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.main-image:hover::before {
opacity: 1;
}
.product-main-image {
width: 100%;
height: 450px;
object-fit: cover;
transition: all 0.4s ease;
border-radius: 16px;
}
.product-main-image:hover {
transform: scale(1.02);
} }
.image-placeholder { .image-placeholder {
width: 100%; width: 100%;
height: 400px; height: 450px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--background-light); background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border: 2px dashed #dee2e6;
border-radius: 16px;
}
.image-placeholder svg {
opacity: 0.5;
transition: opacity 0.3s ease;
}
.main-image:hover .image-placeholder svg {
opacity: 0.7;
} }
.thumbnail-list { .thumbnail-list {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
flex-wrap: wrap;
} }
.thumbnail { .thumbnail {
width: 80px; width: 90px;
height: 80px; height: 90px;
border-radius: 4px; border-radius: 12px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
border: 2px solid transparent; border: 3px solid transparent;
transition: border-color 0.3s ease; transition: all 0.3s ease;
position: relative;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
.thumbnail.active, .thumbnail img {
.thumbnail:hover { width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.thumbnail:hover img {
transform: scale(1.1);
}
.thumbnail.active {
border-color: var(--primary-color); border-color: var(--primary-color);
box-shadow: 0 4px 12px rgba(var(--primary-color-rgb), 0.3);
transform: translateY(-2px);
}
.thumbnail:hover {
border-color: var(--secondary-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.thumb-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
color: white;
}
.thumbnail:hover .thumb-overlay {
opacity: 1;
} }
/* 产品信息 */ /* 产品信息 */
@ -981,4 +1222,78 @@ const getRandomColor = () => {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
} }
} }
/* 图片预览对话框样式 */
.image-preview-dialog {
.el-dialog__body {
padding: 0;
}
}
.image-preview-container {
display: flex;
flex-direction: column;
align-items: center;
}
.preview-main {
width: 100%;
max-height: 60vh;
display: flex;
justify-content: center;
align-items: center;
background: #f5f5f5;
margin-bottom: 1rem;
}
.preview-image {
max-width: 100%;
max-height: 60vh;
object-fit: contain;
}
.preview-navigation {
display: flex;
align-items: center;
gap: 2rem;
margin-bottom: 1rem;
}
.image-counter {
font-size: 1.1rem;
font-weight: 500;
color: var(--text-color);
}
.preview-thumbnails {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
max-width: 100%;
overflow-x: auto;
padding: 0.5rem;
}
.preview-thumb {
width: 60px;
height: 60px;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.3s ease;
flex-shrink: 0;
}
.preview-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.preview-thumb.active,
.preview-thumb:hover {
border-color: var(--primary-color);
}
</style> </style>

View File

@ -63,12 +63,18 @@
@click="$router.push(`/product/${product.id}`)" @click="$router.push(`/product/${product.id}`)"
> >
<div class="product-image"> <div class="product-image">
<div class="image-placeholder"> <img
<svg viewBox="0 0 250 200" width="100%" height="200"> v-if="product.mainImage && getFileAccessHttpUrl(product.mainImage)"
<rect width="250" height="200" :fill="product.color" opacity="0.2"/> :src="getFileAccessHttpUrl(product.mainImage)"
<circle cx="125" cy="100" r="40" :fill="product.color" opacity="0.5"/> :alt="product.name"
<text x="125" y="110" text-anchor="middle" :fill="product.color" class="product-cover-image"
font-size="14" font-weight="bold">{{ product.productName ? product.productName.substring(0, 2) : '产品' }}</text> @error="handleImageError"
/>
<div v-else class="image-placeholder">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21,15 16,10 5,21"/>
</svg> </svg>
</div> </div>
<div class="product-badges"> <div class="product-badges">
@ -300,6 +306,34 @@ const onCategoryChange = () => {
getProducts() getProducts()
} }
//
const getFileAccessHttpUrl = (path: string) => {
if (!path) return ''
// HTTP URLblob URL
if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('blob:')) {
return path
}
// URL
if (path.startsWith('/')) {
return `http://localhost:8080/jeecg-boot/sys/common/static${path}`
}
//
return `http://localhost:8080/jeecg-boot/sys/common/static/${path}`
}
//
const handleImageError = (event: Event) => {
const img = event.target as HTMLImageElement
img.style.display = 'none'
const placeholder = img.nextElementSibling as HTMLElement
if (placeholder) {
placeholder.style.display = 'flex'
}
}
// //
onMounted(() => { onMounted(() => {
getCategories() getCategories()
@ -405,7 +439,22 @@ onMounted(() => {
.product-image { .product-image {
position: relative; position: relative;
width: 100%; width: 100%;
height: 200px; height: 250px;
border-radius: 12px;
overflow: hidden;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.product-cover-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.3s ease;
}
.product-cover-image:hover {
transform: scale(1.05);
} }
.image-placeholder { .image-placeholder {
@ -414,7 +463,17 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--background-light); background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border: 2px dashed #dee2e6;
}
.image-placeholder svg {
opacity: 0.6;
transition: opacity 0.3s ease;
}
.product-image:hover .image-placeholder svg {
opacity: 0.8;
} }
.product-badges { .product-badges {