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

616 lines
25 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 {
--primary: #0f172a;
--bg-body: #f8fafc;
--accent: #3b82f6;
}
body {
background: var(--bg-body);
padding-bottom: 120px;
font-family: -apple-system, sans-serif;
color: var(--primary);
overflow-x: hidden; /* 防止滑动动画导致横向滚动条 */
}
/* 顶部导航 */
.nav-header {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
background: #fff; 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; }
.btn-back { border: none; background: transparent; font-size: 20px; padding: 0; }
/* 通用卡片 */
.section-card {
background: #fff; margin: 16px; padding: 16px; border-radius: 16px;
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
transition: background 0.2s;
}
.section-card:active { background: #f8fafc; }
.card-header {
font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center;
}
.card-header i { margin-right: 6px; color: #64748b; font-size: 16px; }
/* 地址卡片 */
.address-card-content {
display: flex; align-items: center; justify-content: space-between;
min-height: 60px;
}
.addr-info { flex: 1; margin-right: 12px; }
.addr-top { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.addr-detail { font-size: 13px; color: #64748b; line-height: 1.4; }
.no-addr-hint { color: var(--accent); font-weight: 500; font-size: 14px; }
.icon-arrow { color: #cbd5e1; font-size: 14px; }
/* 商品列表 */
.checkout-item { display: flex; margin-bottom: 16px; }
.checkout-item:last-child { margin-bottom: 0; }
.item-img {
width: 56px; height: 56px; border-radius: 8px; background: #f1f5f9; object-fit: cover;
}
.item-content { flex: 1; margin-left: 12px; }
.item-row { display: flex; justify-content: space-between; align-items: flex-start; }
.item-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.item-spec { font-size: 12px; color: #94a3b8; }
.item-price { font-size: 14px; font-weight: 600; }
.item-qty { font-size: 12px; color: #94a3b8; margin-left: 4px; }
/* 支付方式 */
.pay-option {
display: flex; align-items: center; padding: 12px 0; border-bottom: 1px solid #f1f5f9;
cursor: pointer;
}
.pay-option:last-child { border-bottom: none; }
.pay-icon { font-size: 20px; margin-right: 12px; }
.pay-wechat { color: #07c160; }
.pay-alipay { color: #1677ff; }
.pay-name { flex: 1; font-size: 14px; font-weight: 500; }
.pay-radio { accent-color: var(--primary); width: 18px; height: 18px; }
/* 备注输入 */
.remark-input {
width: 100%; border: none; background: #f8fafc; padding: 12px;
border-radius: 8px; font-size: 13px; color: var(--primary);
}
/* 底部结算栏 */
.bottom-bar {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 20px 30px;
box-shadow: 0 -4px 16px rgba(0,0,0,0.05);
display: flex; justify-content: space-between; align-items: center; z-index: 100;
}
.total-area { display: flex; align-items: baseline; }
.total-label { font-size: 12px; color: #64748b; margin-right: 4px; }
.total-val { font-size: 24px; font-weight: 800; color: var(--primary); }
.btn-submit {
background: var(--primary); color: #fff; border: none;
padding: 14px 32px; border-radius: 12px; font-weight: 600; font-size: 16px;
}
.btn-submit:active { transform: scale(0.98); opacity: 0.9; }
.btn-submit:disabled { background: #cbd5e1; }
/* ============================
伪二级页面:地址管理
============================ */
.page-layer {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: var(--bg-body); z-index: 2000;
transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
display: flex; flex-direction: column;
}
.page-layer.active { transform: translateX(0); }
.layer-body {
flex: 1; overflow-y: auto; padding: 16px; padding-bottom: 100px;
margin-top: 50px; /* Header Height */
}
/* 地址管理列表项 */
.addr-manage-item {
background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px;
position: relative; border: 1px solid transparent;
}
.addr-manage-item.selected { border-color: var(--primary); background: #f8fafc; }
.addr-manage-top { display: flex; justify-content: space-between; margin-bottom: 6px; }
.addr-manage-name { font-weight: 700; font-size: 15px; }
.addr-manage-phone { font-weight: 400; color: #64748b; margin-left: 8px; font-size: 14px; }
.addr-manage-detail { color: #475569; font-size: 13px; line-height: 1.4; padding-right: 40px; }
.addr-tag { font-size: 10px; padding: 2px 6px; background: var(--primary); color: #fff; border-radius: 4px; vertical-align: middle; margin-right: 4px; }
/* 编辑按钮区域 */
.addr-manage-actions {
position: absolute; right: 16px; top: 50%; transform: translateY(-50%);
display: flex; flex-direction: column; gap: 12px;
padding-left: 12px; border-left: 1px solid #f1f5f9;
}
.action-icon { color: #94a3b8; font-size: 18px; padding: 4px; }
/* 底部新增按钮 */
.layer-footer {
position: fixed; bottom: 0; left: 0; right: 0;
background: #fff; padding: 12px 16px 30px;
box-shadow: 0 -4px 12px rgba(0,0,0,0.03);
}
/* 扁平化输入框 */
.flat-input {
width: 100%; padding: 12px 0; border: none; border-bottom: 1px solid #e2e8f0;
outline: none; transition: border-color 0.2s; background: transparent;
}
.flat-input:focus { border-color: var(--primary); }
/* === 修复模态框层级问题 === */
/* 确保模态框在伪二级页面(z-index: 2000)之上 */
.modal-backdrop { z-index: 2040 !important; }
.modal { z-index: 2050 !important; }
</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>
<div style="height: 50px;"></div>
<!-- 主页面:地址卡片 -->
<div class="section-card" onclick="openAddressManager()">
<div class="card-header"><i class="bi bi-geo-alt-fill"></i> 配送地址</div>
<div class="address-card-content">
<div class="addr-info" id="addrDisplay">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm text-muted"></div>
<span class="ms-2 small text-muted">加载地址...</span>
</div>
</div>
<i class="bi bi-chevron-right icon-arrow"></i>
</div>
<input type="hidden" id="selectedAddrId">
</div>
<!-- 主页面:商品清单 -->
<div class="section-card">
<div class="card-header"><i class="bi bi-bag-fill"></i> 订单商品</div>
<div id="itemList"></div>
</div>
<!-- 支付方式选择已迁移到支付页面 -->
<!-- 主页面:备注 -->
<div class="section-card">
<div class="card-header"><i class="bi bi-chat-left-text-fill"></i> 订单备注</div>
<textarea class="remark-input" id="remark" rows="2" placeholder="如有特殊需求,请在此填写"></textarea>
</div>
<!-- 主页面:底部操作栏 -->
<div class="bottom-bar">
<div class="total-area">
<span class="total-label">合计</span>
<span class="total-val" id="totalAmount">¥0.00</span>
</div>
<button class="btn-submit" onclick="submitOrder()">提交订单</button>
</div>
<!-- ========================================== -->
<!-- 伪二级页面:地址管理 (全屏覆盖) -->
<!-- ========================================== -->
<div id="addressManagerPage" class="page-layer">
<div class="nav-header">
<button class="btn-back" onclick="closeAddressManager()"><i class="bi bi-chevron-left"></i></button>
<span class="nav-title">选择地址</span>
<div style="width: 24px;"></div>
</div>
<div class="layer-body" id="addrManagerList">
<!-- 列表动态渲染 -->
</div>
<div class="layer-footer">
<button class="btn-submit w-100" onclick="openEditModal()">+ 新增收货地址</button>
</div>
</div>
<!-- 地址编辑模态框 (Bootstrap Modal) -->
<div class="modal fade" id="addrEditModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border:none;">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold" id="modalTitle">新增地址</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="addrForm">
<input type="hidden" id="editAddrId">
<div class="mb-3">
<input type="text" class="flat-input" id="editName" placeholder="收货人姓名" required>
</div>
<div class="mb-3">
<input type="tel" class="flat-input" id="editPhone" placeholder="手机号码" required>
</div>
<div class="mb-3">
<textarea class="flat-input" id="editDetail" rows="2" placeholder="详细地址 (街道/楼牌号)" required></textarea>
</div>
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" id="editDefault">
<label class="form-check-label small text-muted" for="editDefault">设为默认地址</label>
</div>
</form>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-pill" onclick="deleteCurrentAddr()" id="btnDelete" style="display:none;">删除</button>
<button type="button" class="btn btn-dark rounded-pill px-4" onclick="saveAddress()">保存并使用</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
let cartItems = [];
let addresses = [];
let currentSelectedAddr = null; // 全局变量存储当前选中对象
// 初始化
(async () => {
await Promise.all([loadAddresses(), loadCart()]);
})();
// --- 核心逻辑:加载地址 ---
async function loadAddresses() {
try {
const res = await UIEnhancements.enhancedFetch('/address', { credentials: 'include' });
const json = res.data;
if(json.code === 200) {
addresses = json.data || [];
// 策略:如果没有选中过,优先选默认,否则选第一个
if (!currentSelectedAddr && addresses.length > 0) {
currentSelectedAddr = addresses.find(a => a.isDefault) || addresses[0];
}
// 如果当前选中的地址被删了,重置
else if (currentSelectedAddr && !addresses.find(a => a.id === currentSelectedAddr.id)) {
currentSelectedAddr = addresses.find(a => a.isDefault) || addresses[0] || null;
}
renderCurrentAddress();
}
} catch(e) { console.error(e); }
}
// 渲染主页面地址卡片
function renderCurrentAddress() {
const el = document.getElementById('addrDisplay');
const hidden = document.getElementById('selectedAddrId');
if (currentSelectedAddr) {
hidden.value = currentSelectedAddr.id;
// 兼容字段名:后端可能返回 contactName/recipient 或 contactPhone/phone
const name = currentSelectedAddr.contactName || currentSelectedAddr.recipient;
const phone = currentSelectedAddr.contactPhone || currentSelectedAddr.phone;
el.innerHTML = `
<div class="addr-top">
${name} <span class="fw-normal ms-2 text-muted" style="font-size:14px;">${phone}</span>
</div>
<div class="addr-detail text-truncate">${currentSelectedAddr.address}</div>
`;
} else {
hidden.value = '';
el.innerHTML = `<span class="no-addr-hint">+ 请添加收货地址</span>`;
}
}
// --- 核心逻辑:加载购物车 ---
async function loadCart() {
try {
const res = await UIEnhancements.enhancedFetch('/cart', { credentials: 'include' });
const json = res.data;
if(json.code === 200) {
cartItems = json.data || [];
if(cartItems.length === 0) {
alert('购物袋为空,请先选购');
location.href = '/index.html';
return;
}
renderItems();
calcTotal();
} else if (json.message?.includes('登录')) {
location.href = '/login.html';
}
} catch(e) { console.error(e); }
}
function renderItems() {
const container = document.getElementById('itemList');
let html = '';
cartItems.forEach(item => {
let specDesc = item.spec?.specName || '';
let price = item.product.basePrice + (item.spec?.priceAdjust || 0);
// 简单处理配料显示
let toppingTxt = '';
try {
// 实际需要请求产品详情获得配料名,此处简化
// const tIds = JSON.parse(item.toppings || '[]');
// if(tIds.length) toppingTxt = ` +配料(${tIds.length})`;
} catch(e){}
html += `
<div class="checkout-item">
<img src="${item.product.image || '/static/images/default.jpg'}" class="item-img">
<div class="item-content">
<div class="item-row">
<span class="item-name">${item.product.name}</span>
<span class="item-price">¥${price}</span>
</div>
<div class="item-row mt-1">
<span class="item-spec">${specDesc}${toppingTxt}</span>
<span class="item-qty">x${item.quantity}</span>
</div>
</div>
</div>
`;
});
container.innerHTML = html;
}
async function calcTotal() {
let total = 0;
for (let item of cartItems) {
let p = item.product.basePrice + (item.spec?.priceAdjust || 0);
// 简化计算如果需要配料价格应在cart接口返回完整price
total += p * item.quantity;
}
document.getElementById('totalAmount').innerText = '¥' + total.toFixed(2);
}
// ==========================================
// 二级页面交互逻辑
// ==========================================
// 1. 打开地址管理页
function openAddressManager() {
const page = document.getElementById('addressManagerPage');
renderManagerList();
page.classList.add('active');
// 禁止背景滚动
document.body.style.overflow = 'hidden';
}
// 2. 关闭地址管理页
function closeAddressManager() {
const page = document.getElementById('addressManagerPage');
page.classList.remove('active');
document.body.style.overflow = '';
// 关闭时刷新主页显示(可能改了选中项)
renderCurrentAddress();
}
// 3. 渲染管理列表
function renderManagerList() {
const container = document.getElementById('addrManagerList');
if (addresses.length === 0) {
container.innerHTML = '<div class="text-center py-5 text-muted">暂无地址,请添加</div>';
return;
}
container.innerHTML = addresses.map(a => {
const name = a.contactName || a.recipient;
const phone = a.contactPhone || a.phone;
const isSelected = currentSelectedAddr && currentSelectedAddr.id === a.id;
return `
<div class="addr-manage-item ${isSelected ? 'selected' : ''}" onclick="selectAndClose('${a.id}')">
<div class="addr-manage-top">
<div class="text-truncate">
${a.isDefault ? '<span class="addr-tag">默认</span>' : ''}
<span class="addr-manage-name">${name}</span>
<span class="addr-manage-phone">${phone}</span>
</div>
</div>
<div class="addr-manage-detail">${a.address}</div>
<div class="addr-manage-actions" onclick="event.stopPropagation()">
<i class="bi bi-pencil-square action-icon" onclick="openEditModal('${a.id}')"></i>
</div>
</div>`;
}).join('');
}
// 4. 在管理页选中并返回
function selectAndClose(id) {
// == 比较以兼容 string/number
const target = addresses.find(a => a.id == id);
if (target) {
currentSelectedAddr = target;
closeAddressManager();
}
}
// ==========================================
// 增删改逻辑 (修复传参问题)
// ==========================================
let editingAddrId = null;
// 打开编辑/新增模态框
function openEditModal(id = null) {
const modal = new bootstrap.Modal(document.getElementById('addrEditModal'));
const form = document.getElementById('addrForm');
const btnDel = document.getElementById('btnDelete');
form.reset();
editingAddrId = id; // 保存当前编辑ID
if (id) {
document.getElementById('modalTitle').innerText = '编辑地址';
btnDel.style.display = 'block';
// 查找对象
const target = addresses.find(a => a.id == id);
if (target) {
document.getElementById('editName').value = target.contactName || target.recipient;
document.getElementById('editPhone').value = target.contactPhone || target.phone;
document.getElementById('editDetail').value = target.address;
document.getElementById('editDefault').checked = target.isDefault;
}
} else {
document.getElementById('modalTitle').innerText = '新增地址';
btnDel.style.display = 'none';
}
modal.show();
}
// 保存地址
async function saveAddress() {
const name = document.getElementById('editName').value.trim();
const phone = document.getElementById('editPhone').value.trim();
const detail = document.getElementById('editDetail').value.trim();
const isDefault = document.getElementById('editDefault').checked;
if (!name || !phone || !detail) return alert('请填写完整信息');
// 构造符合后端规范的 Payload
// 注意:使用后端实体类字段名 contactName 和 contactPhone
const payload = {
contactName: name,
contactPhone: phone,
address: detail,
isDefault: isDefault ? 1 : 0
};
// 如果是编辑,加上 ID
// 注意RESTful 风格通常把 ID 放在 URL 里PUT body 里不需要 ID但放了也无妨
try {
const url = editingAddrId ? `/address/${editingAddrId}` : '/address';
const method = editingAddrId ? 'PUT' : 'POST';
const res = await fetch(url, {
method: method,
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify(payload)
});
const json = await res.json();
if (json.code === 200) {
// 成功后关闭模态框
bootstrap.Modal.getInstance(document.getElementById('addrEditModal')).hide();
// 重新加载列表
await loadAddresses();
renderManagerList(); // 刷新管理页列表
// 如果是新增的,自动选中
if (!editingAddrId) {
// 重新获取后,默认可能不是最新的,这里简单处理:不自动选中,让用户点
}
} else {
alert(json.message);
}
} catch (e) {
console.error(e);
alert('操作失败');
}
}
// 删除地址
async function deleteCurrentAddr() {
if (!editingAddrId) return;
if (!confirm('确认删除此地址?')) return;
try {
const res = await fetch(`/address/${editingAddrId}`, { method: 'DELETE' });
const json = await res.json();
if (json.code === 200) {
bootstrap.Modal.getInstance(document.getElementById('addrEditModal')).hide();
// 如果删的是当前选中的,清空选中态
if (currentSelectedAddr && currentSelectedAddr.id == editingAddrId) {
currentSelectedAddr = null;
}
await loadAddresses();
renderManagerList();
} else {
alert(json.message);
}
} catch (e) { alert('删除失败'); }
}
// ==========================================
// 提交订单(跳转到支付页面)
// ==========================================
async function submitOrder() {
if(!currentSelectedAddr) return alert('请选择收货地址');
const remark = document.getElementById('remark').value;
const btn = document.querySelector('.btn-submit');
btn.disabled = true;
btn.innerText = '提交中...';
try {
// === 提取 merchantId ===
// 从购物车第一项获取 merchantId后端也会从购物车验证但前端需要传参以满足验证要求
let merchantId = null;
if (cartItems.length > 0 && cartItems[0].product) {
merchantId = cartItems[0].product.merchantId;
}
if (!merchantId) {
alert('无法获取商家信息,请刷新页面重试');
btn.disabled = false;
btn.innerText = '提交订单';
return;
}
// 构造符合后端 OrderCreateRequest 的 payload
// 后端只需要merchantId, address, contactPhone
const payload = {
merchantId: merchantId,
address: currentSelectedAddr.address,
contactPhone: currentSelectedAddr.contactPhone || currentSelectedAddr.phone
};
const res = await fetch('/order/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify(payload)
});
const json = await res.json();
if(json.code === 200) {
const orderId = json.data.id || json.data;
// 跳转到支付页面而不是订单详情页
location.href = `/payment.html?orderId=${orderId}`;
} else {
alert(json.message);
btn.disabled = false;
btn.innerText = '提交订单';
}
} catch(e) {
console.error(e);
alert('提交失败');
btn.disabled = false;
btn.innerText = '提交订单';
}
}
</script>
</body>
</html>