后台完成修复,初始化项目
This commit is contained in:
891
src/main/webapp/WEB-INF/views/admin/flashsales.jsp
Normal file
891
src/main/webapp/WEB-INF/views/admin/flashsales.jsp
Normal file
@@ -0,0 +1,891 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="秒杀管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">秒杀管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#addFlashSaleModal">
|
||||
<i class="fas fa-plus"></i> 创建秒杀
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshFlashSales()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索秒杀活动...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchFlashSales()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterFlashSales()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">未开始</option>
|
||||
<option value="active">进行中</option>
|
||||
<option value="ended">已结束</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortFlashSales()">
|
||||
<option value="startTime">按开始时间</option>
|
||||
<option value="endTime">按结束时间</option>
|
||||
<option value="createdAt">按创建时间</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 秒杀活动列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>活动名称</th>
|
||||
<th>商品</th>
|
||||
<th>原价/秒杀价</th>
|
||||
<th>库存</th>
|
||||
<th>开始时间</th>
|
||||
<th>结束时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="flashSalesTableBody">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="秒杀分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建秒杀模态框 -->
|
||||
<div class="modal fade" id="addFlashSaleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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">
|
||||
<form id="addFlashSaleForm">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="mb-3">
|
||||
<label for="productSelect" class="form-label">选择商品 *</label>
|
||||
<select class="form-select" id="productSelect" required>
|
||||
<option value="">请选择商品</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="flashSalePrice" class="form-label">秒杀价格 *</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">¥</span>
|
||||
<input type="number" class="form-control" id="flashSalePrice" step="0.01" min="0"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="flashSaleStock" class="form-label">秒杀库存 *</label>
|
||||
<input type="number" class="form-control" id="flashSaleStock" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="startTime" class="form-label">开始时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="startTime" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="endTime" class="form-label">结束时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="endTime" required>
|
||||
</div>
|
||||
</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="saveFlashSale()">创建活动</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看详情模态框 -->
|
||||
<div class="modal fade" id="viewFlashSaleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">活动ID</label>
|
||||
<p id="viewFlashSaleId" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">商品名称</label>
|
||||
<p id="viewProductName" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">原价</label>
|
||||
<p id="viewOriginalPrice" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">秒杀价</label>
|
||||
<p id="viewFlashPrice" class="form-control-plaintext text-danger fw-bold">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">总库存</label>
|
||||
<p id="viewFlashStock" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">剩余库存</label>
|
||||
<p id="viewRemainingStock" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">开始时间</label>
|
||||
<p id="viewStartTime" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">结束时间</label>
|
||||
<p id="viewEndTime" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">活动状态</label>
|
||||
<p id="viewStatus" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">创建时间</label>
|
||||
<p id="viewCreatedAt" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑秒杀模态框 -->
|
||||
<div class="modal fade" id="editFlashSaleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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">
|
||||
<form id="editFlashSaleForm">
|
||||
<input type="hidden" id="editFlashSaleId">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editFlashPrice" class="form-label">秒杀价格 *</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">¥</span>
|
||||
<input type="number" class="form-control" id="editFlashPrice" step="0.01" min="0"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editFlashStock" class="form-label">秒杀库存 *</label>
|
||||
<input type="number" class="form-control" id="editFlashStock" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editStartTime" class="form-label">开始时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="editStartTime" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="editEndTime" class="form-label">结束时间 *</label>
|
||||
<input type="datetime-local" class="form-control" id="editEndTime" required>
|
||||
</div>
|
||||
</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="saveEditFlashSale()">保存修改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-ended {
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadFlashSales();
|
||||
loadProducts();
|
||||
|
||||
// 设置默认时间
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
$('#startTime').val(formatDateTime(now));
|
||||
$('#endTime').val(formatDateTime(tomorrow));
|
||||
});
|
||||
|
||||
function formatDateTime(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return year + '-' + month + '-' + day + 'T' + hours + ':' + minutes;
|
||||
}
|
||||
|
||||
function loadProducts() {
|
||||
// 获取商品列表用于下拉框
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/admin/products',
|
||||
type: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
size: 100, // 获取足够多的商品
|
||||
status: 1 // 只获取上架的商品
|
||||
},
|
||||
success: function (response) {
|
||||
if (response.success && response.data) {
|
||||
const products = response.data.content || response.data.products || [];
|
||||
let options = '<option value="">请选择商品</option>';
|
||||
|
||||
products.forEach(product => {
|
||||
options += `<option value="${product.id}">${product.name} - ¥${product.price}</option>`;
|
||||
});
|
||||
|
||||
$('#productSelect').html(options);
|
||||
} else {
|
||||
console.error('获取商品列表失败:', response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('获取商品列表失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadFlashSales(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建查询参数
|
||||
const queryData = {
|
||||
page: page - 1, // 后端使用0基索引
|
||||
size: pageSize,
|
||||
sortBy: $('#sortBy').val() || 'startTime',
|
||||
sortDirection: 'desc'
|
||||
};
|
||||
|
||||
// 添加状态筛选
|
||||
const statusFilter = $('#statusFilter').val();
|
||||
if (statusFilter) {
|
||||
// 将前端状态值转换为后端状态值
|
||||
switch (statusFilter) {
|
||||
case 'pending':
|
||||
queryData.status = 1; // 未开始
|
||||
break;
|
||||
case 'active':
|
||||
queryData.status = 2; // 进行中
|
||||
break;
|
||||
case 'ended':
|
||||
queryData.status = 3; // 已结束
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/list',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(queryData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
renderFlashSalesTable(response.data.content || response.data.flashSales || []);
|
||||
renderPagination(response.data.totalElements || response.data.total || 0, pageSize);
|
||||
} else {
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center text-danger">获取秒杀数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('获取秒杀列表失败:', error);
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderFlashSalesTable(flashSales) {
|
||||
let html = '';
|
||||
|
||||
if (flashSales.length === 0) {
|
||||
html = '<tr><td colspan="9" class="text-center">暂无秒杀活动</td></tr>';
|
||||
} else {
|
||||
flashSales.forEach(flashSale => {
|
||||
const statusText = getStatusText(flashSale.status);
|
||||
const statusClass = getStatusClass(flashSale.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + flashSale.id + `</td>
|
||||
<td>
|
||||
<div class="fw-bold">` + (flashSale.productName || '秒杀活动') + `</div>
|
||||
<small class="text-muted">` + (flashSale.statusDescription || '') + `</small>
|
||||
</td>
|
||||
<td>` + (flashSale.productName || '-') + `</td>
|
||||
<td>
|
||||
<div>原价: ¥` + (flashSale.originalPrice ? flashSale.originalPrice.toFixed(2) : '0.00') + `</div>
|
||||
<div class="text-danger fw-bold">秒杀: ¥` + (flashSale.flashPrice ? flashSale.flashPrice.toFixed(2) : '0.00') + `</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge ` + (flashSale.remainingStock > 0 ? 'bg-success' : 'bg-danger') + `">
|
||||
` + (flashSale.remainingStock || 0) + ` / ` + (flashSale.flashStock || 0) + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(flashSale.startTime) + `</td>
|
||||
<td>` + formatDateTime(flashSale.endTime) + `</td>
|
||||
<td>
|
||||
<span class="badge ` + statusClass + `">
|
||||
` + statusText + `
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="editFlashSale(` + flashSale.id + `)" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="deleteFlashSale(` + flashSale.id + `)" title="删除">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="viewFlashSale(` + flashSale.id + `)" title="查看">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#flashSalesTableBody').html(html);
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '未开始';
|
||||
case 2:
|
||||
return '进行中';
|
||||
case 3:
|
||||
return '已结束';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 未开始
|
||||
case 2:
|
||||
return 'bg-success'; // 进行中
|
||||
case 3:
|
||||
return 'bg-secondary'; // 已结束
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('#pagination').html('');
|
||||
return;
|
||||
}
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadFlashSales(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshFlashSales() {
|
||||
$('#flashSalesTableBody').html('<tr><td colspan="9" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadFlashSales(currentPage);
|
||||
}
|
||||
|
||||
function searchFlashSales() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索秒杀活动:', keyword);
|
||||
// 重置到第一页并重新加载
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
function filterFlashSales() {
|
||||
const status = $('#statusFilter').val();
|
||||
console.log('筛选状态:', status);
|
||||
// 重置到第一页并重新加载
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
function sortFlashSales() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
// 重置到第一页并重新加载
|
||||
loadFlashSales(1);
|
||||
}
|
||||
|
||||
function saveFlashSale() {
|
||||
// 验证表单
|
||||
if (!$('#productSelect').val()) {
|
||||
alert('请选择商品');
|
||||
return;
|
||||
}
|
||||
if (!$('#flashSalePrice').val()) {
|
||||
alert('请输入秒杀价格');
|
||||
return;
|
||||
}
|
||||
if (!$('#flashSaleStock').val()) {
|
||||
alert('请输入秒杀库存');
|
||||
return;
|
||||
}
|
||||
if (!$('#startTime').val() || !$('#endTime').val()) {
|
||||
alert('请选择开始和结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
const flashSaleData = {
|
||||
productId: parseInt($('#productSelect').val()),
|
||||
flashPrice: parseFloat($('#flashSalePrice').val()),
|
||||
flashStock: parseInt($('#flashSaleStock').val()),
|
||||
startTime: $('#startTime').val().replace('T', ' ') + ':00',
|
||||
endTime: $('#endTime').val().replace('T', ' ') + ':00'
|
||||
};
|
||||
|
||||
console.log('创建秒杀活动:', flashSaleData);
|
||||
|
||||
// 调用真实API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/create',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(flashSaleData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#addFlashSaleModal').modal('hide');
|
||||
$('#addFlashSaleForm')[0].reset();
|
||||
alert('秒杀活动创建成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('创建失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
console.error('创建秒杀活动失败:', error);
|
||||
alert('创建失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editFlashSale(id) {
|
||||
console.log('编辑秒杀活动:', id);
|
||||
|
||||
// 获取秒杀活动详情
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
const flashSale = response.data;
|
||||
|
||||
// 检查是否可以编辑(只有未开始的活动才能编辑)
|
||||
if (flashSale.status !== 1) {
|
||||
alert('只有未开始的秒杀活动才能编辑');
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充编辑表单
|
||||
$('#editFlashSaleId').val(flashSale.id);
|
||||
$('#editFlashPrice').val(flashSale.flashPrice);
|
||||
$('#editFlashStock').val(flashSale.flashStock);
|
||||
$('#editStartTime').val(formatDateTimeForInput(flashSale.startTime));
|
||||
$('#editEndTime').val(formatDateTimeForInput(flashSale.endTime));
|
||||
|
||||
// 显示编辑模态框
|
||||
$('#editFlashSaleModal').modal('show');
|
||||
} else {
|
||||
alert('获取秒杀活动详情失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert('获取秒杀活动详情失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFlashSale(id) {
|
||||
if (confirm('确定要删除这个秒杀活动吗?此操作不可恢复。')) {
|
||||
console.log('删除秒杀活动:', id);
|
||||
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id,
|
||||
type: 'DELETE',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
alert('秒杀活动删除成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('删除失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '删除失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function viewFlashSale(id) {
|
||||
console.log('查看秒杀详情:', id);
|
||||
|
||||
// 获取秒杀活动详情
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + id,
|
||||
type: 'GET',
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
const flashSale = response.data;
|
||||
|
||||
// 填充详情模态框
|
||||
$('#viewFlashSaleId').text(flashSale.id);
|
||||
$('#viewProductName').text(flashSale.productName || '-');
|
||||
$('#viewOriginalPrice').text(flashSale.originalPrice ? '¥' + flashSale.originalPrice.toFixed(2) : '-');
|
||||
$('#viewFlashPrice').text(flashSale.flashPrice ? '¥' + flashSale.flashPrice.toFixed(2) : '-');
|
||||
$('#viewFlashStock').text(flashSale.flashStock || '-');
|
||||
$('#viewRemainingStock').text(flashSale.remainingStock || '-');
|
||||
$('#viewStartTime').text(formatDateTime(flashSale.startTime));
|
||||
$('#viewEndTime').text(formatDateTime(flashSale.endTime));
|
||||
$('#viewStatus').html('<span class="badge ' + getStatusClass(flashSale.status) + '">' + getStatusText(flashSale.status) + '</span>');
|
||||
$('#viewCreatedAt').text(formatDateTime(flashSale.createdAt));
|
||||
|
||||
// 显示详情模态框
|
||||
$('#viewFlashSaleModal').modal('show');
|
||||
} else {
|
||||
alert('获取秒杀活动详情失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert('获取秒杀活动详情失败,请稍后重试');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 保存编辑的秒杀活动
|
||||
function saveEditFlashSale() {
|
||||
// 验证表单
|
||||
if (!$('#editFlashPrice').val()) {
|
||||
alert('请输入秒杀价格');
|
||||
return;
|
||||
}
|
||||
if (!$('#editFlashStock').val()) {
|
||||
alert('请输入秒杀库存');
|
||||
return;
|
||||
}
|
||||
if (!$('#editStartTime').val() || !$('#editEndTime').val()) {
|
||||
alert('请选择开始和结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
const flashSaleId = $('#editFlashSaleId').val();
|
||||
const updateData = {
|
||||
flashPrice: parseFloat($('#editFlashPrice').val()),
|
||||
flashStock: parseInt($('#editFlashStock').val()),
|
||||
startTime: $('#editStartTime').val().replace('T', ' ') + ':00',
|
||||
endTime: $('#editEndTime').val().replace('T', ' ') + ':00'
|
||||
};
|
||||
|
||||
// 验证时间
|
||||
const startTime = new Date($('#editStartTime').val());
|
||||
const endTime = new Date($('#editEndTime').val());
|
||||
const now = new Date();
|
||||
|
||||
if (startTime < now) {
|
||||
alert('开始时间不能早于当前时间');
|
||||
return;
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
alert('开始时间不能晚于或等于结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('更新秒杀活动:', updateData);
|
||||
|
||||
// 调用更新API
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/' + flashSaleId,
|
||||
type: 'PUT',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(updateData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
$('#editFlashSaleModal').modal('hide');
|
||||
alert('秒杀活动更新成功!');
|
||||
refreshFlashSales();
|
||||
} else {
|
||||
alert('更新失败: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
let errorMessage = '更新失败,请稍后重试';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatDateTime(dateTimeStr) {
|
||||
if (!dateTimeStr) return '-';
|
||||
|
||||
// 如果是ISO格式,转换为本地时间显示
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateTimeStr; // 如果转换失败,返回原字符串
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期时间用于input[type="datetime-local"]
|
||||
function formatDateTimeForInput(dateTimeStr) {
|
||||
if (!dateTimeStr) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateTimeStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return year + '-' + month + '-' + day + 'T' + hours + ':' + minutes;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
468
src/main/webapp/WEB-INF/views/admin/index.jsp
Normal file
468
src/main/webapp/WEB-INF/views/admin/index.jsp
Normal file
@@ -0,0 +1,468 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="管理后台"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">管理后台仪表盘</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshData()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新数据
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||
总用户数
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="totalUsers">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-users fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
||||
总商品数
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="totalProducts">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-box fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||
活跃秒杀
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="activeFlashSales">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-bolt fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-warning shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||
今日订单
|
||||
</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="todayOrders">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-shopping-cart fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-rocket"></i> 快速操作</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/products"
|
||||
class="btn btn-primary btn-block">
|
||||
<i class="fas fa-plus"></i> 添加商品
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/flashsales"
|
||||
class="btn btn-success btn-block">
|
||||
<i class="fas fa-bolt"></i> 创建秒杀
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/orders"
|
||||
class="btn btn-info btn-block">
|
||||
<i class="fas fa-list"></i> 查看订单
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="${pageContext.request.contextPath}/admin/monitor"
|
||||
class="btn btn-warning btn-block">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近活动 -->
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-clock"></i> 最近订单</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>用户</th>
|
||||
<th>商品</th>
|
||||
<th>金额</th>
|
||||
<th>状态</th>
|
||||
<th>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recentOrders">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-fire"></i> 热门商品</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="hotProducts">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.border-left-primary {
|
||||
border-left: 0.25rem solid #4e73df !important;
|
||||
}
|
||||
|
||||
.border-left-success {
|
||||
border-left: 0.25rem solid #1cc88a !important;
|
||||
}
|
||||
|
||||
.border-left-info {
|
||||
border-left: 0.25rem solid #36b9cc !important;
|
||||
}
|
||||
|
||||
.border-left-warning {
|
||||
border-left: 0.25rem solid #f6c23e !important;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
loadDashboardData();
|
||||
});
|
||||
|
||||
function loadDashboardData() {
|
||||
// 加载统计数据
|
||||
loadStatistics();
|
||||
|
||||
// 加载最近订单
|
||||
loadRecentOrders();
|
||||
|
||||
// 加载热门商品
|
||||
loadHotProducts();
|
||||
}
|
||||
|
||||
function loadStatistics() {
|
||||
// 调用真实API获取统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/dashboard/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateDashboardStats(response.data);
|
||||
} else {
|
||||
console.error('获取仪表盘数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateDashboardStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取仪表盘数据请求失败');
|
||||
// 显示默认值
|
||||
updateDashboardStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新仪表盘统计数据
|
||||
function updateDashboardStats(stats) {
|
||||
$('#totalUsers').text(formatNumber(stats.totalUsers || 0));
|
||||
$('#totalProducts').text(formatNumber(stats.totalProducts || 0));
|
||||
$('#activeFlashSales').text(formatNumber(stats.activeFlashSales || 0));
|
||||
$('#todayOrders').text(formatNumber(stats.todayOrders || 0));
|
||||
|
||||
// 更新订单统计卡片
|
||||
$('#totalOrders').text(formatNumber(stats.totalOrders || 0));
|
||||
$('#paidOrders').text(formatNumber(stats.paidOrders || 0));
|
||||
$('#pendingOrders').text(formatNumber(stats.pendingOrders || 0));
|
||||
$('#totalAmount').text('¥' + formatNumber(stats.totalAmount || 0));
|
||||
}
|
||||
|
||||
function loadRecentOrders() {
|
||||
// 调用真实API获取最近订单
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders/recent?limit=10')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateRecentOrders(response.data);
|
||||
} else {
|
||||
console.error('获取最近订单失败:', response.message);
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center">获取订单数据失败</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取最近订单请求失败');
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center">网络请求失败</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新最近订单列表
|
||||
function updateRecentOrders(orders) {
|
||||
let html = '';
|
||||
if (orders && orders.length > 0) {
|
||||
orders.forEach(function (order) {
|
||||
let statusClass = getOrderStatusClass(order.status);
|
||||
let statusText = getOrderStatusText(order.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + order.id + `</td>
|
||||
<td>` + order.username + `</td>
|
||||
<td>` + order.productName + `</td>
|
||||
<td>¥` + formatNumber(order.totalAmount) + `</td>
|
||||
<td><span class="badge ` + statusClass + `">` + statusText + `</span></td>
|
||||
<td>` + formatDateTime(order.createdAt) + `</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<tr><td colspan="6" class="text-center">暂无订单数据</td></tr>';
|
||||
}
|
||||
$('#recentOrders').html(html);
|
||||
}
|
||||
|
||||
function loadHotProducts() {
|
||||
// 模拟数据,实际应该调用API
|
||||
setTimeout(function () {
|
||||
const products = [
|
||||
{name: 'iPhone 15 Pro Max', sales: 156},
|
||||
{name: 'MacBook Pro 16英寸', sales: 89},
|
||||
{name: 'iPad Air', sales: 67},
|
||||
{name: 'AirPods Pro 2', sales: 234}
|
||||
];
|
||||
|
||||
let html = '';
|
||||
products.forEach((product, index) => {
|
||||
html += `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<small class="text-muted">#${index + 1}</small>
|
||||
<span class="ms-2">${product.name}</span>
|
||||
</div>
|
||||
<span class="badge bg-success">${product.sales}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#hotProducts').html(html);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
// 显示加载状态
|
||||
$('#totalUsers, #totalProducts, #activeFlashSales, #todayOrders').html('<i class="fas fa-spinner fa-spin"></i>');
|
||||
$('#recentOrders').html('<tr><td colspan="6" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#hotProducts').html('<div class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</div>');
|
||||
|
||||
// 重新加载数据
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 待支付
|
||||
case 2:
|
||||
return 'bg-success'; // 已支付
|
||||
case 3:
|
||||
return 'bg-info'; // 已发货
|
||||
case 4:
|
||||
return 'bg-primary'; // 已完成
|
||||
case 5:
|
||||
return 'bg-danger'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
533
src/main/webapp/WEB-INF/views/admin/monitor.jsp
Normal file
533
src/main/webapp/WEB-INF/views/admin/monitor.jsp
Normal file
@@ -0,0 +1,533 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="系统监控"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">系统监控</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshMonitorData()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新数据
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportReport()">
|
||||
<i class="fas fa-download"></i> 导出报告
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态概览 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="systemStatus">正常</h5>
|
||||
<p class="card-text">系统状态</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="cpuUsage">0%</h5>
|
||||
<p class="card-text">CPU使用率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="memoryUsage">0%</h5>
|
||||
<p class="card-text">内存使用率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="diskUsage">0%</h5>
|
||||
<p class="card-text">磁盘使用率</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis监控 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-database"></i> Redis集群状态</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>节点</th>
|
||||
<th>状态</th>
|
||||
<th>内存使用</th>
|
||||
<th>连接数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="redisNodesTable">
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-bar"></i> 数据库连接池</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-success" id="activeConnections">0</h4>
|
||||
<small>活跃连接</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-info" id="idleConnections">0</h4>
|
||||
<small>空闲连接</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-warning" id="maxConnections">0</h4>
|
||||
<small>最大连接</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<h4 class="text-primary" id="totalConnections">0</h4>
|
||||
<small>总连接数</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 接口性能监控 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-tachometer-alt"></i> 接口性能监控</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>接口路径</th>
|
||||
<th>请求次数</th>
|
||||
<th>平均响应时间</th>
|
||||
<th>成功率</th>
|
||||
<th>最后调用</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="apiPerformanceTable">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误日志 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-exclamation-triangle"></i> 最近错误日志</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>级别</th>
|
||||
<th>模块</th>
|
||||
<th>错误信息</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="errorLogsTable">
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时监控图表 -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-line"></i> CPU & 内存使用趋势</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="systemChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-area"></i> 请求量统计</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="requestChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
loadMonitorData();
|
||||
|
||||
// 每2分钟自动刷新数据(降低频率)
|
||||
setInterval(function () {
|
||||
loadMonitorData();
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
function loadMonitorData() {
|
||||
loadSystemStatus();
|
||||
loadRedisStatus();
|
||||
loadDatabaseStatus();
|
||||
loadApiPerformance();
|
||||
loadErrorLogs();
|
||||
updateCharts();
|
||||
}
|
||||
|
||||
function loadSystemStatus() {
|
||||
// 调用真实API获取系统状态
|
||||
$.get('${pageContext.request.contextPath}/api/admin/monitor/system')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateSystemStatus(response.data);
|
||||
} else {
|
||||
console.error('获取系统状态失败:', response.message);
|
||||
// 显示默认状态
|
||||
updateSystemStatus({
|
||||
status: '未知',
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取系统状态请求失败');
|
||||
// 显示默认状态
|
||||
updateSystemStatus({
|
||||
status: '连接失败',
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新系统状态显示
|
||||
function updateSystemStatus(data) {
|
||||
const statusClass = data.status === '正常' ? 'text-success' : 'text-danger';
|
||||
$('#systemStatus').text(data.status || '未知').removeClass().addClass('card-title ' + statusClass);
|
||||
$('#cpuUsage').text((data.cpuUsage || 0) + '%');
|
||||
$('#memoryUsage').text((data.memoryUsage || 0) + '%');
|
||||
$('#diskUsage').text((data.diskUsage || 0) + '%');
|
||||
}
|
||||
|
||||
function loadRedisStatus() {
|
||||
// 调用真实API获取Redis状态
|
||||
$.get('${pageContext.request.contextPath}/api/admin/monitor/redis')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateRedisStatus(response.data);
|
||||
} else {
|
||||
console.error('获取Redis状态失败:', response.message);
|
||||
// 显示默认状态
|
||||
updateRedisStatus([]);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取Redis状态请求失败');
|
||||
// 显示默认状态
|
||||
updateRedisStatus([]);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新Redis状态显示
|
||||
function updateRedisStatus(nodes) {
|
||||
let html = '';
|
||||
if (nodes && nodes.length > 0) {
|
||||
nodes.forEach(node => {
|
||||
const statusClass = node.status === '正常' ? 'bg-success' : 'bg-danger';
|
||||
html += `
|
||||
<tr>
|
||||
<td>${node.node}</td>
|
||||
<td><span class="badge ${statusClass}">${node.status}</span></td>
|
||||
<td>${node.memory}</td>
|
||||
<td>${node.connections}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html = '<tr><td colspan="4" class="text-center">无法获取Redis状态</td></tr>';
|
||||
}
|
||||
$('#redisNodesTable').html(html);
|
||||
}
|
||||
|
||||
function loadDatabaseStatus() {
|
||||
// 模拟数据库连接池状态
|
||||
setTimeout(function () {
|
||||
$('#activeConnections').text('12');
|
||||
$('#idleConnections').text('8');
|
||||
$('#maxConnections').text('20');
|
||||
$('#totalConnections').text('20');
|
||||
}, 600);
|
||||
}
|
||||
|
||||
function loadApiPerformance() {
|
||||
// 模拟接口性能数据
|
||||
setTimeout(function () {
|
||||
const apis = [
|
||||
{
|
||||
path: '/api/flashsale/participate',
|
||||
requests: 1234,
|
||||
avgTime: '45ms',
|
||||
successRate: '99.8%',
|
||||
lastCall: '10:30:15',
|
||||
status: '正常'
|
||||
},
|
||||
{
|
||||
path: '/api/product/hot',
|
||||
requests: 567,
|
||||
avgTime: '23ms',
|
||||
successRate: '100%',
|
||||
lastCall: '10:29:45',
|
||||
status: '正常'
|
||||
},
|
||||
{
|
||||
path: '/api/user/login',
|
||||
requests: 89,
|
||||
avgTime: '156ms',
|
||||
successRate: '98.9%',
|
||||
lastCall: '10:28:30',
|
||||
status: '正常'
|
||||
},
|
||||
{
|
||||
path: '/api/order/create',
|
||||
requests: 234,
|
||||
avgTime: '89ms',
|
||||
successRate: '99.5%',
|
||||
lastCall: '10:27:20',
|
||||
status: '正常'
|
||||
}
|
||||
];
|
||||
|
||||
let html = '';
|
||||
apis.forEach(api => {
|
||||
html += `
|
||||
<tr>
|
||||
<td><code>${api.path}</code></td>
|
||||
<td>${api.requests}</td>
|
||||
<td>${api.avgTime}</td>
|
||||
<td>${api.successRate}</td>
|
||||
<td>${api.lastCall}</td>
|
||||
<td><span class="badge bg-success">${api.status}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#apiPerformanceTable').html(html);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function loadErrorLogs() {
|
||||
// 模拟错误日志数据
|
||||
setTimeout(function () {
|
||||
const logs = [
|
||||
{
|
||||
time: '10:25:30',
|
||||
level: 'WARN',
|
||||
module: 'RedisService',
|
||||
message: 'Redis连接池使用率较高',
|
||||
action: '查看详情'
|
||||
},
|
||||
{
|
||||
time: '10:20:15',
|
||||
level: 'ERROR',
|
||||
module: 'FlashSaleService',
|
||||
message: '秒杀库存扣减失败',
|
||||
action: '查看详情'
|
||||
},
|
||||
{time: '10:15:45', level: 'INFO', module: 'UserService', message: '用户登录成功', action: '查看详情'}
|
||||
];
|
||||
|
||||
let html = '';
|
||||
if (logs.length === 0) {
|
||||
html = '<tr><td colspan="5" class="text-center text-success">暂无错误日志</td></tr>';
|
||||
} else {
|
||||
logs.forEach(log => {
|
||||
const levelClass = log.level === 'ERROR' ? 'bg-danger' :
|
||||
log.level === 'WARN' ? 'bg-warning' : 'bg-info';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${log.time}</td>
|
||||
<td><span class="badge ${levelClass}">${log.level}</span></td>
|
||||
<td>${log.module}</td>
|
||||
<td>${log.message}</td>
|
||||
<td><button class="btn btn-sm btn-outline-primary">${log.action}</button></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#errorLogsTable').html(html);
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
function updateCharts() {
|
||||
// 这里应该使用Chart.js或其他图表库来绘制实时图表
|
||||
// 由于简化,这里只是模拟
|
||||
console.log('更新监控图表...');
|
||||
}
|
||||
|
||||
function refreshMonitorData() {
|
||||
// 显示加载状态
|
||||
$('#redisNodesTable').html('<tr><td colspan="4" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#apiPerformanceTable').html('<tr><td colspan="6" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
$('#errorLogsTable').html('<tr><td colspan="5" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 重新加载数据
|
||||
loadMonitorData();
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
console.log('导出监控报告');
|
||||
alert('监控报告导出功能开发中...');
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
549
src/main/webapp/WEB-INF/views/admin/orders.jsp
Normal file
549
src/main/webapp/WEB-INF/views/admin/orders.jsp
Normal file
@@ -0,0 +1,549 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="订单管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">订单管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshOrders()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportOrders()">
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索订单号/用户...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchOrders()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterOrders()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="pending">待支付</option>
|
||||
<option value="paid">已支付</option>
|
||||
<option value="shipped">已发货</option>
|
||||
<option value="completed">已完成</option>
|
||||
<option value="cancelled">已取消</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" id="dateFilter" onchange="filterOrders()">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortOrders()">
|
||||
<option value="created_at">按创建时间</option>
|
||||
<option value="total_amount">按订单金额</option>
|
||||
<option value="status">按订单状态</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单统计 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="totalOrders">0</h5>
|
||||
<p class="card-text">总订单数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="paidOrders">0</h5>
|
||||
<p class="card-text">已支付订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="pendingOrders">0</h5>
|
||||
<p class="card-text">待处理订单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="totalAmount">¥0</h5>
|
||||
<p class="card-text">总交易额</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>订单号</th>
|
||||
<th>用户</th>
|
||||
<th>商品信息</th>
|
||||
<th>数量</th>
|
||||
<th>总金额</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersTableBody">
|
||||
<tr>
|
||||
<td colspan="8" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="订单分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情模态框 -->
|
||||
<div class="modal fade" id="orderDetailModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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" id="orderDetailContent">
|
||||
<!-- 订单详情内容 -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadOrders();
|
||||
loadOrderStats();
|
||||
});
|
||||
|
||||
function loadOrders(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建请求参数
|
||||
let params = {
|
||||
page: page,
|
||||
size: 10
|
||||
};
|
||||
|
||||
const keyword = $('#searchKeyword').val();
|
||||
const status = $('#statusFilter').val();
|
||||
|
||||
if (keyword && keyword.trim()) {
|
||||
params.keyword = keyword.trim();
|
||||
}
|
||||
|
||||
if (status && status !== '') {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders', params)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
renderOrdersTable(response.data.orders);
|
||||
renderPagination(response.data.currentPage, response.data.totalPages);
|
||||
} else {
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center text-danger">获取订单数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#orderTableBody').html('<tr><td colspan="8" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
function loadOrderStats() {
|
||||
// 调用真实API获取订单统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/orders/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateOrderStats(response.data);
|
||||
} else {
|
||||
console.error('获取订单统计数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateOrderStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取订单统计数据请求失败');
|
||||
// 显示默认值
|
||||
updateOrderStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新订单统计数据
|
||||
function updateOrderStats(stats) {
|
||||
$('#totalOrders').text(formatNumber(stats.totalOrders || 0));
|
||||
$('#paidOrders').text(formatNumber(stats.paidOrders || 0));
|
||||
$('#pendingOrders').text(formatNumber(stats.pendingOrders || 0));
|
||||
$('#totalAmount').text('¥' + formatNumber(stats.totalAmount || 0));
|
||||
}
|
||||
|
||||
function renderOrdersTable(orders) {
|
||||
let html = '';
|
||||
|
||||
if (orders.length === 0) {
|
||||
html = '<tr><td colspan="8" class="text-center">暂无订单数据</td></tr>';
|
||||
} else {
|
||||
orders.forEach(order => {
|
||||
const statusText = getOrderStatusText(order.status);
|
||||
const statusClass = getOrderStatusClass(order.status);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">` + order.id + `</div>
|
||||
` + (order.isFlashSale ? '<small class="text-danger"><i class="fas fa-bolt"></i> 秒杀订单</small>' : '') + `
|
||||
</td>
|
||||
<td>` + order.username + `</td>
|
||||
<td>` + order.productName + `</td>
|
||||
<td>` + order.quantity + `</td>
|
||||
<td class="fw-bold">¥` + formatNumber(order.totalAmount || 0) + `</td>
|
||||
<td>
|
||||
<span class="badge ` + statusClass + `">
|
||||
` + statusText + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(order.createdAt) + `</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="viewOrderDetail('` + order.id + `')" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
` + (order.status === 2 ?
|
||||
'<button class="btn btn-outline-success" onclick="shipOrder(\'' + order.id + '\')" title="发货"><i class="fas fa-shipping-fast"></i></button>' : '') + `
|
||||
` + (order.status === 1 ?
|
||||
'<button class="btn btn-outline-danger" onclick="cancelOrder(\'' + order.id + '\')" title="取消"><i class="fas fa-times"></i></button>' : '') + `
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#ordersTableBody').html(html);
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待支付';
|
||||
case 'paid':
|
||||
return '已支付';
|
||||
case 'shipped':
|
||||
return '已发货';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'bg-warning';
|
||||
case 'paid':
|
||||
return 'bg-success';
|
||||
case 'shipped':
|
||||
return 'bg-info';
|
||||
case 'completed':
|
||||
return 'bg-primary';
|
||||
case 'cancelled':
|
||||
return 'bg-secondary';
|
||||
default:
|
||||
return 'bg-light';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadOrders(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshOrders() {
|
||||
$('#ordersTableBody').html('<tr><td colspan="8" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadOrders(currentPage);
|
||||
loadOrderStats();
|
||||
}
|
||||
|
||||
function searchOrders() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索订单:', keyword);
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function filterOrders() {
|
||||
const status = $('#statusFilter').val();
|
||||
const date = $('#dateFilter').val();
|
||||
console.log('筛选订单:', {status, date});
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function sortOrders() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
loadOrders(1);
|
||||
}
|
||||
|
||||
function viewOrderDetail(orderId) {
|
||||
console.log('查看订单详情:', orderId);
|
||||
|
||||
// 模拟获取订单详情
|
||||
const orderDetail = `
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>订单信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>订单号:</td><td>` + orderId + `</td></tr>
|
||||
<tr><td>用户:</td><td>demo1</td></tr>
|
||||
<tr><td>状态:</td><td><span class="badge bg-success">已支付</span></td></tr>
|
||||
<tr><td>创建时间:</td><td>2025-06-29 10:30:15</td></tr>
|
||||
<tr><td>支付时间:</td><td>2025-06-29 10:31:20</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>商品信息</h6>
|
||||
<table class="table table-sm">
|
||||
<tr><td>商品名称:</td><td>iPhone 15 Pro Max</td></tr>
|
||||
<tr><td>单价:</td><td>¥8,888.00</td></tr>
|
||||
<tr><td>数量:</td><td>1</td></tr>
|
||||
<tr><td>总金额:</td><td class="fw-bold">¥8,888.00</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6>收货地址</h6>
|
||||
<p>北京市朝阳区xxx街道xxx号xxx小区xxx楼xxx室<br>
|
||||
收货人: 张三 13800138001</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#orderDetailContent').html(orderDetail);
|
||||
$('#orderDetailModal').modal('show');
|
||||
}
|
||||
|
||||
function shipOrder(orderId) {
|
||||
if (confirm('确定要将此订单标记为已发货吗?')) {
|
||||
console.log('发货订单:', orderId);
|
||||
|
||||
setTimeout(function () {
|
||||
alert('订单已标记为已发货!');
|
||||
refreshOrders();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelOrder(orderId) {
|
||||
if (confirm('确定要取消此订单吗?此操作不可恢复。')) {
|
||||
console.log('取消订单:', orderId);
|
||||
|
||||
setTimeout(function () {
|
||||
alert('订单已取消!');
|
||||
refreshOrders();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function exportOrders() {
|
||||
console.log('导出订单数据');
|
||||
alert('订单数据导出功能开发中...');
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getOrderStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-warning'; // 待支付
|
||||
case 2:
|
||||
return 'bg-success'; // 已支付
|
||||
case 3:
|
||||
return 'bg-info'; // 已发货
|
||||
case 4:
|
||||
return 'bg-primary'; // 已完成
|
||||
case 5:
|
||||
return 'bg-danger'; // 已取消
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getOrderStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '待支付';
|
||||
case 2:
|
||||
return '已支付';
|
||||
case 3:
|
||||
return '已发货';
|
||||
case 4:
|
||||
return '已完成';
|
||||
case 5:
|
||||
return '已取消';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
1188
src/main/webapp/WEB-INF/views/admin/products.jsp
Normal file
1188
src/main/webapp/WEB-INF/views/admin/products.jsp
Normal file
File diff suppressed because it is too large
Load Diff
409
src/main/webapp/WEB-INF/views/admin/users.jsp
Normal file
409
src/main/webapp/WEB-INF/views/admin/users.jsp
Normal file
@@ -0,0 +1,409 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户管理"/>
|
||||
<%@ include file="../common/header.jsp" %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
|
||||
<span>管理功能</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin">
|
||||
<i class="fas fa-tachometer-alt"></i> 仪表盘
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/products">
|
||||
<i class="fas fa-box"></i> 商品管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/flashsales">
|
||||
<i class="fas fa-bolt"></i> 秒杀管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/orders">
|
||||
<i class="fas fa-shopping-cart"></i> 订单管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="${pageContext.request.contextPath}/admin/users">
|
||||
<i class="fas fa-users"></i> 用户管理
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/admin/monitor">
|
||||
<i class="fas fa-chart-line"></i> 系统监控
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">用户管理</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshUsers()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-success" onclick="exportUsers()">
|
||||
<i class="fas fa-download"></i> 导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="搜索用户名/邮箱/手机...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchUsers()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="statusFilter" onchange="filterUsers()">
|
||||
<option value="">全部状态</option>
|
||||
<option value="1">正常</option>
|
||||
<option value="0">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="date" class="form-control" id="dateFilter" onchange="filterUsers()">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select class="form-select" id="sortBy" onchange="sortUsers()">
|
||||
<option value="created_at">按注册时间</option>
|
||||
<option value="username">按用户名</option>
|
||||
<option value="last_login">按最后登录</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户统计 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary" id="totalUsers">0</h5>
|
||||
<p class="card-text">总用户数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success" id="activeUsers">0</h5>
|
||||
<p class="card-text">活跃用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning" id="newUsers">0</h5>
|
||||
<p class="card-text">今日新增</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-info" id="onlineUsers">0</h5>
|
||||
<p class="card-text">在线用户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>手机号</th>
|
||||
<th>状态</th>
|
||||
<th>注册时间</th>
|
||||
<th>最后登录</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<nav aria-label="用户分页">
|
||||
<ul class="pagination justify-content-center" id="pagination">
|
||||
<!-- 分页按钮将通过JavaScript生成 -->
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
padding: 48px 0 0;
|
||||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 240px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let pageSize = 10;
|
||||
let totalPages = 1;
|
||||
|
||||
$(document).ready(function () {
|
||||
loadUsers();
|
||||
loadUserStats();
|
||||
});
|
||||
|
||||
function loadUsers(page = 1) {
|
||||
currentPage = page;
|
||||
|
||||
// 显示加载状态
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
|
||||
// 构建请求参数
|
||||
let params = {
|
||||
page: page,
|
||||
size: 10
|
||||
};
|
||||
|
||||
const keyword = $('#searchKeyword').val();
|
||||
const status = $('#statusFilter').val();
|
||||
|
||||
if (keyword && keyword.trim()) {
|
||||
params.keyword = keyword.trim();
|
||||
}
|
||||
|
||||
if (status && status !== '') {
|
||||
params.status = status;
|
||||
}
|
||||
|
||||
// 调用真实API
|
||||
$.get('${pageContext.request.contextPath}/api/admin/users', params)
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
renderUsersTable(response.data.users);
|
||||
renderPagination(response.data.currentPage, response.data.totalPages);
|
||||
} else {
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center text-danger">获取用户数据失败: ' + response.message + '</td></tr>');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#userTableBody').html('<tr><td colspan="7" class="text-center text-danger">网络请求失败,请稍后重试</td></tr>');
|
||||
});
|
||||
}
|
||||
|
||||
function loadUserStats() {
|
||||
// 调用真实API获取用户统计数据
|
||||
$.get('${pageContext.request.contextPath}/api/admin/users/stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
updateUserStats(response.data);
|
||||
} else {
|
||||
console.error('获取用户统计数据失败:', response.message);
|
||||
// 显示默认值
|
||||
updateUserStats({});
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
console.error('获取用户统计数据请求失败');
|
||||
// 显示默认值
|
||||
updateUserStats({});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户统计数据
|
||||
function updateUserStats(stats) {
|
||||
$('#totalUsers').text(formatNumber(stats.totalUsers || 0));
|
||||
$('#activeUsers').text(formatNumber(stats.activeUsers || 0));
|
||||
$('#newUsers').text(formatNumber(stats.newUsers || 0));
|
||||
$('#onlineUsers').text(formatNumber(stats.onlineUsers || 0));
|
||||
}
|
||||
|
||||
function renderUsersTable(users) {
|
||||
let html = '';
|
||||
|
||||
if (users.length === 0) {
|
||||
html = '<tr><td colspan="7" class="text-center">暂无用户数据</td></tr>';
|
||||
} else {
|
||||
users.forEach(user => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>` + user.id + `</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-2">` + user.username + `</span>
|
||||
` + (user.isOnline ? '<span class="badge bg-success">在线</span>' : '') + `
|
||||
</div>
|
||||
</td>
|
||||
<td>` + (user.email || '-') + `</td>
|
||||
<td>` + (user.phone || '-') + `</td>
|
||||
<td>
|
||||
<span class="badge ` + getUserStatusClass(user.status) + `">
|
||||
` + getUserStatusText(user.status) + `
|
||||
</span>
|
||||
</td>
|
||||
<td>` + formatDateTime(user.createdAt) + `</td>
|
||||
<td>` + (user.lastLogin ? formatDateTime(user.lastLogin) : '从未登录') + `</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#usersTableBody').html(html);
|
||||
}
|
||||
|
||||
function renderPagination(total, pageSize) {
|
||||
totalPages = Math.ceil(total / pageSize);
|
||||
let html = '';
|
||||
|
||||
// 上一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === 1 ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + (currentPage - 1) + `)">上一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
html += `
|
||||
<li class="page-item ` + (i === currentPage ? 'active' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + i + `)">` + i + `</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `
|
||||
<li class="page-item ` + (currentPage === totalPages ? 'disabled' : '') + `">
|
||||
<a class="page-link" href="#" onclick="loadUsers(` + (currentPage + 1) + `)">下一页</a>
|
||||
</li>
|
||||
`;
|
||||
|
||||
$('#pagination').html(html);
|
||||
}
|
||||
|
||||
function refreshUsers() {
|
||||
$('#usersTableBody').html('<tr><td colspan="7" class="text-center"><i class="fas fa-spinner fa-spin"></i> 加载中...</td></tr>');
|
||||
loadUsers(currentPage);
|
||||
loadUserStats();
|
||||
}
|
||||
|
||||
function searchUsers() {
|
||||
const keyword = $('#searchInput').val();
|
||||
console.log('搜索用户:', keyword);
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
function filterUsers() {
|
||||
const status = $('#statusFilter').val();
|
||||
const date = $('#dateFilter').val();
|
||||
console.log('筛选用户:', {status, date});
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
function sortUsers() {
|
||||
const sortBy = $('#sortBy').val();
|
||||
console.log('排序方式:', sortBy);
|
||||
loadUsers(1);
|
||||
}
|
||||
|
||||
|
||||
function exportUsers() {
|
||||
console.log('导出用户数据');
|
||||
alert('用户数据导出功能开发中...');
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
function formatDateTime(dateTime) {
|
||||
if (!dateTime) return '';
|
||||
return new Date(dateTime).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function getUserStatusClass(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return 'bg-success'; // 正常
|
||||
case 0:
|
||||
return 'bg-danger'; // 禁用
|
||||
default:
|
||||
return 'bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getUserStatusText(status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '正常';
|
||||
case 0:
|
||||
return '禁用';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="../common/footer.jsp" %>
|
||||
244
src/main/webapp/WEB-INF/views/common/footer.jsp
Normal file
244
src/main/webapp/WEB-INF/views/common/footer.jsp
Normal file
@@ -0,0 +1,244 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="bg-dark text-light mt-5 py-4">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5><i class="fas fa-bolt"></i> 秒杀系统</h5>
|
||||
<p class="mb-2">基于Spring Boot + Redis构建的高并发秒杀系统</p>
|
||||
<p class="text-muted small">
|
||||
<i class="fas fa-server"></i> Redis集群 |
|
||||
<i class="fas fa-lock"></i> 分布式锁 |
|
||||
<i class="fas fa-tachometer-alt"></i> 高性能
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>核心功能</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-fire text-danger"></i> 秒杀抢购</li>
|
||||
<li><i class="fas fa-shopping-cart text-primary"></i> 购物车</li>
|
||||
<li><i class="fas fa-list-alt text-success"></i> 订单管理</li>
|
||||
<li><i class="fas fa-chart-line text-warning"></i> 销量排行</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<h6>技术特性</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li><i class="fas fa-database text-info"></i> Redis缓存</li>
|
||||
<li><i class="fas fa-shield-alt text-success"></i> 防超卖机制</li>
|
||||
<li><i class="fas fa-stopwatch text-warning"></i> 接口限流</li>
|
||||
<li><i class="fas fa-code text-primary"></i> Lua脚本</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<p class="mb-0 text-muted">
|
||||
© 2025 秒杀系统演示项目.
|
||||
<span class="text-danger">❤</span>
|
||||
基于Redis集群构建
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<div class="d-flex justify-content-md-end align-items-center">
|
||||
<span class="me-3 text-muted small">
|
||||
<i class="fas fa-users"></i>
|
||||
在线用户: <span id="onlineUserCount">-</span>
|
||||
</span>
|
||||
<span class="me-3 text-muted small">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="currentTime"></span>
|
||||
</span>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-light btn-sm" onclick="checkSystemStatus()">
|
||||
<i class="fas fa-heartbeat"></i> 系统状态
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 系统状态模态框 -->
|
||||
<div class="modal fade" id="systemStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-heartbeat text-success"></i> 系统状态
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-database fa-2x text-success mb-2"></i>
|
||||
<h6>Redis集群</h6>
|
||||
<span class="badge bg-success">正常</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-server fa-2x text-primary mb-2"></i>
|
||||
<h6>应用服务</h6>
|
||||
<span class="badge bg-primary">运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h6>实时统计</h6>
|
||||
<div class="row text-center">
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-primary fw-bold" id="totalUsers">-</div>
|
||||
<small class="text-muted">总用户</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-success fw-bold" id="totalProducts">-</div>
|
||||
<small class="text-muted">商品数</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-warning fw-bold" id="totalOrders">-</div>
|
||||
<small class="text-muted">订单数</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="border rounded p-2">
|
||||
<div class="text-danger fw-bold" id="activeFlashSales">-</div>
|
||||
<small class="text-muted">秒杀中</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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="refreshSystemStatus()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString('zh-CN');
|
||||
$('#currentTime').text(timeString);
|
||||
}
|
||||
|
||||
// 获取在线用户数
|
||||
function updateOnlineUserCount() {
|
||||
// 只有在用户登录时才更新在线用户数,并且不在登录页面执行
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
// 检查当前页面是否为登录页面
|
||||
if (window.location.pathname.indexOf('/login') === -1) {
|
||||
$.get('${pageContext.request.contextPath}/api/user/online-stats')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#onlineUserCount').text(response.data.onlineUserCount);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
$('#onlineUserCount').text('N/A');
|
||||
});
|
||||
}
|
||||
</c:if>
|
||||
}
|
||||
|
||||
// 检查系统状态
|
||||
function checkSystemStatus() {
|
||||
$('#systemStatusModal').modal('show');
|
||||
refreshSystemStatus();
|
||||
}
|
||||
|
||||
// 刷新系统状态
|
||||
function refreshSystemStatus() {
|
||||
// 获取订单统计
|
||||
$.get('${pageContext.request.contextPath}/api/order/statistics')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
const stats = response.data;
|
||||
$('#totalOrders').text(stats.totalOrders || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取活跃秒杀数量
|
||||
$.get('${pageContext.request.contextPath}/api/flashsale/active')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#activeFlashSales').text(response.data.length || 0);
|
||||
}
|
||||
});
|
||||
|
||||
// 模拟其他统计数据
|
||||
$('#totalUsers').text(Math.floor(Math.random() * 1000) + 500);
|
||||
$('#totalProducts').text(Math.floor(Math.random() * 100) + 50);
|
||||
}
|
||||
|
||||
// 页面加载完成后执行
|
||||
$(document).ready(function () {
|
||||
// 立即更新时间
|
||||
updateCurrentTime();
|
||||
|
||||
// 只有在用户登录时才更新在线用户数
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
updateOnlineUserCount();
|
||||
// 每2分钟更新一次在线用户数(减少频率)
|
||||
setInterval(updateOnlineUserCount, 120000);
|
||||
</c:if>
|
||||
|
||||
// 每秒更新时间
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
});
|
||||
|
||||
// 页面可见性变化时的处理
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (!document.hidden) {
|
||||
// 页面变为可见时,更新数据
|
||||
updateOnlineUserCount();
|
||||
}
|
||||
});
|
||||
|
||||
// 全局错误处理
|
||||
$(document).ajaxError(function (event, xhr, settings, thrownError) {
|
||||
if (xhr.status === 401) {
|
||||
showMessage('登录已过期,请重新登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 2000);
|
||||
} else if (xhr.status >= 500) {
|
||||
showMessage('服务器错误,请稍后重试', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 添加加载动画
|
||||
function showLoading(element) {
|
||||
$(element).html('<i class="fas fa-spinner fa-spin"></i> 加载中...');
|
||||
}
|
||||
|
||||
// 隐藏加载动画
|
||||
function hideLoading(element, originalText) {
|
||||
$(element).html(originalText);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
272
src/main/webapp/WEB-INF/views/common/header.jsp
Normal file
272
src/main/webapp/WEB-INF/views/common/header.jsp
Normal file
@@ -0,0 +1,272 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${pageTitle} - 秒杀系统</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<style>
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.flash-sale-badge {
|
||||
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 0.7em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.online-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #28a745;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="${pageContext.request.contextPath}/">
|
||||
<i class="fas fa-bolt"></i> 秒杀系统
|
||||
<span class="flash-sale-badge">FLASH SALE</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/">
|
||||
<i class="fas fa-home"></i> 首页
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/products">
|
||||
<i class="fas fa-shopping-bag"></i> 商品列表
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/flashsales">
|
||||
<i class="fas fa-fire"></i> 秒杀活动
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav">
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
<!-- 购物车 -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link position-relative" href="${pageContext.request.contextPath}/cart">
|
||||
<i class="fas fa-shopping-cart"></i> 购物车
|
||||
<span class="cart-badge" id="cartCount">0</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- 用户菜单 -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="userDropdown"
|
||||
role="button" data-bs-toggle="dropdown">
|
||||
<div class="user-avatar me-2">
|
||||
${sessionScope.user.username.substring(0,1).toUpperCase()}
|
||||
</div>
|
||||
<span class="online-indicator"></span>
|
||||
${sessionScope.user.username}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/orders">
|
||||
<i class="fas fa-list-alt"></i> 我的订单
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="${pageContext.request.contextPath}/profile">
|
||||
<i class="fas fa-user-cog"></i> 个人设置
|
||||
</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item" href="#" onclick="logout()">
|
||||
<i class="fas fa-sign-out-alt"></i> 退出登录
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/login">
|
||||
<i class="fas fa-sign-in-alt"></i> 登录
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="${pageContext.request.contextPath}/register">
|
||||
<i class="fas fa-user-plus"></i> 注册
|
||||
</a>
|
||||
</li>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 消息提示区域 -->
|
||||
<div id="messageContainer" class="container mt-3">
|
||||
<!-- 动态消息将在这里显示 -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局JavaScript函数
|
||||
|
||||
// 显示消息
|
||||
function showMessage(message, type = 'info') {
|
||||
const alertClass = {
|
||||
'success': 'alert-success',
|
||||
'error': 'alert-danger',
|
||||
'warning': 'alert-warning',
|
||||
'info': 'alert-info'
|
||||
}[type] || 'alert-info';
|
||||
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#messageContainer').html(alertHtml);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
$('.alert').alert('close');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
function logout() {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
$.post('${pageContext.request.contextPath}/api/user/logout')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
showMessage('退出登录失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新购物车数量
|
||||
function updateCartCount() {
|
||||
<c:if test="${not empty sessionScope.user}">
|
||||
$.get('${pageContext.request.contextPath}/api/cart/count')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
$('#cartCount').text(response.data.count);
|
||||
}
|
||||
});
|
||||
</c:if>
|
||||
}
|
||||
|
||||
// 页面加载完成后执行
|
||||
$(document).ready(function () {
|
||||
updateCartCount();
|
||||
|
||||
// 每30秒更新一次购物车数量
|
||||
setInterval(updateCartCount, 30000);
|
||||
});
|
||||
|
||||
// 格式化价格
|
||||
function formatPrice(price) {
|
||||
return '¥' + parseFloat(price).toFixed(2);
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 倒计时函数
|
||||
function countdown(endTime, elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
function updateCountdown() {
|
||||
const now = new Date().getTime();
|
||||
const distance = endTime - now;
|
||||
|
||||
if (distance < 0) {
|
||||
element.innerHTML = "已结束";
|
||||
return;
|
||||
}
|
||||
|
||||
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
|
||||
let countdownText = "";
|
||||
if (days > 0) countdownText += days + "天 ";
|
||||
countdownText += String(hours).padStart(2, '0') + ":" +
|
||||
String(minutes).padStart(2, '0') + ":" +
|
||||
String(seconds).padStart(2, '0');
|
||||
|
||||
element.innerHTML = countdownText;
|
||||
}
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 1000);
|
||||
|
||||
// 返回清理函数
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
</script>
|
||||
83
src/main/webapp/WEB-INF/views/error.jsp
Normal file
83
src/main/webapp/WEB-INF/views/error.jsp
Normal file
@@ -0,0 +1,83 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="系统错误"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card border-danger">
|
||||
<div class="card-header bg-danger text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle"></i> 系统错误
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-bug fa-5x text-danger mb-3"></i>
|
||||
<h5>抱歉,系统遇到了一个错误</h5>
|
||||
<p class="text-muted">我们正在努力修复这个问题,请稍后再试。</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息(仅在开发环境显示) -->
|
||||
<c:if test="${not empty error}">
|
||||
<div class="alert alert-warning text-start">
|
||||
<h6><i class="fas fa-info-circle"></i> 错误详情:</h6>
|
||||
<p class="mb-0">${error}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${not empty exception}">
|
||||
<div class="alert alert-danger text-start">
|
||||
<h6><i class="fas fa-bug"></i> 异常信息:</h6>
|
||||
<p class="mb-0">${exception.message}</p>
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-primary" onclick="history.back()">
|
||||
<i class="fas fa-arrow-left"></i> 返回上页
|
||||
</button>
|
||||
<a href="${pageContext.request.contextPath}/" class="btn btn-success">
|
||||
<i class="fas fa-home"></i> 返回首页
|
||||
</a>
|
||||
<button class="btn btn-info" onclick="location.reload()">
|
||||
<i class="fas fa-redo"></i> 刷新页面
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常见问题解决方案 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h6><i class="fas fa-question-circle"></i> 常见问题解决方案</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>如果页面无法加载:</h6>
|
||||
<ul class="small">
|
||||
<li>检查网络连接</li>
|
||||
<li>清除浏览器缓存</li>
|
||||
<li>尝试刷新页面</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>如果功能异常:</h6>
|
||||
<ul class="small">
|
||||
<li>重新登录账号</li>
|
||||
<li>检查输入信息</li>
|
||||
<li>联系系统管理员</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
472
src/main/webapp/WEB-INF/views/index.jsp
Normal file
472
src/main/webapp/WEB-INF/views/index.jsp
Normal file
@@ -0,0 +1,472 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
<%@ taglib prefix="fn" uri="http://flashsale.org/functions" %>
|
||||
|
||||
<c:set var="pageTitle" value="首页" />
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<!-- 轮播图 -->
|
||||
<div id="heroCarousel" class="carousel slide" data-bs-ride="carousel">
|
||||
<div class="carousel-indicators">
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0" class="active"></button>
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="1"></button>
|
||||
<button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="2"></button>
|
||||
</div>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<div class="bg-gradient-danger text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-bolt"></i> 秒杀系统
|
||||
</h1>
|
||||
<p class="lead mb-4">基于Redis集群构建的高并发秒杀系统,支持分布式锁、接口限流、库存预热等核心功能。</p>
|
||||
<div class="d-flex gap-3">
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-fire"></i> 立即抢购
|
||||
</a>
|
||||
<a href="${pageContext.request.contextPath}/products" class="btn btn-outline-light btn-lg">
|
||||
<i class="fas fa-shopping-bag"></i> 浏览商品
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-rocket fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<div class="bg-gradient-primary text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-shield-alt"></i> 防超卖机制
|
||||
</h1>
|
||||
<p class="lead mb-4">采用Redis分布式锁和Lua脚本,确保高并发场景下的数据一致性,彻底解决超卖问题。</p>
|
||||
<a href="#features" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-info-circle"></i> 了解更多
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-lock fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<div class="bg-gradient-success text-white py-5" style="min-height: 400px;">
|
||||
<div class="container d-flex align-items-center h-100">
|
||||
<div class="row w-100">
|
||||
<div class="col-md-6">
|
||||
<h1 class="display-4 fw-bold mb-4">
|
||||
<i class="fas fa-tachometer-alt"></i> 高性能缓存
|
||||
</h1>
|
||||
<p class="lead mb-4">Redis集群架构,支持五种数据类型应用,实现毫秒级响应,轻松应对高并发访问。</p>
|
||||
<a href="#performance" class="btn btn-light btn-lg">
|
||||
<i class="fas fa-chart-line"></i> 性能指标
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<i class="fas fa-database fa-10x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#heroCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon"></span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#heroCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container my-5">
|
||||
<!-- 正在进行的秒杀活动 -->
|
||||
<section class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-fire text-danger"></i> 正在秒杀
|
||||
</h2>
|
||||
<a href="${pageContext.request.contextPath}/flashsales" class="btn btn-outline-danger">
|
||||
查看全部 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="activeFlashSales" class="row">
|
||||
<!-- 动态加载秒杀活动 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 热门商品 -->
|
||||
<section class="mb-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">
|
||||
<i class="fas fa-star text-warning"></i> 热门商品
|
||||
</h2>
|
||||
<a href="${pageContext.request.contextPath}/products" class="btn btn-outline-primary">
|
||||
查看全部 <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="hotProducts" class="row">
|
||||
<!-- 动态加载热门商品 -->
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 系统特性 -->
|
||||
<section id="features" class="mb-5">
|
||||
<h2 class="text-center fw-bold mb-5">
|
||||
<i class="fas fa-cogs"></i> 系统特性
|
||||
</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-bolt fa-3x text-danger mb-3"></i>
|
||||
<h5 class="card-title">秒杀抢购</h5>
|
||||
<p class="card-text text-muted">高并发秒杀系统,支持大量用户同时抢购</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-shield-alt fa-3x text-success mb-3"></i>
|
||||
<h5 class="card-title">防超卖</h5>
|
||||
<p class="card-text text-muted">分布式锁机制,确保库存数据一致性</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-database fa-3x text-info mb-3"></i>
|
||||
<h5 class="card-title">Redis缓存</h5>
|
||||
<p class="card-text text-muted">五种数据类型应用,毫秒级响应</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card h-100 text-center border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-tachometer-alt fa-3x text-warning mb-3"></i>
|
||||
<h5 class="card-title">接口限流</h5>
|
||||
<p class="card-text text-muted">多种限流策略,防止恶意刷单</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 性能指标 -->
|
||||
<section id="performance" class="mb-5">
|
||||
<h2 class="text-center fw-bold mb-5">
|
||||
<i class="fas fa-chart-line"></i> 性能指标
|
||||
</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-primary fw-bold" id="qpsCounter">10000+</h3>
|
||||
<p class="card-text">QPS并发处理</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-success fw-bold" id="responseTime"><100ms</h3>
|
||||
<p class="card-text">平均响应时间</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-warning fw-bold" id="successRate">99.9%</h3>
|
||||
<p class="card-text">系统可用性</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center">
|
||||
<h3 class="text-info fw-bold" id="concurrentUsers">50000+</h3>
|
||||
<p class="card-text">并发用户支持</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// 加载正在进行的秒杀活动
|
||||
loadActiveFlashSales();
|
||||
|
||||
// 加载热门商品
|
||||
loadHotProducts();
|
||||
|
||||
// 启动性能指标动画
|
||||
animateCounters();
|
||||
});
|
||||
|
||||
// 加载正在进行的秒杀活动
|
||||
function loadActiveFlashSales() {
|
||||
$.get('${pageContext.request.contextPath}/api/flashsale/active')
|
||||
.done(function(response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderFlashSales(response.data.slice(0, 4)); // 只显示前4个
|
||||
} else {
|
||||
$('#activeFlashSales').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">暂无进行中的秒杀活动</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
$('#activeFlashSales').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
||||
<p class="text-muted mt-2">加载失败,请刷新页面重试</p>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染秒杀活动
|
||||
function renderFlashSales(flashSales) {
|
||||
let html = '';
|
||||
|
||||
flashSales.forEach(function(flashSale) {
|
||||
const discountPercent = Math.round((1 - flashSale.flashPrice / flashSale.originalPrice) * 100);
|
||||
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100 border-danger">
|
||||
<div class="position-relative">
|
||||
<img src="` + (flashSale.productImageUrl || '${pageContext.request.contextPath}/images/default-product.svg') + `"
|
||||
class="card-img-top" alt="` + flashSale.productName + `" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="position-absolute top-0 start-0 bg-danger text-white px-2 py-1 rounded-end">
|
||||
<small><i class="fas fa-fire"></i> 秒杀中</small>
|
||||
</div>
|
||||
<div class="position-absolute top-0 end-0 bg-warning text-dark px-2 py-1 rounded-start">
|
||||
<small>${discountPercent}% OFF</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + flashSale.productName + `</h6>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<span class="text-danger fw-bold fs-5">¥` + (flashSale.flashPrice ? flashSale.flashPrice.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted text-decoration-line-through ms-2">¥` + (flashSale.originalPrice ? flashSale.originalPrice.toFixed(2) : '0.00') + `</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">剩余: ` + (flashSale.remainingStock || 0) + `件</small>
|
||||
<div class="progress" style="height: 4px;">
|
||||
<div class="progress-bar bg-danger" style="width: ` + ((flashSale.remainingStock || 0) / (flashSale.flashStock || 1) * 100) + `%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-danger fw-bold mb-2" id="countdown_${flashSale.id}">
|
||||
计算中...
|
||||
</div>
|
||||
<button class="btn btn-danger btn-sm w-100" onclick="participateFlashSale(${flashSale.id})">
|
||||
<i class="fas fa-bolt"></i> 立即抢购
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 启动倒计时
|
||||
setTimeout(() => {
|
||||
if (flashSale.timeToEnd > 0) {
|
||||
countdown(Date.now() + flashSale.timeToEnd, 'countdown_' + flashSale.id);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
$('#activeFlashSales').html(html);
|
||||
}
|
||||
|
||||
// 加载热门商品
|
||||
function loadHotProducts() {
|
||||
$.get('${pageContext.request.contextPath}/api/product/hot?limit=8')
|
||||
.done(function(response) {
|
||||
if (response.success && response.data.length > 0) {
|
||||
renderHotProducts(response.data);
|
||||
} else {
|
||||
$('#hotProducts').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">暂无热门商品</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
$('#hotProducts').html(`
|
||||
<div class="col-12 text-center py-5">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning"></i>
|
||||
<p class="text-muted mt-2">加载失败,请刷新页面重试</p>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染热门商品
|
||||
function renderHotProducts(products) {
|
||||
let html = '';
|
||||
|
||||
products.forEach(function(product) {
|
||||
html += `
|
||||
<div class="col-lg-3 col-md-6 mb-4">
|
||||
<div class="card h-100">
|
||||
<img src="${product.imageUrl || '${pageContext.request.contextPath}/images/default-product.svg'}"
|
||||
class="card-img-top" alt="${product.name}" style="height: 200px; object-fit: cover;"
|
||||
onerror="this.src='${pageContext.request.contextPath}/images/default-product.svg'; this.onerror=null;">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title text-truncate">` + product.name + `</h6>
|
||||
<p class="card-text text-muted small text-truncate">` + (product.description || '暂无描述') + `</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-primary fw-bold">¥` + (product.price ? product.price.toFixed(2) : '0.00') + `</span>
|
||||
<small class="text-muted">库存: ` + (product.stock || 0) + `</small>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="addToCart(` + product.id + `)">
|
||||
<i class="fas fa-cart-plus"></i> 加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#hotProducts').html(html);
|
||||
}
|
||||
|
||||
// 参与秒杀
|
||||
function participateFlashSale(flashSaleId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
if (confirm('确定要参与这个秒杀活动吗?')) {
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/flashsale/participate',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
flashSaleId: flashSaleId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showMessage('秒杀成功!订单已生成', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/orders';
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showMessage('秒杀失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 添加到购物车
|
||||
function addToCart(productId) {
|
||||
<c:choose>
|
||||
<c:when test="${not empty sessionScope.user}">
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/cart/add',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
productId: productId,
|
||||
quantity: 1
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
showMessage('商品已添加到购物车', 'success');
|
||||
updateCartCount();
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showMessage('添加失败,请重试', 'error');
|
||||
}
|
||||
});
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
showMessage('请先登录', 'warning');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 1000);
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
}
|
||||
|
||||
// 性能指标动画
|
||||
function animateCounters() {
|
||||
const counters = [
|
||||
{ id: 'qpsCounter', target: 10000, suffix: '+' },
|
||||
{ id: 'concurrentUsers', target: 50000, suffix: '+' }
|
||||
];
|
||||
|
||||
counters.forEach(counter => {
|
||||
animateCounter(counter.id, counter.target, counter.suffix);
|
||||
});
|
||||
}
|
||||
|
||||
function animateCounter(elementId, target, suffix = '') {
|
||||
const element = document.getElementById(elementId);
|
||||
let current = 0;
|
||||
const increment = target / 100;
|
||||
const timer = setInterval(() => {
|
||||
current += increment;
|
||||
if (current >= target) {
|
||||
current = target;
|
||||
clearInterval(timer);
|
||||
}
|
||||
element.textContent = Math.floor(current).toLocaleString() + suffix;
|
||||
}, 20);
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
268
src/main/webapp/WEB-INF/views/login.jsp
Normal file
268
src/main/webapp/WEB-INF/views/login.jsp
Normal file
@@ -0,0 +1,268 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户登录"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-sign-in-alt"></i> 用户登录
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="loginForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user"></i> 用户名
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="请输入用户名" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock"></i> 密码
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="请输入密码" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="rememberMe">
|
||||
<label class="form-check-label" for="rememberMe">
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">
|
||||
<i class="fas fa-sign-in-alt"></i> 登录
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-2">还没有账号?</p>
|
||||
<a href="${pageContext.request.contextPath}/register" class="btn btn-outline-success">
|
||||
<i class="fas fa-user-plus"></i> 立即注册
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 快速登录演示账号 -->
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">演示账号(快速登录):</small>
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="quickLogin('demo1', '123456')">
|
||||
demo1
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="quickLogin('demo2', '123456')">
|
||||
demo2
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
onclick="quickLogin('admin', 'admin123')">
|
||||
admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统特性介绍 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle"></i> 系统特性
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-3">
|
||||
<i class="fas fa-bolt fa-2x text-danger mb-2"></i>
|
||||
<h6>秒杀抢购</h6>
|
||||
<small class="text-muted">高并发秒杀系统</small>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<i class="fas fa-shield-alt fa-2x text-success mb-2"></i>
|
||||
<h6>防超卖</h6>
|
||||
<small class="text-muted">分布式锁机制</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<i class="fas fa-database fa-2x text-info mb-2"></i>
|
||||
<h6>Redis缓存</h6>
|
||||
<small class="text-muted">高性能缓存</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<i class="fas fa-tachometer-alt fa-2x text-warning mb-2"></i>
|
||||
<h6>接口限流</h6>
|
||||
<small class="text-muted">防刷机制</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码显示/隐藏切换
|
||||
$('#togglePassword').click(function () {
|
||||
const passwordField = $('#password');
|
||||
const icon = $(this).find('i');
|
||||
|
||||
if (passwordField.attr('type') === 'password') {
|
||||
passwordField.attr('type', 'text');
|
||||
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.attr('type', 'password');
|
||||
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
$('#loginForm').submit(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const username = $('#username').val().trim();
|
||||
const password = $('#password').val();
|
||||
|
||||
// 基本验证
|
||||
if (!username) {
|
||||
showFieldError('username', '请输入用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
showFieldError('password', '请输入密码');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showFieldError('password', '密码长度至少6位');
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的错误状态
|
||||
clearFieldErrors();
|
||||
|
||||
// 显示加载状态
|
||||
const loginBtn = $('#loginBtn');
|
||||
const originalText = loginBtn.html();
|
||||
loginBtn.html('<i class="fas fa-spinner fa-spin"></i> 登录中...').prop('disabled', true);
|
||||
|
||||
// 发送登录请求
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/user/login',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('登录成功,正在跳转...', 'success');
|
||||
|
||||
// 保存记住我状态
|
||||
if ($('#rememberMe').is(':checked')) {
|
||||
localStorage.setItem('rememberedUsername', username);
|
||||
} else {
|
||||
localStorage.removeItem('rememberedUsername');
|
||||
}
|
||||
|
||||
// 跳转到首页或之前访问的页面
|
||||
setTimeout(() => {
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('returnUrl');
|
||||
window.location.href = returnUrl || '${pageContext.request.contextPath}/';
|
||||
}, 1000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.status === 400) {
|
||||
const response = xhr.responseJSON;
|
||||
showMessage(response.message || '登录失败', 'error');
|
||||
} else {
|
||||
showMessage('网络错误,请稍后重试', 'error');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
// 恢复按钮状态
|
||||
loginBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 页面加载时检查记住的用户名
|
||||
const rememberedUsername = localStorage.getItem('rememberedUsername');
|
||||
if (rememberedUsername) {
|
||||
$('#username').val(rememberedUsername);
|
||||
$('#rememberMe').prop('checked', true);
|
||||
$('#password').focus();
|
||||
} else {
|
||||
$('#username').focus();
|
||||
}
|
||||
|
||||
// 回车键快速登录
|
||||
$(document).keypress(function (e) {
|
||||
if (e.which === 13) { // Enter键
|
||||
$('#loginForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 快速登录演示账号
|
||||
function quickLogin(username, password) {
|
||||
$('#username').val(username);
|
||||
$('#password').val(password);
|
||||
$('#loginForm').submit();
|
||||
}
|
||||
|
||||
// 显示字段错误
|
||||
function showFieldError(fieldName, message) {
|
||||
const field = $('#' + fieldName);
|
||||
field.addClass('is-invalid');
|
||||
field.siblings('.invalid-feedback').text(message);
|
||||
}
|
||||
|
||||
// 清除字段错误
|
||||
function clearFieldErrors() {
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
$('.invalid-feedback').text('');
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
function checkLoginStatus() {
|
||||
$.get('${pageContext.request.contextPath}/api/user/current')
|
||||
.done(function (response) {
|
||||
if (response.success) {
|
||||
// 已登录,跳转到首页
|
||||
window.location.href = '${pageContext.request.contextPath}/';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态(延迟执行,避免影响用户输入)
|
||||
// 注释掉自动检查,避免页面刷新影响用户输入
|
||||
// setTimeout(function() {
|
||||
// checkLoginStatus();
|
||||
// }, 5000);
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
383
src/main/webapp/WEB-INF/views/register.jsp
Normal file
383
src/main/webapp/WEB-INF/views/register.jsp
Normal file
@@ -0,0 +1,383 @@
|
||||
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||
|
||||
<c:set var="pageTitle" value="用户注册"/>
|
||||
<%@ include file="common/header.jsp" %>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-success text-white text-center">
|
||||
<h4 class="mb-0">
|
||||
<i class="fas fa-user-plus"></i> 用户注册
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="registerForm">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">
|
||||
<i class="fas fa-user"></i> 用户名 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="3-50个字符,支持字母数字下划线" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle"></i> 用户名将作为您的登录凭证
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">
|
||||
<i class="fas fa-lock"></i> 密码 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="至少6位字符" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="togglePassword">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback"></div>
|
||||
<div class="progress mt-1" style="height: 3px;">
|
||||
<div class="progress-bar" id="passwordStrength" role="progressbar"
|
||||
style="width: 0%"></div>
|
||||
</div>
|
||||
<small class="form-text text-muted" id="passwordStrengthText">密码强度:无</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmPassword" class="form-label">
|
||||
<i class="fas fa-lock"></i> 确认密码 <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword"
|
||||
placeholder="请再次输入密码" required>
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
<i class="fas fa-envelope"></i> 邮箱
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
placeholder="example@domain.com">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">
|
||||
<i class="fas fa-phone"></i> 手机号
|
||||
</label>
|
||||
<input type="tel" class="form-control" id="phone" name="phone"
|
||||
placeholder="请输入手机号">
|
||||
<div class="invalid-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="agreeTerms" required>
|
||||
<label class="form-check-label" for="agreeTerms">
|
||||
我已阅读并同意 <a href="#" data-bs-toggle="modal"
|
||||
data-bs-target="#termsModal">用户协议</a> 和
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#privacyModal">隐私政策</a>
|
||||
</label>
|
||||
<div class="invalid-feedback">请同意用户协议和隐私政策</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success" id="registerBtn">
|
||||
<i class="fas fa-user-plus"></i> 注册
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mb-2">已有账号?</p>
|
||||
<a href="${pageContext.request.contextPath}/login" class="btn btn-outline-primary">
|
||||
<i class="fas fa-sign-in-alt"></i> 立即登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户协议模态框 -->
|
||||
<div class="modal fade" id="termsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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">
|
||||
<h6>1. 服务条款</h6>
|
||||
<p>本系统为秒杀演示系统,仅供学习和演示使用。</p>
|
||||
|
||||
<h6>2. 用户责任</h6>
|
||||
<p>用户应当合理使用系统功能,不得进行恶意操作。</p>
|
||||
|
||||
<h6>3. 隐私保护</h6>
|
||||
<p>我们承诺保护用户隐私,不会泄露用户个人信息。</p>
|
||||
|
||||
<h6>4. 免责声明</h6>
|
||||
<p>本系统仅为演示目的,不承担任何商业责任。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
onclick="$('#agreeTerms').prop('checked', true)">同意
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐私政策模态框 -->
|
||||
<div class="modal fade" id="privacyModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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">
|
||||
<h6>信息收集</h6>
|
||||
<p>我们仅收集必要的用户信息用于系统功能实现。</p>
|
||||
|
||||
<h6>信息使用</h6>
|
||||
<p>收集的信息仅用于系统功能,不会用于其他目的。</p>
|
||||
|
||||
<h6>信息保护</h6>
|
||||
<p>我们采用适当的技术措施保护用户信息安全。</p>
|
||||
|
||||
<h6>信息共享</h6>
|
||||
<p>我们不会与第三方共享用户个人信息。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
onclick="$('#agreeTerms').prop('checked', true)">同意
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
// 密码显示/隐藏切换
|
||||
$('#togglePassword').click(function () {
|
||||
const passwordField = $('#password');
|
||||
const icon = $(this).find('i');
|
||||
|
||||
if (passwordField.attr('type') === 'password') {
|
||||
passwordField.attr('type', 'text');
|
||||
icon.removeClass('fa-eye').addClass('fa-eye-slash');
|
||||
} else {
|
||||
passwordField.attr('type', 'password');
|
||||
icon.removeClass('fa-eye-slash').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
|
||||
// 密码强度检测
|
||||
$('#password').on('input', function () {
|
||||
const password = $(this).val();
|
||||
const strength = calculatePasswordStrength(password);
|
||||
updatePasswordStrengthUI(strength);
|
||||
});
|
||||
|
||||
// 确认密码验证
|
||||
$('#confirmPassword').on('input', function () {
|
||||
const password = $('#password').val();
|
||||
const confirmPassword = $(this).val();
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
$(this).addClass('is-invalid');
|
||||
$(this).siblings('.invalid-feedback').text('两次输入的密码不一致');
|
||||
} else {
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
// 用户名实时验证
|
||||
$('#username').on('input', function () {
|
||||
const username = $(this).val();
|
||||
if (username.length >= 3) {
|
||||
checkUsernameAvailability(username);
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
$('#registerForm').submit(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
username: $('#username').val().trim(),
|
||||
password: $('#password').val(),
|
||||
confirmPassword: $('#confirmPassword').val(),
|
||||
email: $('#email').val().trim() || null,
|
||||
phone: $('#phone').val().trim() || null
|
||||
};
|
||||
|
||||
// 显示加载状态
|
||||
const registerBtn = $('#registerBtn');
|
||||
const originalText = registerBtn.html();
|
||||
registerBtn.html('<i class="fas fa-spinner fa-spin"></i> 注册中...').prop('disabled', true);
|
||||
|
||||
// 发送注册请求
|
||||
$.ajax({
|
||||
url: '${pageContext.request.contextPath}/api/user/register',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function (response) {
|
||||
if (response.success) {
|
||||
showMessage('注册成功!正在跳转到登录页面...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '${pageContext.request.contextPath}/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
showMessage(response.message, 'error');
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
if (xhr.status === 400) {
|
||||
const response = xhr.responseJSON;
|
||||
showMessage(response.message || '注册失败', 'error');
|
||||
} else {
|
||||
showMessage('网络错误,请稍后重试', 'error');
|
||||
}
|
||||
},
|
||||
complete: function () {
|
||||
// 恢复按钮状态
|
||||
registerBtn.html(originalText).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
function validateForm() {
|
||||
let isValid = true;
|
||||
|
||||
// 清除之前的错误状态
|
||||
$('.form-control').removeClass('is-invalid');
|
||||
|
||||
// 用户名验证
|
||||
const username = $('#username').val().trim();
|
||||
if (!username) {
|
||||
showFieldError('username', '请输入用户名');
|
||||
isValid = false;
|
||||
} else if (username.length < 3 || username.length > 50) {
|
||||
showFieldError('username', '用户名长度必须在3-50个字符之间');
|
||||
isValid = false;
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
showFieldError('username', '用户名只能包含字母、数字和下划线');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 密码验证
|
||||
const password = $('#password').val();
|
||||
if (!password) {
|
||||
showFieldError('password', '请输入密码');
|
||||
isValid = false;
|
||||
} else if (password.length < 6) {
|
||||
showFieldError('password', '密码长度至少6位');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 确认密码验证
|
||||
const confirmPassword = $('#confirmPassword').val();
|
||||
if (!confirmPassword) {
|
||||
showFieldError('confirmPassword', '请确认密码');
|
||||
isValid = false;
|
||||
} else if (password !== confirmPassword) {
|
||||
showFieldError('confirmPassword', '两次输入的密码不一致');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 邮箱验证(可选)
|
||||
const email = $('#email').val().trim();
|
||||
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
showFieldError('email', '请输入有效的邮箱地址');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 手机号验证(可选)
|
||||
const phone = $('#phone').val().trim();
|
||||
if (phone && !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
showFieldError('phone', '请输入有效的手机号');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// 协议同意验证
|
||||
if (!$('#agreeTerms').is(':checked')) {
|
||||
$('#agreeTerms').addClass('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// 计算密码强度
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
|
||||
if (password.length >= 6) score += 20;
|
||||
if (password.length >= 8) score += 20;
|
||||
if (/[a-z]/.test(password)) score += 20;
|
||||
if (/[A-Z]/.test(password)) score += 20;
|
||||
if (/[0-9]/.test(password)) score += 10;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score += 10;
|
||||
|
||||
return Math.min(score, 100);
|
||||
}
|
||||
|
||||
// 更新密码强度UI
|
||||
function updatePasswordStrengthUI(strength) {
|
||||
const progressBar = $('#passwordStrength');
|
||||
const strengthText = $('#passwordStrengthText');
|
||||
|
||||
let color, text;
|
||||
|
||||
if (strength < 30) {
|
||||
color = 'bg-danger';
|
||||
text = '弱';
|
||||
} else if (strength < 60) {
|
||||
color = 'bg-warning';
|
||||
text = '中等';
|
||||
} else if (strength < 80) {
|
||||
color = 'bg-info';
|
||||
text = '强';
|
||||
} else {
|
||||
color = 'bg-success';
|
||||
text = '很强';
|
||||
}
|
||||
|
||||
progressBar.removeClass('bg-danger bg-warning bg-info bg-success').addClass(color);
|
||||
progressBar.css('width', strength + '%');
|
||||
strengthText.text('密码强度:' + text);
|
||||
}
|
||||
|
||||
// 检查用户名可用性
|
||||
function checkUsernameAvailability(username) {
|
||||
// 这里可以添加实时检查用户名是否已存在的逻辑
|
||||
// 为了演示,暂时省略
|
||||
}
|
||||
|
||||
// 显示字段错误
|
||||
function showFieldError(fieldName, message) {
|
||||
const field = $('#' + fieldName);
|
||||
field.addClass('is-invalid');
|
||||
field.siblings('.invalid-feedback').text(message);
|
||||
}
|
||||
</script>
|
||||
|
||||
<%@ include file="common/footer.jsp" %>
|
||||
Reference in New Issue
Block a user