401 lines
17 KiB
HTML
401 lines
17 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;
|
|||
|
|
--primary: #0f172a;
|
|||
|
|
--accent: #3b82f6;
|
|||
|
|
}
|
|||
|
|
body {
|
|||
|
|
background: var(--bg-body);
|
|||
|
|
padding-bottom: 90px;
|
|||
|
|
font-family: -apple-system, sans-serif;
|
|||
|
|
color: #334155;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 头部展示 */
|
|||
|
|
.profile-card {
|
|||
|
|
background: #fff; padding: 30px 20px; margin-bottom: 16px;
|
|||
|
|
display: flex; flex-direction: column; align-items: center; text-align: center;
|
|||
|
|
}
|
|||
|
|
.avatar-placeholder {
|
|||
|
|
width: 80px; height: 80px; border-radius: 50%; background: #f1f5f9; color: #cbd5e1;
|
|||
|
|
font-size: 32px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px;
|
|||
|
|
}
|
|||
|
|
.user-name-disp { font-size: 20px; font-weight: 800; color: var(--primary); margin-bottom: 4px; }
|
|||
|
|
.user-phone-disp { font-size: 14px; color: #64748b; }
|
|||
|
|
.btn-edit-profile {
|
|||
|
|
margin-top: 16px; font-size: 12px; padding: 6px 16px; border-radius: 20px;
|
|||
|
|
border: 1px solid #e2e8f0; background: transparent; color: #475569;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 菜单组 */
|
|||
|
|
.menu-group { background: #fff; padding: 0 20px; margin-bottom: 16px; }
|
|||
|
|
.menu-item {
|
|||
|
|
padding: 16px 0; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center;
|
|||
|
|
color: var(--primary); font-size: 15px; cursor: pointer; text-decoration: none;
|
|||
|
|
}
|
|||
|
|
.menu-item:last-child { border-bottom: none; }
|
|||
|
|
.menu-icon { margin-right: 12px; font-size: 18px; color: #94a3b8; }
|
|||
|
|
.menu-arrow { margin-left: auto; color: #cbd5e1; font-size: 14px; }
|
|||
|
|
|
|||
|
|
/* 模态框扁平化 */
|
|||
|
|
.modal-content { border-radius: 16px; border: none; }
|
|||
|
|
.modal-header { border-bottom: 1px solid #f1f5f9; }
|
|||
|
|
|
|||
|
|
/* 表单输入 */
|
|||
|
|
.flat-input {
|
|||
|
|
width: 100%; padding: 12px 0; border: none; border-bottom: 1px solid #e2e8f0;
|
|||
|
|
outline: none; transition: border-color 0.2s;
|
|||
|
|
}
|
|||
|
|
.flat-input:focus { border-color: var(--primary); }
|
|||
|
|
.flat-input[readonly] { color: #94a3b8; border-bottom-style: dashed; }
|
|||
|
|
|
|||
|
|
.btn-black {
|
|||
|
|
background: var(--primary); color: #fff; border: none; border-radius: 8px;
|
|||
|
|
padding: 10px 20px; font-size: 14px; font-weight: 600; width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 地址项 */
|
|||
|
|
.addr-item {
|
|||
|
|
border: 1px solid #f1f5f9; border-radius: 12px; padding: 16px; margin-bottom: 12px;
|
|||
|
|
background: #fff; position: relative;
|
|||
|
|
}
|
|||
|
|
.addr-main { font-weight: 600; color: var(--primary); font-size: 15px; margin-bottom: 4px; }
|
|||
|
|
.addr-sub { font-size: 13px; color: #64748b; }
|
|||
|
|
.addr-tag { font-size: 10px; padding: 2px 6px; background: var(--primary); color: #fff; border-radius: 4px; margin-left: 8px; }
|
|||
|
|
.addr-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #f8fafc; }
|
|||
|
|
.act-btn { font-size: 12px; color: #64748b; border: none; background: none; padding: 0; }
|
|||
|
|
|
|||
|
|
/* 底部导航 */
|
|||
|
|
.mobile-tabbar {
|
|||
|
|
position: fixed; bottom: 0; left: 0; right: 0; background: #fff;
|
|||
|
|
padding: 8px 0 20px; display: flex; border-top: 1px solid #f1f5f9; z-index: 99;
|
|||
|
|
}
|
|||
|
|
.tab-item { flex: 1; text-align: center; color: #94a3b8; text-decoration: none; font-size: 10px; }
|
|||
|
|
.tab-item.active { color: var(--accent); }
|
|||
|
|
.tab-item i { display: block; font-size: 24px; margin-bottom: 2px; }
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<!-- 头部展示 -->
|
|||
|
|
<div class="profile-card">
|
|||
|
|
<div class="avatar-placeholder"><i class="bi bi-person"></i></div>
|
|||
|
|
<div class="user-name-disp" id="dispName">--</div>
|
|||
|
|
<div class="user-phone-disp" id="dispPhone">--</div>
|
|||
|
|
<button class="btn-edit-profile" onclick="openProfileModal()">编辑资料</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 菜单 -->
|
|||
|
|
<div class="menu-group">
|
|||
|
|
<div class="menu-item" onclick="openAddressModalList()">
|
|||
|
|
<i class="bi bi-geo-alt menu-icon"></i>
|
|||
|
|
<span>地址管理</span>
|
|||
|
|
<i class="bi bi-chevron-right menu-arrow"></i>
|
|||
|
|
</div>
|
|||
|
|
<a href="/orders.html" class="menu-item">
|
|||
|
|
<i class="bi bi-receipt menu-icon"></i>
|
|||
|
|
<span>我的订单</span>
|
|||
|
|
<i class="bi bi-chevron-right menu-arrow"></i>
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="px-4 mt-4">
|
|||
|
|
<button class="btn btn-outline-danger w-100 border-0 bg-white py-3" onclick="logout()">退出登录</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 编辑资料模态框 -->
|
|||
|
|
<div class="modal fade" id="profileModal" tabindex="-1">
|
|||
|
|
<div class="modal-dialog modal-dialog-centered">
|
|||
|
|
<div class="modal-content">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h5 class="modal-title fw-bold">编辑资料</h5>
|
|||
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<form id="profileForm">
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label class="small text-muted">用户名</label>
|
|||
|
|
<input type="text" class="flat-input" id="username" readonly>
|
|||
|
|
</div>
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label class="small text-muted">姓名</label>
|
|||
|
|
<input type="text" class="flat-input" id="name" placeholder="设置昵称">
|
|||
|
|
</div>
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label class="small text-muted">手机号</label>
|
|||
|
|
<input type="tel" class="flat-input" id="phone" placeholder="绑定手机">
|
|||
|
|
</div>
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<label class="small text-muted">邮箱</label>
|
|||
|
|
<input type="email" class="flat-input" id="email" placeholder="绑定邮箱">
|
|||
|
|
</div>
|
|||
|
|
<button type="submit" class="btn-black mt-3">保存修改</button>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 地址列表模态框 (简单起见,用全屏 Modal 或普通 Modal 模拟新页面) -->
|
|||
|
|
<div class="modal fade" id="addressListModal" tabindex="-1">
|
|||
|
|
<div class="modal-dialog modal-fullscreen-sm-down modal-dialog-scrollable">
|
|||
|
|
<div class="modal-content">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h5 class="modal-title fw-bold">我的地址</h5>
|
|||
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body bg-light">
|
|||
|
|
<div id="addressListContent"></div>
|
|||
|
|
<button class="btn-black mt-3" onclick="openAddressEditModal()">+ 新增地址</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 地址编辑模态框 -->
|
|||
|
|
<div class="modal fade" id="addressEditModal" tabindex="-1">
|
|||
|
|
<div class="modal-dialog modal-dialog-centered">
|
|||
|
|
<div class="modal-content">
|
|||
|
|
<div class="modal-header">
|
|||
|
|
<h5 class="modal-title fw-bold" id="addrModalTitle">新增地址</h5>
|
|||
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-body">
|
|||
|
|
<form id="addressForm">
|
|||
|
|
<input type="hidden" id="addrId">
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<input type="text" class="flat-input" id="recipient" placeholder="收货人" required>
|
|||
|
|
</div>
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<input type="tel" class="flat-input" id="addressPhone" placeholder="联系电话" required>
|
|||
|
|
</div>
|
|||
|
|
<div class="mb-3">
|
|||
|
|
<textarea class="flat-input" id="address" rows="2" placeholder="详细地址" required></textarea>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-check mt-3">
|
|||
|
|
<input class="form-check-input" type="checkbox" id="isDefault">
|
|||
|
|
<label class="form-check-label small text-muted" for="isDefault">设为默认</label>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
<div class="modal-footer border-0">
|
|||
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">取消</button>
|
|||
|
|
<button type="button" class="btn btn-dark" onclick="saveAddress()">保存</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 底部导航 -->
|
|||
|
|
<div class="mobile-tabbar">
|
|||
|
|
<a href="/index.html" class="tab-item"><i class="bi bi-cup-hot"></i>点餐</a>
|
|||
|
|
<a href="/cart.html" class="tab-item"><i class="bi bi-bag"></i>购物袋</a>
|
|||
|
|
<a href="/orders.html" class="tab-item"><i class="bi bi-receipt"></i>订单</a>
|
|||
|
|
<a href="/profile.html" class="tab-item active"><i class="bi bi-person-fill"></i>我的</a>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|||
|
|
<script src="/static/js/custom-modal.js"></script>
|
|||
|
|
<script>
|
|||
|
|
// Init
|
|||
|
|
loadProfile();
|
|||
|
|
|
|||
|
|
async function loadProfile() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/profile', { credentials: 'include' });
|
|||
|
|
const data = await res.json();
|
|||
|
|
if (data.code === 200) {
|
|||
|
|
const u = data.data;
|
|||
|
|
// 展示
|
|||
|
|
document.getElementById('dispName').textContent = u.name || u.username;
|
|||
|
|
document.getElementById('dispPhone').textContent = u.phone || u.email || '未绑定手机';
|
|||
|
|
|
|||
|
|
// 填充表单
|
|||
|
|
document.getElementById('username').value = u.username || '';
|
|||
|
|
document.getElementById('name').value = u.name || '';
|
|||
|
|
document.getElementById('phone').value = u.phone || '';
|
|||
|
|
document.getElementById('email').value = u.email || '';
|
|||
|
|
} else if (data.message?.includes('登录')) {
|
|||
|
|
location.href = '/login.html';
|
|||
|
|
}
|
|||
|
|
} catch (e) { console.error(e); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function openProfileModal() {
|
|||
|
|
// 使用自定义模态框
|
|||
|
|
const modalContent = document.getElementById('profileForm').outerHTML;
|
|||
|
|
const modal = new CustomModal({
|
|||
|
|
title: '编辑资料',
|
|||
|
|
content: modalContent,
|
|||
|
|
size: 'medium',
|
|||
|
|
showFooter: false,
|
|||
|
|
onClose: () => {
|
|||
|
|
// 模态框关闭时的处理
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
modal.show();
|
|||
|
|
|
|||
|
|
// 绑定表单提交事件
|
|||
|
|
const form = modal.modal.querySelector('#profileForm');
|
|||
|
|
if (form) {
|
|||
|
|
form.onsubmit = async (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
const btn = form.querySelector('button[type="submit"]');
|
|||
|
|
const old = btn.textContent;
|
|||
|
|
btn.textContent = '保存中...'; btn.disabled = true;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/profile', {
|
|||
|
|
method: 'PUT',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
name: form.querySelector('#name').value,
|
|||
|
|
phone: form.querySelector('#phone').value,
|
|||
|
|
email: form.querySelector('#email').value
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
const d = await res.json();
|
|||
|
|
if(d.code===200) {
|
|||
|
|
modal.hide();
|
|||
|
|
loadProfile();
|
|||
|
|
} else alert(d.message);
|
|||
|
|
} catch(e) { alert('失败'); }
|
|||
|
|
btn.textContent = old; btn.disabled = false;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById('profileForm').onsubmit = async (e) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
const btn = e.target.querySelector('button');
|
|||
|
|
const old = btn.textContent;
|
|||
|
|
btn.textContent = '保存中...'; btn.disabled = true;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/profile', {
|
|||
|
|
method: 'PUT',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
name: document.getElementById('name').value,
|
|||
|
|
phone: document.getElementById('phone').value,
|
|||
|
|
email: document.getElementById('email').value
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
const d = await res.json();
|
|||
|
|
if(d.code===200) {
|
|||
|
|
// 如果使用自定义模态框,这里已经通过onClose处理
|
|||
|
|
loadProfile(); // 刷新展示
|
|||
|
|
} else alert(d.message);
|
|||
|
|
} catch(e) { alert('失败'); }
|
|||
|
|
btn.textContent = old; btn.disabled = false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 地址管理相关
|
|||
|
|
async function openAddressModalList() {
|
|||
|
|
const listModal = new bootstrap.Modal(document.getElementById('addressListModal'));
|
|||
|
|
listModal.show();
|
|||
|
|
loadAddressesInternal();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function loadAddressesInternal() {
|
|||
|
|
const box = document.getElementById('addressListContent');
|
|||
|
|
box.innerHTML = '<div class="text-center py-3 text-muted">加载中...</div>';
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/address', { credentials: 'include' });
|
|||
|
|
const d = await res.json();
|
|||
|
|
if (d.code === 200 && d.data?.length) {
|
|||
|
|
box.innerHTML = d.data.map(a => `
|
|||
|
|
<div class="addr-item">
|
|||
|
|
<div class="addr-main">
|
|||
|
|
${a.contactName||a.recipient} <span class="ms-2 small fw-normal text-muted">${a.contactPhone||a.phone}</span>
|
|||
|
|
${a.isDefault?'<span class="addr-tag">默认</span>':''}
|
|||
|
|
</div>
|
|||
|
|
<div class="addr-sub">${a.address}</div>
|
|||
|
|
<div class="addr-actions">
|
|||
|
|
<button class="act-btn" onclick="openAddressEditModal(${a.id})">编辑</button>
|
|||
|
|
<button class="act-btn text-danger" onclick="deleteAddress(${a.id})">删除</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`).join('');
|
|||
|
|
} else {
|
|||
|
|
box.innerHTML = '<div class="text-center py-5 text-muted">暂无地址</div>';
|
|||
|
|
}
|
|||
|
|
} catch(e) { box.innerHTML = '加载失败'; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function openAddressEditModal(id = null) {
|
|||
|
|
// 如果是在列表模态框之上打开,Bootstrap会自动处理层级
|
|||
|
|
const modal = new bootstrap.Modal(document.getElementById('addressEditModal'));
|
|||
|
|
document.getElementById('addressForm').reset();
|
|||
|
|
document.getElementById('addrId').value = '';
|
|||
|
|
document.getElementById('addrModalTitle').textContent = '新增地址';
|
|||
|
|
|
|||
|
|
if (id) {
|
|||
|
|
document.getElementById('addrModalTitle').textContent = '编辑地址';
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/address', { credentials: 'include' });
|
|||
|
|
const d = await res.json();
|
|||
|
|
const addr = d.data.find(a => a.id === id);
|
|||
|
|
if (addr) {
|
|||
|
|
document.getElementById('addrId').value = addr.id;
|
|||
|
|
document.getElementById('recipient').value = addr.contactName || '';
|
|||
|
|
document.getElementById('addressPhone').value = addr.contactPhone || '';
|
|||
|
|
document.getElementById('address').value = addr.address || '';
|
|||
|
|
document.getElementById('isDefault').checked = addr.isDefault === 1;
|
|||
|
|
}
|
|||
|
|
} catch(e) {}
|
|||
|
|
}
|
|||
|
|
modal.show();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function saveAddress() {
|
|||
|
|
const id = document.getElementById('addrId').value;
|
|||
|
|
const payload = {
|
|||
|
|
contactName: document.getElementById('recipient').value.trim(),
|
|||
|
|
contactPhone: document.getElementById('addressPhone').value.trim(),
|
|||
|
|
address: document.getElementById('address').value.trim(),
|
|||
|
|
isDefault: document.getElementById('isDefault').checked ? 1 : 0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 验证必填字段
|
|||
|
|
if (!payload.contactName || !payload.contactPhone || !payload.address) {
|
|||
|
|
alert('请填写完整信息');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch(id ? `/address/${id}` : '/address', {
|
|||
|
|
method: id ? 'PUT' : 'POST',
|
|||
|
|
headers: {'Content-Type': 'application/json'},
|
|||
|
|
credentials: 'include',
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
});
|
|||
|
|
const d = await res.json();
|
|||
|
|
if(d.code === 200) {
|
|||
|
|
bootstrap.Modal.getInstance(document.getElementById('addressEditModal')).hide();
|
|||
|
|
loadAddressesInternal(); // 刷新列表
|
|||
|
|
} else alert(d.message);
|
|||
|
|
} catch(e) { alert('保存失败'); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function deleteAddress(id) {
|
|||
|
|
if(!confirm('确认删除?')) return;
|
|||
|
|
await fetch(`/address/${id}`, { method: 'DELETE', credentials: 'include' });
|
|||
|
|
loadAddressesInternal();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function logout() {
|
|||
|
|
location.href = '/login.html';
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|