Files

616 lines
25 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 {
--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>