初始化v1

This commit is contained in:
2026-01-11 18:14:42 +08:00
commit 641b03e8e2
297 changed files with 28656 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
<!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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link active" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-megaphone"></i> 公告管理</h2>
<button class="btn btn-primary" onclick="showAddAnnouncementModal()">
<i class="bi bi-plus-circle"></i> 新增公告
</button>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div id="announcementsList"></div>
</div>
</div>
</div>
<!-- 添加/编辑公告模态框 -->
<div class="modal fade" id="announcementModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="announcementModalTitle">新增公告</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="announcementForm">
<input type="hidden" id="announcementId">
<div class="mb-3">
<label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" required>
</div>
<div class="mb-3">
<label for="type" class="form-label">类型</label>
<select class="form-control" id="type">
<option value="ACTIVITY">活动</option>
<option value="NOTICE">通知</option>
</select>
</div>
<div class="mb-3">
<label for="content" class="form-label">内容 <span class="text-danger">*</span></label>
<textarea class="form-control" id="content" rows="5" required></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="status" checked>
<label class="form-check-label" for="status">
发布
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveAnnouncement()">保存</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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
// 加载公告列表
async function loadAnnouncements() {
try {
const response = await fetch('/merchant/announcements', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('announcementsList');
if (data.code === 200 && data.data) {
const announcements = data.data || [];
if (announcements.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无公告,请点击"新增公告"按钮添加</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>标题</th>
<th>类型</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${announcements.map(announcement => `
<tr>
<td><strong>${announcement.title || ''}</strong></td>
<td><span class="badge ${announcement.type === 'ACTIVITY' ? 'bg-warning' : 'bg-info'}">${announcement.type === 'ACTIVITY' ? '活动' : '通知'}</span></td>
<td>
<span class="badge ${announcement.status === 1 ? 'bg-success' : 'bg-secondary'}">
${announcement.status === 1 ? '已发布' : '已下架'}
</span>
</td>
<td>${announcement.createTime ? new Date(announcement.createTime).toLocaleString('zh-CN') : '-'}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editAnnouncement(${announcement.id})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteAnnouncement(${announcement.id})" title="删除">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载公告列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('announcementsList').innerHTML = '<div class="alert alert-danger">加载公告列表失败,请刷新页面重试</div>';
}
}
// 显示添加公告模态框
function showAddAnnouncementModal() {
document.getElementById('announcementModalTitle').textContent = '新增公告';
document.getElementById('announcementId').value = '';
document.getElementById('announcementForm').reset();
document.getElementById('status').checked = true;
const modal = new bootstrap.Modal(document.getElementById('announcementModal'));
modal.show();
}
// 编辑公告
async function editAnnouncement(announcementId) {
try {
const response = await fetch(`/merchant/announcements/${announcementId}`, {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const announcement = data.data;
document.getElementById('announcementModalTitle').textContent = '编辑公告';
document.getElementById('announcementId').value = announcement.id;
document.getElementById('title').value = announcement.title || '';
document.getElementById('type').value = announcement.type || 'NOTICE';
document.getElementById('content').value = announcement.content || '';
document.getElementById('status').checked = announcement.status === 1;
const modal = new bootstrap.Modal(document.getElementById('announcementModal'));
modal.show();
} else {
alert('加载公告信息失败');
}
} catch (error) {
console.error('Error:', error);
alert('加载公告信息失败');
}
}
// 保存公告
async function saveAnnouncement() {
const announcementId = document.getElementById('announcementId').value;
const announcementData = {
title: document.getElementById('title').value,
type: document.getElementById('type').value,
content: document.getElementById('content').value,
status: document.getElementById('status').checked ? 1 : 0
};
if (!announcementData.title || !announcementData.content) {
UIEnhancements.showWarning('请填写完整信息');
return;
}
const saveBtn = document.querySelector('#announcementModal .btn-primary');
try {
const url = announcementId ? `/merchant/announcements/${announcementId}` : '/merchant/announcements';
const method = announcementId ? 'PUT' : 'POST';
const { data } = await UIEnhancements.enhancedFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(announcementData),
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess(announcementId ? '公告已更新' : '公告已发布');
const modal = bootstrap.Modal.getInstance(document.getElementById('announcementModal'));
modal.hide();
loadAnnouncements();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 删除公告
async function deleteAnnouncement(announcementId) {
const confirmed = await UIEnhancements.confirmAction('确定要删除该公告吗?', '确认删除');
if (!confirmed) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/announcements/${announcementId}`, {
method: 'DELETE',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('公告已删除');
loadAnnouncements();
} else {
UIEnhancements.showError(data.message || '删除失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('删除失败,请稍后重试');
}
}
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadAnnouncements();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,283 @@
<!-- 侧边栏导航 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/merchant/dashboard.html" data-page="dashboard">
<i class="bi bi-speedometer2"></i>
<span class="nav-text">首页</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/products.html" data-page="products">
<i class="bi bi-box-seam"></i>
<span class="nav-text">商品管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/orders.html" data-page="orders">
<i class="bi bi-receipt-cutoff"></i>
<span class="nav-text">订单管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/inventory.html" data-page="inventory">
<i class="bi bi-archive"></i>
<span class="nav-text">库存管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/statistics.html" data-page="statistics">
<i class="bi bi-bar-chart"></i>
<span class="nav-text">数据统计</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/announcements.html" data-page="announcements">
<i class="bi bi-megaphone"></i>
<span class="nav-text">公告管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/messages.html" data-page="messages">
<i class="bi bi-chat-dots"></i>
<span class="nav-text">留言管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/reviews.html" data-page="reviews">
<i class="bi bi-star"></i>
<span class="nav-text">评价管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/users.html" data-page="users">
<i class="bi bi-people"></i>
<span class="nav-text">用户管理</span>
</a>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<!-- 侧边栏遮罩层(移动端) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 顶部导航栏(移动端) -->
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
<style>
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 移动端样式 */
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
/* 桌面端主内容区域 */
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
}
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
<script>
// 侧边栏交互逻辑
(function() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
// 移动端切换侧边栏
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('show');
sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 设置当前页面的导航项为激活状态
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active');
}
});
// 点击导航项时,移动端自动关闭侧边栏
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
})();
</script>

View File

@@ -0,0 +1,413 @@
<!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>
.stat-card {
transition: transform 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.stat-icon {
font-size: 3rem;
opacity: 0.3;
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 移动端样式 */
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
/* 桌面端主内容区域 */
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
position: relative;
z-index: 1;
width: calc(100% - 260px);
}
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<!-- 侧边栏导航 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="/merchant/dashboard.html" data-page="dashboard">
<i class="bi bi-speedometer2"></i>
<span class="nav-text">首页</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/products.html" data-page="products">
<i class="bi bi-box-seam"></i>
<span class="nav-text">商品管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/orders.html" data-page="orders">
<i class="bi bi-receipt-cutoff"></i>
<span class="nav-text">订单管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/inventory.html" data-page="inventory">
<i class="bi bi-archive"></i>
<span class="nav-text">库存管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/statistics.html" data-page="statistics">
<i class="bi bi-bar-chart"></i>
<span class="nav-text">数据统计</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/announcements.html" data-page="announcements">
<i class="bi bi-megaphone"></i>
<span class="nav-text">公告管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/messages.html" data-page="messages">
<i class="bi bi-chat-dots"></i>
<span class="nav-text">留言管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/reviews.html" data-page="reviews">
<i class="bi bi-star"></i>
<span class="nav-text">评价管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/users.html" data-page="users">
<i class="bi bi-people"></i>
<span class="nav-text">用户管理</span>
</a>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<!-- 侧边栏遮罩层(移动端) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 顶部导航栏(移动端) -->
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
<div class="main-content">
<div class="container-fluid mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> 数据概览</h2>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card stat-card shadow-sm border-primary">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">今日订单</h6>
<h3 class="mb-0" id="todayOrders">-</h3>
</div>
<i class="bi bi-receipt-cutoff stat-icon text-primary"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card stat-card shadow-sm border-success">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">今日营收</h6>
<h3 class="mb-0 text-success" id="todayRevenue">-</h3>
</div>
<i class="bi bi-currency-yen stat-icon text-success"></i>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card stat-card shadow-sm border-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">待处理订单</h6>
<h3 class="mb-0 text-warning" id="pendingOrders">-</h3>
</div>
<i class="bi bi-clock-history stat-icon text-warning"></i>
</div>
</div>
</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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session,浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
async function loadStatistics() {
try {
const { data } = await UIEnhancements.enhancedFetch('/merchant/statistics/sales?period=day', {
credentials: 'include'
});
if (data.code === 200 && data.data) {
document.getElementById('todayOrders').textContent = data.data.totalOrders || 0;
document.getElementById('todayRevenue').textContent = '¥' + UIEnhancements.formatNumber(data.data.totalAmount || 0);
} else {
document.getElementById('todayOrders').textContent = '-';
document.getElementById('todayRevenue').textContent = '-';
}
// 加载待处理订单数
try {
const { data: ordersData } = await UIEnhancements.enhancedFetch('/merchant/orders?status=MAKING&pageNum=1&pageSize=1', {
credentials: 'include'
});
if (ordersData.code === 200 && ordersData.data) {
document.getElementById('pendingOrders').textContent = ordersData.data.total || 0;
} else {
document.getElementById('pendingOrders').textContent = '-';
}
} catch (e) {
document.getElementById('pendingOrders').textContent = '-';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('todayOrders').textContent = '-';
document.getElementById('todayRevenue').textContent = '-';
document.getElementById('pendingOrders').textContent = '-';
}
}
// 侧边栏交互逻辑
(function() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
// 移动端切换侧边栏
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('show');
sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 点击导航项时,移动端自动关闭侧边栏
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
})();
// 页面加载时检查登录
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadStatistics();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,466 @@
<!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>
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
transition: all 0.2s;
text-decoration: none;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.sidebar-nav .nav-link.active {
background-color: rgba(255, 255, 255, 0.15);
color: #fff;
border-left: 3px solid #3498db;
}
.sidebar-nav .nav-link i {
width: 20px;
margin-right: 0.75rem;
font-size: 1.1rem;
}
.main-content {
margin-left: 260px;
padding: 2rem;
min-height: 100vh;
}
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">商家后台</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link active" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between mb-3">
<h2>库存管理</h2>
<button class="btn btn-primary" onclick="showAddInventoryModal()">新增库存</button>
</div>
<div class="mb-3">
<button class="btn btn-outline-warning" onclick="loadLowStockAlerts()">低库存预警</button>
</div>
<div id="inventoryList"></div>
</div>
<!-- 添加/编辑库存模态框 -->
<div class="modal fade" id="inventoryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="inventoryModalTitle">新增库存</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="inventoryForm">
<input type="hidden" id="inventoryId">
<div class="mb-3">
<label for="materialName" class="form-label">原料名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="materialName" required>
</div>
<div class="mb-3">
<label for="quantity" class="form-label">数量 <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="quantity" step="0.01" min="0" required>
</div>
<div class="mb-3">
<label for="unit" class="form-label">单位</label>
<input type="text" class="form-control" id="unit" placeholder="例如:kg、L、个">
</div>
<div class="mb-3">
<label for="minThreshold" class="form-label">最低库存阈值</label>
<input type="number" class="form-control" id="minThreshold" step="0.01" min="0" value="0">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveInventory()">保存</button>
</div>
</div>
</div>
</div>
<!-- 入库/出库模态框 -->
<div class="modal fade" id="stockModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="stockModalTitle">入库</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="stockForm">
<input type="hidden" id="stockInventoryId">
<div class="mb-3">
<label for="stockQuantity" class="form-label">数量 <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="stockQuantity" step="0.01" min="0" required>
</div>
<div class="mb-3">
<label for="operator" class="form-label">操作人</label>
<input type="text" class="form-control" id="operator">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveStock()">确认</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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session,浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let stockOperationType = 'in'; // 'in' 或 'out'
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadInventory();
}
});
// 加载库存列表
async function loadInventory() {
try {
const response = await fetch('/merchant/inventory', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('inventoryList');
if (data.code === 200 && data.data) {
const inventory = data.data || [];
if (inventory.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无库存记录,请点击"新增库存"按钮添加</div>';
return;
}
container.innerHTML = `
<table class="table table-hover">
<thead>
<tr>
<th>原料名称</th>
<th>数量</th>
<th>单位</th>
<th>最低阈值</th>
<th>库存状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${inventory.map(item => {
const isLowStock = parseFloat(item.quantity || 0) <= parseFloat(item.minThreshold || 0);
return `
<tr class="${isLowStock ? 'table-warning' : ''}">
<td>${item.materialName || ''}</td>
<td>${item.quantity || '0'}</td>
<td>${item.unit || '-'}</td>
<td>${item.minThreshold || '0'}</td>
<td>
${isLowStock ?
'<span class="badge bg-warning">库存不足</span>' :
'<span class="badge bg-success">正常</span>'
}
</td>
<td>
<button class="btn btn-sm btn-success" onclick="showStockModal(${item.id}, 'in')">入库</button>
<button class="btn btn-sm btn-warning" onclick="showStockModal(${item.id}, 'out')">出库</button>
<button class="btn btn-sm btn-primary" onclick="editInventory(${item.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteInventory(${item.id})">删除</button>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载库存列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('inventoryList').innerHTML = '<div class="alert alert-danger">加载库存列表失败,请刷新页面重试</div>';
}
}
// 加载低库存预警
async function loadLowStockAlerts() {
try {
const response = await fetch('/merchant/inventory/alerts', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const alerts = data.data || [];
if (alerts.length === 0) {
alert('暂无低库存预警');
} else {
alert(`有 ${alerts.length} 个原料库存不足:\n${alerts.map(a => `${a.materialName} (当前: ${a.quantity}, 阈值: ${a.minThreshold})`).join('\n')}`);
}
}
} catch (error) {
console.error('Error:', error);
alert('加载低库存预警失败');
}
}
// 显示添加库存模态框
function showAddInventoryModal() {
document.getElementById('inventoryModalTitle').textContent = '新增库存';
document.getElementById('inventoryId').value = '';
document.getElementById('inventoryForm').reset();
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
modal.show();
}
// 编辑库存
async function editInventory(inventoryId) {
try {
const response = await fetch('/merchant/inventory', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const inventory = data.data.find(i => i.id === inventoryId);
if (inventory) {
document.getElementById('inventoryModalTitle').textContent = '编辑库存';
document.getElementById('inventoryId').value = inventory.id;
document.getElementById('materialName').value = inventory.materialName || '';
document.getElementById('quantity').value = inventory.quantity || '';
document.getElementById('unit').value = inventory.unit || '';
document.getElementById('minThreshold').value = inventory.minThreshold || '0';
const modal = new bootstrap.Modal(document.getElementById('inventoryModal'));
modal.show();
}
}
} catch (error) {
console.error('Error:', error);
}
}
// 保存库存
async function saveInventory() {
const inventoryId = document.getElementById('inventoryId').value;
const inventoryData = {
materialName: document.getElementById('materialName').value,
quantity: parseFloat(document.getElementById('quantity').value) || 0,
unit: document.getElementById('unit').value,
minThreshold: parseFloat(document.getElementById('minThreshold').value) || 0
};
if (!inventoryData.materialName) {
UIEnhancements.showWarning('请填写材料名称');
return;
}
const saveBtn = document.querySelector('#inventoryModal .btn-primary');
try {
const url = inventoryId ? `/merchant/inventory/${inventoryId}` : '/merchant/inventory';
const method = inventoryId ? 'PUT' : 'POST';
const { data } = await UIEnhancements.enhancedFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(inventoryData),
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess(inventoryId ? '库存信息已更新' : '库存已添加');
const modal = bootstrap.Modal.getInstance(document.getElementById('inventoryModal'));
modal.hide();
loadInventory();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 显示入库/出库模态框
function showStockModal(inventoryId, type) {
stockOperationType = type;
document.getElementById('stockModalTitle').textContent = type === 'in' ? '入库' : '出库';
document.getElementById('stockInventoryId').value = inventoryId;
document.getElementById('stockForm').reset();
const modal = new bootstrap.Modal(document.getElementById('stockModal'));
modal.show();
}
// 保存入库/出库
async function saveStock() {
const inventoryId = document.getElementById('stockInventoryId').value;
const quantity = parseFloat(document.getElementById('stockQuantity').value);
const operator = document.getElementById('operator').value;
if (!quantity || quantity <= 0) {
UIEnhancements.showWarning('请输入有效的数量');
return;
}
const saveBtn = document.querySelector('#stockModal .btn-primary');
try {
const url = `/merchant/inventory/${inventoryId}/${stockOperationType}`;
const { data } = await UIEnhancements.enhancedFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
credentials: 'include',
body: `quantity=${quantity}&operator=${encodeURIComponent(operator || '')}`,
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess(stockOperationType === 'in' ? '入库成功' : '出库成功');
const modal = bootstrap.Modal.getInstance(document.getElementById('stockModal'));
modal.hide();
loadInventory();
} else {
UIEnhancements.showError(data.message || '操作失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('操作失败,请稍后重试');
}
}
// 删除库存
async function deleteInventory(inventoryId) {
const confirmed = await UIEnhancements.confirmAction('确定要删除这个库存记录吗?', '确认删除');
if (!confirmed) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/inventory/${inventoryId}`, {
method: 'DELETE',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('库存记录已删除');
loadInventory();
} else {
UIEnhancements.showError(data.message || '删除失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('删除失败,请稍后重试');
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,81 @@
<!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>
body {
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
align-items: center;
}
.login-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card login-card shadow-lg">
<div class="card-header bg-dark text-white text-center py-4">
<h3 class="mb-0">
<i class="bi bi-shop"></i> 商家后台
</h3>
<p class="mb-0 mt-2">商家登录</p>
</div>
<div class="card-body p-4">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">
<i class="bi bi-person"></i> 用户名
</label>
<input type="text" class="form-control form-control-lg" id="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">
<i class="bi bi-lock"></i> 密码
</label>
<input type="password" class="form-control form-control-lg" id="password" required>
</div>
<button type="submit" class="btn btn-dark btn-lg w-100 mt-3">
<i class="bi bi-box-arrow-in-right"></i> 登录
</button>
</form>
<div class="mt-4 text-center">
<a href="/merchant/register.html" class="text-decoration-none">还没有账号?立即注册</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const response = await fetch('/merchant/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include', // 确保发送Cookie
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value
})
});
const data = await response.json();
if (data.code === 200) {
// 使用Session,不需要存储token
window.location.href = '/merchant/dashboard.html';
} else {
alert(data.message);
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,209 @@
<!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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link active" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-chat-dots"></i> 留言管理</h2>
<div class="card shadow-sm">
<div class="card-body">
<div id="messagesList"></div>
</div>
</div>
</div>
<!-- 回复留言模态框 -->
<div class="modal fade" id="replyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">回复留言</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">留言内容</label>
<div class="form-control" id="messageContent" style="min-height: 60px; background-color: #f8f9fa;"></div>
</div>
<div class="mb-3">
<label for="replyContent" class="form-label">回复内容 <span class="text-danger">*</span></label>
<textarea class="form-control" id="replyContent" rows="4" required></textarea>
</div>
<input type="hidden" id="replyMessageId">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveReply()">保存回复</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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
// 加载留言列表
async function loadMessages() {
try {
const response = await fetch('/merchant/messages', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('messagesList');
if (data.code === 200 && data.data) {
const messages = data.data || [];
if (messages.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无留言</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>留言内容</th>
<th>订单号</th>
<th>状态</th>
<th>留言时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${messages.map(message => `
<tr>
<td>${message.content || ''}</td>
<td>${message.orderId ? '订单#' + message.orderId : '-'}</td>
<td>
<span class="badge ${message.status === 1 ? 'bg-success' : 'bg-warning'}">
${message.status === 1 ? '已回复' : '未回复'}
</span>
</td>
<td>${message.createTime ? new Date(message.createTime).toLocaleString('zh-CN') : '-'}</td>
<td>
${message.status === 0 ?
`<button class="btn btn-sm btn-primary" onclick="showReplyModal(${message.id}, '${(message.content || '').replace(/'/g, "\\'")}')">
<i class="bi bi-reply"></i> 回复
</button>` :
`<button class="btn btn-sm btn-outline-primary" onclick="showReplyModal(${message.id}, '${(message.content || '').replace(/'/g, "\\'")}', '${(message.reply || '').replace(/'/g, "\\'")}')">
<i class="bi bi-pencil"></i> 查看/编辑
</button>`
}
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载留言列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('messagesList').innerHTML = '<div class="alert alert-danger">加载留言列表失败,请刷新页面重试</div>';
}
}
// 显示回复模态框
function showReplyModal(messageId, content, reply = '') {
document.getElementById('replyMessageId').value = messageId;
document.getElementById('messageContent').textContent = content;
document.getElementById('replyContent').value = reply;
const modal = new bootstrap.Modal(document.getElementById('replyModal'));
modal.show();
}
// 保存回复
async function saveReply() {
const messageId = document.getElementById('replyMessageId').value;
const reply = document.getElementById('replyContent').value;
if (!reply.trim()) {
UIEnhancements.showWarning('请输入回复内容');
return;
}
const saveBtn = document.querySelector('#replyModal .btn-primary');
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/messages/${messageId}/reply`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
credentials: 'include',
body: `reply=${encodeURIComponent(reply)}`,
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess('回复已保存');
const modal = bootstrap.Modal.getInstance(document.getElementById('replyModal'));
modal.hide();
loadMessages();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadMessages();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,503 @@
<!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-bg: #f8f9fa;
--card-radius: 12px;
--modal-radius: 16px;
}
body {
background-color: #f3f4f6;
}
.filter-btn.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
box-shadow: 0 4px 10px rgba(13, 110, 253, 0.2);
}
.order-card {
border: none;
border-radius: var(--card-radius);
transition: all 0.2s ease;
background: #fff;
}
.order-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
}
/* --- 模态框优化样式 --- */
.modal-content {
border: none;
border-radius: var(--modal-radius);
box-shadow: 0 15px 40px rgba(0,0,0,0.1);
}
.modal-header {
border-bottom: 1px solid #f1f5f9;
padding: 20px 24px;
background-color: #fff;
border-radius: var(--modal-radius) var(--modal-radius) 0 0;
}
.modal-title {
font-weight: 700;
color: #1e293b;
}
.modal-body {
padding: 24px;
background-color: #f8fafc; /* 浅灰底色突出卡片 */
}
.modal-footer {
border-top: none;
padding: 16px 24px;
background: #fff;
border-radius: 0 0 var(--modal-radius) var(--modal-radius);
}
/* 详情卡片块 */
.detail-section {
background: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
}
.section-title {
font-size: 14px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 16px;
border-left: 4px solid #3b82f6;
padding-left: 10px;
}
/* 关键数据胶囊 */
.info-capsule {
background: #f1f5f9;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
}
.info-capsule .label {
font-size: 11px;
color: #64748b;
margin-bottom: 4px;
}
.info-capsule .value {
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
/* 商品表格美化 */
.custom-table thead th {
border-bottom: none;
color: #64748b;
font-weight: 600;
font-size: 13px;
background-color: #f8fafc;
}
.custom-table td {
vertical-align: middle;
color: #334155;
border-bottom: 1px solid #f1f5f9;
padding: 12px 8px;
}
/* --- 时间轴样式 (Timeline) --- */
.timeline {
position: relative;
padding-left: 24px;
border-left: 2px solid #e2e8f0;
margin-left: 8px;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-dot {
position: absolute;
left: -31px; /* Line width 2px + padding 24px + dot radius approx */
top: 4px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #cbd5e1;
border: 2px solid #fff;
box-shadow: 0 0 0 2px #f1f5f9;
}
.timeline-dot.active {
background: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
.timeline-time {
font-size: 12px;
color: #94a3b8;
margin-bottom: 2px;
}
.timeline-content {
font-size: 14px;
color: #334155;
font-weight: 500;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link active" href="/merchant/orders.html">
<i class="bi bi-receipt-cutoff"></i> 订单管理
</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0 fw-bold text-dark"><i class="bi bi-receipt-cutoff text-primary"></i> 订单管理</h2>
<div class="btn-group shadow-sm" role="group">
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('')" id="filter-all">全部</button>
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('MAKING')" id="filter-making">制作中</button>
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('READY')" id="filter-ready">待取餐</button>
<button class="btn btn-outline-primary filter-btn" onclick="filterOrders('COMPLETED')" id="filter-completed">已完成</button>
</div>
</div>
<div id="ordersList"></div>
</div>
<!-- 优化后的订单详情模态框 -->
<div class="modal fade" id="orderDetailModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 text-primary rounded p-2 me-3">
<i class="bi bi-receipt fs-4"></i>
</div>
<div>
<h5 class="modal-title" id="modalOrderNo">订单详情</h5>
<small class="text-muted" id="modalOrderTime">加载中...</small>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="orderDetailContent">
<!-- 内容将由JS动态加载 -->
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
<div class="modal-footer justify-content-between bg-white">
<span class="text-muted small">系统自动生成</span>
<button type="button" class="btn btn-secondary px-4 rounded-pill" data-bs-dismiss="modal">关闭</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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', { credentials: 'include' });
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let currentStatus = '';
async function filterOrders(status) {
currentStatus = status;
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
const filterMap = { '': 'filter-all', 'MAKING': 'filter-making', 'READY': 'filter-ready', 'COMPLETED': 'filter-completed' };
const activeBtn = document.getElementById(filterMap[status] || 'filter-all');
if (activeBtn) activeBtn.classList.add('active');
await loadOrders();
}
async function loadOrders() {
const url = currentStatus ?
`/merchant/orders?status=${currentStatus}&pageNum=1&pageSize=20` :
'/merchant/orders?pageNum=1&pageSize=20';
try {
const response = await fetch(url, { credentials: 'include' });
const data = await response.json();
const container = document.getElementById('ordersList');
if (data.code === 200 && data.data && data.data.list) {
const orders = data.data.list;
if (orders.length === 0) {
container.innerHTML = '<div class="alert alert-light text-center py-4 text-muted border">暂无订单数据</div>';
return;
}
container.innerHTML = orders.map(order => `
<div class="card order-card mb-3">
<div class="card-body py-3">
<div class="row align-items-center g-3">
<div class="col-md-2">
<div class="text-muted small mb-1">订单号</div>
<div class="fw-bold font-monospace">${order.orderNo || ''}</div>
</div>
<div class="col-md-3">
<div class="d-flex align-items-center text-secondary">
<i class="bi bi-person-circle me-2"></i>
<div class="text-truncate" style="max-width: 150px;">${order.address || '自提'}</div>
</div>
<div class="text-muted small mt-1 ps-4">${order.contactPhone || '-'}</div>
</div>
<div class="col-md-2 text-center">
<span class="badge ${getStatusBadgeClass(order.orderStatus)} px-3 py-2 rounded-pill">
${getStatusText(order.orderStatus)}
</span>
</div>
<div class="col-md-2 text-center">
<div class="text-muted small">金额</div>
<strong class="text-primary fs-5">¥${order.totalAmount || '0.00'}</strong>
</div>
<div class="col-md-3 text-end">
<small class="text-muted d-block mb-2">
${order.createTime ? new Date(order.createTime).toLocaleTimeString('zh-CN', {hour:'2-digit', minute:'2-digit'}) : '-'}
</small>
<div class="btn-group">
${getActionButtons(order)}
<button class="btn btn-light text-primary border" onclick="viewDetail(${order.id})" title="详情">
详情
</button>
</div>
</div>
</div>
</div>
</div>
`).join('');
}
} catch (e) { console.error(e); }
}
function getActionButtons(order) {
if (order.orderStatus === 'MAKING') {
return `<button class="btn btn-success text-white" onclick="updateStatus(${order.id}, 'READY')">制作完成</button>`;
} else if (order.orderStatus === 'READY') {
return `<button class="btn btn-primary" onclick="updateStatus(${order.id}, 'COMPLETED')">完成订单</button>`;
}
return '';
}
function getStatusText(status) {
const map = { 'PENDING_PAY': '待支付', 'MAKING': '制作中', 'READY': '待取餐', 'COMPLETED': '已完成', 'CANCELLED': '已取消' };
return map[status] || status;
}
function getStatusBadgeClass(status) {
const map = { 'PENDING_PAY': 'bg-warning', 'MAKING': 'bg-info', 'READY': 'bg-primary', 'COMPLETED': 'bg-success', 'CANCELLED': 'bg-secondary' };
return map[status] || 'bg-secondary';
}
async function updateStatus(orderId, status) {
if(!confirm(`确定将订单更新为 ${getStatusText(status)} 吗?`)) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/orders/${orderId}/status?status=${status}`, {
method: 'PUT', credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('状态已更新');
loadOrders();
} else {
UIEnhancements.showError(data.message || '操作失败');
}
} catch (error) { UIEnhancements.showError('网络错误'); }
}
async function viewDetail(orderId) {
const modal = new bootstrap.Modal(document.getElementById('orderDetailModal'));
modal.show();
try {
const response = await fetch(`/merchant/orders/${orderId}`, { credentials: 'include' });
const data = await response.json();
if (data.code === 200) {
renderModalContent(data.data);
} else {
document.getElementById('orderDetailContent').innerHTML = `<div class="alert alert-danger">${data.message}</div>`;
}
} catch (error) {
document.getElementById('orderDetailContent').innerHTML = '<div class="alert alert-danger">网络错误,无法加载详情</div>';
}
}
async function renderModalContent(detail) {
const order = detail.order;
const items = detail.items || [];
const tracks = detail.tracks || [];
// 设置标题栏信息
document.getElementById('modalOrderNo').innerText = `订单号:${order.orderNo}`;
document.getElementById('modalOrderTime').innerText = `下单时间:${new Date(order.createTime).toLocaleString()}`;
// 1. 处理商品明细 (Toppings逻辑保持不变)
let itemsHtml = '';
// (此处简化了异步获取配料名的逻辑以保证UI响应速度,实际项目中可保留之前的Promise.all)
// 为了演示UI,这里假设toppings已经处理好,或者直接显示简单的文本
itemsHtml = items.map(item => {
let spec = item.specName ? `<span class="badge bg-light text-dark border ms-1">${item.specName}</span>` : '';
let custom = [];
if(item.customSweetness) custom.push(item.customSweetness);
if(item.customIce) custom.push(item.customIce);
// 简化的配料显示
let toppingText = item.toppings && item.toppings !== '[]' ? '<i class="bi bi-plus-circle-dotted ms-1"></i> 加料' : '';
return `
<tr>
<td style="width: 40%">
<div class="fw-bold">${item.productName}</div>
<div class="small text-muted">${custom.join(' / ')} ${toppingText}</div>
</td>
<td class="text-center">${spec}</td>
<td class="text-center">x${item.quantity}</td>
<td class="text-end fw-bold">¥${item.subtotal}</td>
</tr>
`;
}).join('');
// 2. 生成时间轴HTML
const tracksHtml = tracks.map((track, index) => `
<div class="timeline-item">
<div class="timeline-dot ${index === 0 ? 'active' : ''}"></div>
<div class="timeline-time">${new Date(track.createTime).toLocaleString('zh-CN', {hour:'2-digit', minute:'2-digit', month:'2-digit', day:'2-digit'})}</div>
<div class="timeline-content">
<span class="fw-bold text-dark">${getStatusText(track.status)}</span>
${track.description ? `<div class="small text-muted mt-1">${track.description}</div>` : ''}
</div>
</div>
`).join('');
// 3. 组合最终HTML
const content = `
<div class="row g-3">
<!-- 左侧:订单信息与商品 -->
<div class="col-lg-8">
<!-- 状态概览卡片 -->
<div class="detail-section d-flex justify-content-between align-items-center bg-primary bg-opacity-10 mb-3 border-0">
<div>
<span class="text-primary small fw-bold text-uppercase">当前状态</span>
<h3 class="text-primary fw-bold mb-0 mt-1">${getStatusText(order.orderStatus)}</h3>
</div>
<div class="text-end">
<span class="text-muted small">实付金额</span>
<h2 class="text-dark fw-bold mb-0">¥${order.totalAmount}</h2>
</div>
</div>
<!-- 客户信息 -->
<div class="detail-section">
<div class="section-title">配送信息</div>
<div class="row g-3">
<div class="col-md-6">
<div class="info-capsule">
<span class="label">联系人</span>
<span class="value"><i class="bi bi-telephone me-1"></i> ${order.contactPhone}</span>
</div>
</div>
<div class="col-md-6">
<div class="info-capsule">
<span class="label">支付方式</span>
<span class="value">
${order.paymentMethod === 'wechat' ? '<i class="bi bi-wechat text-success"></i> 微信支付' : '在线支付'}
</span>
</div>
</div>
<div class="col-12">
<div class="info-capsule">
<span class="label">收货地址</span>
<span class="value text-break"><i class="bi bi-geo-alt me-1"></i> ${order.address}</span>
</div>
</div>
</div>
</div>
<!-- 商品列表 -->
<div class="detail-section">
<div class="section-title">商品明细</div>
<div class="table-responsive">
<table class="table custom-table table-hover mb-0">
<thead>
<tr>
<th>品名/规格</th>
<th class="text-center">属性</th>
<th class="text-center">数量</th>
<th class="text-end">小计</th>
</tr>
</thead>
<tbody>${itemsHtml}</tbody>
</table>
</div>
<div class="border-top mt-3 pt-3 text-end">
<span class="text-muted me-2">共 ${items.reduce((sum, i) => sum + i.quantity, 0)} 件商品</span>
<span class="fs-5 fw-bold text-dark">合计: ¥${order.totalAmount}</span>
</div>
</div>
</div>
<!-- 右侧:时间轴 -->
<div class="col-lg-4">
<div class="detail-section h-100">
<div class="section-title">订单追踪</div>
<div class="timeline mt-4">
${tracksHtml || '<div class="text-muted small">暂无追踪信息</div>'}
</div>
</div>
</div>
</div>
`;
document.getElementById('orderDetailContent').innerHTML = content;
}
// 页面初始化
checkLogin().then(isLoggedIn => {
if (isLoggedIn) filterOrders('');
});
</script>
</body>
</html>

View File

@@ -0,0 +1,651 @@
<!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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">商家后台</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link active" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4">
<h2 id="pageTitle">新增商品</h2>
<form id="productForm" enctype="multipart/form-data">
<input type="hidden" id="productId">
<div class="card mb-3">
<div class="card-header">基本信息</div>
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">商品名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="mb-3">
<label for="category" class="form-label">分类</label>
<input type="text" class="form-control" id="category" placeholder="例如:奶茶、果茶">
</div>
<div class="mb-3">
<label for="description" class="form-label">商品描述</label>
<textarea class="form-control" id="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="basePrice" class="form-label">基础价格 <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="basePrice" step="0.01" min="0" required>
</div>
<div class="mb-3">
<label for="image" class="form-label">商品图片</label>
<input type="file" class="form-control" id="image" accept="image/*">
<div id="imagePreview" class="mt-2"></div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>规格管理</span>
<button type="button" class="btn btn-sm btn-primary" onclick="addSpec()">添加规格</button>
</div>
<div class="card-body">
<div id="specsList"></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>配料管理</span>
<button type="button" class="btn btn-sm btn-primary" onclick="addTopping()">添加配料</button>
</div>
<div class="card-body">
<div id="toppingsList"></div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>定制选项管理(甜度、冰度)</span>
<div>
<button type="button" class="btn btn-sm btn-primary" onclick="addCustom('sweetness')">添加甜度</button>
<button type="button" class="btn btn-sm btn-primary" onclick="addCustom('ice')">添加冰度</button>
</div>
</div>
<div class="card-body">
<div class="mb-3">
<h6>甜度选项</h6>
<div id="sweetnessList"></div>
</div>
<div class="mb-3">
<h6>冰度选项</h6>
<div id="iceList"></div>
</div>
</div>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">保存</button>
<a href="/merchant/products.html" class="btn btn-secondary">取消</a>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session,浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
const urlParams = new URLSearchParams(window.location.search);
const productId = urlParams.get('id');
let specs = [];
let toppings = [];
let customs = {
sweetness: [],
ice: []
};
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
if (productId) {
document.getElementById('pageTitle').textContent = '编辑商品';
loadProduct();
} else {
// 新商品时初始化默认选项
if (customs.sweetness.length === 0) {
customs.sweetness = [
{ optionType: 'sweetness', optionValue: '无糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '少糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '多糖', priceAdjust: 0 }
];
}
if (customs.ice.length === 0) {
customs.ice = [
{ optionType: 'ice', optionValue: '去冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '少冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '多冰', priceAdjust: 0 }
];
}
renderCustoms();
}
}
});
// 图片预览
document.getElementById('image').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('imagePreview').innerHTML =
`<img src="${e.target.result}" class="img-thumbnail" style="max-width: 200px;">`;
};
reader.readAsDataURL(file);
}
});
// 加载商品信息
async function loadProduct() {
try {
const response = await fetch(`/merchant/products/${productId}`, {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200) {
const product = data.data.product;
document.getElementById('productId').value = product.id;
document.getElementById('name').value = product.name || '';
document.getElementById('category').value = product.category || '';
document.getElementById('description').value = product.description || '';
document.getElementById('basePrice').value = product.basePrice || '';
if (product.image) {
document.getElementById('imagePreview').innerHTML =
`<img src="${product.image}" class="img-thumbnail" style="max-width: 200px;">`;
}
specs = data.data.specs || [];
toppings = data.data.toppings || [];
const allCustoms = data.data.customs || [];
// 分离甜度和冰度选项
customs.sweetness = allCustoms.filter(c => c.optionType === 'sweetness');
customs.ice = allCustoms.filter(c => c.optionType === 'ice');
// 如果没有甜度选项,创建默认选项
if (customs.sweetness.length === 0) {
customs.sweetness = [
{ optionType: 'sweetness', optionValue: '无糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '少糖', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'sweetness', optionValue: '多糖', priceAdjust: 0 }
];
}
// 如果没有冰度选项,创建默认选项
if (customs.ice.length === 0) {
customs.ice = [
{ optionType: 'ice', optionValue: '去冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '少冰', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '正常', priceAdjust: 0 },
{ optionType: 'ice', optionValue: '多冰', priceAdjust: 0 }
];
}
renderSpecs();
renderToppings();
renderCustoms();
} else {
if (data.message && data.message.includes('登录')) {
alert('请先登录');
window.location.href = '/merchant/login.html';
} else {
alert(data.message || '加载商品信息失败');
window.location.href = '/merchant/products.html';
}
}
} catch (error) {
console.error('Error:', error);
alert('加载商品信息失败');
window.location.href = '/merchant/products.html';
}
}
// 渲染规格列表
function renderSpecs() {
const container = document.getElementById('specsList');
if (specs.length === 0) {
container.innerHTML = '<p class="text-muted">暂无规格,点击"添加规格"按钮添加</p>';
return;
}
container.innerHTML = `
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>规格名称</th>
<th>价格调整</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${specs.map((spec, index) => `
<tr>
<td>
<input type="text" class="form-control form-control-sm"
value="${spec.specName || ''}"
onchange="updateSpec(${index}, 'specName', this.value)"
placeholder="例如:大杯、中杯、小杯">
</td>
<td>
<input type="number" class="form-control form-control-sm"
value="${spec.priceAdjust || 0}"
step="0.01"
onchange="updateSpec(${index}, 'priceAdjust', this.value)"
placeholder="价格调整">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="removeSpec(${index})">删除</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// 渲染配料列表
function renderToppings() {
const container = document.getElementById('toppingsList');
if (toppings.length === 0) {
container.innerHTML = '<p class="text-muted">暂无配料,点击"添加配料"按钮添加</p>';
return;
}
container.innerHTML = `
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>配料名称</th>
<th>价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${toppings.map((topping, index) => `
<tr>
<td>
<input type="text" class="form-control form-control-sm"
value="${topping.toppingName || ''}"
onchange="updateTopping(${index}, 'toppingName', this.value)"
placeholder="例如:珍珠、椰果、布丁">
</td>
<td>
<input type="number" class="form-control form-control-sm"
value="${topping.price || 0}"
step="0.01"
onchange="updateTopping(${index}, 'price', this.value)"
placeholder="价格">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="removeTopping(${index})">删除</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// 添加规格
function addSpec() {
specs.push({ specName: '', priceAdjust: 0 });
renderSpecs();
}
// 更新规格
function updateSpec(index, field, value) {
if (specs[index]) {
specs[index][field] = field === 'priceAdjust' ? parseFloat(value) || 0 : value;
}
}
// 删除规格
function removeSpec(index) {
if (confirm('确定要删除这个规格吗?')) {
specs.splice(index, 1);
renderSpecs();
}
}
// 添加配料
function addTopping() {
toppings.push({ toppingName: '', price: 0 });
renderToppings();
}
// 更新配料
function updateTopping(index, field, value) {
if (toppings[index]) {
toppings[index][field] = field === 'price' ? parseFloat(value) || 0 : value;
}
}
// 删除配料
function removeTopping(index) {
if (confirm('确定要删除这个配料吗?')) {
toppings.splice(index, 1);
renderToppings();
}
}
// 渲染定制选项
function renderCustoms() {
renderCustomType('sweetness');
renderCustomType('ice');
}
// 渲染特定类型的定制选项
function renderCustomType(type) {
const container = document.getElementById(type + 'List');
const options = customs[type] || [];
if (options.length === 0) {
container.innerHTML = '<p class="text-muted">暂无' + (type === 'sweetness' ? '甜度' : '冰度') + '选项,点击"添加' + (type === 'sweetness' ? '甜度' : '冰度') + '"按钮添加</p>';
return;
}
container.innerHTML = `
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>选项值</th>
<th>价格调整</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${options.map((custom, index) => `
<tr>
<td>
<input type="text" class="form-control form-control-sm"
value="${custom.optionValue || ''}"
onchange="updateCustom('${type}', ${index}, 'optionValue', this.value)"
placeholder="例如:${type === 'sweetness' ? '无糖、少糖、正常、多糖' : '去冰、少冰、正常、多冰'}">
</td>
<td>
<input type="number" class="form-control form-control-sm"
value="${custom.priceAdjust || 0}"
step="0.01"
onchange="updateCustom('${type}', ${index}, 'priceAdjust', this.value)"
placeholder="价格调整">
</td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="removeCustom('${type}', ${index})">删除</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// 添加定制选项
function addCustom(type) {
if (!customs[type]) {
customs[type] = [];
}
customs[type].push({
optionType: type,
optionValue: '',
priceAdjust: 0
});
renderCustomType(type);
}
// 更新定制选项
function updateCustom(type, index, field, value) {
if (customs[type] && customs[type][index]) {
customs[type][index][field] = field === 'priceAdjust' ? parseFloat(value) || 0 : value;
}
}
// 删除定制选项
function removeCustom(type, index) {
if (confirm('确定要删除这个选项吗?')) {
customs[type].splice(index, 1);
renderCustomType(type);
}
}
// 提交表单
document.getElementById('productForm').addEventListener('submit', async function(e) {
e.preventDefault();
// 表单验证
if (!UIEnhancements.validateForm('productForm')) {
UIEnhancements.showWarning('请填写所有必填项');
return;
}
const submitBtn = e.target.querySelector('button[type="submit"]');
const formData = new FormData();
formData.append('name', document.getElementById('name').value);
formData.append('category', document.getElementById('category').value);
formData.append('description', document.getElementById('description').value);
formData.append('basePrice', document.getElementById('basePrice').value);
const imageFile = document.getElementById('image').files[0];
if (imageFile) {
formData.append('image', imageFile);
}
const productIdValue = document.getElementById('productId').value;
const url = productIdValue ? `/merchant/products/${productIdValue}` : '/merchant/products';
const method = productIdValue ? 'PUT' : 'POST';
try {
UIEnhancements.setButtonLoading(submitBtn, true);
// 先保存商品基本信息
const response = await fetch(url, {
method: method,
credentials: 'include',
body: formData
});
const data = await response.json();
if (data.code === 200) {
const savedProductId = data.data.id || productIdValue;
// 保存规格、配料和定制选项(如果有商品ID)
if (savedProductId) {
await saveSpecsAndToppings(savedProductId);
await saveCustoms(savedProductId);
}
UIEnhancements.showSuccess('商品保存成功!');
setTimeout(() => {
window.location.href = '/merchant/products.html';
}, 1000);
} else {
if (data.message && data.message.includes('登录')) {
UIEnhancements.showWarning('请先登录');
setTimeout(() => {
window.location.href = '/merchant/login.html';
}, 1500);
} else {
UIEnhancements.showError(data.message || '保存失败');
}
UIEnhancements.setButtonLoading(submitBtn, false);
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请检查网络连接');
UIEnhancements.setButtonLoading(submitBtn, false);
}
});
// 保存规格和配料
async function saveSpecsAndToppings(productId) {
// 获取现有的规格和配料
const existingResponse = await fetch(`/merchant/products/${productId}`, {
credentials: 'include'
});
const existingData = await existingResponse.json();
const existingSpecs = existingData.code === 200 ? (existingData.data.specs || []) : [];
const existingToppings = existingData.code === 200 ? (existingData.data.toppings || []) : [];
// 删除所有现有规格和配料
for (const spec of existingSpecs) {
if (spec.id) {
await fetch(`/merchant/products/specs/${spec.id}`, {
method: 'DELETE',
credentials: 'include'
});
}
}
for (const topping of existingToppings) {
if (topping.id) {
await fetch(`/merchant/products/toppings/${topping.id}`, {
method: 'DELETE',
credentials: 'include'
});
}
}
// 添加新的规格
for (const spec of specs) {
if (spec.specName && spec.specName.trim()) {
await fetch(`/merchant/products/${productId}/specs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
specName: spec.specName,
priceAdjust: spec.priceAdjust || 0
})
});
}
}
// 添加新的配料
for (const topping of toppings) {
if (topping.toppingName && topping.toppingName.trim()) {
await fetch(`/merchant/products/${productId}/toppings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
toppingName: topping.toppingName,
price: topping.price || 0
})
});
}
}
}
// 保存定制选项
async function saveCustoms(productId) {
// 获取现有的定制选项
const existingResponse = await fetch(`/merchant/products/${productId}/customs`, {
credentials: 'include'
});
const existingData = await existingResponse.json();
const existingCustoms = existingData.code === 200 ? (existingData.data || []) : [];
// 删除所有现有定制选项
for (const custom of existingCustoms) {
if (custom.id) {
await fetch(`/merchant/products/customs/${custom.id}`, {
method: 'DELETE',
credentials: 'include'
});
}
}
// 添加新的甜度选项
for (const custom of customs.sweetness) {
if (custom.optionValue && custom.optionValue.trim()) {
await fetch(`/merchant/products/${productId}/customs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
optionType: 'sweetness',
optionValue: custom.optionValue,
priceAdjust: custom.priceAdjust || 0
})
});
}
}
// 添加新的冰度选项
for (const custom of customs.ice) {
if (custom.optionValue && custom.optionValue.trim()) {
await fetch(`/merchant/products/${productId}/customs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
optionType: 'ice',
optionValue: custom.optionValue,
priceAdjust: custom.priceAdjust || 0
})
});
}
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,470 @@
<!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>
.product-table-card {
transition: transform 0.2s;
}
.product-table-card:hover {
transform: translateX(5px);
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
color: #fff;
z-index: 1050;
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-brand {
font-size: 1.25rem;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.sidebar-nav .nav-link {
color: rgba(255, 255, 255, 0.8);
padding: 0.75rem 1.5rem;
transition: all 0.3s;
border-left: 3px solid transparent;
display: flex;
align-items: center;
text-decoration: none;
}
.sidebar-nav .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
border-left-color: #0d6efd;
}
.sidebar-nav .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
color: #fff;
border-left-color: #0d6efd;
font-weight: 600;
}
.sidebar-nav .nav-link i {
width: 24px;
font-size: 1.1rem;
margin-right: 0.75rem;
}
.sidebar-nav .nav-text {
flex: 1;
}
.sidebar-footer {
padding: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
/* 移动端样式 */
@media (max-width: 767.98px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1049;
display: none;
transition: opacity 0.3s;
}
.sidebar-overlay.show {
display: block;
}
.top-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%);
z-index: 1048;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
body {
padding-top: 56px;
}
}
/* 桌面端主内容区域 */
@media (min-width: 768px) {
.main-content {
margin-left: 260px;
min-height: 100vh;
position: relative;
z-index: 1;
width: calc(100% - 260px);
}
}
/* 滚动条样式 */
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.sidebar-nav::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<!-- 侧边栏导航 -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="d-flex align-items-center">
<i class="bi bi-shop fs-4 me-2"></i>
<span class="sidebar-brand">商家后台</span>
</div>
<button class="btn btn-link text-white p-0 d-md-none" id="sidebarCloseBtn">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/merchant/dashboard.html" data-page="dashboard">
<i class="bi bi-speedometer2"></i>
<span class="nav-text">首页</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/merchant/products.html" data-page="products">
<i class="bi bi-box-seam"></i>
<span class="nav-text">商品管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/orders.html" data-page="orders">
<i class="bi bi-receipt-cutoff"></i>
<span class="nav-text">订单管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/inventory.html" data-page="inventory">
<i class="bi bi-archive"></i>
<span class="nav-text">库存管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/statistics.html" data-page="statistics">
<i class="bi bi-bar-chart"></i>
<span class="nav-text">数据统计</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/announcements.html" data-page="announcements">
<i class="bi bi-megaphone"></i>
<span class="nav-text">公告管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/messages.html" data-page="messages">
<i class="bi bi-chat-dots"></i>
<span class="nav-text">留言管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/reviews.html" data-page="reviews">
<i class="bi bi-star"></i>
<span class="nav-text">评价管理</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/merchant/users.html" data-page="users">
<i class="bi bi-people"></i>
<span class="nav-text">用户管理</span>
</a>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<a href="/merchant/logout" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-right"></i>
<span class="ms-2">退出登录</span>
</a>
</div>
</div>
<!-- 侧边栏遮罩层(移动端) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- 顶部导航栏(移动端) -->
<nav class="top-navbar d-md-none">
<div class="container-fluid d-flex align-items-center">
<button class="btn btn-link text-white p-0" id="sidebarToggleBtn">
<i class="bi bi-list fs-4"></i>
</button>
<span class="ms-3 text-white fw-bold">商家后台</span>
</div>
</nav>
<div class="main-content">
<div class="container-fluid mt-4 mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-box-seam"></i> 商品管理</h2>
<button class="btn btn-primary" onclick="window.location.href='/merchant/product-edit.html'">
<i class="bi bi-plus-circle"></i> 新增商品
</button>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div id="productsList"></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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session,浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
async function loadProducts() {
const container = document.getElementById('productsList');
showLoading('productsList', '加载商品列表...');
try {
const { data } = await UIEnhancements.enhancedFetch('/merchant/products?pageNum=1&pageSize=20', {
credentials: 'include',
showLoading: 'productsList'
});
if (data.code === 200 && data.data && data.data.list) {
const products = data.data.list;
if (products.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无商品,请点击"新增商品"按钮添加商品</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>图片</th>
<th>商品名称</th>
<th>分类</th>
<th>价格</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${products.map(product => `
<tr class="product-table-card">
<td>
<img src="${product.image || '/static/images/default.jpg'}"
class="img-thumbnail rounded"
style="width: 80px; height: 80px; object-fit: cover;"
alt="${product.name}">
</td>
<td><strong>${product.name || ''}</strong></td>
<td><span class="badge bg-secondary">${product.category || '-'}</span></td>
<td><strong class="text-primary">¥${product.basePrice || '0.00'}</strong></td>
<td>
<span class="badge ${product.status === 1 ? 'bg-success' : 'bg-secondary'}">
${product.status === 1 ? '上架' : '下架'}
</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editProduct(${product.id})" title="编辑">
<i class="bi bi-pencil"></i>
</button>
${product.status === 1 ?
`<button class="btn btn-outline-warning" onclick="offlineProduct(${product.id})" title="下架">
<i class="bi bi-arrow-down-circle"></i>
</button>` :
`<button class="btn btn-outline-success" onclick="onlineProduct(${product.id})" title="上架">
<i class="bi bi-arrow-up-circle"></i>
</button>`
}
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
if (data.message && data.message.includes('登录')) {
alert('请先登录');
window.location.href = '/merchant/login.html';
} else {
container.innerHTML = '<div class="alert alert-warning">加载商品列表失败:' + (data.message || '未知错误') + '</div>';
}
}
} catch (error) {
console.error('Error:', error);
document.getElementById('productsList').innerHTML = '<div class="alert alert-danger">加载商品列表失败,请刷新页面重试</div>';
}
}
async function offlineProduct(productId) {
const confirmed = await UIEnhancements.confirmAction('确定要下架该商品吗?', '确认下架');
if (!confirmed) return;
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/products/${productId}/offline`, {
method: 'PUT',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('商品已下架');
loadProducts();
} else {
UIEnhancements.showError(data.message || '下架失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('操作失败,请稍后重试');
}
}
async function onlineProduct(productId) {
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/products/${productId}/online`, {
method: 'PUT',
credentials: 'include'
});
if (data.code === 200) {
UIEnhancements.showSuccess('商品已上架');
loadProducts();
} else {
UIEnhancements.showError(data.message || '上架失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('操作失败,请稍后重试');
}
}
function editProduct(productId) {
window.location.href = `/merchant/product-edit.html?id=${productId}`;
}
// 侧边栏交互逻辑
(function() {
const sidebar = document.getElementById('sidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarCloseBtn = document.getElementById('sidebarCloseBtn');
// 移动端切换侧边栏
if (sidebarToggleBtn) {
sidebarToggleBtn.addEventListener('click', function() {
sidebar.classList.add('show');
sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
});
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.remove('show');
sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
if (sidebarCloseBtn) {
sidebarCloseBtn.addEventListener('click', closeSidebar);
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener('click', closeSidebar);
}
// 点击导航项时,移动端自动关闭侧边栏
const navLinks = document.querySelectorAll('.sidebar-nav .nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (window.innerWidth < 768) {
closeSidebar();
}
});
});
})();
// 页面加载时检查登录
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadProducts();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,120 @@
<!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>
body {
background-color: #f5f5f5;
min-height: 100vh;
display: flex;
align-items: center;
}
.register-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-7">
<div class="card register-card shadow-lg">
<div class="card-header bg-dark text-white text-center py-4">
<h3 class="mb-0">
<i class="bi bi-shop"></i> 商家后台
</h3>
<p class="mb-0 mt-2">商家注册</p>
</div>
<div class="card-body p-4">
<form id="registerForm">
<div class="row">
<div class="col-md-6 mb-3">
<label for="username" class="form-label">
<i class="bi bi-person"></i> 用户名 <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="col-md-6 mb-3">
<label for="password" class="form-label">
<i class="bi bi-lock"></i> 密码 <span class="text-danger">*</span>
</label>
<input type="password" class="form-control" id="password" required>
</div>
</div>
<div class="mb-3">
<label for="storeName" class="form-label">
<i class="bi bi-shop-window"></i> 门店名称 <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="storeName" required>
</div>
<div class="mb-3">
<label for="address" class="form-label">
<i class="bi bi-geo-alt"></i> 门店地址
</label>
<input type="text" class="form-control" id="address">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">
<i class="bi bi-phone"></i> 联系电话
</label>
<input type="tel" class="form-control" id="phone">
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">
<i class="bi bi-envelope"></i> 邮箱
</label>
<input type="email" class="form-control" id="email">
</div>
</div>
<div class="mb-3">
<label for="managerName" class="form-label">
<i class="bi bi-person-badge"></i> 负责人姓名
</label>
<input type="text" class="form-control" id="managerName">
</div>
<button type="submit" class="btn btn-dark btn-lg w-100 mt-3">
<i class="bi bi-person-plus"></i> 注册
</button>
</form>
<div class="mt-4 text-center">
<a href="/merchant/login.html" class="text-decoration-none">已有账号?立即登录</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.getElementById('registerForm').addEventListener('submit', async function(e) {
e.preventDefault();
const response = await fetch('/merchant/register', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include', // 确保发送Cookie
body: JSON.stringify({
username: document.getElementById('username').value,
password: document.getElementById('password').value,
storeName: document.getElementById('storeName').value,
address: document.getElementById('address').value,
phone: document.getElementById('phone').value,
email: document.getElementById('email').value,
managerName: document.getElementById('managerName').value
})
});
const data = await response.json();
if (data.code === 200) {
alert('注册成功!');
window.location.href = '/merchant/login.html';
} else {
alert(data.message || '注册失败');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,212 @@
<!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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link active" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-star"></i> 评价管理</h2>
<div class="card shadow-sm">
<div class="card-body">
<div id="reviewsList"></div>
</div>
</div>
</div>
<!-- 回复评价模态框 -->
<div class="modal fade" id="replyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">回复评价</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">评价内容</label>
<div class="form-control" id="reviewContent" style="min-height: 60px; background-color: #f8f9fa;"></div>
</div>
<div class="mb-3">
<label for="replyContent" class="form-label">回复内容 <span class="text-danger">*</span></label>
<textarea class="form-control" id="replyContent" rows="4" required></textarea>
</div>
<input type="hidden" id="replyReviewId">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveReply()">保存回复</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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
// 加载评价列表
async function loadReviews() {
try {
const response = await fetch('/merchant/reviews', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('reviewsList');
if (data.code === 200 && data.data) {
const reviews = data.data || [];
if (reviews.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无评价</div>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>商品</th>
<th>评分</th>
<th>评价内容</th>
<th>评价时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${reviews.map(review => `
<tr>
<td>商品ID: ${review.productId || '-'}</td>
<td>
<div class="d-flex align-items-center">
${Array.from({length: 5}, (_, i) =>
`<i class="bi bi-star${i < (review.rating || 0) ? '-fill text-warning' : ''}"></i>`
).join('')}
<span class="ms-2">${review.rating || 0}分</span>
</div>
</td>
<td>${review.content || '-'}</td>
<td>${review.createTime ? new Date(review.createTime).toLocaleString('zh-CN') : '-'}</td>
<td>
${review.reply ?
`<button class="btn btn-sm btn-outline-primary" onclick="showReplyModal(${review.id}, '${(review.content || '').replace(/'/g, "\\'")}', '${(review.reply || '').replace(/'/g, "\\'")}')">
<i class="bi bi-pencil"></i> 查看/编辑
</button>` :
`<button class="btn btn-sm btn-primary" onclick="showReplyModal(${review.id}, '${(review.content || '').replace(/'/g, "\\'")}')">
<i class="bi bi-reply"></i> 回复
</button>`
}
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<div class="alert alert-warning">加载评价列表失败:' + (data.message || '未知错误') + '</div>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('reviewsList').innerHTML = '<div class="alert alert-danger">加载评价列表失败,请刷新页面重试</div>';
}
}
// 显示回复模态框
function showReplyModal(reviewId, content, reply = '') {
document.getElementById('replyReviewId').value = reviewId;
document.getElementById('reviewContent').textContent = content;
document.getElementById('replyContent').value = reply;
const modal = new bootstrap.Modal(document.getElementById('replyModal'));
modal.show();
}
// 保存回复
async function saveReply() {
const reviewId = document.getElementById('replyReviewId').value;
const reply = document.getElementById('replyContent').value;
if (!reply.trim()) {
UIEnhancements.showWarning('请输入回复内容');
return;
}
const saveBtn = document.querySelector('#replyModal .btn-primary');
try {
const { data } = await UIEnhancements.enhancedFetch(`/merchant/reviews/${reviewId}/reply`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
credentials: 'include',
body: `reply=${encodeURIComponent(reply)}`,
button: saveBtn
});
if (data.code === 200) {
UIEnhancements.showSuccess('回复已保存');
const modal = bootstrap.Modal.getInstance(document.getElementById('replyModal'));
modal.hide();
loadReviews();
} else {
UIEnhancements.showError(data.message || '保存失败');
}
} catch (error) {
console.error('Error:', error);
UIEnhancements.showError('保存失败,请稍后重试');
}
}
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadReviews();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,491 @@
<!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>
.filter-btn.active {
background-color: #0d6efd;
color: white;
border-color: #0d6efd;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">商家后台</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link active" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-graph-up"></i> 数据统计</h2>
<div class="mb-4">
<div class="btn-group" role="group">
<button class="btn btn-outline-primary filter-btn" onclick="loadStatistics('day')" id="filter-day">今日</button>
<button class="btn btn-outline-primary filter-btn" onclick="loadStatistics('week')" id="filter-week">本周</button>
<button class="btn btn-outline-primary filter-btn" onclick="loadStatistics('month')" id="filter-month">本月</button>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-cart-check"></i> 销售统计</h5>
</div>
<div class="card-body" id="salesStats">
<p class="text-muted">请选择时间段</p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-currency-yen"></i> 营收统计</h5>
</div>
<div class="card-body" id="revenueStats">
<p class="text-muted">请选择时间段</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-people"></i> 活跃用户</h5>
</div>
<div class="card-body" id="activeUsersStats">
<p class="text-muted">加载中...</p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-warning text-white">
<h5 class="mb-0"><i class="bi bi-star-fill"></i> 粘性用户</h5>
</div>
<div class="card-body" id="stickyUsersStats">
<p class="text-muted">加载中...</p>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-fire"></i> 热销商品</h5>
</div>
<div class="card-body">
<div id="hotProducts"></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/sidebar-replacer.js"></script>
<script src="/static/js/ui-enhancements.js"></script>
<script>
// 使用Session,浏览器自动发送Cookie
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let currentPeriod = 'day';
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadStatistics('day');
loadHotProducts();
loadActiveUsers();
loadStickyUsers();
// 激活"今日"按钮
document.getElementById('filter-day').classList.add('active');
}
});
// 加载统计数据
async function loadStatistics(period) {
currentPeriod = period;
// 更新按钮激活状态
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
const activeBtn = document.getElementById('filter-' + period);
if (activeBtn) activeBtn.classList.add('active');
try {
// 加载销售统计
const salesResponse = await fetch(`/merchant/statistics/sales?period=${period}`, {
credentials: 'include'
});
const salesData = await salesResponse.json();
if (salesData.code === 200 && salesData.data) {
const stats = salesData.data;
const totalAmount = parseFloat(stats.totalAmount || 0).toFixed(2);
const avgAmount = parseFloat(stats.avgOrderAmount || 0).toFixed(2);
document.getElementById('salesStats').innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总订单数</span>
<strong class="fs-4">${stats.totalOrders || 0}</strong>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总销售额</span>
<strong class="fs-4 text-primary">¥${totalAmount}</strong>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted">平均订单金额</span>
<strong>¥${avgAmount}</strong>
</div>
</div>
`;
} else {
document.getElementById('salesStats').innerHTML = '<p class="text-muted">加载失败:' + (salesData.message || '未知错误') + '</p>';
}
// 加载营收统计
const revenueResponse = await fetch(`/merchant/statistics/revenue?period=${period}`, {
credentials: 'include'
});
const revenueData = await revenueResponse.json();
if (revenueData.code === 200 && revenueData.data) {
const stats = revenueData.data;
const totalRevenue = parseFloat(stats.totalRevenue || 0).toFixed(2);
const totalCost = parseFloat(stats.totalCost || 0).toFixed(2);
const netProfit = parseFloat(stats.netProfit || 0).toFixed(2);
document.getElementById('revenueStats').innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总收入</span>
<strong class="fs-4 text-success">¥${totalRevenue}</strong>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总成本</span>
<strong>¥${totalCost}</strong>
</div>
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted">净利润</span>
<strong class="fs-5 text-primary">¥${netProfit}</strong>
</div>
</div>
`;
} else {
document.getElementById('revenueStats').innerHTML = '<p class="text-muted">加载失败:' + (revenueData.message || '未知错误') + '</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('salesStats').innerHTML = '<p class="text-danger">加载失败,请刷新重试</p>';
document.getElementById('revenueStats').innerHTML = '<p class="text-danger">加载失败,请刷新重试</p>';
}
}
// 加载热销商品
async function loadHotProducts() {
try {
const response = await fetch('/merchant/statistics/hot-products?limit=10', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('hotProducts');
if (data.code === 200 && data.data) {
const products = data.data || [];
if (products.length === 0) {
container.innerHTML = '<p class="text-muted">暂无数据</p>';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>排名</th>
<th>商品名称</th>
<th>销量</th>
<th>销售额</th>
</tr>
</thead>
<tbody>
${products.map((product, index) => `
<tr>
<td>
<span class="badge ${index < 3 ? 'bg-warning' : 'bg-secondary'}">${index + 1}</span>
</td>
<td><strong>${product.productName || '未知商品'}</strong></td>
<td><span class="badge bg-info">${product.salesCount || 0}</span></td>
<td><strong class="text-primary">¥${parseFloat(product.salesAmount || 0).toFixed(2)}</strong></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
} else {
container.innerHTML = '<p class="text-muted">加载失败</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('hotProducts').innerHTML = '<p class="text-muted">加载失败</p>';
}
}
// 加载活跃用户统计
async function loadActiveUsers() {
try {
const response = await fetch('/merchant/statistics/active-users?days=30', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('activeUsersStats');
if (data.code === 200 && data.data) {
const users = data.data || [];
const count = users.length;
container.innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">活跃用户数</span>
<strong class="fs-4 text-info">${count}</strong>
</div>
<div class="text-muted small mb-2">最近30天有浏览行为的用户</div>
${count > 0 ? `
<button class="btn btn-sm btn-outline-info w-100" onclick="showActiveUsersModal()">
<i class="bi bi-eye"></i> 查看详情
</button>
` : ''}
</div>
`;
} else {
container.innerHTML = '<p class="text-muted">加载失败</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('activeUsersStats').innerHTML = '<p class="text-muted">加载失败</p>';
}
}
// 加载粘性用户统计
async function loadStickyUsers() {
try {
const response = await fetch('/merchant/statistics/sticky-users?days=30&minOrders=5', {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('stickyUsersStats');
if (data.code === 200 && data.data) {
const users = data.data || [];
const count = users.length;
const totalAmount = users.reduce((sum, user) => sum + parseFloat(user.totalAmount || 0), 0).toFixed(2);
container.innerHTML = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">粘性用户数</span>
<strong class="fs-4 text-warning">${count}</strong>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">总消费金额</span>
<strong>¥${totalAmount}</strong>
</div>
<div class="text-muted small mb-2">最近30天下单5次以上</div>
${count > 0 ? `
<button class="btn btn-sm btn-outline-warning w-100" onclick="showStickyUsersModal()">
<i class="bi bi-eye"></i> 查看详情
</button>
` : ''}
</div>
`;
} else {
container.innerHTML = '<p class="text-muted">加载失败</p>';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('stickyUsersStats').innerHTML = '<p class="text-muted">加载失败</p>';
}
}
// 显示活跃用户列表模态框
async function showActiveUsersModal() {
try {
const response = await fetch('/merchant/statistics/active-users?days=30', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const users = data.data || [];
let modalContent = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>用户名</th>
<th>姓名</th>
<th>浏览次数</th>
<th>最后浏览时间</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
const lastViewTime = user.lastViewTime ? new Date(user.lastViewTime).toLocaleString('zh-CN') : '-';
modalContent += `
<tr>
<td>${user.username || '-'}</td>
<td>${user.name || '-'}</td>
<td><span class="badge bg-info">${user.viewCount || 0}</span></td>
<td>${lastViewTime}</td>
</tr>
`;
});
modalContent += `
</tbody>
</table>
</div>
`;
showModal('活跃用户列表', modalContent);
}
} catch (error) {
console.error('Error:', error);
alert('加载活跃用户列表失败');
}
}
// 显示粘性用户列表模态框
async function showStickyUsersModal() {
try {
const response = await fetch('/merchant/statistics/sticky-users?days=30&minOrders=5', {
credentials: 'include'
});
const data = await response.json();
if (data.code === 200 && data.data) {
const users = data.data || [];
let modalContent = `
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>用户名</th>
<th>姓名</th>
<th>订单数</th>
<th>总消费金额</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
const totalAmount = parseFloat(user.totalAmount || 0).toFixed(2);
modalContent += `
<tr>
<td>${user.username || '-'}</td>
<td>${user.name || '-'}</td>
<td><span class="badge bg-warning">${user.orderCount || 0}</span></td>
<td><strong class="text-primary">¥${totalAmount}</strong></td>
</tr>
`;
});
modalContent += `
</tbody>
</table>
</div>
`;
showModal('粘性用户列表', modalContent);
}
} catch (error) {
console.error('Error:', error);
alert('加载粘性用户列表失败');
}
}
// 显示模态框的通用函数
function showModal(title, content) {
const modalHtml = `
<div class="modal fade" id="userListModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
${content}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
`;
// 移除旧的模态框
const oldModal = document.getElementById('userListModal');
if (oldModal) {
oldModal.remove();
}
// 添加新的模态框
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('userListModal'));
modal.show();
// 模态框关闭后移除
document.getElementById('userListModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,163 @@
<!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">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/merchant/dashboard.html">
<i class="bi bi-shop"></i> 商家后台
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/merchant/dashboard.html">首页</a>
<a class="nav-link" href="/merchant/products.html">商品管理</a>
<a class="nav-link" href="/merchant/orders.html">订单管理</a>
<a class="nav-link" href="/merchant/inventory.html">库存管理</a>
<a class="nav-link" href="/merchant/statistics.html">数据统计</a>
<a class="nav-link" href="/merchant/announcements.html">公告管理</a>
<a class="nav-link" href="/merchant/messages.html">留言管理</a>
<a class="nav-link" href="/merchant/reviews.html">评价管理</a>
<a class="nav-link active" href="/merchant/users.html">用户管理</a>
</div>
</div>
</nav>
<div class="container mt-4 mb-5">
<h2 class="mb-4"><i class="bi bi-people"></i> 用户管理</h2>
<div class="card shadow-sm">
<div class="card-body">
<div id="usersList"></div>
<nav aria-label="用户列表分页">
<ul class="pagination justify-content-center" id="pagination"></ul>
</nav>
</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/sidebar-replacer.js"></script>
<script>
// 检查登录状态
async function checkLogin() {
try {
const response = await fetch('/merchant/profile', {
credentials: 'include'
});
const data = await response.json();
if (data.code !== 200 || !data.data) {
window.location.href = '/merchant/login.html';
return false;
}
return true;
} catch (error) {
console.error('Error:', error);
window.location.href = '/merchant/login.html';
return false;
}
}
let currentPage = 1;
const pageSize = 10;
// 页面加载
checkLogin().then(isLoggedIn => {
if (isLoggedIn) {
loadUsers(1);
}
});
// 加载用户列表
async function loadUsers(pageNum) {
try {
currentPage = pageNum;
const response = await fetch(`/merchant/users?pageNum=${pageNum}&pageSize=${pageSize}`, {
credentials: 'include'
});
const data = await response.json();
const container = document.getElementById('usersList');
const pagination = document.getElementById('pagination');
if (data.code === 200 && data.data) {
const pageResponse = data.data;
const users = pageResponse.list || [];
if (users.length === 0) {
container.innerHTML = '<div class="alert alert-info">暂无用户</div>';
pagination.innerHTML = '';
return;
}
container.innerHTML = `
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>用户名</th>
<th>姓名</th>
<th>手机号</th>
<th>邮箱</th>
<th>注册时间</th>
</tr>
</thead>
<tbody>
${users.map(user => `
<tr>
<td><strong>${user.username || '-'}</strong></td>
<td>${user.name || '-'}</td>
<td>${user.phone || '-'}</td>
<td>${user.email || '-'}</td>
<td>${user.createTime ? new Date(user.createTime).toLocaleString('zh-CN') : '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
// 生成分页
const totalPages = pageResponse.pages || 1;
let paginationHtml = '';
// 上一页
paginationHtml += `<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadUsers(${currentPage - 1})">上一页</a>
</li>`;
// 页码
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
paginationHtml += `<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadUsers(${i})">${i}</a>
</li>`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
paginationHtml += `<li class="page-item disabled">
<span class="page-link">...</span>
</li>`;
}
}
// 下一页
paginationHtml += `<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="javascript:void(0)" onclick="loadUsers(${currentPage + 1})">下一页</a>
</li>`;
pagination.innerHTML = paginationHtml;
} else {
container.innerHTML = '<div class="alert alert-warning">加载用户列表失败:' + (data.message || '未知错误') + '</div>';
pagination.innerHTML = '';
}
} catch (error) {
console.error('Error:', error);
document.getElementById('usersList').innerHTML = '<div class="alert alert-danger">加载用户列表失败,请刷新页面重试</div>';
document.getElementById('pagination').innerHTML = '';
}
}
</script>
</body>
</html>