Files
2026-01-11 18:14:42 +08:00

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