|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>管理货源页面</title>
|
|
|
|
|
<script src="js/chart.js"></script>
|
|
|
|
|
<style>
|
|
|
|
|
body {
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
max-width: 1200px;
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-bar {
|
|
|
|
|
background-color: #1677ff;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-bar h1 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-section {
|
|
|
|
|
background-color: white;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
position: relative;
|
|
|
|
|
height: 400px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls button {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: #1677ff;
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls button:hover {
|
|
|
|
|
background-color: #4096ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls button.active {
|
|
|
|
|
background-color: #0958d9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-info {
|
|
|
|
|
background-color: white;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
background-color: #f0f5ff;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-number {
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #666;
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supplies-section {
|
|
|
|
|
background-color: white;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supplies-section h3 {
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supplies-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supply-card {
|
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 15px;
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supply-card:hover {
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supply-card img {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
height: 200px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supply-info {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supply-info p {
|
|
|
|
|
margin: 5px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supply-title {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.supply-status {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-pending_review { background-color: #fff2cc; color: #d68910; }
|
|
|
|
|
.status-published { background-color: #d4edda; color: #155724; }
|
|
|
|
|
.status-sold_out { background-color: #f8d7da; color: #721c24; }
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 40px;
|
|
|
|
|
color: #666;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 标题栏操作按钮样式 */
|
|
|
|
|
.title-bar-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-bar-actions button {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
|
|
|
color: white;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-bar-actions button:hover {
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-bar-actions button.active {
|
|
|
|
|
background-color: white;
|
|
|
|
|
color: #1677ff;
|
|
|
|
|
border-color: white;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 日期输入框样式 */
|
|
|
|
|
.title-bar-actions input[type="date"] {
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
|
|
|
color: white;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-bar-actions input[type="date"]::-webkit-calendar-picker-indicator {
|
|
|
|
|
filter: invert(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-bar-actions input[type="date"]:hover {
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 自定义按钮样式 */
|
|
|
|
|
#customBtn {
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#customBtn:hover {
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
|
|
|
border-color: rgba(255, 255, 255, 0.6);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<!-- 图片预览样式 -->
|
|
|
|
|
<style>
|
|
|
|
|
/* 图片预览模态框 */
|
|
|
|
|
.preview-modal {
|
|
|
|
|
display: none;
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.9);
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-modal.active {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-content {
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
max-height: 90%;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.preview-content img,
|
|
|
|
|
.preview-content video {
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
max-height: 80vh;
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.close-preview {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: -30px;
|
|
|
|
|
right: -30px;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
|
|
|
color: white;
|
|
|
|
|
border: none;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
width: 30px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.close-preview:hover {
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 点击图片时的光标样式 */
|
|
|
|
|
.supply-card img,
|
|
|
|
|
.supply-card video {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<!-- 图片预览模态框 -->
|
|
|
|
|
<div id="previewModal" class="preview-modal">
|
|
|
|
|
<div class="preview-content">
|
|
|
|
|
<button id="closePreview" class="close-preview">×</button>
|
|
|
|
|
<div id="previewMedia"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="title-bar">
|
|
|
|
|
<div style="display: flex; align-items: center; gap: 15px;">
|
|
|
|
|
<button id="backBtn" style="background-color: rgba(255, 255, 255, 0.2); color: white; border: none; border-radius: 4px; padding: 8px 16px; cursor: pointer; font-size: 14px;">返回</button>
|
|
|
|
|
<h1 style="margin: 0;">管理货源页面</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="title-bar-actions">
|
|
|
|
|
<button id="todayBtn" class="active">今日</button>
|
|
|
|
|
<button id="yesterdayBtn">昨天</button>
|
|
|
|
|
<button id="beforeYesterdayBtn">前天</button>
|
|
|
|
|
<button id="weekBtn">本周</button>
|
|
|
|
|
<button id="monthBtn">本月</button>
|
|
|
|
|
<button id="allBtn">全部</button>
|
|
|
|
|
<div style="display: inline-flex; align-items: center; gap: 5px; margin-left: 10px;">
|
|
|
|
|
<input type="date" id="startDate" style="padding: 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;">
|
|
|
|
|
<span style="color: white;">-</span>
|
|
|
|
|
<input type="date" id="endDate" style="padding: 6px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px;">
|
|
|
|
|
<button id="customBtn" style="padding: 6px 12px; background-color: rgba(255, 255, 255, 0.2); color: white; border: 1px solid white; border-radius: 4px; cursor: pointer; font-size: 14px;">自定义</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="stats-info">
|
|
|
|
|
<div class="stats-grid">
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-number" id="totalSupplies">0</div>
|
|
|
|
|
<div class="stat-label">总货源数</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-number" id="totalUsers">0</div>
|
|
|
|
|
<div class="stat-label">活跃用户数</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
<div class="stat-number" id="avgPerUser">0</div>
|
|
|
|
|
<div class="stat-label">人均创建数</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="chart-section">
|
|
|
|
|
<h3>用户创建货源统计</h3>
|
|
|
|
|
<div class="chart-container">
|
|
|
|
|
<canvas id="suppliesChart"></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="supplies-section" id="suppliesSection" style="display: none;">
|
|
|
|
|
<h3 id="suppliesTitle">用户货源详情</h3>
|
|
|
|
|
<div class="supplies-grid" id="suppliesGrid">
|
|
|
|
|
<!-- 货源卡片将动态生成 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// 全局变量
|
|
|
|
|
let currentChart = null;
|
|
|
|
|
let currentFilter = 'today';
|
|
|
|
|
let suppliesData = [];
|
|
|
|
|
let usersData = [];
|
|
|
|
|
let chartData = [];
|
|
|
|
|
|
|
|
|
|
// WebSocket相关变量
|
|
|
|
|
let ws = null;
|
|
|
|
|
let wsReconnectAttempts = 0;
|
|
|
|
|
const maxReconnectAttempts = 5;
|
|
|
|
|
const reconnectDelay = 3000;
|
|
|
|
|
let wsUrl = '';
|
|
|
|
|
|
|
|
|
|
// 缓存相关变量
|
|
|
|
|
const CACHE_KEY = 'management_stats_cache';
|
|
|
|
|
const CACHE_EXPIRY = 5 * 60 * 1000; // 缓存5分钟
|
|
|
|
|
|
|
|
|
|
// 定时刷新相关
|
|
|
|
|
const REFRESH_INTERVAL = 30 * 1000; // 每30秒自动刷新一次
|
|
|
|
|
|
|
|
|
|
// DOM元素
|
|
|
|
|
const backBtn = document.getElementById('backBtn');
|
|
|
|
|
const todayBtn = document.getElementById('todayBtn');
|
|
|
|
|
const yesterdayBtn = document.getElementById('yesterdayBtn');
|
|
|
|
|
const beforeYesterdayBtn = document.getElementById('beforeYesterdayBtn');
|
|
|
|
|
const weekBtn = document.getElementById('weekBtn');
|
|
|
|
|
const monthBtn = document.getElementById('monthBtn');
|
|
|
|
|
const allBtn = document.getElementById('allBtn');
|
|
|
|
|
const startDateInput = document.getElementById('startDate');
|
|
|
|
|
const endDateInput = document.getElementById('endDate');
|
|
|
|
|
const customBtn = document.getElementById('customBtn');
|
|
|
|
|
const totalSuppliesEl = document.getElementById('totalSupplies');
|
|
|
|
|
const totalUsersEl = document.getElementById('totalUsers');
|
|
|
|
|
const avgPerUserEl = document.getElementById('avgPerUser');
|
|
|
|
|
const suppliesChartEl = document.getElementById('suppliesChart');
|
|
|
|
|
const suppliesSection = document.getElementById('suppliesSection');
|
|
|
|
|
const suppliesTitle = document.getElementById('suppliesTitle');
|
|
|
|
|
const suppliesGrid = document.getElementById('suppliesGrid');
|
|
|
|
|
// 图片预览相关DOM元素
|
|
|
|
|
const previewModal = document.getElementById('previewModal');
|
|
|
|
|
const closePreview = document.getElementById('closePreview');
|
|
|
|
|
const previewMedia = document.getElementById('previewMedia');
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
// 检查登录状态和角色
|
|
|
|
|
checkLoginAndRole();
|
|
|
|
|
|
|
|
|
|
initEventListeners();
|
|
|
|
|
initWebSocket();
|
|
|
|
|
loadStats(currentFilter);
|
|
|
|
|
|
|
|
|
|
// 设置定时刷新
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
if (currentFilter === 'custom') {
|
|
|
|
|
// 如果是自定义筛选,获取当前日期输入值
|
|
|
|
|
const startDate = startDateInput.value;
|
|
|
|
|
const endDate = endDateInput.value;
|
|
|
|
|
loadStats('custom', startDate, endDate);
|
|
|
|
|
} else {
|
|
|
|
|
loadStats(currentFilter);
|
|
|
|
|
}
|
|
|
|
|
}, REFRESH_INTERVAL);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 页面卸载时清理资源
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
|
|
// 关闭WebSocket连接
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
try {
|
|
|
|
|
ws.close(1000, '页面关闭');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('关闭WebSocket连接失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 初始化WebSocket连接
|
|
|
|
|
function initWebSocket() {
|
|
|
|
|
try {
|
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
|
|
wsUrl = `${protocol}//${window.location.host}`;
|
|
|
|
|
console.log('正在尝试连接WebSocket服务器:', wsUrl);
|
|
|
|
|
|
|
|
|
|
// 关闭现有的WebSocket连接,避免连接泄漏
|
|
|
|
|
if (ws) {
|
|
|
|
|
try {
|
|
|
|
|
ws.close(1000, '重新连接');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('关闭旧WebSocket连接失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建新的WebSocket连接
|
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
|
|
|
|
|
|
// 设置WebSocket事件监听器
|
|
|
|
|
ws.onopen = function(event) {
|
|
|
|
|
console.log('WebSocket连接已打开');
|
|
|
|
|
wsReconnectAttempts = 0; // 重置重连计数器
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onmessage = function(event) {
|
|
|
|
|
console.log('收到WebSocket消息:', event.data);
|
|
|
|
|
try {
|
|
|
|
|
handleWebSocketMessage(event.data);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('处理WebSocket消息失败:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onclose = function(event) {
|
|
|
|
|
console.log('WebSocket连接已关闭:', event.code, event.reason);
|
|
|
|
|
|
|
|
|
|
// 尝试重新连接,最多重试maxReconnectAttempts次
|
|
|
|
|
if (wsReconnectAttempts < maxReconnectAttempts) {
|
|
|
|
|
wsReconnectAttempts++;
|
|
|
|
|
console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${maxReconnectAttempts})...`);
|
|
|
|
|
setTimeout(initWebSocket, reconnectDelay);
|
|
|
|
|
} else {
|
|
|
|
|
console.error('WebSocket重连失败,已达到最大重试次数');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ws.onerror = function(error) {
|
|
|
|
|
console.error('WebSocket错误:', error);
|
|
|
|
|
console.error('WebSocket错误详情:', error.message);
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('WebSocket连接失败:', error);
|
|
|
|
|
|
|
|
|
|
// 尝试重新连接
|
|
|
|
|
if (wsReconnectAttempts < maxReconnectAttempts) {
|
|
|
|
|
wsReconnectAttempts++;
|
|
|
|
|
console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${maxReconnectAttempts})...`);
|
|
|
|
|
setTimeout(initWebSocket, reconnectDelay);
|
|
|
|
|
} else {
|
|
|
|
|
console.error('WebSocket重连失败,已达到最大重试次数');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理WebSocket消息
|
|
|
|
|
function handleWebSocketMessage(message) {
|
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(message);
|
|
|
|
|
console.log('处理WebSocket消息:', data);
|
|
|
|
|
|
|
|
|
|
// 对于任何类型的WebSocket消息,都重新加载数据,确保实时更新
|
|
|
|
|
if (data.type) {
|
|
|
|
|
console.log('收到WebSocket消息,刷新数据:', data.type);
|
|
|
|
|
if (currentFilter === 'custom') {
|
|
|
|
|
// 如果是自定义筛选,获取当前日期输入值
|
|
|
|
|
const startDate = startDateInput.value;
|
|
|
|
|
const endDate = endDateInput.value;
|
|
|
|
|
loadStats('custom', startDate, endDate);
|
|
|
|
|
} else {
|
|
|
|
|
loadStats(currentFilter);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('收到未知格式的WebSocket消息');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析WebSocket消息失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 向WebSocket服务器发送消息
|
|
|
|
|
function sendWebSocketMessage(message) {
|
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
|
|
try {
|
|
|
|
|
ws.send(typeof message === 'string' ? message : JSON.stringify(message));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('发送WebSocket消息失败:', error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('WebSocket未连接,无法发送消息');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查登录状态和角色
|
|
|
|
|
function checkLoginAndRole() {
|
|
|
|
|
const userInfoStr = localStorage.getItem('userInfo');
|
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
|
|
|
|
|
|
if (!userInfoStr || !token) {
|
|
|
|
|
// 未登录,跳转到登录页面
|
|
|
|
|
window.location.href = 'login.html';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const userInfo = JSON.parse(userInfoStr);
|
|
|
|
|
// 检查是否为管理员角色
|
|
|
|
|
if (userInfo.projectName !== '管理员') {
|
|
|
|
|
// 不是管理员,根据角色跳转到相应页面
|
|
|
|
|
if (userInfo.projectName === '采购员') {
|
|
|
|
|
window.location.href = 'supply.html';
|
|
|
|
|
} else {
|
|
|
|
|
window.location.href = 'Reject.html';
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('解析用户信息失败:', e);
|
|
|
|
|
// 解析失败,跳转到登录页面
|
|
|
|
|
window.location.href = 'login.html';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示媒体预览
|
|
|
|
|
function showPreview(mediaUrl) {
|
|
|
|
|
// 清空预览内容
|
|
|
|
|
previewMedia.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// 检查是图片还是视频
|
|
|
|
|
const isVideo = mediaUrl.startsWith('data:video/') || mediaUrl.match(/\.(mp4|mov|avi|wmv|flv|webm|mkv)$/i);
|
|
|
|
|
|
|
|
|
|
if (isVideo) {
|
|
|
|
|
// 视频预览
|
|
|
|
|
const video = document.createElement('video');
|
|
|
|
|
video.src = mediaUrl;
|
|
|
|
|
video.controls = true;
|
|
|
|
|
previewMedia.appendChild(video);
|
|
|
|
|
} else {
|
|
|
|
|
// 图片预览
|
|
|
|
|
const img = document.createElement('img');
|
|
|
|
|
img.src = mediaUrl;
|
|
|
|
|
previewMedia.appendChild(img);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 显示预览模态框
|
|
|
|
|
previewModal.classList.add('active');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关闭媒体预览
|
|
|
|
|
function closePreviewModal() {
|
|
|
|
|
previewModal.classList.remove('active');
|
|
|
|
|
// 清空预览内容,释放资源
|
|
|
|
|
previewMedia.innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化事件监听器
|
|
|
|
|
function initEventListeners() {
|
|
|
|
|
// 返回按钮点击事件
|
|
|
|
|
backBtn.addEventListener('click', () => {
|
|
|
|
|
window.location.href = 'Reject.html';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 时间筛选按钮
|
|
|
|
|
todayBtn.addEventListener('click', () => setFilter('today'));
|
|
|
|
|
yesterdayBtn.addEventListener('click', () => setFilter('yesterday'));
|
|
|
|
|
beforeYesterdayBtn.addEventListener('click', () => setFilter('beforeYesterday'));
|
|
|
|
|
weekBtn.addEventListener('click', () => setFilter('week'));
|
|
|
|
|
monthBtn.addEventListener('click', () => setFilter('month'));
|
|
|
|
|
allBtn.addEventListener('click', () => setFilter('all'));
|
|
|
|
|
|
|
|
|
|
// 自定义时间筛选按钮
|
|
|
|
|
customBtn.addEventListener('click', applyCustomFilter);
|
|
|
|
|
|
|
|
|
|
// 日期输入框改变时,自动清除预设筛选的active状态
|
|
|
|
|
startDateInput.addEventListener('change', () => {
|
|
|
|
|
if (startDateInput.value || endDateInput.value) {
|
|
|
|
|
[todayBtn, yesterdayBtn, beforeYesterdayBtn, weekBtn, monthBtn, allBtn].forEach(btn => btn.classList.remove('active'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
endDateInput.addEventListener('change', () => {
|
|
|
|
|
if (startDateInput.value || endDateInput.value) {
|
|
|
|
|
[todayBtn, yesterdayBtn, beforeYesterdayBtn, weekBtn, monthBtn, allBtn].forEach(btn => btn.classList.remove('active'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 图片预览相关事件
|
|
|
|
|
closePreview.addEventListener('click', closePreviewModal);
|
|
|
|
|
|
|
|
|
|
// 点击模态框背景关闭预览
|
|
|
|
|
previewModal.addEventListener('click', (e) => {
|
|
|
|
|
if (e.target === previewModal) {
|
|
|
|
|
closePreviewModal();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 按ESC键关闭预览
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Escape' && previewModal.classList.contains('active')) {
|
|
|
|
|
closePreviewModal();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置筛选条件
|
|
|
|
|
function setFilter(filter) {
|
|
|
|
|
currentFilter = filter;
|
|
|
|
|
|
|
|
|
|
// 更新按钮状态
|
|
|
|
|
[todayBtn, yesterdayBtn, beforeYesterdayBtn, weekBtn, monthBtn, allBtn].forEach(btn => btn.classList.remove('active'));
|
|
|
|
|
document.getElementById(filter + 'Btn').classList.add('active');
|
|
|
|
|
|
|
|
|
|
// 清空自定义日期输入
|
|
|
|
|
startDateInput.value = '';
|
|
|
|
|
endDateInput.value = '';
|
|
|
|
|
|
|
|
|
|
// 隐藏货源详情
|
|
|
|
|
suppliesSection.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
// 重新加载数据
|
|
|
|
|
loadStats(filter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 应用自定义时间筛选
|
|
|
|
|
function applyCustomFilter() {
|
|
|
|
|
const startDate = startDateInput.value;
|
|
|
|
|
const endDate = endDateInput.value;
|
|
|
|
|
|
|
|
|
|
if (!startDate && !endDate) {
|
|
|
|
|
// 没有选择日期,默认显示全部
|
|
|
|
|
setFilter('all');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新当前筛选条件为自定义
|
|
|
|
|
currentFilter = 'custom';
|
|
|
|
|
|
|
|
|
|
// 清除预设筛选的active状态
|
|
|
|
|
[todayBtn, yesterdayBtn, beforeYesterdayBtn, weekBtn, monthBtn, allBtn].forEach(btn => btn.classList.remove('active'));
|
|
|
|
|
|
|
|
|
|
// 隐藏货源详情
|
|
|
|
|
suppliesSection.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
// 重新加载数据,传递自定义日期范围
|
|
|
|
|
loadStats('custom', startDate, endDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 从缓存中获取数据
|
|
|
|
|
function getCachedData(filter) {
|
|
|
|
|
try {
|
|
|
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
|
|
|
if (cached) {
|
|
|
|
|
const cache = JSON.parse(cached);
|
|
|
|
|
if (cache[filter] && (Date.now() - cache[filter].timestamp < CACHE_EXPIRY)) {
|
|
|
|
|
console.log('使用缓存数据:', filter);
|
|
|
|
|
return cache[filter].data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('读取缓存失败:', error);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存数据到缓存
|
|
|
|
|
function saveCachedData(filter, data) {
|
|
|
|
|
try {
|
|
|
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
|
|
|
let cache = cached ? JSON.parse(cached) : {};
|
|
|
|
|
cache[filter] = {
|
|
|
|
|
data: data,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
};
|
|
|
|
|
localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
|
|
|
|
|
console.log('保存数据到缓存:', filter);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存缓存失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 加载统计数据
|
|
|
|
|
async function loadStats(filter, startDate = '', endDate = '') {
|
|
|
|
|
try {
|
|
|
|
|
// 构建API请求URL,包含自定义日期参数
|
|
|
|
|
let url = `/api/admin/stats/supplies?filter=${filter}`;
|
|
|
|
|
if (filter === 'custom') {
|
|
|
|
|
if (startDate) url += `&startDate=${startDate}`;
|
|
|
|
|
if (endDate) url += `&endDate=${endDate}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
// 更新统计信息
|
|
|
|
|
updateStatsInfo(data.data.stats);
|
|
|
|
|
|
|
|
|
|
// 渲染图表并保存chartData
|
|
|
|
|
renderChart(data.data.chartData);
|
|
|
|
|
chartData = data.data.chartData;
|
|
|
|
|
|
|
|
|
|
// 保存到全局变量
|
|
|
|
|
suppliesData = data.data.suppliesData;
|
|
|
|
|
usersData = data.data.usersData;
|
|
|
|
|
|
|
|
|
|
// 保存到缓存,自定义日期使用特殊的缓存键
|
|
|
|
|
const cacheKey = filter === 'custom' ? `custom_${startDate}_${endDate}` : filter;
|
|
|
|
|
saveCachedData(cacheKey, data.data);
|
|
|
|
|
} else {
|
|
|
|
|
console.error('加载统计数据失败:', data.message);
|
|
|
|
|
|
|
|
|
|
// 尝试使用缓存数据
|
|
|
|
|
const cacheKey = filter === 'custom' ? `custom_${startDate}_${endDate}` : filter;
|
|
|
|
|
const cachedData = getCachedData(cacheKey);
|
|
|
|
|
if (cachedData) {
|
|
|
|
|
updateStatsInfo(cachedData.stats);
|
|
|
|
|
renderChart(cachedData.chartData);
|
|
|
|
|
chartData = cachedData.chartData;
|
|
|
|
|
suppliesData = cachedData.suppliesData;
|
|
|
|
|
usersData = cachedData.usersData;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载统计数据出错:', error);
|
|
|
|
|
|
|
|
|
|
// 尝试使用缓存数据
|
|
|
|
|
const cacheKey = filter === 'custom' ? `custom_${startDate}_${endDate}` : filter;
|
|
|
|
|
const cachedData = getCachedData(cacheKey);
|
|
|
|
|
if (cachedData) {
|
|
|
|
|
updateStatsInfo(cachedData.stats);
|
|
|
|
|
renderChart(cachedData.chartData);
|
|
|
|
|
chartData = cachedData.chartData;
|
|
|
|
|
suppliesData = cachedData.suppliesData;
|
|
|
|
|
usersData = cachedData.usersData;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新统计信息卡片
|
|
|
|
|
function updateStatsInfo(stats) {
|
|
|
|
|
totalSuppliesEl.textContent = stats.totalSupplies;
|
|
|
|
|
totalUsersEl.textContent = stats.totalUsers;
|
|
|
|
|
avgPerUserEl.textContent = Math.round(stats.avgPerUser);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染图表
|
|
|
|
|
function renderChart(chartData) {
|
|
|
|
|
// 销毁现有图表
|
|
|
|
|
if (currentChart) {
|
|
|
|
|
currentChart.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 准备图表数据
|
|
|
|
|
const labels = chartData.map(item => item.nickName || item.sellerId);
|
|
|
|
|
const counts = chartData.map(item => item.count);
|
|
|
|
|
|
|
|
|
|
// 创建新图表
|
|
|
|
|
currentChart = new Chart(suppliesChartEl, {
|
|
|
|
|
type: 'bar',
|
|
|
|
|
data: {
|
|
|
|
|
labels: labels,
|
|
|
|
|
datasets: [{
|
|
|
|
|
label: '创建货源数量',
|
|
|
|
|
data: counts,
|
|
|
|
|
backgroundColor: 'rgba(22, 119, 255, 0.6)',
|
|
|
|
|
borderColor: 'rgba(22, 119, 255, 1)',
|
|
|
|
|
borderWidth: 1
|
|
|
|
|
}]
|
|
|
|
|
},
|
|
|
|
|
options: {
|
|
|
|
|
responsive: true,
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
scales: {
|
|
|
|
|
y: {
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
ticks: {
|
|
|
|
|
stepSize: 1
|
|
|
|
|
},
|
|
|
|
|
// 动态计算Y轴最大值,为顶部数字留出空间
|
|
|
|
|
max: function(context) {
|
|
|
|
|
const max = Math.max(...context.chart.data.datasets[0].data);
|
|
|
|
|
return max + 1;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
x: {
|
|
|
|
|
ticks: {
|
|
|
|
|
autoSkip: true,
|
|
|
|
|
maxRotation: 0,
|
|
|
|
|
minRotation: 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
plugins: {
|
|
|
|
|
tooltip: {
|
|
|
|
|
callbacks: {
|
|
|
|
|
title: function(context) {
|
|
|
|
|
const index = context[0].dataIndex;
|
|
|
|
|
return `${chartData[index].nickName || chartData[index].sellerId}`;
|
|
|
|
|
},
|
|
|
|
|
label: function(context) {
|
|
|
|
|
return `创建数量: ${context.parsed.y}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
display: false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onClick: function(event, elements) {
|
|
|
|
|
if (elements.length > 0) {
|
|
|
|
|
const index = elements[0].index;
|
|
|
|
|
const sellerId = chartData[index].sellerId;
|
|
|
|
|
showSuppliesBySeller(sellerId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加Chart.js自定义插件用于显示数据标签
|
|
|
|
|
Chart.register({
|
|
|
|
|
id: 'customDataLabels',
|
|
|
|
|
afterDraw: function(chart) {
|
|
|
|
|
const ctx = chart.ctx;
|
|
|
|
|
chart.data.datasets.forEach(function(dataset, datasetIndex) {
|
|
|
|
|
const meta = chart.getDatasetMeta(datasetIndex);
|
|
|
|
|
if (meta.hidden) return;
|
|
|
|
|
|
|
|
|
|
meta.data.forEach(function(element, index) {
|
|
|
|
|
// 获取柱子的位置
|
|
|
|
|
const x = element.x;
|
|
|
|
|
const y = element.y; // 柱子顶部的Y坐标
|
|
|
|
|
const value = dataset.data[index];
|
|
|
|
|
|
|
|
|
|
// 设置文本样式
|
|
|
|
|
ctx.fillStyle = '#333';
|
|
|
|
|
ctx.font = 'bold 14px Arial';
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
ctx.textBaseline = 'bottom';
|
|
|
|
|
|
|
|
|
|
// 在柱子顶部上方5像素处绘制数值
|
|
|
|
|
ctx.fillText(value, x, y - 5);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 显示指定卖家的货源
|
|
|
|
|
async function showSuppliesBySeller(sellerId) {
|
|
|
|
|
// 获取当前筛选条件和日期范围
|
|
|
|
|
let startDate = '';
|
|
|
|
|
let endDate = '';
|
|
|
|
|
let filter = currentFilter;
|
|
|
|
|
|
|
|
|
|
// 如果是自定义筛选,获取日期输入值
|
|
|
|
|
if (filter === 'custom') {
|
|
|
|
|
startDate = startDateInput.value;
|
|
|
|
|
endDate = endDateInput.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建缓存键,包含自定义日期信息
|
|
|
|
|
const cacheKey = `supplies_${sellerId}_${filter}_${startDate}_${endDate}`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 构建API请求URL,包含自定义日期参数
|
|
|
|
|
let url = `/api/admin/supplies?sellerId=${sellerId}&filter=${filter}`;
|
|
|
|
|
if (filter === 'custom') {
|
|
|
|
|
if (startDate) url += `&startDate=${startDate}`;
|
|
|
|
|
if (endDate) url += `&endDate=${endDate}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
// 获取创建人的姓名
|
|
|
|
|
let creatorName = '';
|
|
|
|
|
|
|
|
|
|
// 从chartData中查找对应的nickName
|
|
|
|
|
const chartItem = chartData.find(item => item.sellerId === sellerId);
|
|
|
|
|
if (chartItem && chartItem.nickName) {
|
|
|
|
|
creatorName = chartItem.nickName;
|
|
|
|
|
}
|
|
|
|
|
// 如果chartData中没有,从返回的supplies中获取
|
|
|
|
|
else if (data.data.supplies && data.data.supplies.length > 0 && data.data.supplies[0].nickName) {
|
|
|
|
|
creatorName = data.data.supplies[0].nickName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建标题
|
|
|
|
|
let title = `${sellerId} 创建的货源`;
|
|
|
|
|
if (creatorName) {
|
|
|
|
|
title = `${creatorName} (${sellerId}) 创建的货源`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suppliesTitle.textContent = title;
|
|
|
|
|
renderSupplies(data.data.supplies);
|
|
|
|
|
suppliesSection.style.display = 'block';
|
|
|
|
|
// 滚动到货源详情区域
|
|
|
|
|
suppliesSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
|
|
|
|
|
|
// 保存到缓存
|
|
|
|
|
try {
|
|
|
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
|
|
|
let cache = cached ? JSON.parse(cached) : {};
|
|
|
|
|
cache[cacheKey] = {
|
|
|
|
|
data: data.data,
|
|
|
|
|
title: title,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
};
|
|
|
|
|
localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
|
|
|
|
|
console.log('保存货源详情到缓存:', cacheKey);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存货源详情缓存失败:', error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.error('加载货源数据失败:', data.message);
|
|
|
|
|
|
|
|
|
|
// 尝试使用缓存数据
|
|
|
|
|
try {
|
|
|
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
|
|
|
if (cached) {
|
|
|
|
|
const cache = JSON.parse(cached);
|
|
|
|
|
if (cache[cacheKey] && (Date.now() - cache[cacheKey].timestamp < CACHE_EXPIRY)) {
|
|
|
|
|
const cachedData = cache[cacheKey];
|
|
|
|
|
suppliesTitle.textContent = cachedData.title;
|
|
|
|
|
renderSupplies(cachedData.data.supplies);
|
|
|
|
|
suppliesSection.style.display = 'block';
|
|
|
|
|
suppliesSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
|
console.log('使用缓存的货源详情:', cacheKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('读取货源详情缓存失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载货源数据出错:', error);
|
|
|
|
|
|
|
|
|
|
// 尝试使用缓存数据
|
|
|
|
|
try {
|
|
|
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
|
|
|
if (cached) {
|
|
|
|
|
const cache = JSON.parse(cached);
|
|
|
|
|
if (cache[cacheKey] && (Date.now() - cache[cacheKey].timestamp < CACHE_EXPIRY)) {
|
|
|
|
|
const cachedData = cache[cacheKey];
|
|
|
|
|
suppliesTitle.textContent = cachedData.title;
|
|
|
|
|
renderSupplies(cachedData.data.supplies);
|
|
|
|
|
suppliesSection.style.display = 'block';
|
|
|
|
|
suppliesSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
|
console.log('使用缓存的货源详情:', cacheKey);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('读取货源详情缓存失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染货源卡片
|
|
|
|
|
function renderSupplies(supplies) {
|
|
|
|
|
suppliesGrid.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
if (supplies.length === 0) {
|
|
|
|
|
suppliesGrid.innerHTML = '<p style="text-align: center; color: #666; grid-column: 1 / -1;">暂无货源数据</p>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
supplies.forEach(supply => {
|
|
|
|
|
const card = document.createElement('div');
|
|
|
|
|
card.className = 'supply-card';
|
|
|
|
|
card.style.cssText = 'border: 1px solid #e8e8e8; border-radius: 8px; padding: 15px; transition: all 0.3s;';
|
|
|
|
|
card.style.width = '100%';
|
|
|
|
|
card.style.boxSizing = 'border-box';
|
|
|
|
|
|
|
|
|
|
// 解析媒体URL
|
|
|
|
|
let mediaUrl = '';
|
|
|
|
|
try {
|
|
|
|
|
const imageUrls = JSON.parse(supply.imageUrls || '[]');
|
|
|
|
|
if (imageUrls.length > 0) {
|
|
|
|
|
mediaUrl = imageUrls[0];
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('解析媒体URL失败:', e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 状态样式
|
|
|
|
|
const statusClass = `status-${supply.status}`;
|
|
|
|
|
let statusText = '待审核';
|
|
|
|
|
switch (supply.status) {
|
|
|
|
|
case 'published':
|
|
|
|
|
statusText = '已发布';
|
|
|
|
|
break;
|
|
|
|
|
case 'sold_out':
|
|
|
|
|
statusText = '已下架';
|
|
|
|
|
break;
|
|
|
|
|
case 'hidden':
|
|
|
|
|
statusText = '已隐藏';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否为视频文件
|
|
|
|
|
let mediaElement = '';
|
|
|
|
|
if (mediaUrl) {
|
|
|
|
|
const isVideo = mediaUrl.startsWith('data:video/') || mediaUrl.match(/\.(mp4|mov|avi|wmv|flv|webm|mkv)$/i);
|
|
|
|
|
if (isVideo) {
|
|
|
|
|
// 视频文件
|
|
|
|
|
mediaElement = `<video src="${mediaUrl}" alt="${supply.productName}" controls style="width: 100%; height: 250px; object-fit: contain; cursor: pointer; background-color: #f5f5f5; border-radius: 4px; margin-bottom: 10px;" onclick="showPreview('${mediaUrl}')"></video>`;
|
|
|
|
|
} else {
|
|
|
|
|
// 图片文件
|
|
|
|
|
mediaElement = `<img src="${mediaUrl}" alt="${supply.productName}" style="width: 100%; height: 250px; object-fit: contain; border-radius: 4px; margin-bottom: 10px; cursor: pointer; background-color: #f5f5f5;" onclick="showPreview('${mediaUrl}')">`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建详细信息HTML
|
|
|
|
|
let detailsHTML = '';
|
|
|
|
|
|
|
|
|
|
// 产品ID
|
|
|
|
|
if (supply.productId) {
|
|
|
|
|
detailsHTML += `<p style="margin: 5px 0; font-size: 14px;"><strong>产品ID:</strong> ${supply.productId}</p>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 规格+件数+采购价+销售价组合展示
|
|
|
|
|
let specifications = [];
|
|
|
|
|
let quantities = [];
|
|
|
|
|
let costprices = [];
|
|
|
|
|
let prices = [];
|
|
|
|
|
|
|
|
|
|
// 处理规格
|
|
|
|
|
if (supply.specification) {
|
|
|
|
|
if (typeof supply.specification === 'string') {
|
|
|
|
|
// 规格可能用逗号或中文逗号分隔
|
|
|
|
|
specifications = supply.specification.split(/[,,]/).filter(spec => spec.trim());
|
|
|
|
|
} else if (Array.isArray(supply.specification)) {
|
|
|
|
|
specifications = supply.specification;
|
|
|
|
|
} else {
|
|
|
|
|
specifications = [supply.specification];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理件数
|
|
|
|
|
if (supply.quantity) {
|
|
|
|
|
if (typeof supply.quantity === 'string') {
|
|
|
|
|
quantities = supply.quantity.split(',').filter(qty => qty.trim());
|
|
|
|
|
} else if (Array.isArray(supply.quantity)) {
|
|
|
|
|
quantities = supply.quantity;
|
|
|
|
|
} else {
|
|
|
|
|
quantities = [supply.quantity];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理采购价
|
|
|
|
|
if (supply.costprice) {
|
|
|
|
|
if (typeof supply.costprice === 'string') {
|
|
|
|
|
costprices = supply.costprice.split(',').filter(cp => cp.trim());
|
|
|
|
|
} else if (Array.isArray(supply.costprice)) {
|
|
|
|
|
costprices = supply.costprice;
|
|
|
|
|
} else {
|
|
|
|
|
costprices = [supply.costprice];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 处理销售价
|
|
|
|
|
if (supply.price) {
|
|
|
|
|
if (typeof supply.price === 'string') {
|
|
|
|
|
prices = supply.price.split(',').filter(p => p.trim());
|
|
|
|
|
} else if (Array.isArray(supply.price)) {
|
|
|
|
|
prices = supply.price;
|
|
|
|
|
} else {
|
|
|
|
|
prices = [supply.price];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算最大长度,确保每个规格都有对应的数据
|
|
|
|
|
const maxLength = Math.max(specifications.length, quantities.length, costprices.length, prices.length);
|
|
|
|
|
|
|
|
|
|
// 生成组合展示HTML
|
|
|
|
|
if (maxLength > 0) {
|
|
|
|
|
detailsHTML += `<div style="margin: 10px 0;">`;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < maxLength; i++) {
|
|
|
|
|
const spec = specifications[i] || '';
|
|
|
|
|
const quantity = quantities[i] || '';
|
|
|
|
|
const costprice = costprices[i] || '';
|
|
|
|
|
const price = prices[i] || '';
|
|
|
|
|
|
|
|
|
|
// 只显示有数据的行
|
|
|
|
|
if (spec || quantity || costprice || price) {
|
|
|
|
|
let specHtml = `规格${i+1}: ${spec}`;
|
|
|
|
|
let parts = [];
|
|
|
|
|
if (spec) parts.push(specHtml);
|
|
|
|
|
if (quantity) parts.push(`件数: ${quantity}件`);
|
|
|
|
|
if (costprice) parts.push(`采购价: ¥${costprice}`);
|
|
|
|
|
if (price) parts.push(`销售价: ¥${price}`);
|
|
|
|
|
|
|
|
|
|
detailsHTML += `<div style="background-color: #f5f5f5; padding: 8px 12px; border-radius: 4px; margin: 5px 0; font-size: 13px; display: block; word-break: break-all;">
|
|
|
|
|
${parts.join(' - ')}
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
detailsHTML += `</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 商品地区
|
|
|
|
|
if (supply.region) {
|
|
|
|
|
detailsHTML += `<p style="margin: 5px 0; font-size: 14px;"><strong>商品地区:</strong> ${supply.region}</p>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 商品联系人和联系电话一行展示,只显示联系人名称和电话号码
|
|
|
|
|
if (supply.product_contact || supply.contact_phone) {
|
|
|
|
|
let contactText = `<strong>商品联系人:</strong> `;
|
|
|
|
|
if (supply.product_contact) {
|
|
|
|
|
contactText += supply.product_contact;
|
|
|
|
|
}
|
|
|
|
|
if (supply.contact_phone) {
|
|
|
|
|
if (supply.product_contact) {
|
|
|
|
|
contactText += ` ${supply.contact_phone}`;
|
|
|
|
|
} else {
|
|
|
|
|
contactText += supply.contact_phone;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
detailsHTML += `<p style="margin: 5px 0; font-size: 14px;">${contactText}</p>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建时间
|
|
|
|
|
detailsHTML += `<p style="margin: 5px 0; font-size: 14px;"><strong>创建时间:</strong> ${new Date(supply.created_at).toLocaleString()}</p>`;
|
|
|
|
|
|
|
|
|
|
// 修改时间
|
|
|
|
|
if (supply.updated_at) {
|
|
|
|
|
detailsHTML += `<p style="margin: 5px 0; font-size: 14px;"><strong>修改时间:</strong> ${new Date(supply.updated_at).toLocaleString()}</p>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 货源描述(限制显示长度)- 灰色背景放在修改时间后面
|
|
|
|
|
if (supply.description) {
|
|
|
|
|
const shortDesc = supply.description.length > 50 ? `${supply.description.substring(0, 50)}...` : supply.description;
|
|
|
|
|
detailsHTML += `<div style="margin: 10px 0; font-size: 14px; background-color: #f5f5f5; padding: 10px 12px; border-radius: 4px;">
|
|
|
|
|
<strong style="display: block; margin-bottom: 5px;">货源描述:</strong>
|
|
|
|
|
<div>${shortDesc}</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 状态
|
|
|
|
|
detailsHTML += `<span class="supply-status ${statusClass}" style="display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; margin-top: 5px;">${statusText}</span>`;
|
|
|
|
|
|
|
|
|
|
// 第一行展示:种类|蛋黄|货源类型|产品包装|新鲜程度,只展示数据
|
|
|
|
|
let firstLineParts = [];
|
|
|
|
|
if (supply.category) firstLineParts.push(supply.category);
|
|
|
|
|
if (supply.yolk) firstLineParts.push(supply.yolk);
|
|
|
|
|
if (supply.sourceType) firstLineParts.push(supply.sourceType);
|
|
|
|
|
if (supply.producting) firstLineParts.push(supply.producting);
|
|
|
|
|
if (supply.freshness) firstLineParts.push(supply.freshness);
|
|
|
|
|
|
|
|
|
|
let firstLineHTML = '';
|
|
|
|
|
if (firstLineParts.length > 0) {
|
|
|
|
|
firstLineHTML = `<p style="margin: 5px 0 10px 0; font-size: 14px; line-height: 1.4; color: #666; font-weight: 500;">${firstLineParts.join(' | ')}</p>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
card.innerHTML = `
|
|
|
|
|
<div style="margin-bottom: 15px;">
|
|
|
|
|
${mediaElement}
|
|
|
|
|
<div class="supply-title" style="font-weight: bold; font-size: 16px; margin: 10px 0;">${supply.productName}</div>
|
|
|
|
|
${firstLineHTML}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="supply-info" style="font-size: 14px;">
|
|
|
|
|
${detailsHTML}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// 悬停效果
|
|
|
|
|
card.addEventListener('mouseenter', () => {
|
|
|
|
|
card.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
|
|
|
|
|
card.style.transform = 'translateY(-2px)';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
card.addEventListener('mouseleave', () => {
|
|
|
|
|
card.style.boxShadow = 'none';
|
|
|
|
|
card.style.transform = 'translateY(0)';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
suppliesGrid.appendChild(card);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|