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> |