初始化v1
This commit is contained in:
291
target/classes/templates/merchant/announcements.html
Normal file
291
target/classes/templates/merchant/announcements.html
Normal 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>
|
||||
283
target/classes/templates/merchant/common/sidebar.html
Normal file
283
target/classes/templates/merchant/common/sidebar.html
Normal 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>
|
||||
413
target/classes/templates/merchant/dashboard.html
Normal file
413
target/classes/templates/merchant/dashboard.html
Normal 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>
|
||||
466
target/classes/templates/merchant/inventory.html
Normal file
466
target/classes/templates/merchant/inventory.html
Normal 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>
|
||||
81
target/classes/templates/merchant/login.html
Normal file
81
target/classes/templates/merchant/login.html
Normal 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>
|
||||
209
target/classes/templates/merchant/messages.html
Normal file
209
target/classes/templates/merchant/messages.html
Normal 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>
|
||||
503
target/classes/templates/merchant/orders.html
Normal file
503
target/classes/templates/merchant/orders.html
Normal 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>
|
||||
651
target/classes/templates/merchant/product-edit.html
Normal file
651
target/classes/templates/merchant/product-edit.html
Normal 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>
|
||||
470
target/classes/templates/merchant/products.html
Normal file
470
target/classes/templates/merchant/products.html
Normal 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>
|
||||
120
target/classes/templates/merchant/register.html
Normal file
120
target/classes/templates/merchant/register.html
Normal 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>
|
||||
212
target/classes/templates/merchant/reviews.html
Normal file
212
target/classes/templates/merchant/reviews.html
Normal 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>
|
||||
491
target/classes/templates/merchant/statistics.html
Normal file
491
target/classes/templates/merchant/statistics.html
Normal 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>
|
||||
163
target/classes/templates/merchant/users.html
Normal file
163
target/classes/templates/merchant/users.html
Normal 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>
|
||||
Reference in New Issue
Block a user