Files

389 lines
15 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 {
--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>