Files

356 lines
14 KiB
HTML
Raw Permalink Normal View History

2026-01-11 18:14:42 +08:00
<!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>