389 lines
15 KiB
HTML
389 lines
15 KiB
HTML
|
|
<!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 {
|
|||
|
|
--bg-body: #f8fafc;
|
|||
|
|
--text-primary: #0f172a;
|
|||
|
|
--accent-color: #3b82f6;
|
|||
|
|
}
|
|||
|
|
body {
|
|||
|
|
background: var(--bg-body);
|
|||
|
|
/* 增加底部留白:TabBar高度(约70px) + 结算栏高度(约70px) + 间距(20px) */
|
|||
|
|
padding-bottom: 160px;
|
|||
|
|
font-family: -apple-system, sans-serif;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.page-title { padding: 20px 20px 10px; font-weight: 800; font-size: 24px; color: var(--text-primary); }
|
|||
|
|
|
|||
|
|
/* 列表项 */
|
|||
|
|
.cart-item {
|
|||
|
|
background: #fff;
|
|||
|
|
margin: 0 16px 16px;
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
|
|||
|
|
}
|
|||
|
|
.item-img {
|
|||
|
|
width: 64px; height: 64px; border-radius: 8px; object-fit: cover; background: #f1f5f9; flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
.item-info { flex: 1; margin-left: 12px; min-width: 0; /* 防止文本溢出撑开 */ }
|
|||
|
|
.item-name { font-weight: 600; font-size: 15px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|||
|
|
.item-desc { font-size: 12px; color: #64748b; margin-bottom: 8px; }
|
|||
|
|
.item-meta { display: flex; justify-content: space-between; align-items: center; }
|
|||
|
|
.item-price { font-weight: 700; font-size: 15px; }
|
|||
|
|
|
|||
|
|
/* 数量微调器 */
|
|||
|
|
.mini-stepper {
|
|||
|
|
display: flex; align-items: center; background: #f8fafc; border-radius: 6px; padding: 2px;
|
|||
|
|
}
|
|||
|
|
.mini-btn { width: 24px; height: 24px; border: none; background: #fff; color: #64748b; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); display: flex; align-items: center; justify-content: center; }
|
|||
|
|
.mini-val { font-size: 12px; font-weight: 600; width: 24px; text-align: center; }
|
|||
|
|
|
|||
|
|
/* 底部结算栏 */
|
|||
|
|
.settle-bar {
|
|||
|
|
position: fixed;
|
|||
|
|
/* 关键修改:抬高位置,避开TabBar */
|
|||
|
|
bottom: 85px;
|
|||
|
|
left: 16px; right: 16px;
|
|||
|
|
background: #fff; padding: 12px 16px;
|
|||
|
|
border-radius: 16px;
|
|||
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|||
|
|
display: flex; justify-content: space-between; align-items: center;
|
|||
|
|
z-index: 900; /* 层级低于TabBar(通常999或1000) */
|
|||
|
|
}
|
|||
|
|
.total-info span { font-size: 12px; color: #64748b; }
|
|||
|
|
.total-info strong { font-size: 18px; color: var(--text-primary); margin-left: 4px; }
|
|||
|
|
.btn-checkout {
|
|||
|
|
background: var(--text-primary); color: #fff; border: none;
|
|||
|
|
padding: 10px 24px; border-radius: 10px; font-weight: 600; font-size: 14px;
|
|||
|
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 底部导航 */
|
|||
|
|
.mobile-tabbar {
|
|||
|
|
position: fixed; bottom: 0; left: 0; right: 0;
|
|||
|
|
background: #fff; padding: 8px 0 20px; /* 适配 iPhone Home Indicator */
|
|||
|
|
display: flex; justify-content: space-around;
|
|||
|
|
border-top: 1px solid #f1f5f9;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
.tab-item {
|
|||
|
|
text-decoration: none; color: #94a3b8; font-size: 10px;
|
|||
|
|
display: flex; flex-direction: column; align-items: center; flex: 1;
|
|||
|
|
}
|
|||
|
|
.tab-item.active { color: var(--accent-color); }
|
|||
|
|
.tab-item i { font-size: 24px; margin-bottom: 2px; }
|
|||
|
|
|
|||
|
|
/* 空状态 */
|
|||
|
|
.empty-state { text-align: center; padding: 60px 20px; color: #94a3b8; }
|
|||
|
|
.empty-icon { font-size: 48px; margin-bottom: 16px; display: block; opacity: 0.5; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div class="page-title">购物袋</div>
|
|||
|
|
<div id="cartList">
|
|||
|
|
<div class="empty-state">
|
|||
|
|
<i class="bi bi-cart3 empty-icon"></i>
|
|||
|
|
<div>加载中...</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 底部结算 -->
|
|||
|
|
<div class="settle-bar" id="settleBar" style="display: none;">
|
|||
|
|
<div class="total-info">
|
|||
|
|
<span>合计</span>
|
|||
|
|
<strong id="totalPrice">¥0.00</strong>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn-checkout" onclick="checkout()">去结算 (<span id="count">0</span>)</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 导航 -->
|
|||
|
|
<div class="mobile-tabbar">
|
|||
|
|
<a href="/index.html" class="tab-item">
|
|||
|
|
<i class="bi bi-cup-hot"></i>
|
|||
|
|
<span>点餐</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="/cart.html" class="tab-item active">
|
|||
|
|
<i class="bi bi-bag-fill"></i>
|
|||
|
|
<span>购物袋</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="/orders.html" class="tab-item">
|
|||
|
|
<i class="bi bi-receipt"></i>
|
|||
|
|
<span>订单</span>
|
|||
|
|
</a>
|
|||
|
|
<a href="/profile.html" class="tab-item">
|
|||
|
|
<i class="bi bi-person"></i>
|
|||
|
|
<span>我的</span>
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script src="/static/js/ui-enhancements.js"></script>
|
|||
|
|
<script src="/static/js/cookie-utils.js"></script>
|
|||
|
|
<script>
|
|||
|
|
let cartItems = [];
|
|||
|
|
let productCache = {};
|
|||
|
|
let isLoggedIn = false;
|
|||
|
|
|
|||
|
|
// 检查用户是否登录
|
|||
|
|
async function checkLoginStatus() {
|
|||
|
|
try {
|
|||
|
|
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
|
|||
|
|
isLoggedIn = res.data.code === 200;
|
|||
|
|
return isLoggedIn;
|
|||
|
|
} catch(e) {
|
|||
|
|
isLoggedIn = false;
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function init() {
|
|||
|
|
const loggedIn = await checkLoginStatus();
|
|||
|
|
|
|||
|
|
if (loggedIn) {
|
|||
|
|
// 已登录:从后端API获取
|
|||
|
|
try {
|
|||
|
|
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
|
|||
|
|
const json = res.data;
|
|||
|
|
if(json.code === 200) {
|
|||
|
|
cartItems = json.data || [];
|
|||
|
|
await loadCartItems();
|
|||
|
|
} else {
|
|||
|
|
showEmptyState();
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
console.error(e);
|
|||
|
|
showEmptyState();
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 未登录:从cookies读取
|
|||
|
|
const cookieCart = CookieUtils.getJSON('cartItems') || [];
|
|||
|
|
if (cookieCart.length > 0) {
|
|||
|
|
// 需要加载商品详情信息
|
|||
|
|
cartItems = await loadCartItemsFromCookies(cookieCart);
|
|||
|
|
await loadCartItems();
|
|||
|
|
} else {
|
|||
|
|
showEmptyState();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 从cookies加载购物车商品(需要获取商品详情)
|
|||
|
|
async function loadCartItemsFromCookies(cookieItems) {
|
|||
|
|
const items = [];
|
|||
|
|
for (const item of cookieItems) {
|
|||
|
|
try {
|
|||
|
|
// 获取商品详情
|
|||
|
|
const res = await fetch(`/products/${item.productId}`);
|
|||
|
|
const json = await res.json();
|
|||
|
|
if (json.code === 200 && json.data) {
|
|||
|
|
const product = json.data.product;
|
|||
|
|
const spec = json.data.specs?.find(s => s.id === item.specId);
|
|||
|
|
items.push({
|
|||
|
|
id: `cookie_${item.productId}_${Date.now()}_${Math.random()}`, // 临时ID
|
|||
|
|
productId: item.productId,
|
|||
|
|
specId: item.specId,
|
|||
|
|
quantity: item.quantity,
|
|||
|
|
customSweetness: item.customSweetness,
|
|||
|
|
customIce: item.customIce,
|
|||
|
|
toppings: item.toppings,
|
|||
|
|
product: product,
|
|||
|
|
spec: spec
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
console.error('加载商品详情失败:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return items;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载购物车商品(计算价格并渲染)
|
|||
|
|
async function loadCartItems() {
|
|||
|
|
if (cartItems.length > 0) {
|
|||
|
|
document.getElementById('settleBar').style.display = 'flex';
|
|||
|
|
// 预加载价格信息
|
|||
|
|
await Promise.all(cartItems.map(loadItemPrice));
|
|||
|
|
render();
|
|||
|
|
calcTotal();
|
|||
|
|
} else {
|
|||
|
|
showEmptyState();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showEmptyState() {
|
|||
|
|
document.getElementById('cartList').innerHTML = `
|
|||
|
|
<div class="empty-state">
|
|||
|
|
<i class="bi bi-cart-x empty-icon"></i>
|
|||
|
|
<div>购物袋空空如也</div>
|
|||
|
|
<a href="/index.html" class="btn btn-sm btn-outline-dark mt-3 rounded-pill px-4">去点餐</a>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
document.getElementById('settleBar').style.display = 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadItemPrice(item) {
|
|||
|
|
let price = parseFloat(item.product?.basePrice || 0);
|
|||
|
|
if(item.spec?.priceAdjust) price += parseFloat(item.spec.priceAdjust);
|
|||
|
|
|
|||
|
|
// 解析配料价格
|
|||
|
|
if(item.toppings) {
|
|||
|
|
try {
|
|||
|
|
const tIds = JSON.parse(item.toppings);
|
|||
|
|
if(tIds.length) {
|
|||
|
|
if(!productCache[item.productId]) {
|
|||
|
|
const pr = await fetch(`/products/${item.productId}`);
|
|||
|
|
const pd = await pr.json();
|
|||
|
|
productCache[item.productId] = pd.data?.toppings || [];
|
|||
|
|
}
|
|||
|
|
tIds.forEach(tid => {
|
|||
|
|
const t = productCache[item.productId].find(x => x.id == tid);
|
|||
|
|
if(t) price += parseFloat(t.price);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
} catch(e){}
|
|||
|
|
}
|
|||
|
|
item._unitPrice = price;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function render() {
|
|||
|
|
const list = document.getElementById('cartList');
|
|||
|
|
list.innerHTML = cartItems.map((item, idx) => `
|
|||
|
|
<div class="cart-item">
|
|||
|
|
<img src="${item.product?.image || '/static/images/default.jpg'}" class="item-img" onclick="location.href='/product-detail.html?id=${item.product?.id}'">
|
|||
|
|
<div class="item-info">
|
|||
|
|
<div class="d-flex justify-content-between">
|
|||
|
|
<div class="item-name">${item.product?.name}</div>
|
|||
|
|
<i class="bi bi-x text-muted" style="font-size: 20px; cursor: pointer;" onclick="delItem(${item.id})"></i>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-desc">${item.spec?.specName || '默认规格'}</div>
|
|||
|
|
<div class="item-meta">
|
|||
|
|
<div class="item-price">¥${item._unitPrice.toFixed(2)}</div>
|
|||
|
|
<div class="mini-stepper">
|
|||
|
|
<button class="mini-btn" onclick="updateQty(${item.id}, ${item.quantity-1})">-</button>
|
|||
|
|
<span class="mini-val">${item.quantity}</span>
|
|||
|
|
<button class="mini-btn" onclick="updateQty(${item.id}, ${item.quantity+1})">+</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function calcTotal() {
|
|||
|
|
// 默认计算所有商品
|
|||
|
|
let total = 0;
|
|||
|
|
let count = 0;
|
|||
|
|
cartItems.forEach(item => {
|
|||
|
|
if (item._unitPrice !== undefined) {
|
|||
|
|
total += item._unitPrice * item.quantity;
|
|||
|
|
count += item.quantity;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
document.getElementById('totalPrice').textContent = `¥${total.toFixed(2)}`;
|
|||
|
|
document.getElementById('count').textContent = count;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function updateQty(id, qty) {
|
|||
|
|
if(qty < 1) return;
|
|||
|
|
// 乐观更新UI
|
|||
|
|
const item = cartItems.find(i => i.id === id);
|
|||
|
|
if(item) {
|
|||
|
|
const oldQty = item.quantity;
|
|||
|
|
item.quantity = qty;
|
|||
|
|
render();
|
|||
|
|
calcTotal();
|
|||
|
|
|
|||
|
|
if (isLoggedIn) {
|
|||
|
|
// 已登录:调用后端接口
|
|||
|
|
try {
|
|||
|
|
await fetch(`/cart/${id}?quantity=${qty}`, { method: 'PUT', credentials: 'include' });
|
|||
|
|
} catch(e) {
|
|||
|
|
item.quantity = oldQty; // 回滚
|
|||
|
|
render();
|
|||
|
|
calcTotal();
|
|||
|
|
alert('更新失败');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 未登录:更新cookies
|
|||
|
|
try {
|
|||
|
|
let cookieCart = CookieUtils.getJSON('cartItems') || [];
|
|||
|
|
const cookieItem = cookieCart.find(ci =>
|
|||
|
|
ci.productId === item.productId &&
|
|||
|
|
ci.specId === item.specId &&
|
|||
|
|
ci.customSweetness === item.customSweetness &&
|
|||
|
|
ci.customIce === item.customIce &&
|
|||
|
|
ci.toppings === item.toppings
|
|||
|
|
);
|
|||
|
|
if (cookieItem) {
|
|||
|
|
cookieItem.quantity = qty;
|
|||
|
|
CookieUtils.setJSON('cartItems', cookieCart, 7);
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
item.quantity = oldQty; // 回滚
|
|||
|
|
render();
|
|||
|
|
calcTotal();
|
|||
|
|
alert('更新失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function delItem(id) {
|
|||
|
|
if(confirm('确认将商品移出购物袋?')) {
|
|||
|
|
if (isLoggedIn) {
|
|||
|
|
// 已登录:调用后端接口
|
|||
|
|
try {
|
|||
|
|
await fetch(`/cart/${id}`, { method: 'DELETE', credentials: 'include' });
|
|||
|
|
init();
|
|||
|
|
} catch(e) {
|
|||
|
|
alert('操作失败');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 未登录:从cookies删除
|
|||
|
|
try {
|
|||
|
|
const item = cartItems.find(i => i.id === id);
|
|||
|
|
if (item) {
|
|||
|
|
let cookieCart = CookieUtils.getJSON('cartItems') || [];
|
|||
|
|
cookieCart = cookieCart.filter(ci =>
|
|||
|
|
!(ci.productId === item.productId &&
|
|||
|
|
ci.specId === item.specId &&
|
|||
|
|
ci.customSweetness === item.customSweetness &&
|
|||
|
|
ci.customIce === item.customIce &&
|
|||
|
|
ci.toppings === item.toppings)
|
|||
|
|
);
|
|||
|
|
CookieUtils.setJSON('cartItems', cookieCart, 7);
|
|||
|
|
init();
|
|||
|
|
}
|
|||
|
|
} catch(e) {
|
|||
|
|
alert('操作失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function checkout() {
|
|||
|
|
// 未登录用户需要先登录
|
|||
|
|
if (!isLoggedIn) {
|
|||
|
|
if (confirm('结算需要登录,是否前往登录?')) {
|
|||
|
|
// 保存当前页面路径,登录后返回
|
|||
|
|
sessionStorage.setItem('returnUrl', '/cart.html');
|
|||
|
|
location.href = '/login.html';
|
|||
|
|
}
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
// 已登录:跳转到结算页面
|
|||
|
|
location.href = '/checkout.html';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|