Files
wb-java-nc-shop/target/classes/templates/user/product-detail.html
2026-01-11 18:14:42 +08:00

356 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品详情</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
:root { --primary: #0f172a; --accent: #3b82f6; --bg-gray: #f8fafc; }
body { background: #fff; padding-bottom: 100px; font-family: -apple-system, sans-serif; }
/* 顶部导航 Header */
.nav-header {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px);
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; z-index: 1000; box-shadow: 0 1px 0 rgba(0,0,0,0.05);
}
.nav-title { font-size: 16px; font-weight: 600; color: var(--primary); }
.btn-back { border: none; background: transparent; font-size: 20px; color: var(--primary); padding: 0; }
/* 顶部大图 */
.hero-img {
width: 100%; height: 320px; object-fit: cover; background: #f1f5f9;
margin-top: 50px; /* 避开Header */
}
/* 内容容器 - 上浮圆角 */
.content-box {
position: relative; top: -24px; border-radius: 24px 24px 0 0;
background: #fff; padding: 24px 20px; margin-bottom: -24px;
}
.prod-title { font-size: 22px; font-weight: 800; color: var(--primary); margin-bottom: 8px; }
.prod-desc { font-size: 13px; color: #64748b; line-height: 1.6; margin-bottom: 24px; }
/* 选项组 */
.opt-group { margin-bottom: 24px; }
.opt-title { font-size: 14px; font-weight: 700; color: var(--primary); margin-bottom: 12px; }
/* 扁平化 Chips */
.chips-wrap { display: flex; flex-wrap: wrap; gap: 10px; }
.chip-input { display: none; } /* Hide default checkbox/radio */
.chip-label {
padding: 8px 16px; border-radius: 8px; background: var(--bg-gray);
font-size: 13px; color: #475569; transition: all 0.2s; border: 1px solid transparent;
cursor: pointer;
}
/* 选中态 */
.chip-input:checked + .chip-label {
background: #eff6ff; color: var(--accent); border-color: var(--accent); font-weight: 600;
}
/* 数量步进器 */
.stepper {
display: inline-flex; align-items: center; background: var(--bg-gray); border-radius: 8px; padding: 4px;
}
.step-btn {
width: 32px; height: 32px; border: none; background: #fff; border-radius: 6px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05); color: var(--primary); font-size: 18px; display: flex; align-items: center; justify-content: center;
}
.step-val { width: 40px; text-align: center; font-size: 15px; font-weight: 600; background: transparent; border: none; }
/* 底部固定栏 */
.bottom-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 20px 30px; /* Adapt to safe area */
box-shadow: 0 -4px 16px rgba(0,0,0,0.04);
display: flex; justify-content: space-between; align-items: center; z-index: 100;
}
.price-area { display: flex; flex-direction: column; }
.price-label { font-size: 11px; color: #94a3b8; }
.price-val { font-size: 24px; font-weight: 800; color: var(--primary); }
.btn-add {
background: var(--primary); color: #fff; border: none; padding: 14px 40px;
border-radius: 12px; font-weight: 600; font-size: 16px;
}
.btn-add:active { transform: scale(0.98); }
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="nav-header">
<button class="btn-back" onclick="history.back()"><i class="bi bi-arrow-left"></i></button>
<span class="nav-title">商品详情</span>
<div style="width: 24px;"></div> <!-- 占位符确保标题居中 -->
</div>
<img id="prodImg" src="" class="hero-img">
<div class="content-box">
<h1 class="prod-title" id="prodName">Loading...</h1>
<p class="prod-desc" id="prodDesc"></p>
<form id="addForm">
<input type="hidden" id="pid">
<!-- 动态渲染选项 -->
<div id="dynamicOptions"></div>
<div class="opt-group">
<div class="opt-title">数量</div>
<div class="stepper">
<button type="button" class="step-btn" onclick="changeQty(-1)">-</button>
<input type="number" class="step-val" id="qty" value="1" readonly>
<button type="button" class="step-btn" onclick="changeQty(1)">+</button>
</div>
</div>
</form>
</div>
<div class="bottom-bar">
<div class="price-area">
<span class="price-label">总计金额</span>
<span class="price-val" id="totalPrice">¥0.00</span>
</div>
<button class="btn-add" onclick="submitCart()">加入购物袋</button>
</div>
<script src="/static/js/ui-enhancements.js"></script>
<script src="/static/js/cookie-utils.js"></script>
<script>
let basePrice = 0;
let productData = null;
let currentProductId = null;
let isLoggedIn = false;
// 检查用户是否登录
async function checkLoginStatus() {
try {
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
isLoggedIn = res.data.code === 200;
} catch(e) {
isLoggedIn = false;
}
return isLoggedIn;
}
// 记录浏览行为
async function recordView(productId) {
const loggedIn = await checkLoginStatus();
if (loggedIn) {
// 已登录:调用后端接口记录(后端会自动记录)
// 这里不需要额外调用因为getProductDetail已经记录了
} else {
// 未登录存储到cookies
let viewHistory = CookieUtils.getJSON('viewHistory') || [];
// 检查是否已存在该商品的浏览记录
const exists = viewHistory.some(v => v.productId === productId);
if (!exists) {
viewHistory.push({
productId: productId,
timestamp: Date.now()
});
// 限制浏览历史数量避免cookies过大最多保存50条
if (viewHistory.length > 50) {
viewHistory = viewHistory.slice(-50);
}
CookieUtils.setJSON('viewHistory', viewHistory, 7); // 7天过期
}
}
}
// 初始化
(async () => {
const id = new URLSearchParams(location.search).get('id');
if(!id) return alert('商品不存在');
currentProductId = parseInt(id);
try {
const res = await UIEnhancements.enhancedFetch(`/products/${id}`, { credentials: 'include' });
const json = res.data;
if(json.code === 200) {
productData = json.data;
render(productData);
// 记录浏览行为
await recordView(currentProductId);
} else {
alert(json.message);
}
} catch(e) { console.error(e); }
})();
function render(data) {
const p = data.product;
basePrice = p.basePrice;
document.getElementById('pid').value = p.id;
document.getElementById('prodImg').src = p.image || '/static/images/default.jpg';
document.getElementById('prodName').innerText = p.name;
document.getElementById('prodDesc').innerText = p.description || '';
let html = '';
// 1. 规格 (Radio)
if (data.specs && data.specs.length) {
html += `<div class="opt-group"><div class="opt-title">规格</div><div class="chips-wrap">`;
data.specs.forEach((s, i) => {
html += `
<input type="radio" name="spec" id="sp_${s.id}" value="${s.id}" class="chip-input"
data-price="${s.priceAdjust}" ${i===0?'checked':''} onchange="calcPrice()">
<label for="sp_${s.id}" class="chip-label">${s.specName}</label>
`;
});
html += `</div></div>`;
}
// 2. 甜度 (Radio)
if (data.customs?.sweetness?.length) {
html += `<div class="opt-group"><div class="opt-title">甜度</div><div class="chips-wrap">`;
data.customs.sweetness.forEach((o, i) => {
html += `
<input type="radio" name="sweetness" id="sw_${i}" value="${o.optionValue}" class="chip-input" ${i===0?'checked':''}>
<label for="sw_${i}" class="chip-label">${o.optionValue}</label>
`;
});
html += `</div></div>`;
}
// 3. 冰度 (Radio)
if (data.customs?.ice?.length) {
html += `<div class="opt-group"><div class="opt-title">温度</div><div class="chips-wrap">`;
data.customs.ice.forEach((o, i) => {
html += `
<input type="radio" name="ice" id="ic_${i}" value="${o.optionValue}" class="chip-input" ${i===0?'checked':''}>
<label for="ic_${i}" class="chip-label">${o.optionValue}</label>
`;
});
html += `</div></div>`;
}
// 4. 配料 (Checkbox)
if (data.toppings && data.toppings.length) {
html += `<div class="opt-group"><div class="opt-title">加料</div><div class="chips-wrap">`;
data.toppings.forEach(t => {
html += `
<input type="checkbox" name="topping" id="tp_${t.id}" value="${t.id}" class="chip-input"
data-price="${t.price}" onchange="calcPrice()">
<label for="tp_${t.id}" class="chip-label">${t.toppingName} (+¥${t.price})</label>
`;
});
html += `</div></div>`;
}
document.getElementById('dynamicOptions').innerHTML = html;
calcPrice();
}
function changeQty(d) {
const input = document.getElementById('qty');
let v = parseInt(input.value) + d;
if(v < 1) v = 1;
input.value = v;
calcPrice();
}
function calcPrice() {
let total = parseFloat(basePrice);
// 规格加价
const spec = document.querySelector('input[name="spec"]:checked');
if (spec) total += parseFloat(spec.dataset.price || 0);
// 配料加价
document.querySelectorAll('input[name="topping"]:checked').forEach(el => {
total += parseFloat(el.dataset.price || 0);
});
const qty = parseInt(document.getElementById('qty').value);
document.getElementById('totalPrice').innerText = '¥' + (total * qty).toFixed(2);
}
async function submitCart() {
const btn = document.querySelector('.btn-add');
btn.innerText = '添加中...';
btn.disabled = true;
try {
const payload = {
productId: parseInt(document.getElementById('pid').value),
specId: document.querySelector('input[name="spec"]:checked')?.value ?
parseInt(document.querySelector('input[name="spec"]:checked').value) : null,
customSweetness: document.querySelector('input[name="sweetness"]:checked')?.value || null,
customIce: document.querySelector('input[name="ice"]:checked')?.value || null,
quantity: parseInt(document.getElementById('qty').value),
toppings: JSON.stringify(
Array.from(document.querySelectorAll('input[name="topping"]:checked')).map(el => parseInt(el.value))
)
};
const loggedIn = await checkLoginStatus();
if (loggedIn) {
// 已登录:调用后端接口
const res = await fetch('/cart/add', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
credentials: 'include'
});
const json = await res.json();
if(json.code === 200) {
window.location.href = '/cart.html';
} else {
alert(json.message);
btn.disabled = false;
btn.innerText = '加入购物袋';
}
} else {
// 未登录存储到cookies
let cartItems = CookieUtils.getJSON('cartItems') || [];
// 检查是否已存在相同配置的商品
const existingIndex = cartItems.findIndex(item =>
item.productId === payload.productId &&
item.specId === payload.specId &&
item.customSweetness === payload.customSweetness &&
item.customIce === payload.customIce &&
item.toppings === payload.toppings
);
if (existingIndex >= 0) {
// 合并数量
cartItems[existingIndex].quantity += payload.quantity;
} else {
// 添加新商品
cartItems.push(payload);
}
// 限制购物车数量避免cookies过大最多保存30个商品
if (cartItems.length > 30) {
cartItems = cartItems.slice(-30);
}
CookieUtils.setJSON('cartItems', cartItems, 7); // 7天过期
// 提示并跳转
if (confirm('商品已添加到购物袋,是否前往购物袋?')) {
window.location.href = '/cart.html';
} else {
btn.disabled = false;
btn.innerText = '加入购物袋';
}
}
} catch(e) {
console.error(e);
alert('添加失败');
btn.disabled = false;
btn.innerText = '加入购物袋';
}
}
</script>
</body>
</html>