758 lines
25 KiB
JavaScript
758 lines
25 KiB
JavaScript
|
|
// 商品详情页功能 - 使用真实API
|
|||
|
|
|
|||
|
|
let currentProduct = null;
|
|||
|
|
let selectedSku = null;
|
|||
|
|
let currentCommentPage = 1;
|
|||
|
|
let currentCommentFilter = 'all'; // all, good, medium, bad, image
|
|||
|
|
|
|||
|
|
// 页面初始化
|
|||
|
|
$(document).ready(function() {
|
|||
|
|
const productId = getProductIdFromUrl();
|
|||
|
|
if (productId) {
|
|||
|
|
loadProductDetail(productId);
|
|||
|
|
loadCommentsCount(productId);
|
|||
|
|
loadComments(productId);
|
|||
|
|
} else {
|
|||
|
|
Toast.error('商品不存在');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.location.href = 'index.html';
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bindEvents();
|
|||
|
|
loadCartCount();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 从URL获取商品ID
|
|||
|
|
function getProductIdFromUrl() {
|
|||
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|||
|
|
return urlParams.get('id');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载商品详情
|
|||
|
|
function loadProductDetail(productId) {
|
|||
|
|
console.log('加载商品详情:', productId);
|
|||
|
|
|
|||
|
|
API.get(`/frontend/products/${productId}/detail`)
|
|||
|
|
.then(data => {
|
|||
|
|
console.log('商品详情数据:', data);
|
|||
|
|
currentProduct = data;
|
|||
|
|
renderProductDetail(data);
|
|||
|
|
loadRelatedProducts();
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('加载商品详情失败:', error);
|
|||
|
|
Toast.error(error.message || '加载商品详情失败');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
window.location.href = 'index.html';
|
|||
|
|
}, 2000);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染商品详情
|
|||
|
|
function renderProductDetail(product) {
|
|||
|
|
const lang = i18n.currentLang;
|
|||
|
|
|
|||
|
|
// 设置商品名称
|
|||
|
|
const productName = product.title || product.name || '未知商品';
|
|||
|
|
$('#productName').text(productName);
|
|||
|
|
$('#breadcrumbProduct').text(productName);
|
|||
|
|
document.title = `${productName} - vizee为之甄选`;
|
|||
|
|
|
|||
|
|
// 设置价格
|
|||
|
|
const minPrice = PriceUtils.fenToYuan(product.minSalePrice || product.price || 0);
|
|||
|
|
const maxPrice = PriceUtils.fenToYuan(product.maxSalePrice || product.price || 0);
|
|||
|
|
const originPrice = PriceUtils.fenToYuan(product.maxLinePrice || product.originPrice || 0);
|
|||
|
|
|
|||
|
|
if (minPrice === maxPrice) {
|
|||
|
|
$('#currentPrice').text(`¥${minPrice.toFixed(2)}`);
|
|||
|
|
} else {
|
|||
|
|
$('#currentPrice').text(`¥${minPrice.toFixed(2)} - ¥${maxPrice.toFixed(2)}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (originPrice > maxPrice) {
|
|||
|
|
$('#originalPrice').text(`¥${originPrice.toFixed(2)}`).show();
|
|||
|
|
const discount = Math.round((1 - maxPrice / originPrice) * 100);
|
|||
|
|
$('#priceSave').text(`${i18n.t('save') || '省'} ${discount}%`).show();
|
|||
|
|
} else {
|
|||
|
|
$('#originalPrice').hide();
|
|||
|
|
$('#priceSave').hide();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置库存状态
|
|||
|
|
const spuStock = product.spuStockQuantity ?? product.stock ?? 0;
|
|||
|
|
const isInStock = spuStock > 0;
|
|||
|
|
|
|||
|
|
if (isInStock) {
|
|||
|
|
$('#stockStatus').text('有货').addClass('in-stock').removeClass('out-of-stock');
|
|||
|
|
} else {
|
|||
|
|
$('#stockStatus').text('缺货').addClass('out-of-stock').removeClass('in-stock');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存库存状态到currentProduct
|
|||
|
|
currentProduct.isStock = isInStock;
|
|||
|
|
|
|||
|
|
// 设置图片
|
|||
|
|
loadProductImages(product.images || [product.primaryImage]);
|
|||
|
|
|
|||
|
|
// 设置描述
|
|||
|
|
const description = product.descriptionText || product.details || '';
|
|||
|
|
$('#productDescription').text(description);
|
|||
|
|
|
|||
|
|
// 渲染详情图片
|
|||
|
|
if (product.desc && Array.isArray(product.desc) && product.desc.length > 0) {
|
|||
|
|
const detailImagesHtml = product.desc.map(img =>
|
|||
|
|
`<img src="${img}" alt="商品详情" loading="lazy">`
|
|||
|
|
).join('');
|
|||
|
|
$('#productDetailImages').html(detailImagesHtml);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染规格选择器
|
|||
|
|
if (product.specList && product.specList.length > 0) {
|
|||
|
|
renderSpecSelector(product.specList, product.skuList);
|
|||
|
|
} else {
|
|||
|
|
$('#specSelector').hide();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示已售数量
|
|||
|
|
if (product.soldNum) {
|
|||
|
|
$('#soldCount').text(`已售 ${product.soldNum}`).show();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载商品图片
|
|||
|
|
function loadProductImages(images) {
|
|||
|
|
// 显示加载动画
|
|||
|
|
showImageLoading();
|
|||
|
|
|
|||
|
|
if (!images || images.length === 0) {
|
|||
|
|
// 如果没有图片,隐藏加载动画并显示默认图片
|
|||
|
|
hideImageLoading();
|
|||
|
|
images = ['https://picsum.photos/800/800?random=default'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 主图
|
|||
|
|
const mainImg = new Image();
|
|||
|
|
mainImg.onload = function() {
|
|||
|
|
$('#mainImage').attr('src', images[0]);
|
|||
|
|
hideImageLoading();
|
|||
|
|
};
|
|||
|
|
mainImg.onerror = function() {
|
|||
|
|
// 加载失败,使用占位图
|
|||
|
|
$('#mainImage').attr('src', 'https://picsum.photos/800/800?random=error');
|
|||
|
|
hideImageLoading();
|
|||
|
|
};
|
|||
|
|
mainImg.src = images[0];
|
|||
|
|
|
|||
|
|
// 缩略图
|
|||
|
|
const thumbnailsHtml = images.map((img, index) =>
|
|||
|
|
`<div class="thumbnail ${index === 0 ? 'active' : ''}" data-index="${index}">
|
|||
|
|
<img src="${img}" alt="商品图片 ${index + 1}" loading="lazy">
|
|||
|
|
</div>`
|
|||
|
|
).join('');
|
|||
|
|
|
|||
|
|
$('#thumbnails').html(thumbnailsHtml);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示图片加载动画
|
|||
|
|
function showImageLoading() {
|
|||
|
|
const loadingHtml = `
|
|||
|
|
<div class="image-loading" id="imageLoading">
|
|||
|
|
<div class="loading-spinner"></div>
|
|||
|
|
<div class="loading-text">加载中...</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
if ($('#imageLoading').length === 0) {
|
|||
|
|
$('.main-image').append(loadingHtml);
|
|||
|
|
}
|
|||
|
|
$('#mainImage').css('opacity', '0');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 隐藏图片加载动画
|
|||
|
|
function hideImageLoading() {
|
|||
|
|
$('#imageLoading').fadeOut(300, function() {
|
|||
|
|
$(this).remove();
|
|||
|
|
});
|
|||
|
|
$('#mainImage').css('opacity', '1');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染规格选择器
|
|||
|
|
function renderSpecSelector(specList, skuList) {
|
|||
|
|
let specHtml = '';
|
|||
|
|
|
|||
|
|
specList.forEach((spec, specIndex) => {
|
|||
|
|
// 优先使用title,其次specName,最后name
|
|||
|
|
const specName = spec.title || spec.specName || spec.name || `规格${specIndex + 1}`;
|
|||
|
|
const specValues = spec.specValueList || [];
|
|||
|
|
|
|||
|
|
specHtml += `
|
|||
|
|
<div class="spec-group">
|
|||
|
|
<label class="spec-label">${specName}</label>
|
|||
|
|
<div class="spec-options" data-spec-index="${specIndex}">
|
|||
|
|
${specValues.map((value, valueIndex) => {
|
|||
|
|
const valueName = value.specValue || value.name || value;
|
|||
|
|
return `
|
|||
|
|
<button class="spec-option"
|
|||
|
|
data-spec-index="${specIndex}"
|
|||
|
|
data-value-index="${valueIndex}"
|
|||
|
|
data-spec-value="${valueName}">
|
|||
|
|
${valueName}
|
|||
|
|
</button>
|
|||
|
|
`;
|
|||
|
|
}).join('')}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$('#specSelector').html(specHtml).show();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载相关商品
|
|||
|
|
function loadRelatedProducts() {
|
|||
|
|
API.get('/products', { page: 1, page_size: 4 })
|
|||
|
|
.then(data => {
|
|||
|
|
const products = data.list || [];
|
|||
|
|
renderRelatedProducts(products.slice(0, 4));
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('加载相关商品失败:', error);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染相关商品
|
|||
|
|
function renderRelatedProducts(products) {
|
|||
|
|
const lang = i18n.currentLang;
|
|||
|
|
|
|||
|
|
const productsHtml = products.map(product => {
|
|||
|
|
const productName = product.name || '未知商品';
|
|||
|
|
const productImage = product.main_image || 'https://picsum.photos/400/400?random=default';
|
|||
|
|
const price = PriceUtils.fenToYuan(product.price || 0);
|
|||
|
|
const originalPrice = product.orig_price ? PriceUtils.fenToYuan(product.orig_price) : null;
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="product-card">
|
|||
|
|
<a href="product-detail.html?id=${product.id}">
|
|||
|
|
<div class="product-image">
|
|||
|
|
<img src="${productImage}" alt="${productName}" loading="lazy">
|
|||
|
|
</div>
|
|||
|
|
<div class="product-info">
|
|||
|
|
<h3 class="product-title">${productName}</h3>
|
|||
|
|
<div class="product-price">
|
|||
|
|
<span class="price-current">¥${price.toFixed(2)}</span>
|
|||
|
|
${originalPrice ? `<span class="price-original">¥${originalPrice.toFixed(2)}</span>` : ''}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
$('#relatedProducts').html(productsHtml);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 绑定事件
|
|||
|
|
function bindEvents() {
|
|||
|
|
// 缩略图点击
|
|||
|
|
$(document).on('click', '.thumbnail', function() {
|
|||
|
|
const index = $(this).data('index');
|
|||
|
|
const imgSrc = $(this).find('img').attr('src');
|
|||
|
|
$('#mainImage').attr('src', imgSrc);
|
|||
|
|
$('.thumbnail').removeClass('active');
|
|||
|
|
$(this).addClass('active');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 数量增减
|
|||
|
|
$('.qty-btn.minus').on('click', function() {
|
|||
|
|
const $input = $('#quantityInput');
|
|||
|
|
let qty = parseInt($input.val()) || 1;
|
|||
|
|
if (qty > 1) {
|
|||
|
|
$input.val(qty - 1);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$('.qty-btn.plus').on('click', function() {
|
|||
|
|
const $input = $('#quantityInput');
|
|||
|
|
let qty = parseInt($input.val()) || 1;
|
|||
|
|
const max = parseInt($input.attr('max')) || 99;
|
|||
|
|
if (qty < max) {
|
|||
|
|
$input.val(qty + 1);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
$('#quantityInput').on('change', function() {
|
|||
|
|
let qty = parseInt($(this).val()) || 1;
|
|||
|
|
const max = parseInt($(this).attr('max')) || 99;
|
|||
|
|
if (qty < 1) qty = 1;
|
|||
|
|
if (qty > max) qty = max;
|
|||
|
|
$(this).val(qty);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 规格选择
|
|||
|
|
$(document).on('click', '.spec-option', function() {
|
|||
|
|
const $option = $(this);
|
|||
|
|
const specIndex = $option.data('spec-index');
|
|||
|
|
|
|||
|
|
// 取消同组其他选项
|
|||
|
|
$(`.spec-option[data-spec-index="${specIndex}"]`).removeClass('selected');
|
|||
|
|
// 选中当前选项
|
|||
|
|
$option.addClass('selected');
|
|||
|
|
|
|||
|
|
// 更新选中的SKU
|
|||
|
|
updateSelectedSku();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 评论标签切换
|
|||
|
|
$(document).on('click', '.comment-tab', function() {
|
|||
|
|
const $tab = $(this);
|
|||
|
|
const filter = $tab.data('filter');
|
|||
|
|
|
|||
|
|
$('.comment-tab').removeClass('active');
|
|||
|
|
$tab.addClass('active');
|
|||
|
|
|
|||
|
|
currentCommentFilter = filter;
|
|||
|
|
currentCommentPage = 1;
|
|||
|
|
|
|||
|
|
const productId = getProductIdFromUrl();
|
|||
|
|
loadComments(productId, 1, filter);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 评论分页
|
|||
|
|
$(document).on('click', '#commentPagination .page-btn', function() {
|
|||
|
|
const page = parseInt($(this).data('page'));
|
|||
|
|
const productId = getProductIdFromUrl();
|
|||
|
|
|
|||
|
|
currentCommentPage = page;
|
|||
|
|
loadComments(productId, page, currentCommentFilter);
|
|||
|
|
|
|||
|
|
// 滚动到评论区域
|
|||
|
|
$('html, body').animate({
|
|||
|
|
scrollTop: $('#reviews').offset().top - 100
|
|||
|
|
}, 300);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Tab切换
|
|||
|
|
$(document).on('click', '.tab-header', function() {
|
|||
|
|
const $tab = $(this);
|
|||
|
|
const tabId = $tab.data('tab');
|
|||
|
|
|
|||
|
|
$('.tab-header').removeClass('active');
|
|||
|
|
$tab.addClass('active');
|
|||
|
|
|
|||
|
|
$('.tab-content').removeClass('active');
|
|||
|
|
$(`#${tabId}`).addClass('active');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 立即购买
|
|||
|
|
$('#buyNowBtn').on('click', function() {
|
|||
|
|
if (!currentProduct || !currentProduct.isStock) {
|
|||
|
|
Toast.error('商品缺货');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有规格,检查是否已选择
|
|||
|
|
if (currentProduct.specList && currentProduct.specList.length > 0) {
|
|||
|
|
const selectedCount = $('.spec-option.selected').length;
|
|||
|
|
if (selectedCount < currentProduct.specList.length) {
|
|||
|
|
// 找到未选择的规格组
|
|||
|
|
const unselectedSpecs = [];
|
|||
|
|
currentProduct.specList.forEach((spec, index) => {
|
|||
|
|
const hasSelected = $(`.spec-option.selected[data-spec-index="${index}"]`).length > 0;
|
|||
|
|
if (!hasSelected) {
|
|||
|
|
const specName = spec.title || spec.specName || spec.name || `规格${index + 1}`;
|
|||
|
|
unselectedSpecs.push(specName);
|
|||
|
|
// 高亮未选择的规格组
|
|||
|
|
$(`.spec-options[data-spec-index="${index}"]`).addClass('spec-required');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
$(`.spec-options[data-spec-index="${index}"]`).removeClass('spec-required');
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
Toast.error(`请选择${unselectedSpecs.join('、')}`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const quantity = parseInt($('#quantityInput').val()) || 1;
|
|||
|
|
|
|||
|
|
// 先加入购物车,然后跳转结算
|
|||
|
|
addToCart(quantity, true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加入购物车
|
|||
|
|
$('#addToCartBtn').on('click', function() {
|
|||
|
|
if (!currentProduct || !currentProduct.isStock) {
|
|||
|
|
Toast.error('商品缺货');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有规格,检查是否已选择
|
|||
|
|
if (currentProduct.specList && currentProduct.specList.length > 0) {
|
|||
|
|
const selectedCount = $('.spec-option.selected').length;
|
|||
|
|
if (selectedCount < currentProduct.specList.length) {
|
|||
|
|
// 找到未选择的规格组
|
|||
|
|
const unselectedSpecs = [];
|
|||
|
|
currentProduct.specList.forEach((spec, index) => {
|
|||
|
|
const hasSelected = $(`.spec-option.selected[data-spec-index="${index}"]`).length > 0;
|
|||
|
|
if (!hasSelected) {
|
|||
|
|
const specName = spec.title || spec.specName || spec.name || `规格${index + 1}`;
|
|||
|
|
unselectedSpecs.push(specName);
|
|||
|
|
// 高亮未选择的规格组
|
|||
|
|
$(`.spec-options[data-spec-index="${index}"]`).addClass('spec-required');
|
|||
|
|
setTimeout(() => {
|
|||
|
|
$(`.spec-options[data-spec-index="${index}"]`).removeClass('spec-required');
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
Toast.error(`请选择${unselectedSpecs.join('、')}`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const quantity = parseInt($('#quantityInput').val()) || 1;
|
|||
|
|
addToCart(quantity, false);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新选中的SKU
|
|||
|
|
function updateSelectedSku() {
|
|||
|
|
if (!currentProduct || !currentProduct.skuList || currentProduct.skuList.length === 0) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取所有选中的规格值
|
|||
|
|
const selectedSpecs = [];
|
|||
|
|
$('.spec-option.selected').each(function() {
|
|||
|
|
selectedSpecs.push($(this).data('spec-value'));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 查找匹配的SKU
|
|||
|
|
const sku = currentProduct.skuList.find(s => {
|
|||
|
|
if (!s.specInfo || s.specInfo.length !== selectedSpecs.length) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return s.specInfo.every(spec => selectedSpecs.includes(spec.specValue));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (sku) {
|
|||
|
|
selectedSku = sku;
|
|||
|
|
// 更新价格显示
|
|||
|
|
const price = PriceUtils.fenToYuan(sku.priceInfo && sku.priceInfo[0] ? sku.priceInfo[0].price : 0);
|
|||
|
|
$('#currentPrice').text(`¥${price.toFixed(2)}`);
|
|||
|
|
|
|||
|
|
// 更新库存
|
|||
|
|
const stock = sku.stockQuantity || 0;
|
|||
|
|
if (stock > 0) {
|
|||
|
|
$('#stockStatus').text('有货').addClass('in-stock').removeClass('out-of-stock');
|
|||
|
|
$('#quantity').attr('max', stock);
|
|||
|
|
} else {
|
|||
|
|
$('#stockStatus').text('缺货').addClass('out-of-stock').removeClass('in-stock');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加入购物车
|
|||
|
|
function addToCart(quantity, buyNow = false) {
|
|||
|
|
// 如果是立即购买,直接跳转到结算页,不需要加入购物车
|
|||
|
|
if (buyNow) {
|
|||
|
|
const productId = currentProduct.spuId || currentProduct.id;
|
|||
|
|
const skuId = selectedSku ? (selectedSku.skuId || selectedSku.id) : '';
|
|||
|
|
|
|||
|
|
// 构建 URL 参数
|
|||
|
|
const params = new URLSearchParams({
|
|||
|
|
type: 'buynow',
|
|||
|
|
product_id: productId,
|
|||
|
|
quantity: quantity
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (skuId) {
|
|||
|
|
params.append('sku_id', skuId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 直接跳转到结算页
|
|||
|
|
window.location.href = `checkout.html?${params.toString()}`;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 普通加入购物车流程
|
|||
|
|
const data = {
|
|||
|
|
product_id: parseInt(currentProduct.spuId || currentProduct.id),
|
|||
|
|
quantity: quantity
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 如果选择了SKU
|
|||
|
|
if (selectedSku) {
|
|||
|
|
data.sku_id = parseInt(selectedSku.skuId || selectedSku.id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('加入购物车:', data);
|
|||
|
|
|
|||
|
|
API.post('/cart', data)
|
|||
|
|
.then(() => {
|
|||
|
|
Toast.success('已添加到购物车');
|
|||
|
|
loadCartCount();
|
|||
|
|
|
|||
|
|
// 打开购物车抽屉
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (typeof window.openCartDrawer === 'function') {
|
|||
|
|
window.openCartDrawer();
|
|||
|
|
}
|
|||
|
|
}, 300);
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('添加到购物车失败:', error);
|
|||
|
|
Toast.error(error.message || '添加失败');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载购物车数量
|
|||
|
|
function loadCartCount() {
|
|||
|
|
// 检查是否登录,未登录时不请求
|
|||
|
|
const user = localStorage.getItem('currentUser');
|
|||
|
|
if (!user) {
|
|||
|
|
$('.cart-count').text(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const userData = JSON.parse(user);
|
|||
|
|
if (!userData.token) {
|
|||
|
|
$('.cart-count').text(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
$('.cart-count').text(0);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
API.get('/cart')
|
|||
|
|
.then(data => {
|
|||
|
|
const totalQuantity = data.total_quantity || 0;
|
|||
|
|
$('.cart-count').text(totalQuantity);
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('加载购物车数量失败:', error);
|
|||
|
|
$('.cart-count').text(0);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载评论统计
|
|||
|
|
function loadCommentsCount(productId) {
|
|||
|
|
API.get(`/comments/products/${productId}/stats`)
|
|||
|
|
.then(data => {
|
|||
|
|
const totalCount = data.total_count || 0;
|
|||
|
|
const avgRating = data.average_rating || 0;
|
|||
|
|
const goodCount = (data.rating_4_count || 0) + (data.rating_5_count || 0);
|
|||
|
|
const mediumCount = data.rating_3_count || 0;
|
|||
|
|
const badCount = (data.rating_1_count || 0) + (data.rating_2_count || 0);
|
|||
|
|
const imageCount = data.has_images_count || 0;
|
|||
|
|
|
|||
|
|
// 更新评论数量显示
|
|||
|
|
$('#reviewCount').text(`${totalCount} reviews`);
|
|||
|
|
|
|||
|
|
// 更新星级显示
|
|||
|
|
renderStars(avgRating);
|
|||
|
|
|
|||
|
|
// 更新评论标签
|
|||
|
|
updateCommentTabs(totalCount, goodCount, mediumCount, badCount, imageCount);
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('加载评论统计失败:', error);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染星级
|
|||
|
|
function renderStars(rating) {
|
|||
|
|
const fullStars = Math.floor(rating);
|
|||
|
|
const hasHalfStar = rating % 1 >= 0.5;
|
|||
|
|
let starsHtml = '';
|
|||
|
|
|
|||
|
|
for (let i = 0; i < 5; i++) {
|
|||
|
|
if (i < fullStars) {
|
|||
|
|
starsHtml += '★';
|
|||
|
|
} else if (i === fullStars && hasHalfStar) {
|
|||
|
|
starsHtml += '☆';
|
|||
|
|
} else {
|
|||
|
|
starsHtml += '☆';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$('#productStars').html(starsHtml);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新评论标签
|
|||
|
|
function updateCommentTabs(total, good, medium, bad, image) {
|
|||
|
|
const tabsHtml = `
|
|||
|
|
<div class="comment-tabs">
|
|||
|
|
<button class="comment-tab active" data-filter="all">全部 (${total})</button>
|
|||
|
|
<button class="comment-tab" data-filter="good">好评 (${good})</button>
|
|||
|
|
<button class="comment-tab" data-filter="medium">中评 (${medium})</button>
|
|||
|
|
<button class="comment-tab" data-filter="bad">差评 (${bad})</button>
|
|||
|
|
<button class="comment-tab" data-filter="image">有图 (${image})</button>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
$('#reviewsList').before(tabsHtml);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载评论列表
|
|||
|
|
function loadComments(productId, page = 1, filter = 'all') {
|
|||
|
|
const params = {
|
|||
|
|
page: page,
|
|||
|
|
page_size: 10
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 根据筛选条件添加参数
|
|||
|
|
if (filter === 'good') {
|
|||
|
|
params.rating = 4; // 4-5星
|
|||
|
|
} else if (filter === 'medium') {
|
|||
|
|
params.rating = 3; // 3星
|
|||
|
|
} else if (filter === 'bad') {
|
|||
|
|
params.rating = 2; // 1-2星
|
|||
|
|
} else if (filter === 'image') {
|
|||
|
|
params.has_images = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
API.get(`/comments/products/${productId}`, params)
|
|||
|
|
.then(data => {
|
|||
|
|
const comments = data.list || [];
|
|||
|
|
renderComments(comments);
|
|||
|
|
|
|||
|
|
// 更新分页
|
|||
|
|
renderCommentPagination(data.page, data.total, data.page_size);
|
|||
|
|
})
|
|||
|
|
.catch(error => {
|
|||
|
|
console.error('加载评论失败:', error);
|
|||
|
|
$('#reviewsList').html('<div class="empty-reviews">暂无评论</div>');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染评论列表
|
|||
|
|
function renderComments(comments) {
|
|||
|
|
if (comments.length === 0) {
|
|||
|
|
$('#reviewsList').html('<div class="empty-reviews">暂无评论</div>');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const commentsHtml = comments.map(comment => {
|
|||
|
|
const userName = comment.user_name || '匿名用户';
|
|||
|
|
const userAvatar = comment.user_avatar || 'https://picsum.photos/40/40?random=' + comment.id;
|
|||
|
|
const rating = comment.rating || 5;
|
|||
|
|
const content = comment.content || '';
|
|||
|
|
const images = comment.images || [];
|
|||
|
|
const createdAt = formatDate(comment.created_at);
|
|||
|
|
const productSpec = comment.product_spec || '';
|
|||
|
|
const replyContent = comment.reply_content || '';
|
|||
|
|
|
|||
|
|
// 星级
|
|||
|
|
let starsHtml = '';
|
|||
|
|
for (let i = 0; i < 5; i++) {
|
|||
|
|
starsHtml += i < rating ? '★' : '☆';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片
|
|||
|
|
let imagesHtml = '';
|
|||
|
|
if (images.length > 0) {
|
|||
|
|
imagesHtml = `
|
|||
|
|
<div class="comment-images">
|
|||
|
|
${images.map(img => `
|
|||
|
|
<img src="${img}" alt="评论图片" class="comment-image" onclick="previewImage('${img}')">
|
|||
|
|
`).join('')}
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 商家回复
|
|||
|
|
let replyHtml = '';
|
|||
|
|
if (replyContent) {
|
|||
|
|
replyHtml = `
|
|||
|
|
<div class="merchant-reply">
|
|||
|
|
<div class="reply-label">商家回复:</div>
|
|||
|
|
<div class="reply-content">${replyContent}</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return `
|
|||
|
|
<div class="review-item">
|
|||
|
|
<div class="review-header">
|
|||
|
|
<div class="review-author-info">
|
|||
|
|
<img src="${userAvatar}" alt="${userName}" class="review-avatar">
|
|||
|
|
<div>
|
|||
|
|
<div class="review-author-name">${userName}</div>
|
|||
|
|
${productSpec ? `<div class="review-spec">${productSpec}</div>` : ''}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="review-meta">
|
|||
|
|
<div class="review-rating">${starsHtml}</div>
|
|||
|
|
<div class="review-date">${createdAt}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="review-content">
|
|||
|
|
<p class="review-text">${content}</p>
|
|||
|
|
${imagesHtml}
|
|||
|
|
</div>
|
|||
|
|
${replyHtml}
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}).join('');
|
|||
|
|
|
|||
|
|
$('#reviewsList').html(commentsHtml);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染评论分页
|
|||
|
|
function renderCommentPagination(currentPage, total, pageSize) {
|
|||
|
|
const totalPages = Math.ceil(total / pageSize);
|
|||
|
|
|
|||
|
|
if (totalPages <= 1) {
|
|||
|
|
$('#commentPagination').hide();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let paginationHtml = `<div class="pagination">`;
|
|||
|
|
|
|||
|
|
// 上一页
|
|||
|
|
if (currentPage > 1) {
|
|||
|
|
paginationHtml += `<button class="page-btn" data-page="${currentPage - 1}">上一页</button>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 页码
|
|||
|
|
for (let i = 1; i <= Math.min(totalPages, 5); i++) {
|
|||
|
|
const active = i === currentPage ? 'active' : '';
|
|||
|
|
paginationHtml += `<button class="page-btn ${active}" data-page="${i}">${i}</button>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 下一页
|
|||
|
|
if (currentPage < totalPages) {
|
|||
|
|
paginationHtml += `<button class="page-btn" data-page="${currentPage + 1}">下一页</button>`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
paginationHtml += `</div>`;
|
|||
|
|
|
|||
|
|
$('#commentPagination').html(paginationHtml).show();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
function formatDate(dateStr) {
|
|||
|
|
if (!dateStr) return '';
|
|||
|
|
|
|||
|
|
const date = new Date(dateStr);
|
|||
|
|
const year = date.getFullYear();
|
|||
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|||
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|||
|
|
|
|||
|
|
return `${year}-${month}-${day}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片预览
|
|||
|
|
function previewImage(imgUrl) {
|
|||
|
|
// 创建预览模态框
|
|||
|
|
const modal = `
|
|||
|
|
<div class="image-preview-modal" onclick="this.remove()">
|
|||
|
|
<img src="${imgUrl}" alt="预览图片">
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
$('body').append(modal);
|
|||
|
|
}
|