修复文件
This commit is contained in:
585
src/main/webapp/WEB-INF/views/cart.jsp
Normal file
585
src/main/webapp/WEB-INF/views/cart.jsp
Normal file
@@ -0,0 +1,585 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="购物车"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item active">购物车</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 购物车内容 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-shopping-cart text-primary"></i> 我的购物车
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="clearCart()" id="clearCartBtn"
|
||||
style="display: none;">
|
||||
<i class="fas fa-trash"></i> 清空购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingCart" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载购物车中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空购物车状态 -->
|
||||
<div id="emptyCart" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-shopping-cart fa-4x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">购物车空空如也</h5>
|
||||
<p class="text-muted">快去挑选您喜欢的商品吧~</p>
|
||||
<a href="${pageContext.request.contextPath}/" class="btn btn-primary">
|
||||
<i class="fas fa-shopping-bag"></i> 去购物
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 购物车商品列表 -->
|
||||
<div id="cartItems" style="display: none;">
|
||||
<!-- 全选区域 -->
|
||||
<div class="row mb-3 border-bottom pb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="selectAll">
|
||||
<label class="form-check-label" for="selectAll">
|
||||
全选
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="batchRemoveSelected()">
|
||||
<i class="fas fa-trash"></i> 删除选中
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表容器 -->
|
||||
<div id="cartItemsList"></div>
|
||||
|
||||
<!-- 结算区域 -->
|
||||
<div class="row mt-4 pt-3 border-top">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted me-3">已选择 <span id="selectedCount">0</span> 件商品</span>
|
||||
<span class="text-muted">总计:<span id="totalQuantity">0</span> 件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<span class="h5 text-danger me-3 mb-0">
|
||||
¥<span id="totalPrice">0.00</span>
|
||||
</span>
|
||||
<button class="btn btn-danger btn-lg" onclick="checkout()" id="checkoutBtn"
|
||||
disabled>
|
||||
<i class="fas fa-credit-card"></i> 去结算
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推荐商品 -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-heart text-danger"></i> 猜你喜欢
|
||||
</h5>
|
||||
<div id="recommendedProducts" class="row">
|
||||
<!-- 推荐商品将通过AJAX加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
loadCart();
|
||||
loadRecommendedProducts();
|
||||
});
|
||||
|
||||
// 加载购物车
|
||||
function loadCart() {
|
||||
$('#loadingCart').show();
|
||||
$('#emptyCart').hide();
|
||||
$('#cartItems').hide();
|
||||
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart',
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data) {
|
||||
renderCart(response.data);
|
||||
} else {
|
||||
showEmptyCart();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('加载购物车失败,请刷新页面重试', 'error');
|
||||
showEmptyCart();
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingCart').hide();
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
$('#loadingCart').hide();
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 渲染购物车
|
||||
function renderCart(cart) {
|
||||
if (!cart.items || cart.items.length === 0) {
|
||||
showEmptyCart();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
cart.items.forEach(function (item) {
|
||||
html += `
|
||||
<div class="row align-items-center py-3 border-bottom cart-item" data-product-id="` + item.productId + `">
|
||||
<div class="col-md-1">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input item-checkbox" type="checkbox" value="` + item.productId + `">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<img src="` + (item.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
class="img-fluid rounded" alt="` + item.productName + `" style="max-height: 80px;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6 class="mb-1">` + item.productName + `</h6>
|
||||
<small class="text-muted">库存:` + item.stock + ` 件</small>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<span class="text-danger fw-bold">¥` + item.productPrice.toFixed(2) + `</span>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="input-group input-group-sm" style="width: 120px;">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="updateQuantity(` + item.productId + `, ` + (item.quantity - 1) + `)">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
<input type="number" class="form-control text-center" value="` + item.quantity + `"
|
||||
min="1" max="` + item.stock + `"
|
||||
onchange="updateQuantity(` + item.productId + `, this.value)">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="updateQuantity(` + item.productId + `, ` + (item.quantity + 1) + `)">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<span class="fw-bold text-danger">¥` + item.subtotal.toFixed(2) + `</span>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="removeFromCart(` + item.productId + `)" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#cartItemsList').html(html);
|
||||
$('#cartItems').show();
|
||||
$('#clearCartBtn').show();
|
||||
|
||||
// 更新总计
|
||||
updateCartSummary(cart);
|
||||
|
||||
// 绑定事件
|
||||
bindCartEvents();
|
||||
}
|
||||
|
||||
// 显示空购物车
|
||||
function showEmptyCart() {
|
||||
$('#emptyCart').show();
|
||||
$('#cartItems').hide();
|
||||
$('#clearCartBtn').hide();
|
||||
}
|
||||
|
||||
// 更新购物车摘要
|
||||
function updateCartSummary(cart) {
|
||||
$('#totalQuantity').text(cart.totalQuantity || 0);
|
||||
$('#totalPrice').text((cart.totalPrice || 0).toFixed(2));
|
||||
}
|
||||
|
||||
// 绑定购物车事件
|
||||
function bindCartEvents() {
|
||||
// 全选/反选
|
||||
$('#selectAll').on('change', function () {
|
||||
const isChecked = $(this).is(':checked');
|
||||
$('.item-checkbox').prop('checked', isChecked);
|
||||
updateSelectedSummary();
|
||||
});
|
||||
|
||||
// 单个选择
|
||||
$('.item-checkbox').on('change', function () {
|
||||
updateSelectedSummary();
|
||||
|
||||
// 检查是否全选
|
||||
const totalItems = $('.item-checkbox').length;
|
||||
const selectedItems = $('.item-checkbox:checked').length;
|
||||
$('#selectAll').prop('checked', totalItems === selectedItems);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新已选中商品摘要
|
||||
function updateSelectedSummary() {
|
||||
const selectedItems = $('.item-checkbox:checked');
|
||||
let selectedCount = 0;
|
||||
let selectedTotal = 0;
|
||||
|
||||
selectedItems.each(function () {
|
||||
const productId = $(this).val();
|
||||
const cartItem = $(this).closest('.cart-item');
|
||||
const quantity = parseInt(cartItem.find('input[type="number"]').val());
|
||||
const price = parseFloat(cartItem.find('.text-danger.fw-bold').text().replace('¥', ''));
|
||||
|
||||
selectedCount += quantity;
|
||||
selectedTotal += price;
|
||||
});
|
||||
|
||||
$('#selectedCount').text(selectedCount);
|
||||
$('#checkoutBtn').prop('disabled', selectedItems.length === 0);
|
||||
}
|
||||
|
||||
// 更新商品数量
|
||||
function updateQuantity(productId, newQuantity) {
|
||||
newQuantity = parseInt(newQuantity);
|
||||
|
||||
if (newQuantity < 1) {
|
||||
if (confirm('确定要从购物车中移除这个商品吗?')) {
|
||||
removeFromCart(productId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
const cartItem = $(`.cart-item[data-product-id="` + productId + `"]`);
|
||||
const maxStock = parseInt(cartItem.find('input[type="number"]').attr('max'));
|
||||
|
||||
if (newQuantity > maxStock) {
|
||||
showMessage('数量不能超过库存:' + maxStock, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/update',
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: newQuantity
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
loadCart(); // 重新加载购物车
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('更新失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 从购物车移除商品
|
||||
function removeFromCart(productId) {
|
||||
if (!confirm('确定要从购物车中移除这个商品吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/remove',
|
||||
type: 'DELETE',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已从购物车移除', 'success');
|
||||
loadCart(); // 重新加载购物车
|
||||
updateCartCount(); // 更新导航栏购物车数量
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('移除失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 批量移除选中商品
|
||||
function batchRemoveSelected() {
|
||||
const selectedItems = $('.item-checkbox:checked');
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
showMessage('请先选择要删除的商品', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('确定要删除选中的 ' + selectedItems.length + ' 个商品吗?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const productIds = [];
|
||||
selectedItems.each(function () {
|
||||
productIds.push(parseInt($(this).val()));
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/batch-remove',
|
||||
type: 'DELETE',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productIds: productIds
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('选中商品已删除', 'success');
|
||||
loadCart(); // 重新加载购物车
|
||||
updateCartCount(); // 更新导航栏购物车数量
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('删除失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空购物车
|
||||
function clearCart() {
|
||||
if (!confirm('确定要清空购物车吗?此操作不可恢复。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/clear',
|
||||
type: 'DELETE',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('购物车已清空', 'success');
|
||||
showEmptyCart();
|
||||
updateCartCount(); // 更新导航栏购物车数量
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('清空失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 去结算
|
||||
function checkout() {
|
||||
const selectedItems = $('.item-checkbox:checked');
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
showMessage('请先选择要结算的商品', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 收集选中的商品ID
|
||||
const productIds = [];
|
||||
selectedItems.each(function () {
|
||||
productIds.push(parseInt($(this).val()));
|
||||
});
|
||||
|
||||
// 跳转到结算页面
|
||||
const form = $('<form method="post" action="${pageContext.request.contextPath}/checkout">');
|
||||
productIds.forEach(function (productId) {
|
||||
form.append('<input type="hidden" name="productIds" value="' + productId + '">');
|
||||
});
|
||||
$('body').append(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// 加载推荐商品
|
||||
function loadRecommendedProducts() {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/product/hot?limit=4',
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderRecommendedProducts(response.data);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
console.log('加载推荐商品失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染推荐商品
|
||||
function renderRecommendedProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function (product) {
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<img src="` + (product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
class="card-img-top" alt="` + product.name + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + product.name + `</h6>
|
||||
<p class="card-text text-muted small text-truncate">` + (product.description || '暂无描述') + `</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold">¥` + (product.price ? product.price.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted">库存: ` + (product.stock || 0) + `</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="addToCartFromRecommend(` + product.id + `)">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#recommendedProducts').html(html);
|
||||
}
|
||||
|
||||
// 从推荐商品添加到购物车
|
||||
function addToCartFromRecommend(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
loadCart(); // 重新加载购物车
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type == 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
function updateCartCount() {
|
||||
$.get('${pageContext.request.contextPath}/api/cart/count')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
const cartBadge = document.querySelector('.cart-count');
|
||||
if (cartBadge) {
|
||||
const count = response.data.count || 0;
|
||||
cartBadge.textContent = count;
|
||||
cartBadge.style.display = count > 0 ? 'inline' : 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.cart-item {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.cart-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.input-group-sm .form-control {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cart-item .col-md-1,
|
||||
.cart-item .col-md-2,
|
||||
.cart-item .col-md-3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
873
src/main/webapp/WEB-INF/views/flashsale-detail.jsp
Normal file
873
src/main/webapp/WEB-INF/views/flashsale-detail.jsp
Normal file
@@ -0,0 +1,873 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="秒杀详情"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/flashsales">秒杀活动</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">秒杀详情</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingDetail" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载秒杀详情中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div id="errorDetail" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle fa-4x text-warning mb-3"></i>
|
||||
<h5 class="text-muted">秒杀活动不存在或已被删除</h5>
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-primary mt-3">
|
||||
<i class="fas fa-arrow-left"></i> 返回秒杀列表
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<div id="flashSaleDetail" style="display: none;">
|
||||
<div class="row">
|
||||
<!-- 左侧:商品信息 -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="position-relative">
|
||||
<img id="productImage" src="" class="card-img-top" alt=""
|
||||
style="height: 400px; object-fit: cover;">
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<div class="position-absolute top-0 start-0 m-3">
|
||||
<span id="statusBadge" class="badge fs-6"></span>
|
||||
</div>
|
||||
|
||||
<!-- 折扣标签 -->
|
||||
<div id="discountBadge" class="position-absolute top-0 end-0 m-3" style="display: none;">
|
||||
<span class="badge bg-warning text-dark fs-6"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<h4 id="productName" class="card-title fw-bold"></h4>
|
||||
<p id="productDescription" class="card-text text-muted"></p>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<div class="price-section mb-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-6">
|
||||
<span class="text-muted small">秒杀价</span>
|
||||
<div class="text-danger fw-bold" style="font-size: 2.5rem;" id="flashPrice">¥0.00
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<span class="text-muted small">原价</span>
|
||||
<div class="text-muted text-decoration-line-through" style="font-size: 1.5rem;"
|
||||
id="originalPrice">¥0.00
|
||||
</div>
|
||||
<div class="text-success fw-bold" id="savings">节省 ¥0.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:秒杀信息 -->
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-fire"></i> 秒杀信息
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 倒计时 -->
|
||||
<div id="countdownSection" class="text-center mb-4 p-4 bg-light rounded">
|
||||
<div id="countdownLabel" class="text-muted mb-2"></div>
|
||||
<div id="countdown" class="display-4 fw-bold text-danger"
|
||||
style="font-family: 'Courier New', monospace;">
|
||||
--:--:--
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 库存信息 -->
|
||||
<div class="stock-section mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="fw-bold">剩余库存</span>
|
||||
<span id="stockInfo" class="fw-bold text-danger">0/0</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div id="stockProgress" class="progress-bar bg-danger" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-muted">已抢</small>
|
||||
<small class="text-muted">剩余</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动时间 -->
|
||||
<div class="time-section mb-4">
|
||||
<h6 class="fw-bold mb-3">活动时间</h6>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">开始时间</small>
|
||||
<div id="startTime" class="fw-bold">--</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">结束时间</small>
|
||||
<div id="endTime" class="fw-bold">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 购买限制 -->
|
||||
<div class="limit-section mb-4">
|
||||
<h6 class="fw-bold mb-2">购买限制</h6>
|
||||
<ul class="list-unstyled small text-muted">
|
||||
<li><i class="fas fa-check text-success me-2"></i>每人限购 1 件</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>不支持退换货</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>限时限量,售完即止</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-section">
|
||||
<div class="d-grid gap-2">
|
||||
<button id="actionButton" class="btn btn-lg" onclick="handleAction()">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" onclick="addToCart()">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车(原价)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分享和收藏 -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">分享给好友</span>
|
||||
<div>
|
||||
<button class="btn btn-outline-secondary btn-sm me-2" onclick="shareWeChat()">
|
||||
<i class="fab fa-weixin text-success"></i> 微信
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm me-2" onclick="shareWeibo()">
|
||||
<i class="fab fa-weibo text-danger"></i> 微博
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="copyLink()">
|
||||
<i class="fas fa-link"></i> 复制链接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动规则说明 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-info-circle"></i> 活动规则
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6 class="fw-bold text-primary">参与条件</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-check text-success me-2"></i>需要登录账户</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>每个用户限购一件</li>
|
||||
<li><i class="fas fa-check text-success me-2"></i>先到先得,售完即止</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="fw-bold text-primary">注意事项</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-exclamation-circle text-warning me-2"></i>秒杀商品不支持退换
|
||||
</li>
|
||||
<li><i class="fas fa-exclamation-circle text-warning me-2"></i>请在规定时间内完成支付
|
||||
</li>
|
||||
<li><i class="fas fa-exclamation-circle text-warning me-2"></i>恶意刷单将被系统拦截
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推荐商品 -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h5 class="fw-bold mb-3">
|
||||
<i class="fas fa-heart text-danger"></i> 推荐商品
|
||||
</h5>
|
||||
<div id="recommendedProducts" class="row">
|
||||
<!-- 推荐商品将通过AJAX加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let flashSaleId = ${flashSaleId};
|
||||
let flashSaleData = null;
|
||||
let countdownInterval = null;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadFlashSaleDetail();
|
||||
loadRecommendedProducts();
|
||||
});
|
||||
|
||||
// 加载秒杀详情
|
||||
function loadFlashSaleDetail() {
|
||||
$('#loadingDetail').show();
|
||||
$('#errorDetail').hide();
|
||||
$('#flashSaleDetail').hide();
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + flashSaleId,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data) {
|
||||
flashSaleData = response.data;
|
||||
renderFlashSaleDetail(flashSaleData);
|
||||
$('#flashSaleDetail').show();
|
||||
} else {
|
||||
showErrorDetail();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showErrorDetail();
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingDetail').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示错误状态
|
||||
function showErrorDetail() {
|
||||
$('#errorDetail').show();
|
||||
$('#flashSaleDetail').hide();
|
||||
}
|
||||
|
||||
// 渲染秒杀详情
|
||||
function renderFlashSaleDetail(data) {
|
||||
// 更新页面标题
|
||||
document.title = data.productName + ' - 秒杀详情';
|
||||
|
||||
// 商品信息
|
||||
$('#productImage').attr('src', data.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg')
|
||||
.attr('alt', data.productName);
|
||||
$('#productName').text(data.productName || '商品名称');
|
||||
$('#productDescription').text(data.productDescription || '暂无描述');
|
||||
|
||||
// 价格信息
|
||||
const flashPrice = data.flashPrice || 0;
|
||||
const originalPrice = data.originalPrice || 0;
|
||||
const savings = originalPrice - flashPrice;
|
||||
|
||||
$('#flashPrice').text('¥' + flashPrice.toFixed(2));
|
||||
$('#originalPrice').text('¥' + originalPrice.toFixed(2));
|
||||
$('#savings').text('节省 ¥' + savings.toFixed(2));
|
||||
|
||||
// 折扣标签
|
||||
if (originalPrice > flashPrice && originalPrice > 0) {
|
||||
const discountPercent = Math.round((1 - flashPrice / originalPrice) * 100);
|
||||
$('#discountBadge span').text(discountPercent + '% OFF');
|
||||
$('#discountBadge').show();
|
||||
}
|
||||
|
||||
// 库存信息
|
||||
const remainingStock = data.remainingStock || 0;
|
||||
const totalStock = data.flashStock || 0;
|
||||
const soldStock = totalStock - remainingStock;
|
||||
const stockPercent = totalStock > 0 ? (remainingStock / totalStock * 100) : 0;
|
||||
|
||||
$('#stockInfo').text(remainingStock + '/' + totalStock);
|
||||
$('#stockProgress').css('width', Math.max(5, stockPercent) + '%');
|
||||
|
||||
if (stockPercent > 50) {
|
||||
$('#stockProgress').removeClass('bg-warning bg-danger').addClass('bg-success');
|
||||
} else if (stockPercent > 20) {
|
||||
$('#stockProgress').removeClass('bg-success bg-danger').addClass('bg-warning');
|
||||
} else {
|
||||
$('#stockProgress').removeClass('bg-success bg-warning').addClass('bg-danger');
|
||||
}
|
||||
|
||||
// 时间信息
|
||||
$('#startTime').text(formatDateTime(data.startTime));
|
||||
$('#endTime').text(formatDateTime(data.endTime));
|
||||
|
||||
// 状态和按钮
|
||||
updateStatusAndButton(data);
|
||||
|
||||
// 启动倒计时
|
||||
startCountdown(data);
|
||||
}
|
||||
|
||||
// 更新状态和按钮
|
||||
function updateStatusAndButton(data) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(data.startTime);
|
||||
const endTime = new Date(data.endTime);
|
||||
const remainingStock = data.remainingStock || 0;
|
||||
|
||||
let status, buttonText, buttonClass, buttonIcon, buttonDisabled = false;
|
||||
|
||||
if (now < startTime) {
|
||||
// 未开始
|
||||
status = {text: '即将开始', class: 'bg-warning text-dark', icon: 'fas fa-clock'};
|
||||
buttonText = '活动未开始';
|
||||
buttonClass = 'btn-outline-primary';
|
||||
buttonIcon = 'fas fa-clock';
|
||||
buttonDisabled = true;
|
||||
} else if (now >= startTime && now < endTime) {
|
||||
if (remainingStock > 0) {
|
||||
// 进行中
|
||||
status = {text: '正在抢购', class: 'bg-danger', icon: 'fas fa-fire'};
|
||||
buttonText = '立即抢购';
|
||||
buttonClass = 'btn-danger';
|
||||
buttonIcon = 'fas fa-bolt';
|
||||
} else {
|
||||
// 已售罄
|
||||
status = {text: '已售罄', class: 'bg-secondary', icon: 'fas fa-times'};
|
||||
buttonText = '已售罄';
|
||||
buttonClass = 'btn-secondary';
|
||||
buttonIcon = 'fas fa-times';
|
||||
buttonDisabled = true;
|
||||
}
|
||||
} else {
|
||||
// 已结束
|
||||
status = {text: '已结束', class: 'bg-secondary', icon: 'fas fa-check'};
|
||||
buttonText = '活动已结束';
|
||||
buttonClass = 'btn-secondary';
|
||||
buttonIcon = 'fas fa-check';
|
||||
buttonDisabled = true;
|
||||
}
|
||||
|
||||
// 更新状态标签
|
||||
$('#statusBadge').attr('class', 'badge fs-6 ' + status.class)
|
||||
.html('<i class="' + status.icon + '"></i> ' + status.text);
|
||||
|
||||
// 更新按钮
|
||||
$('#actionButton').attr('class', 'btn btn-lg ' + buttonClass)
|
||||
.prop('disabled', buttonDisabled)
|
||||
.html('<i class="' + buttonIcon + '"></i> ' + buttonText);
|
||||
}
|
||||
|
||||
// 启动倒计时
|
||||
function startCountdown(data) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(data.startTime);
|
||||
const endTime = new Date(data.endTime);
|
||||
|
||||
let targetTime, labelText;
|
||||
|
||||
if (now < startTime) {
|
||||
targetTime = startTime.getTime();
|
||||
labelText = '距离开始还有';
|
||||
$('#countdown').removeClass('text-danger').addClass('text-primary');
|
||||
} else if (now >= startTime && now < endTime) {
|
||||
targetTime = endTime.getTime();
|
||||
labelText = '距离结束还有';
|
||||
$('#countdown').removeClass('text-primary').addClass('text-danger');
|
||||
} else {
|
||||
$('#countdownLabel').text('活动已结束');
|
||||
$('#countdown').text('00:00:00').removeClass('text-primary text-danger').addClass('text-muted');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#countdownLabel').text(labelText);
|
||||
|
||||
// 清除之前的定时器
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
|
||||
// 更新倒计时
|
||||
function updateCountdown() {
|
||||
const now = Date.now();
|
||||
const timeLeft = targetTime - now;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
// 重新加载页面数据
|
||||
loadFlashSaleDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
|
||||
|
||||
let timeString;
|
||||
if (days > 0) {
|
||||
timeString = days + '天 ' + hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
} else {
|
||||
timeString = hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
$('#countdown').text(timeString);
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
countdownInterval = setInterval(updateCountdown, 1000);
|
||||
}
|
||||
|
||||
// 处理主要操作(抢购)
|
||||
function handleAction() {
|
||||
if (!flashSaleData) return;
|
||||
|
||||
const now = new Date();
|
||||
const startTime = new Date(flashSaleData.startTime);
|
||||
const endTime = new Date(flashSaleData.endTime);
|
||||
const remainingStock = flashSaleData.remainingStock || 0;
|
||||
|
||||
// 检查活动状态
|
||||
if (now < startTime) {
|
||||
showMessage('活动还未开始,请耐心等待', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (now >= endTime) {
|
||||
showMessage('活动已结束', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingStock <= 0) {
|
||||
showMessage('商品已售罄', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 参与秒杀
|
||||
participateFlashSale();
|
||||
}
|
||||
|
||||
// 参与秒杀(优化版)
|
||||
function participateFlashSale() {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
// 防止重复点击
|
||||
if (window.flashSaleInProgress) {
|
||||
showMessage('操作进行中,请稍候...', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
if (!confirm('确定要参与这个秒杀活动吗?\n\n注意:每人限购一件,确认后将立即抢购!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置全局锁
|
||||
window.flashSaleInProgress = true;
|
||||
|
||||
const button = $('#actionButton');
|
||||
const originalText = button.html();
|
||||
const originalClass = button.attr('class');
|
||||
|
||||
// 更新按钮状态
|
||||
button.prop('disabled', true);
|
||||
button.attr('class', 'btn btn-warning btn-lg');
|
||||
button.html('<i class="fas fa-spinner fa-spin"></i> 抢购中...');
|
||||
|
||||
// 添加视觉反馈
|
||||
button.css('transform', 'scale(0.95)');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
timeout: 10000, // 10秒超时
|
||||
data: JSON.stringify({
|
||||
flashSaleId: flashSaleId,
|
||||
quantity: 1,
|
||||
timestamp: startTime
|
||||
}),
|
||||
success: function (response) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (response.success) {
|
||||
// 成功状态
|
||||
button.attr('class', 'btn btn-success btn-lg');
|
||||
button.html('<i class="fas fa-check"></i> 抢购成功!');
|
||||
button.css('transform', 'scale(1.05)');
|
||||
|
||||
showMessage(`🎉 恭喜您!秒杀成功,订单已生成 (耗时: ${duration}ms)`, 'success');
|
||||
|
||||
// 重新加载详情数据
|
||||
setTimeout(() => {
|
||||
loadFlashSaleDetail();
|
||||
}, 1000);
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 3000);
|
||||
} else {
|
||||
// 失败状态
|
||||
button.attr('class', 'btn btn-danger btn-lg');
|
||||
button.html('<i class="fas fa-times"></i> ' + (response.message || '抢购失败'));
|
||||
|
||||
showMessage(response.message || '抢购失败,请重试', 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreDetailButton(button, originalText, originalClass);
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// 错误状态
|
||||
button.attr('class', 'btn btn-danger btn-lg');
|
||||
|
||||
let errorMessage = '网络异常,请重试';
|
||||
if (status === 'timeout') {
|
||||
errorMessage = '请求超时,请检查网络连接';
|
||||
button.html('<i class="fas fa-clock"></i> 请求超时');
|
||||
} else if (xhr.status === 429) {
|
||||
errorMessage = '请求过于频繁,请稍后再试';
|
||||
button.html('<i class="fas fa-ban"></i> 请求频繁');
|
||||
} else {
|
||||
button.html('<i class="fas fa-exclamation-triangle"></i> 网络异常');
|
||||
}
|
||||
|
||||
showMessage(errorMessage, 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreDetailButton(button, originalText, originalClass);
|
||||
}, 3000);
|
||||
},
|
||||
complete: function () {
|
||||
// 释放全局锁
|
||||
setTimeout(() => {
|
||||
window.flashSaleInProgress = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录后参与秒杀', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login?returnUrl=' +
|
||||
encodeURIComponent('/flashsale/' + flashSaleId);
|
||||
}, 1500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 恢复详情页按钮状态
|
||||
function restoreDetailButton(button, originalText, originalClass) {
|
||||
button.prop('disabled', false);
|
||||
button.attr('class', originalClass);
|
||||
button.html(originalText);
|
||||
button.css('transform', 'scale(1)');
|
||||
}
|
||||
|
||||
// 加入购物车(原价)
|
||||
function addToCart() {
|
||||
if (!flashSaleData) return;
|
||||
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: flashSaleData.productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 分享功能
|
||||
function shareWeChat() {
|
||||
showMessage('请使用微信扫一扫功能分享', 'info');
|
||||
}
|
||||
|
||||
function shareWeibo() {
|
||||
const text = encodeURIComponent('发现一个超值秒杀:' + (flashSaleData ? flashSaleData.productName : '') + ' 限时抢购!');
|
||||
const url = encodeURIComponent(window.location.href);
|
||||
window.open('https://service.weibo.com/share/share.php?title=' + text + '&url=' + url, '_blank');
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
navigator.clipboard.writeText(window.location.href).then(function () {
|
||||
showMessage('链接已复制到剪贴板', 'success');
|
||||
}, function () {
|
||||
showMessage('复制失败,请手动复制链接', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载推荐商品
|
||||
function loadRecommendedProducts() {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/product/hot?limit=4',
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderRecommendedProducts(response.data);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
console.log('加载推荐商品失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染推荐商品
|
||||
function renderRecommendedProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function (product) {
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<img src="` + (product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
class="card-img-top" alt="` + product.name + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + product.name + `</h6>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold">¥` + (product.price ? product.price.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted">库存: ` + (product.stock || 0) + `</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="addProductToCart(` + product.id + `)">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#recommendedProducts').html(html);
|
||||
}
|
||||
|
||||
// 添加推荐商品到购物车
|
||||
function addProductToCart(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
function updateCartCount() {
|
||||
$.get('${pageContext.request.contextPath}/api/cart/count')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
const cartBadge = document.querySelector('.cart-count');
|
||||
if (cartBadge) {
|
||||
const count = response.data.count || 0;
|
||||
cartBadge.textContent = count;
|
||||
cartBadge.style.display = count > 0 ? 'inline' : 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type == 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 页面离开时清理定时器
|
||||
$(window).on('beforeunload', function () {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.price-section {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%);
|
||||
}
|
||||
|
||||
#countdown {
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stock-section {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.time-section,
|
||||
.limit-section {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.price-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-section .col-6 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#countdown {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fa-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 消息提示动画 */
|
||||
.alert.position-fixed {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
889
src/main/webapp/WEB-INF/views/flashsales.jsp
Normal file
889
src/main/webapp/WEB-INF/views/flashsales.jsp
Normal file
@@ -0,0 +1,889 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="秒杀活动"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<!-- 页面标题 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item active">秒杀活动</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-fire text-danger"></i> 秒杀活动
|
||||
<small class="text-muted fs-6">限时抢购,先到先得</small>
|
||||
</h2>
|
||||
<div class="text-end">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="currentTime"></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="statusFilter" onchange="filterFlashSales()">
|
||||
<option value="">全部活动</option>
|
||||
<option value="upcoming">即将开始</option>
|
||||
<option value="active">进行中</option>
|
||||
<option value="ended">已结束</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="sortBy" onchange="sortFlashSales()">
|
||||
<option value="startTime">按开始时间</option>
|
||||
<option value="endTime">按结束时间</option>
|
||||
<option value="flashPrice">按价格</option>
|
||||
<option value="remainingStock">按剩余库存</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索商品名称...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchFlashSales()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-outline-primary w-100" onclick="refreshFlashSales()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-warning">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-clock fa-2x text-warning mb-2"></i>
|
||||
<h5 class="card-title text-warning" id="upcomingCount">0</h5>
|
||||
<p class="card-text">即将开始</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-success">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-fire fa-2x text-success mb-2"></i>
|
||||
<h5 class="card-title text-success" id="activeCount">0</h5>
|
||||
<p class="card-text">正在抢购</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-danger">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-bolt fa-2x text-danger mb-2"></i>
|
||||
<h5 class="card-title text-danger" id="hotCount">0</h5>
|
||||
<p class="card-text">热门活动</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6 mb-3">
|
||||
<div class="card text-center border-secondary">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-check fa-2x text-secondary mb-2"></i>
|
||||
<h5 class="card-title text-secondary" id="endedCount">0</h5>
|
||||
<p class="card-text">已结束</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活动列表 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingFlashSales" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载秒杀活动中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div id="emptyFlashSales" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-fire fa-4x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">暂无秒杀活动</h5>
|
||||
<p class="text-muted">敬请期待更多精彩活动~</p>
|
||||
</div>
|
||||
|
||||
<!-- 活动网格 -->
|
||||
<div id="flashSalesGrid" class="row" style="display: none;">
|
||||
<!-- 活动卡片将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="活动分页" class="mt-4">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 12;
|
||||
let totalPages = 1;
|
||||
let currentFilters = {};
|
||||
|
||||
$(document).ready(function () {
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000); // 每秒更新时间
|
||||
|
||||
loadFlashSales();
|
||||
|
||||
// 每30秒刷新一次数据
|
||||
setInterval(function () {
|
||||
refreshFlashSales();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
$('#currentTime').text(timeString);
|
||||
}
|
||||
|
||||
// 加载秒杀活动
|
||||
function loadFlashSales(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
$('#loadingFlashSales').show();
|
||||
$('#emptyFlashSales').hide();
|
||||
$('#flashSalesGrid').hide();
|
||||
|
||||
// 构建查询参数
|
||||
const queryData = {
|
||||
page: page - 1,
|
||||
size: pageSize,
|
||||
sortBy: $('#sortBy').val() || 'startTime',
|
||||
sortDirection: 'asc',
|
||||
...currentFilters
|
||||
};
|
||||
|
||||
// 添加搜索关键词
|
||||
const keyword = $('#searchInput').val().trim();
|
||||
if (keyword) {
|
||||
queryData.keyword = keyword;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/list',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(queryData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
const flashSales = response.data.content || response.data.flashSales || [];
|
||||
renderFlashSales(flashSales);
|
||||
renderPagination(response.data.totalElements || response.data.total || 0, pageSize);
|
||||
updateStatistics(flashSales);
|
||||
} else {
|
||||
showEmptyFlashSales();
|
||||
showMessage('获取秒杀活动失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showEmptyFlashSales();
|
||||
showMessage('网络请求失败,请稍后重试', 'error');
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingFlashSales').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染秒杀活动
|
||||
function renderFlashSales(flashSales) {
|
||||
if (flashSales.length === 0) {
|
||||
showEmptyFlashSales();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
flashSales.forEach(function (flashSale) {
|
||||
const discountPercent = flashSale.originalPrice > 0 ?
|
||||
Math.round((1 - flashSale.flashPrice / flashSale.originalPrice) * 100) : 0;
|
||||
|
||||
const status = getFlashSaleStatus(flashSale);
|
||||
const stockPercent = flashSale.flashStock > 0 ?
|
||||
(flashSale.remainingStock / flashSale.flashStock * 100) : 0;
|
||||
|
||||
html += `
|
||||
<div class="col-lg-4 col-md-6 mb-4">
|
||||
<div class="card h-100 flashsale-card" data-flashsale-id="` + flashSale.id + `">
|
||||
<div class="position-relative">
|
||||
<img src="` + (flashSale.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
class="card-img-top" alt="` + flashSale.productName + `" style="height: 220px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
|
||||
<!-- 状态标签 -->
|
||||
<div class="position-absolute top-0 start-0">
|
||||
<span class="badge ` + status.badgeClass + ` m-2">
|
||||
<i class="` + status.icon + `"></i> ` + status.text + `
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 折扣标签 -->
|
||||
` + (discountPercent > 0 ? `
|
||||
<div class="position-absolute top-0 end-0">
|
||||
<span class="badge bg-warning text-dark m-2">
|
||||
` + discountPercent + `% OFF
|
||||
</span>
|
||||
</div>
|
||||
` : '') + `
|
||||
</div>
|
||||
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h6 class="card-title text-truncate" title="` + flashSale.productName + `">
|
||||
` + flashSale.productName + `
|
||||
</h6>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<div class="price-section mb-3">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<span class="text-danger fw-bold fs-4">¥` + (flashSale.flashPrice || 0).toFixed(2) + `</span>
|
||||
` + (flashSale.originalPrice > flashSale.flashPrice ? `
|
||||
<small class="text-muted text-decoration-line-through ms-2">
|
||||
¥` + (flashSale.originalPrice || 0).toFixed(2) + `
|
||||
</small>
|
||||
` : '') + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 库存进度 -->
|
||||
<div class="stock-section mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<small class="text-muted">剩余库存</small>
|
||||
<small class="text-muted">` + (flashSale.remainingStock || 0) + `/` + (flashSale.flashStock || 0) + `</small>
|
||||
</div>
|
||||
<div class="progress" style="height: 6px;">
|
||||
<div class="progress-bar ` + (stockPercent > 50 ? 'bg-success' : stockPercent > 20 ? 'bg-warning' : 'bg-danger') + `"
|
||||
style="width: ` + Math.max(5, stockPercent) + `%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间信息 -->
|
||||
<div class="time-section mb-3">
|
||||
` + getTimeDisplay(flashSale, status) + `
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-auto">
|
||||
` + getActionButton(flashSale, status) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#flashSalesGrid').html(html);
|
||||
$('#flashSalesGrid').show();
|
||||
|
||||
// 启动倒计时
|
||||
startCountdowns();
|
||||
}
|
||||
|
||||
// 显示空状态
|
||||
function showEmptyFlashSales() {
|
||||
$('#emptyFlashSales').show();
|
||||
$('#flashSalesGrid').hide();
|
||||
}
|
||||
|
||||
// 获取秒杀状态
|
||||
function getFlashSaleStatus(flashSale) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(flashSale.startTime);
|
||||
const endTime = new Date(flashSale.endTime);
|
||||
|
||||
if (now < startTime) {
|
||||
return {
|
||||
key: 'upcoming',
|
||||
text: '即将开始',
|
||||
badgeClass: 'bg-warning text-dark',
|
||||
icon: 'fas fa-clock'
|
||||
};
|
||||
} else if (now >= startTime && now < endTime) {
|
||||
if ((flashSale.remainingStock || 0) > 0) {
|
||||
return {
|
||||
key: 'active',
|
||||
text: '正在抢购',
|
||||
badgeClass: 'bg-danger',
|
||||
icon: 'fas fa-fire'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
key: 'soldout',
|
||||
text: '已售罄',
|
||||
badgeClass: 'bg-secondary',
|
||||
icon: 'fas fa-times'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
key: 'ended',
|
||||
text: '已结束',
|
||||
badgeClass: 'bg-secondary',
|
||||
icon: 'fas fa-check'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取时间显示
|
||||
function getTimeDisplay(flashSale, status) {
|
||||
if (status.key === 'upcoming') {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">距开始还有</small>
|
||||
<div class="countdown text-primary fw-bold" data-target="` + new Date(flashSale.startTime).getTime() + `">
|
||||
计算中...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (status.key === 'active') {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">距结束还有</small>
|
||||
<div class="countdown text-danger fw-bold" data-target="` + new Date(flashSale.endTime).getTime() + `">
|
||||
计算中...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">活动时间</small>
|
||||
<div class="small">` + formatDateTime(flashSale.startTime) + ` - ` + formatDateTime(flashSale.endTime) + `</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取操作按钮
|
||||
function getActionButton(flashSale, status) {
|
||||
if (status.key === 'upcoming') {
|
||||
return `
|
||||
<button class="btn btn-outline-primary w-100" disabled>
|
||||
<i class="fas fa-clock"></i> 活动未开始
|
||||
</button>
|
||||
`;
|
||||
} else if (status.key === 'active') {
|
||||
return `
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-danger btn-lg flash-sale-btn"
|
||||
onclick="participateFlashSale(` + flashSale.id + `)"
|
||||
data-flashsale-id="` + flashSale.id + `"
|
||||
onmouseover="this.style.transform='scale(1.02)'"
|
||||
onmouseout="this.style.transform='scale(1)'">
|
||||
<i class="fas fa-bolt"></i> 立即抢购
|
||||
</button>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="viewFlashSaleDetail(` + flashSale.id + `)">
|
||||
<i class="fas fa-eye"></i> 查看详情
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else if (status.key === 'soldout') {
|
||||
return `
|
||||
<button class="btn btn-secondary w-100" disabled>
|
||||
<i class="fas fa-times"></i> 已售罄
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<button class="btn btn-outline-secondary w-100" onclick="viewFlashSaleDetail(` + flashSale.id + `)">
|
||||
<i class="fas fa-eye"></i> 查看详情
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 启动倒计时
|
||||
function startCountdowns() {
|
||||
$('.countdown').each(function () {
|
||||
const element = $(this);
|
||||
const targetTime = parseInt(element.data('target'));
|
||||
|
||||
if (targetTime) {
|
||||
updateCountdown(element, targetTime);
|
||||
|
||||
// 每秒更新倒计时
|
||||
element.data('interval', setInterval(function () {
|
||||
updateCountdown(element, targetTime);
|
||||
}, 1000));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新倒计时
|
||||
function updateCountdown(element, targetTime) {
|
||||
const now = Date.now();
|
||||
const timeLeft = targetTime - now;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
element.text('时间到');
|
||||
element.removeClass('text-primary text-danger').addClass('text-muted');
|
||||
clearInterval(element.data('interval'));
|
||||
// 刷新页面数据
|
||||
setTimeout(refreshFlashSales, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
|
||||
|
||||
let timeString = '';
|
||||
if (days > 0) {
|
||||
timeString = days + '天 ' + hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
} else {
|
||||
timeString = hours.toString().padStart(2, '0') + ':' +
|
||||
minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
element.text(timeString);
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStatistics(flashSales) {
|
||||
let upcoming = 0, active = 0, hot = 0, ended = 0;
|
||||
|
||||
flashSales.forEach(function (flashSale) {
|
||||
const status = getFlashSaleStatus(flashSale);
|
||||
|
||||
switch (status.key) {
|
||||
case 'upcoming':
|
||||
upcoming++;
|
||||
break;
|
||||
case 'active':
|
||||
active++;
|
||||
if ((flashSale.remainingStock / flashSale.flashStock) < 0.3) {
|
||||
hot++; // 库存少于30%认为是热门
|
||||
}
|
||||
break;
|
||||
case 'ended':
|
||||
case 'soldout':
|
||||
ended++;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$('#upcomingCount').text(upcoming);
|
||||
$('#activeCount').text(active);
|
||||
$('#hotCount').text(hot);
|
||||
$('#endedCount').text(ended);
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('#pagination').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
// 筛选活动
|
||||
function filterFlashSales() {
|
||||
const status = $('#statusFilter').val();
|
||||
currentFilters.status = status;
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
// 排序活动
|
||||
function sortFlashSales() {
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
// 搜索活动
|
||||
function searchFlashSales() {
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
// 刷新活动
|
||||
function refreshFlashSales() {
|
||||
// 清除所有倒计时
|
||||
$('.countdown').each(function () {
|
||||
clearInterval($(this).data('interval'));
|
||||
});
|
||||
|
||||
loadFlashSales(currentPage);
|
||||
}
|
||||
|
||||
// 参与秒杀(优化版)
|
||||
function participateFlashSale(flashSaleId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
// 防止重复点击
|
||||
if (window.flashSaleInProgress) {
|
||||
showMessage('操作进行中,请稍候...', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确认对话框
|
||||
if (!confirm('确定要参与这个秒杀活动吗?\n\n注意:每人限购一件,确认后将立即抢购!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到按钮元素
|
||||
const button = event.target.closest('button');
|
||||
if (!button) return;
|
||||
|
||||
// 设置全局锁
|
||||
window.flashSaleInProgress = true;
|
||||
|
||||
// 保存原始状态
|
||||
const originalText = button.innerHTML;
|
||||
const originalClass = button.className;
|
||||
|
||||
// 更新按钮状态
|
||||
button.disabled = true;
|
||||
button.className = 'btn btn-warning btn-lg';
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 抢购中...';
|
||||
|
||||
// 添加视觉反馈
|
||||
button.style.transform = 'scale(0.95)';
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
timeout: 10000, // 10秒超时
|
||||
data: JSON.stringify({
|
||||
flashSaleId: flashSaleId,
|
||||
quantity: 1,
|
||||
timestamp: startTime
|
||||
}),
|
||||
success: function (response) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (response.success) {
|
||||
// 成功状态
|
||||
button.className = 'btn btn-success btn-lg';
|
||||
button.innerHTML = '<i class="fas fa-check"></i> 抢购成功!';
|
||||
button.style.transform = 'scale(1.05)';
|
||||
|
||||
showMessage(`🎉 恭喜您!秒杀成功,订单已生成 (耗时: ${duration}ms)`, 'success');
|
||||
|
||||
// 刷新页面数据
|
||||
setTimeout(() => {
|
||||
refreshFlashSales();
|
||||
}, 1000);
|
||||
|
||||
// 跳转到订单页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 3000);
|
||||
} else {
|
||||
// 失败状态
|
||||
button.className = 'btn btn-danger btn-lg';
|
||||
button.innerHTML = '<i class="fas fa-times"></i> ' + (response.message || '抢购失败');
|
||||
|
||||
showMessage(response.message || '抢购失败,请重试', 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreButton(button, originalText, originalClass);
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// 错误状态
|
||||
button.className = 'btn btn-danger btn-lg';
|
||||
|
||||
let errorMessage = '网络异常,请重试';
|
||||
if (status === 'timeout') {
|
||||
errorMessage = '请求超时,请检查网络连接';
|
||||
button.innerHTML = '<i class="fas fa-clock"></i> 请求超时';
|
||||
} else if (xhr.status === 429) {
|
||||
errorMessage = '请求过于频繁,请稍后再试';
|
||||
button.innerHTML = '<i class="fas fa-ban"></i> 请求频繁';
|
||||
} else {
|
||||
button.innerHTML = '<i class="fas fa-exclamation-triangle"></i> 网络异常';
|
||||
}
|
||||
|
||||
showMessage(errorMessage, 'error');
|
||||
|
||||
// 恢复按钮状态
|
||||
setTimeout(() => {
|
||||
restoreButton(button, originalText, originalClass);
|
||||
}, 3000);
|
||||
},
|
||||
complete: function () {
|
||||
// 释放全局锁
|
||||
setTimeout(() => {
|
||||
window.flashSaleInProgress = false;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录后参与秒杀', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login?returnUrl=' +
|
||||
encodeURIComponent('/flashsales');
|
||||
}, 1500);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 恢复按钮状态
|
||||
function restoreButton(button, originalText, originalClass) {
|
||||
button.disabled = false;
|
||||
button.className = originalClass;
|
||||
button.innerHTML = originalText;
|
||||
button.style.transform = 'scale(1)';
|
||||
}
|
||||
|
||||
// 查看秒杀详情
|
||||
function viewFlashSaleDetail(flashSaleId) {
|
||||
window.location.href = '${pageContext.request.contextPath}/flashsale/' + flashSaleId;
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type == 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.flashsale-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.flashsale-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.flashsale-card:hover .card-img-top {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 1.1em;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.price-section .fs-4 {
|
||||
font-size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flashsale-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.flashsale-card:hover .card-img-top {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.price-section .fs-4 {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 抢购按钮样式优化 */
|
||||
.flash-sale-btn {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.flash-sale-btn:hover {
|
||||
box-shadow: 0 8px 25px rgba(220, 53, 69, 0.4);
|
||||
transform: translateY(-2px) scale(1.02);
|
||||
}
|
||||
|
||||
.flash-sale-btn:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
.flash-sale-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.flash-sale-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* 按钮禁用状态 */
|
||||
.flash-sale-btn:disabled {
|
||||
opacity: 0.8;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fa-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 按钮状态动画 */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(220, 53, 69, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 53, 69, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger.flash-sale-btn:not(:disabled) {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
/* 消息提示动画 */
|
||||
.alert.position-fixed {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
631
src/main/webapp/WEB-INF/views/orders.jsp
Normal file
631
src/main/webapp/WEB-INF/views/orders.jsp
Normal file
@@ -0,0 +1,631 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="我的订单"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="${pageContext.request.contextPath}/">首页</a></li>
|
||||
<li class="breadcrumb-item active">我的订单</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单筛选和搜索 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="statusFilter" onchange="filterOrders()">
|
||||
<option value="">全部订单</option>
|
||||
<option value="1">待支付</option>
|
||||
<option value="2">已支付</option>
|
||||
<option value="3">已发货</option>
|
||||
<option value="4">已完成</option>
|
||||
<option value="5">已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="typeFilter" onchange="filterOrders()">
|
||||
<option value="">全部类型</option>
|
||||
<option value="1">普通订单</option>
|
||||
<option value="2">秒杀订单</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput"
|
||||
placeholder="搜索订单号或商品名称...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchOrders()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-outline-primary w-100" onclick="refreshOrders()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list-alt text-primary"></i> 我的订单
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- 加载中状态 -->
|
||||
<div id="loadingOrders" class="text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载订单中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 空订单状态 -->
|
||||
<div id="emptyOrders" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-receipt fa-4x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">暂无订单</h5>
|
||||
<p class="text-muted">快去下单购买您喜欢的商品吧~</p>
|
||||
<a href="${pageContext.request.contextPath}/" class="btn btn-primary">
|
||||
<i class="fas fa-shopping-bag"></i> 去购物
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表容器 -->
|
||||
<div id="ordersList"></div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="订单分页" class="mt-4">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情模态框 -->
|
||||
<div class="modal fade" id="orderDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">订单详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="orderDetailContent">
|
||||
<!-- 订单详情内容将动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
loadOrders();
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
$('#loadingOrders').hide();
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
});
|
||||
|
||||
// 加载订单列表
|
||||
function loadOrders(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
$('#loadingOrders').show();
|
||||
$('#emptyOrders').hide();
|
||||
$('#ordersList').hide();
|
||||
|
||||
// 构建查询参数
|
||||
const queryData = {
|
||||
page: page - 1, // 后端使用0基索引
|
||||
size: pageSize,
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc'
|
||||
};
|
||||
|
||||
// 添加状态筛选
|
||||
const statusFilter = $('#statusFilter').val();
|
||||
if (statusFilter) {
|
||||
queryData.status = parseInt(statusFilter);
|
||||
}
|
||||
|
||||
// 添加类型筛选
|
||||
const typeFilter = $('#typeFilter').val();
|
||||
if (typeFilter) {
|
||||
queryData.orderType = parseInt(typeFilter);
|
||||
}
|
||||
|
||||
// 添加搜索关键词
|
||||
const keyword = $('#searchInput').val().trim();
|
||||
if (keyword) {
|
||||
queryData.keyword = keyword;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/my-orders',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(queryData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
renderOrders(response.data.content || response.data.orders || []);
|
||||
renderPagination(response.data.totalElements || response.data.total || 0, pageSize);
|
||||
} else {
|
||||
showEmptyOrders();
|
||||
showMessage('获取订单数据失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showEmptyOrders();
|
||||
showMessage('网络请求失败,请稍后重试', 'error');
|
||||
},
|
||||
complete: function () {
|
||||
$('#loadingOrders').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染订单列表
|
||||
function renderOrders(orders) {
|
||||
if (orders.length === 0) {
|
||||
showEmptyOrders();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
orders.forEach(function (order) {
|
||||
html += `
|
||||
<div class="order-item border rounded mb-3 p-3">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">订单号</small>
|
||||
<div class="fw-bold text-truncate" style="font-size: 0.9rem;">#` + order.id + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">商品</small>
|
||||
<div class="fw-bold text-truncate">` + (order.productName || '商品信息') + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div>
|
||||
<small class="text-muted">数量</small>
|
||||
<div class="fw-bold">` + order.quantity + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">总价</small>
|
||||
<div class="fw-bold text-danger">¥` + (order.totalPrice ? order.totalPrice.toFixed(2) : '0.00') + `</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div>
|
||||
<small class="text-muted">状态</small>
|
||||
<div>
|
||||
<span class="badge ` + getStatusBadgeClass(order.status) + `">
|
||||
` + getStatusText(order.status) + `
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div>
|
||||
<small class="text-muted">类型</small>
|
||||
<div>
|
||||
<span class="badge ` + getTypeBadgeClass(order.orderType) + `">
|
||||
` + getTypeText(order.orderType) + `
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="btn-group btn-group-sm d-flex">
|
||||
<button class="btn btn-outline-primary" onclick="viewOrderDetail(` + order.id + `)" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
` + getOrderActionButtons(order) + `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-12">
|
||||
<small class="text-muted">
|
||||
下单时间:` + formatDateTime(order.createdAt) + `
|
||||
` + (order.orderType == 2 ? ' | <i class="fas fa-bolt text-danger"></i> 秒杀订单' : '') + `
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#ordersList').html(html);
|
||||
$('#ordersList').show();
|
||||
}
|
||||
|
||||
// 显示空订单状态
|
||||
function showEmptyOrders() {
|
||||
$('#emptyOrders').show();
|
||||
$('#ordersList').hide();
|
||||
}
|
||||
|
||||
// 获取状态徽章样式
|
||||
function getStatusBadgeClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning text-dark'; // 待支付
|
||||
case 2:
|
||||
return 'bg-info'; // 已支付
|
||||
case 3:
|
||||
return 'bg-primary'; // 已发货
|
||||
case 4:
|
||||
return 'bg-success'; // 已完成
|
||||
case 5:
|
||||
return 'bg-secondary'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类型徽章样式
|
||||
function getTypeBadgeClass(orderType) {
|
||||
switch (orderType) {
|
||||
case 1:
|
||||
return 'bg-light text-dark'; // 普通订单
|
||||
case 2:
|
||||
return 'bg-danger'; // 秒杀订单
|
||||
default:
|
||||
return 'bg-light text-dark';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
function getTypeText(orderType) {
|
||||
switch (orderType) {
|
||||
case 1:
|
||||
return '普通';
|
||||
case 2:
|
||||
return '秒杀';
|
||||
default:
|
||||
return '普通';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取订单操作按钮
|
||||
function getOrderActionButtons(order) {
|
||||
let buttons = '';
|
||||
|
||||
switch (order.status) {
|
||||
case 1: // 待支付
|
||||
buttons += `<button class="btn btn-outline-success" onclick="payOrder(` + order.id + `)" title="去支付">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
</button>`;
|
||||
buttons += `<button class="btn btn-outline-danger" onclick="cancelOrder(` + order.id + `)" title="取消订单">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 2: // 已支付
|
||||
buttons += `<button class="btn btn-outline-info" onclick="remindShipping(` + order.id + `)" title="提醒发货">
|
||||
<i class="fas fa-truck"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 3: // 已发货
|
||||
buttons += `<button class="btn btn-outline-success" onclick="confirmReceipt(` + order.id + `)" title="确认收货">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 4: // 已完成
|
||||
buttons += `<button class="btn btn-outline-warning" onclick="reviewOrder(` + order.id + `)" title="评价">
|
||||
<i class="fas fa-star"></i>
|
||||
</button>`;
|
||||
break;
|
||||
case 5: // 已取消
|
||||
// 已取消的订单不显示操作按钮
|
||||
break;
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('#pagination').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
// 筛选订单
|
||||
function filterOrders() {
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
// 搜索订单
|
||||
function searchOrders() {
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
// 刷新订单
|
||||
function refreshOrders() {
|
||||
loadOrders(currentPage);
|
||||
}
|
||||
|
||||
// 查看订单详情
|
||||
function viewOrderDetail(orderId) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
renderOrderDetail(response.data);
|
||||
$('#orderDetailModal').modal('show');
|
||||
} else {
|
||||
showMessage('获取订单详情失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('获取订单详情失败,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染订单详情
|
||||
function renderOrderDetail(order) {
|
||||
const html = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>基本信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>订单号:</td><td>#` + order.id + `</td></tr>
|
||||
<tr><td>订单类型:</td><td><span class="badge ` + getTypeBadgeClass(order.orderType) + `">` + getTypeText(order.orderType) + `</span></td></tr>
|
||||
<tr><td>订单状态:</td><td><span class="badge ` + getStatusBadgeClass(order.status) + `">` + getStatusText(order.status) + `</span></td></tr>
|
||||
<tr><td>创建时间:</td><td>` + formatDateTime(order.createdAt) + `</td></tr>
|
||||
<tr><td>更新时间:</td><td>` + formatDateTime(order.updatedAt) + `</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>商品信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>商品名称:</td><td>` + (order.productName || '商品信息') + `</td></tr>
|
||||
<tr><td>购买数量:</td><td>` + order.quantity + ` 件</td></tr>
|
||||
<tr><td>商品单价:</td><td>¥` + (order.totalPrice / order.quantity).toFixed(2) + `</td></tr>
|
||||
<tr><td>订单总价:</td><td class="text-danger fw-bold">¥` + (order.totalPrice ? order.totalPrice.toFixed(2) : '0.00') + `</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
` + (order.remark ? `
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6>订单备注</h6>
|
||||
<p class="text-muted">` + order.remark + `</p>
|
||||
</div>
|
||||
</div>
|
||||
` : '') + `
|
||||
`;
|
||||
|
||||
$('#orderDetailContent').html(html);
|
||||
}
|
||||
|
||||
// 支付订单
|
||||
function payOrder(orderId) {
|
||||
if (confirm('确定要支付这个订单吗?')) {
|
||||
// 这里可以集成真实的支付接口
|
||||
showMessage('支付功能开发中...', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// 取消订单
|
||||
function cancelOrder(orderId) {
|
||||
if (confirm('确定要取消这个订单吗?')) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/cancel',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('订单已取消', 'success');
|
||||
refreshOrders();
|
||||
} else {
|
||||
showMessage('取消订单失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('取消订单失败,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 提醒发货
|
||||
function remindShipping(orderId) {
|
||||
showMessage('已提醒商家发货', 'success');
|
||||
}
|
||||
|
||||
// 确认收货
|
||||
function confirmReceipt(orderId) {
|
||||
if (confirm('确定已收到商品吗?确认后订单将完成。')) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/order/' + orderId + '/confirm',
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('确认收货成功,订单已完成', 'success');
|
||||
refreshOrders();
|
||||
} else {
|
||||
showMessage('确认收货失败: ' + response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
showMessage('确认收货失败,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 评价订单
|
||||
function reviewOrder(orderId) {
|
||||
showMessage('评价功能开发中...', 'info');
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type == 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.order-item {
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.order-item:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.text-truncate {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.order-item .col-md-1,
|
||||
.order-item .col-md-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-group-sm .btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.table td {
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.table td:first-child {
|
||||
font-weight: 500;
|
||||
width: 30%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
501
src/main/webapp/WEB-INF/views/products.jsp
Normal file
501
src/main/webapp/WEB-INF/views/products.jsp
Normal file
@@ -0,0 +1,501 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="商品列表"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<style>
|
||||
.product-card {
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.product-image {
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.price {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
color: #999;
|
||||
text-decoration: line-through;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stock-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.search-filters {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container my-4">
|
||||
<!-- 页面标题 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="page-title">
|
||||
<i class="fas fa-shopping-bag text-primary"></i>
|
||||
商品列表
|
||||
</h1>
|
||||
<p class="text-muted">发现更多优质商品</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="search-filters">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" id="searchKeyword" placeholder="搜索商品名称...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="categoryFilter">
|
||||
<option value="">全部分类</option>
|
||||
<option value="electronics">数码电子</option>
|
||||
<option value="clothing">服装鞋包</option>
|
||||
<option value="home">家居用品</option>
|
||||
<option value="books">图书文具</option>
|
||||
<option value="sports">运动户外</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-select" id="sortBy">
|
||||
<option value="id,desc">最新上架</option>
|
||||
<option value="price,asc">价格从低到高</option>
|
||||
<option value="price,desc">价格从高到低</option>
|
||||
<option value="sales,desc">销量最高</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-primary w-100" onclick="searchProducts()">
|
||||
<i class="fas fa-search"></i> 搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载动画 -->
|
||||
<div class="loading-spinner" id="loadingSpinner">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">正在加载商品...</p>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<div class="row" id="productList">
|
||||
<!-- 商品卡片将通过JavaScript动态加载 -->
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="商品分页" class="mt-4">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript动态生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="text-center py-5" id="emptyState" style="display: none;">
|
||||
<i class="fas fa-shopping-basket fa-4x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">暂无商品</h4>
|
||||
<p class="text-muted">请尝试调整搜索条件</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品详情模态框 -->
|
||||
<div class="modal fade" id="productModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">商品详情</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="productModalBody">
|
||||
<!-- 商品详情内容 -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" id="addToCartBtn">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 0;
|
||||
let currentSize = 12;
|
||||
let totalPages = 0;
|
||||
let currentProductId = null;
|
||||
|
||||
// 页面加载完成后获取商品列表
|
||||
$(document).ready(function () {
|
||||
loadProducts();
|
||||
|
||||
// 搜索框回车事件
|
||||
$('#searchKeyword').on('keypress', function (e) {
|
||||
if (e.which === 13) {
|
||||
searchProducts();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 加载商品列表
|
||||
function loadProducts(page = 0) {
|
||||
currentPage = page;
|
||||
showLoading(true);
|
||||
|
||||
const keyword = $('#searchKeyword').val().trim();
|
||||
const category = $('#categoryFilter').val();
|
||||
const sortValue = $('#sortBy').val().split(',');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: page,
|
||||
size: currentSize
|
||||
});
|
||||
|
||||
if (keyword) params.append('keyword', keyword);
|
||||
if (category) params.append('category', category);
|
||||
if (sortValue.length === 2) {
|
||||
params.append('sortBy', sortValue[0]);
|
||||
params.append('sortDirection', sortValue[1]);
|
||||
}
|
||||
|
||||
$.get('/api/product/list?' + params.toString())
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
displayProducts(response.data.content);
|
||||
updatePagination(response.data);
|
||||
} else {
|
||||
showError('获取商品列表失败:' + response.message);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showError('网络错误,请稍后重试');
|
||||
})
|
||||
.always(function () {
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索商品
|
||||
function searchProducts() {
|
||||
loadProducts(0);
|
||||
}
|
||||
|
||||
// 显示商品列表
|
||||
function displayProducts(products) {
|
||||
const productList = $('#productList');
|
||||
productList.empty();
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
$('#emptyState').show();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#emptyState').hide();
|
||||
|
||||
products.forEach(function (product) {
|
||||
const productCard = createProductCard(product);
|
||||
productList.append(productCard);
|
||||
});
|
||||
}
|
||||
|
||||
// 创建商品卡片
|
||||
function createProductCard(product) {
|
||||
const stockBadge = product.stock > 0 ?
|
||||
`<span class="stock-badge bg-success">库存 ${product.stock}</span>` :
|
||||
`<span class="stock-badge bg-danger">无库存</span>`;
|
||||
|
||||
const originalPrice = product.originalPrice && product.originalPrice > product.price ?
|
||||
`<span class="original-price">¥${product.originalPrice}</span>` : '';
|
||||
|
||||
const addToCartBtn = product.stock > 0 ?
|
||||
`<button class="btn btn-primary btn-sm" onclick="addToCart(${product.id})">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>` :
|
||||
`<button class="btn btn-secondary btn-sm" disabled>
|
||||
<i class="fas fa-ban"></i> 暂时缺货
|
||||
</button>`;
|
||||
|
||||
return `
|
||||
<div class="col-lg-3 col-md-4 col-sm-6 mb-4">
|
||||
<div class="card product-card h-100">
|
||||
<div class="position-relative">
|
||||
<img src="${product.imageUrl || '/static/images/default-product.svg'}"
|
||||
class="card-img-top product-image" alt="${product.name}">
|
||||
${stockBadge}
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h6 class="card-title" title="${product.name}">
|
||||
${product.name.length > 30 ? product.name.substring(0, 30) + '...' : product.name}
|
||||
</h6>
|
||||
<p class="card-text text-muted small flex-grow-1">
|
||||
${product.description ?
|
||||
(product.description.length > 50 ? product.description.substring(0, 50) + '...' : product.description) :
|
||||
'暂无描述'}
|
||||
</p>
|
||||
<div class="price-section mb-2">
|
||||
<span class="price">¥${product.price}</span>
|
||||
${originalPrice}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
${addToCartBtn}
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="viewProductDetail(${product.id})">
|
||||
<i class="fas fa-eye"></i> 详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 更新分页
|
||||
function updatePagination(pageData) {
|
||||
totalPages = pageData.totalPages;
|
||||
const pagination = $('#pagination');
|
||||
pagination.empty();
|
||||
|
||||
if (totalPages <= 1) return;
|
||||
|
||||
// 上一页
|
||||
const prevDisabled = currentPage === 0 ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${prevDisabled}">
|
||||
<a class="page-link" onclick="loadProducts(${currentPage - 1})" href="javascript:void(0)">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
|
||||
// 页码
|
||||
let startPage = Math.max(0, currentPage - 2);
|
||||
let endPage = Math.min(totalPages - 1, currentPage + 2);
|
||||
|
||||
if (startPage > 0) {
|
||||
pagination.append(`<li class="page-item"><a class="page-link" onclick="loadProducts(0)" href="javascript:void(0)">1</a></li>`);
|
||||
if (startPage > 1) {
|
||||
pagination.append(`<li class="page-item disabled"><span class="page-link">...</span></li>`);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${active}">
|
||||
<a class="page-link" onclick="loadProducts(${i})" href="javascript:void(0)">${i + 1}</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
if (endPage < totalPages - 1) {
|
||||
if (endPage < totalPages - 2) {
|
||||
pagination.append(`<li class="page-item disabled"><span class="page-link">...</span></li>`);
|
||||
}
|
||||
pagination.append(`<li class="page-item"><a class="page-link" onclick="loadProducts(${totalPages - 1})" href="javascript:void(0)">${totalPages}</a></li>`);
|
||||
}
|
||||
|
||||
// 下一页
|
||||
const nextDisabled = currentPage === totalPages - 1 ? 'disabled' : '';
|
||||
pagination.append(`
|
||||
<li class="page-item ${nextDisabled}">
|
||||
<a class="page-link" onclick="loadProducts(${currentPage + 1})" href="javascript:void(0)">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
`);
|
||||
}
|
||||
|
||||
// 查看商品详情
|
||||
function viewProductDetail(productId) {
|
||||
currentProductId = productId;
|
||||
|
||||
$.get(`/api/product/${productId}`)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
displayProductDetail(response.data);
|
||||
$('#productModal').modal('show');
|
||||
} else {
|
||||
showError('获取商品详情失败:' + response.message);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showError('网络错误,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 显示商品详情
|
||||
function displayProductDetail(product) {
|
||||
const modalBody = $('#productModalBody');
|
||||
modalBody.html(`
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<img src="${product.imageUrl || '/static/images/default-product.svg'}"
|
||||
class="img-fluid rounded" alt="${product.name}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4>${product.name}</h4>
|
||||
<p class="text-muted">${product.description || '暂无详细描述'}</p>
|
||||
<div class="mb-3">
|
||||
<span class="price h4">¥${product.price}</span>
|
||||
${product.originalPrice && product.originalPrice > product.price ?
|
||||
'<span class="original-price ms-2">¥' + product.originalPrice + '</span>' : ''}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span class="badge ${product.stock > 0 ? 'bg-success' : 'bg-danger'}">
|
||||
<c:choose>
|
||||
<c:when test="${product.stock > 0}">
|
||||
库存 ${product.stock}
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
暂时缺货
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>商品类别:</strong> ${product.category || '未分类'}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>商品状态:</strong>
|
||||
<span class="badge">
|
||||
<c:choose>
|
||||
<c:when test="${product.status == 1}">
|
||||
<span class="bg-success">上架中</span>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<span class="bg-secondary">已下架</span>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// 更新加入购物车按钮状态
|
||||
const addToCartBtn = $('#addToCartBtn');
|
||||
if (product.stock > 0 && product.status === 1) {
|
||||
addToCartBtn.prop('disabled', false).html('<i class="fas fa-cart-plus"></i> 加入购物车');
|
||||
} else {
|
||||
addToCartBtn.prop('disabled', true).html('<i class="fas fa-ban"></i> 暂时无法购买');
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到购物车
|
||||
function addToCart(productId) {
|
||||
if (!isUserLoggedIn()) {
|
||||
showLoginPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '/api/cart/add',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data)
|
||||
})
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
showSuccess('商品已添加到购物车');
|
||||
updateCartCount();
|
||||
$('#productModal').modal('hide');
|
||||
} else {
|
||||
showError('添加失败:' + response.message);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showError('网络错误,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
function showLoading(show) {
|
||||
if (show) {
|
||||
$('#loadingSpinner').show();
|
||||
$('#productList').hide();
|
||||
$('#emptyState').hide();
|
||||
} else {
|
||||
$('#loadingSpinner').hide();
|
||||
$('#productList').show();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户是否登录
|
||||
function isUserLoggedIn() {
|
||||
// 这里需要根据实际的登录状态检查逻辑来实现
|
||||
return sessionStorage.getItem('userToken') ||
|
||||
document.cookie.includes('JSESSIONID');
|
||||
}
|
||||
|
||||
// 显示登录提示
|
||||
function showLoginPrompt() {
|
||||
if (confirm('请先登录后再进行购物,是否前往登录页面?')) {
|
||||
window.location.href = '/login?returnUrl=' + encodeURIComponent(window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
function updateCartCount() {
|
||||
if (!isUserLoggedIn()) return;
|
||||
|
||||
$.get('/api/cart/count')
|
||||
.done(function (response) {
|
||||
if (response.success && response.data.count > 0) {
|
||||
$('.cart-count').text(response.data.count).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
function showError(message) {
|
||||
alert('错误:' + message);
|
||||
}
|
||||
|
||||
// 成功提示
|
||||
function showSuccess(message) {
|
||||
alert('成功:' + message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
Reference in New Issue
Block a user