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