Files
jurong_circle_frontdesk/src/views/ProductSummary.vue
2025-09-04 16:11:03 +08:00

651 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 头部 -->
<div class="header">
<span @click="$router.go(-1)"><返回</span>
<el-icon @click="$router.push(`/product/${firstProduct.id}`)"><ShoppingCart /></el-icon>
</div>
<div class="products-container">
<div class="product-card">
<div class="product-image">
<div v-if="loading || !firstProduct.images" class="carousel-skeleton">
<div class="skeleton-image"></div>
<div class="skeleton-indicators">
<div class="skeleton-dot"></div>
<div class="skeleton-dot"></div>
<div class="skeleton-dot"></div>
</div>
</div>
<el-carousel
v-else
:interval="4000"
indicator-position="outside"
style="min-height: 300px;"
>
<el-carousel-item v-for="(image, index) in firstProduct.images" :key="index">
<img :src="getImageUrl(image)" :alt="firstProduct.name" class="carousel-image" />
</el-carousel-item>
</el-carousel>
</div>
<div class="product-details">
<img :src="getImageUrl(firstProduct.image)" :alt="firstProduct.name" class="small-image" />
<div class="product-info">
<span class="product-name">{{ firstProduct.name }}</span>
<div class="product-price">
<div class="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ firstProduct.rongdou_price }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ firstProduct.points_price }}</span>
</div>
</div>
</div>
<span @click="$router.push(`/product/${firstProduct.id}`)" class="link">详情></span>
</div>
<div class="action">
<button class="button" @click="$router.push(`/product/${firstProduct.id}`)">立即购买</button>
</div>
</div>
<div v-for="product in products.filter(p => p.id !== firstProduct.id)" :key="product.id" class="product-card">
<!-- 轮播图部分 -->
<div class="product-image">
<div v-if="!detailsLoaded" class="carousel-skeleton">
<div class="skeleton-image"></div>
<div class="skeleton-indicators">
<div class="skeleton-dot"></div>
<div class="skeleton-dot"></div>
<div class="skeleton-dot"></div>
</div>
</div>
<el-carousel
v-else
:interval="4000"
indicator-position="outside"
style="min-height: 300px;"
>
<el-carousel-item v-for="(image, index) in (productDetailsCache[product.id]?.images || [])" :key="index">
<img :src="getImageUrl(image)" :alt="product.name" class="carousel-image" />
</el-carousel-item>
</el-carousel>
</div>
<div class="product-details">
<img :src="getImageUrl(product.image)" :alt="product.name" class="small-image" />
<div class="product-info">
<span class="product-name">{{ product.name }}</span>
<div class="product-price">
<div class="main-price">
<img src='/imgs/profile/rongdou.png' alt="融豆" class="rongdou-icon" />
<span class="rongdou-price">{{ product.rongdou_price }}</span>
</div>
<div class="sub-price">
<el-icon class="points-icon"><Coin /></el-icon>
<span class="points-price">{{ product.points_price }}</span>
</div>
</div>
</div>
<span @click="$router.push(`/product/${product.id}`)" class="link">详情></span>
</div>
<div class="action">
<button class="button" @click="$router.push(`/product/${product.id}`)">立即购买</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/utils/api'
import { useRoute, useRouter } from 'vue-router'
import {ElMessage} from "element-plus";
import {Bottom, ShoppingCart, Coin} from "@element-plus/icons-vue";
import { getImageUrl } from '@/config'
const products = ref([])
const firstProduct = ref([])
const productDetail = ref([])
const productDetailsCache = ref({}) // 缓存所有商品详情
const loading = ref(true) // 添加加载状态
const detailsLoaded = ref(false) // 商品详情是否加载完成
const route = useRoute()
const productId = ref(null)
const getProducts = async () => {
try {
const { data } = await api.get('/products', {
params: {
page: 1,
limit: 20,
category: '',
keyword: '',
sort: ''
}
})
console.log('API 响应数据:', data)
// 根据你的 API 响应结构调整这里
if (data.data && data.data.products) {
products.value = data.data.products
}
else if (Array.isArray(data.products)) {
products.value = data.products
}
else if (Array.isArray(data)) {
products.value = data
}
else {
console.error('无法解析的商品数据格式:', data)
}
// 不需要预处理图片路径在模板中使用getImageUrl处理
console.log('解析后的商品数据:', products.value)
// 预加载所有商品详情
await loadAllProductDetails()
} catch (error) {
console.error('获取商品失败:', error)
console.error('错误详情:', error.response?.data)
}
}
const getFirstProduct = async () => {
try {
const productId = route.params.id
const [productRes] = await Promise.all([
api.get(`/products/${productId}`)
])
console.log(productRes,'productRes');
firstProduct.value = productRes.data.data.product
// 不需要预处理图片路径在模板中使用getImageUrl处理
} catch (error) {
ElMessage.error('获取商品详情失败')
console.log(error)
} finally {
loading.value = false
}
}
// const getProductDetail = async (productId) => {
// try {
// const response = await api.get(`/products/${productId}`)
// if (response.data && response.data.data && response.data.data.product) {
// productDetail.value = response.data.data.product
// // 处理商品详情的图片路径
// if (productDetail.value.image) {
// productDetail.value.image = getImageUrl(productDetail.value.image)
// }
// if (productDetail.value.images && Array.isArray(productDetail.value.images)) {
// productDetail.value.images = productDetail.value.images.map(img => getImageUrl(img))
// }
// // 缓存商品详情
// productDetailsCache.value[productId] = productDetail.value
// } else {
// console.error('无法解析商品详情数据:', response.data)
// ElMessage.error('获取商品详情失败')
// }
// return productDetail.value
// } catch (error) {
// console.error('获取商品详情失败:', error)
// console.error('错误详情:', error.response?.data)
// ElMessage.error('获取商品详情失败')
// throw error
// }
// }
// 预加载所有商品详情的函数
const loadAllProductDetails = async () => {
const uniqueProductIds = [...new Set(products.value.map(p => p.id))]
// 并行加载所有商品详情,避免逐个加载时的多次渲染
const loadPromises = uniqueProductIds.map(async (id) => {
try {
const response = await api.get(`/products/${id}`)
if (response.data && response.data.data && response.data.data.product) {
return { id, product: response.data.data.product }
}
} catch (error) {
console.warn(`Failed to load details for product ${id}:`, error)
return null
}
})
// 等待所有请求完成
const results = await Promise.all(loadPromises)
// 一次性更新缓存,避免多次触发响应式更新
const newCache = { ...productDetailsCache.value }
results.forEach(result => {
if (result) {
// 不需要预处理图片路径在模板中使用getImageUrl处理
newCache[result.id] = result.product
}
})
// 一次性更新缓存和加载状态
productDetailsCache.value = newCache
detailsLoaded.value = true
}
// 添加生命周期钩子来调用函数
onMounted(async () => {
productId.value = route.params.id
console.log('Product ID:', productId.value)
loading.value = true
// 先加载第一个商品,再加载商品列表,确保数据同步
await getFirstProduct()
await getProducts()
})
</script>
<style scoped>
.header {
/* 基础布局 */
padding: 16px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
/* 固定在顶部 */
position: sticky;
top: 0;
z-index: 100;
/* 内容居中 */
display: flex;
align-items: center;
justify-content: space-between; /* 添加此项使内容分布在两端 */
}
.button {
background: #3ab3ff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(50, 142, 240, 0.2);
width: 50%;
margin: 12px auto 0; /* 修改这里上下12px左右自动居中 */
display: block; /* 添加此项使margin: auto生效 */
}
.button:hover {
background: linear-gradient(135deg, #337ecc, #2a6db3);
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(50, 142, 240, 0.3);
}
.button:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(50, 142, 240, 0.2);
}
.header span {
/* 返回按钮样式 */
color: #333333;
font-size: 16px;
font-weight: 500;
cursor: pointer;
padding: 6px 10px;
border-radius: 6px;
transition: all 0.2s ease;
/* 添加返回图标占位 */
display: inline-flex;
align-items: center;
gap: 6px;
}
.products-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
.product-card {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 16px;
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: all 0.3s ease;
overflow: hidden;
width: 374px;
min-height: 526px;
display: flex;
flex-direction: column; /* 改为垂直布局 */
}
.product-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.product-image {
position: relative;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
/* 关键:添加底部内边距,容纳指示器 */
padding-bottom: 30px;
min-height: 300px; /* 保留轮播图最小高度 */
}
.carousel-image {
width: 100%;
height: 100%;
object-fit: contain; /* 完整显示图片,不裁剪 */
background-color: #f5f5f5; /* 空白处用浅灰色填充,避免白屏 */
}
/* 修改部分开始 */
.product-details {
display: flex;
align-items: flex-start;
gap: 12px;
text-align: left;
margin-top: auto; /* 推到卡片底部 */
padding-top: 12px; /* 添加顶部间距 */
border-top: 1px solid #f0f0f0; /* 可选:添加分隔线 */
min-height: 70px; /* 确保有足够高度 */
}
.small-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 8px;
border: 2px solid #f0f0f0;
flex-shrink: 0;
}
.product-info {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
min-width: 0;
padding-right: 8px; /* 为详情链接留出空间 */
}
.product-name {
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-bottom: 8px;
word-break: break-word; /* 允许长单词换行 */
}
.product-price {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
margin-top: auto; /* 推到底部 */
}
.main-price {
display: flex;
align-items: center;
gap: 4px;
color: #e53935;
font-weight: bold;
font-size: 18px;
}
.rongdou-icon {
width: 16px;
height: 16px;
object-fit: contain;
}
.rongdou-price {
color: #e53935;
font-weight: bold;
}
.sub-price {
display: flex;
align-items: center;
gap: 2px;
margin-left: 0;
margin-top: 2px;
}
.points-icon {
width: 12px;
height: 12px;
color: #666;
}
.points-price {
font-size: 12px;
color: #666;
font-weight: normal;
}
.link {
color: #409eff;
font-size: 14px;
cursor: pointer;
text-decoration: none;
flex-shrink: 0;
align-self: flex-start;
margin-top: 4px;
white-space: nowrap;
}
/* 修改部分结束 */
/* 骨架屏样式 */
.carousel-skeleton {
position: relative;
width: 100%;
height: 300px;
border-radius: 8px;
overflow: hidden;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.skeleton-image {
width: 80%;
height: 80%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8px;
}
.skeleton-indicators {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
}
.skeleton-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #d0d0d0;
animation: skeleton-pulse 1.5s infinite;
}
.skeleton-dot:nth-child(2) {
animation-delay: 0.2s;
}
.skeleton-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes skeleton-loading {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes skeleton-pulse {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 0.8;
}
}
/* 轮播图样式调整 */
:deep(.el-carousel) {
border-radius: 8px;
}
:deep(.el-carousel__container) {
border-radius: 8px;
}
:deep(.el-carousel__indicators) {
bottom: 10px; /* 从 -25px 改为 10px放在轮播图底部内侧 */
z-index: 10; /* 确保指示器在图片上方显示 */
}
:deep(.el-carousel__button) {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #ccc;
}
:deep(.el-carousel__indicator.is-active .el-carousel__button) {
background-color: #409eff;
}
/* 响应式设计 */
@media (max-width: 768px) {
.products-container {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 16px;
}
.product-card {
padding: 12px;
min-height: 450px; /* 移动端最小高度适配 */
width: 100%; /* 移动端卡片宽度占满父容器,避免超出屏幕 */
}
.product-image {
padding-bottom: 75%;
min-height: 200px; /* 移动端轮播图最小高度 */
}
/* 响应式调整 */
.product-details {
gap: 10px;
min-height: 60px;
}
.small-image {
width: 50px;
height: 50px;
}
.product-info {
padding-right: 6px;
}
.product-name {
font-size: 14px;
margin-bottom: 6px;
}
.product-price {
font-size: 16px;
}
.link {
font-size: 12px;
}
}
@media (max-width: 480px) {
.products-container {
grid-template-columns: repeat(1, 1fr);
gap: 12px;
padding: 12px;
}
.product-card {
padding: 10px;
}
.product-image {
height: 200px;
}
/* 响应式调整 */
.product-details {
gap: 8px;
min-height: 55px;
}
.small-image {
width: 45px;
height: 45px;
}
.product-info {
padding-right: 4px;
}
.product-name {
font-size: 13px;
margin-bottom: 4px;
}
.product-price {
font-size: 15px;
}
.link {
font-size: 11px;
}
}
</style>