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> |