You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2621 lines
101 KiB
2621 lines
101 KiB
<!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(min(100%, 350px), 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,
|
|
.supply-card video {
|
|
max-width: 100%;
|
|
height: 180px;
|
|
object-fit: contain;
|
|
border-radius: 4px;
|
|
margin-bottom: 10px;
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
/* 响应式设计:调整小屏幕设备上的样式 */
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 10px;
|
|
}
|
|
|
|
.title-bar {
|
|
padding: 12px 15px;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.title-bar h1 {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.title-bar-actions {
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
width: 100%;
|
|
}
|
|
|
|
.title-bar-actions button {
|
|
padding: 6px 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.title-bar-actions input[type="date"] {
|
|
padding: 4px 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.chart-container {
|
|
height: 300px;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 15px;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 28px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.supplies-grid {
|
|
gap: 15px;
|
|
}
|
|
|
|
.supply-card {
|
|
padding: 12px;
|
|
}
|
|
|
|
.supply-card img,
|
|
.supply-card video {
|
|
height: 150px;
|
|
}
|
|
|
|
.supply-title {
|
|
font-size: 15px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.supply-info {
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.title-bar h1 {
|
|
font-size: 18px;
|
|
}
|
|
|
|
.title-bar-actions {
|
|
gap: 4px;
|
|
}
|
|
|
|
.title-bar-actions button {
|
|
padding: 5px 10px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.title-bar-actions input[type="date"] {
|
|
padding: 3px 6px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.chart-container {
|
|
height: 250px;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.supplies-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.supply-card img,
|
|
.supply-card video {
|
|
height: 140px;
|
|
}
|
|
|
|
.supply-title {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.supply-info {
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
|
|
.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;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
/* 可缩放的图片容器 */
|
|
.preview-media {
|
|
position: relative;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
max-width: 100%;
|
|
max-height: 80vh;
|
|
overflow: visible;
|
|
}
|
|
|
|
/* 可缩放的图片样式 */
|
|
.preview-media img {
|
|
cursor: grab;
|
|
transform-origin: center center;
|
|
user-select: none;
|
|
}
|
|
|
|
.preview-media img:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
/* 缩放控制按钮 */
|
|
.zoom-controls {
|
|
position: absolute;
|
|
bottom: -40px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 10px;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.zoom-btn {
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 30px;
|
|
height: 30px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.zoom-btn:hover {
|
|
background-color: rgba(255, 255, 255, 0.4);
|
|
}
|
|
|
|
.zoom-value {
|
|
color: white;
|
|
font-size: 14px;
|
|
align-self: center;
|
|
min-width: 50px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* 关闭按钮 */
|
|
.close-preview {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
color: white;
|
|
border: none;
|
|
font-size: 24px;
|
|
cursor: pointer;
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
transition: all 0.3s ease;
|
|
z-index: 1001; /* 确保关闭按钮在最上层 */
|
|
}
|
|
|
|
.close-preview:hover {
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* 点击图片时的光标样式 */
|
|
.supply-card img,
|
|
.supply-card video {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* 回到顶部按钮样式 */
|
|
.back-to-top {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background-color: rgba(153, 153, 153, 0.7); /* 浅灰色透明背景 */
|
|
color: #333;
|
|
border: none;
|
|
cursor: pointer;
|
|
z-index: 1000;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.back-to-top-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.back-to-top-text {
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
line-height: 1;
|
|
}
|
|
|
|
.back-to-top:hover {
|
|
background-color: rgba(102, 102, 102, 0.9); /* 加深颜色 */
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.back-to-top:active {
|
|
transform: translateY(0);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
/* 货源卡片优化样式 */
|
|
.supply-media {
|
|
width: 100%;
|
|
height: 250px;
|
|
object-fit: contain;
|
|
border-radius: 4px;
|
|
margin-bottom: 10px;
|
|
cursor: pointer;
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.supply-card {
|
|
border: 1px solid #e8e8e8;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
transition: all 0.3s ease;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.supply-card:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.supply-header {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.supply-title {
|
|
font-weight: bold;
|
|
font-size: 16px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.supply-meta {
|
|
margin: 5px 0 10px 0;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
color: #666;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.supply-info {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.supply-info p {
|
|
margin: 5px 0;
|
|
}
|
|
|
|
.spec-section {
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.spec-item {
|
|
background-color: #f5f5f5;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
margin: 5px 0;
|
|
font-size: 13px;
|
|
display: block;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.description-section {
|
|
margin: 10px 0;
|
|
font-size: 14px;
|
|
background-color: #f5f5f5;
|
|
padding: 10px 12px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.description-section strong {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.log-section {
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.log-section h4 {
|
|
margin: 0 0 10px 0;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
padding-bottom: 5px;
|
|
border-bottom: 2px solid #ff4d4f;
|
|
}
|
|
|
|
.log-item {
|
|
margin: 8px 0;
|
|
padding: 8px 12px;
|
|
background-color: #f9f0f0;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.log-badge {
|
|
display: inline-block;
|
|
background-color: #ff4d4f;
|
|
color: white;
|
|
font-size: 10px;
|
|
font-weight: bold;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.supply-status {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 12px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
color: #666;
|
|
grid-column: 1 / -1;
|
|
}
|
|
</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>
|
|
|
|
<!-- 回到顶部按钮 -->
|
|
<button id="backToTop" class="back-to-top" onclick="scrollToSuppliesSection()" style="display: none;">
|
|
<div class="back-to-top-content">
|
|
<span class="back-to-top-text">Top</span>
|
|
</div>
|
|
</button>
|
|
|
|
<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: block;">
|
|
<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 = [];
|
|
|
|
// 缓存相关变量
|
|
let suppliesCache = {}; // 货源数据缓存
|
|
let cacheExpiry = 5 * 60 * 1000; // 缓存过期时间(5分钟)
|
|
|
|
// 分页相关变量
|
|
let currentPage = 1; // 当前页码
|
|
let pageSize = 20; // 每页显示数量
|
|
let totalItems = 0; // 总数据量
|
|
let totalPages = 0; // 总页数
|
|
|
|
// 虚拟滚动相关变量
|
|
let virtualScrollEnabled = false; // 是否启用虚拟滚动
|
|
let itemHeight = 600; // 估计的卡片高度
|
|
let visibleCount = 5; // 可见区域显示的卡片数量
|
|
let bufferCount = 2; // 缓冲区卡片数量
|
|
|
|
// 货源详情相关变量
|
|
let currentSellerId = null; // 当前正在显示的卖家ID
|
|
|
|
// WebSocket相关变量
|
|
let ws = null;
|
|
let wsReconnectAttempts = 0;
|
|
const maxReconnectAttempts = 10; // 增加最大重连次数
|
|
const reconnectDelay = 5000; // 增加重连延迟,避免频繁重连
|
|
let wsUrl = '';
|
|
let wsConnecting = false; // 防止重复连接
|
|
|
|
// 缓存相关变量
|
|
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();
|
|
optimizeEventListeners(); // 优化事件监听器
|
|
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() {
|
|
// 防止重复连接
|
|
if (wsConnecting) {
|
|
console.log('WebSocket连接正在建立中,请勿重复调用');
|
|
return;
|
|
}
|
|
|
|
// 检查是否已经有活跃连接
|
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
console.log('WebSocket连接已存在或正在建立中,无需重新连接');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
wsConnecting = true;
|
|
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);
|
|
} finally {
|
|
ws = null;
|
|
}
|
|
}
|
|
|
|
// 创建新的WebSocket连接
|
|
ws = new WebSocket(wsUrl);
|
|
|
|
// 设置WebSocket事件监听器
|
|
ws.onopen = function(event) {
|
|
console.log('WebSocket连接已打开');
|
|
wsReconnectAttempts = 0; // 重置重连计数器
|
|
wsConnecting = false;
|
|
};
|
|
|
|
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);
|
|
wsConnecting = false;
|
|
ws = null;
|
|
|
|
// 尝试重新连接,最多重试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);
|
|
wsConnecting = false;
|
|
};
|
|
} catch (error) {
|
|
console.error('WebSocket连接失败:', error);
|
|
wsConnecting = false;
|
|
ws = null;
|
|
|
|
// 尝试重新连接
|
|
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);
|
|
|
|
// 只有当消息类型与货源相关时,才重新加载数据
|
|
if (data.type && (data.type === 'supply_created' || data.type === 'supply_updated' || data.type === 'supply_deleted' || data.type === 'supply_status_change')) {
|
|
console.log('收到货源相关WebSocket消息,刷新数据:', data.type);
|
|
|
|
// 保存当前滚动位置
|
|
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
|
|
// 重新加载统计数据
|
|
if (currentFilter === 'custom') {
|
|
// 如果是自定义筛选,获取当前日期输入值
|
|
const startDate = startDateInput.value;
|
|
const endDate = endDateInput.value;
|
|
loadStats('custom', startDate, endDate);
|
|
} else {
|
|
loadStats(currentFilter);
|
|
}
|
|
|
|
// 如果正在显示货源详情,重新加载货源数据
|
|
if (currentSellerId) {
|
|
console.log('正在显示货源详情,重新加载数据');
|
|
showSuppliesBySeller(currentSellerId);
|
|
}
|
|
|
|
// 恢复滚动位置
|
|
window.scrollTo(0, scrollPosition);
|
|
} else {
|
|
console.warn('收到未知格式的WebSocket消息,不刷新数据:', data.type);
|
|
}
|
|
} 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 = 'SupplierReview.html';
|
|
}
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.error('解析用户信息失败:', e);
|
|
// 解析失败,跳转到登录页面
|
|
window.location.href = 'login.html';
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 缩放相关变量
|
|
let currentScale = 1;
|
|
let minScale = 0.5;
|
|
let maxScale = 5;
|
|
let isDragging = false;
|
|
let startX = 0;
|
|
let startY = 0;
|
|
let translateX = 0;
|
|
let translateY = 0;
|
|
let currentMedia = null;
|
|
|
|
// 显示媒体预览
|
|
function showPreview(mediaUrl) {
|
|
// 重置缩放相关变量
|
|
currentScale = 1;
|
|
translateX = 0;
|
|
translateY = 0;
|
|
isDragging = false;
|
|
currentMedia = null;
|
|
|
|
// 清空预览内容
|
|
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);
|
|
currentMedia = video;
|
|
} else {
|
|
// 图片预览
|
|
const img = document.createElement('img');
|
|
img.src = mediaUrl;
|
|
previewMedia.appendChild(img);
|
|
currentMedia = img;
|
|
|
|
// 添加缩放控制按钮
|
|
const zoomControls = document.createElement('div');
|
|
zoomControls.className = 'zoom-controls';
|
|
zoomControls.innerHTML = `
|
|
<button class="zoom-btn" id="zoomOutBtn" title="缩小">−</button>
|
|
<span class="zoom-value" id="zoomValue">100%</span>
|
|
<button class="zoom-btn" id="zoomInBtn" title="放大">+</button>
|
|
<button class="zoom-btn" id="resetZoomBtn" title="重置">⟲</button>
|
|
`;
|
|
previewMedia.appendChild(zoomControls);
|
|
|
|
// 等待图片加载完成后添加事件监听器
|
|
img.onload = function() {
|
|
addZoomEvents(img);
|
|
addDragEvents(img);
|
|
setupZoomControls();
|
|
};
|
|
}
|
|
|
|
// 显示预览模态框
|
|
previewModal.classList.add('active');
|
|
}
|
|
|
|
// 添加缩放事件
|
|
function addZoomEvents(img) {
|
|
// 鼠标滚轮缩放
|
|
img.addEventListener('wheel', function(e) {
|
|
e.preventDefault();
|
|
|
|
// 计算缩放比例
|
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
const newScale = Math.max(minScale, Math.min(maxScale, currentScale + delta));
|
|
|
|
// 计算鼠标在图片上的相对位置
|
|
const rect = img.getBoundingClientRect();
|
|
const mouseX = e.clientX - rect.left;
|
|
const mouseY = e.clientY - rect.top;
|
|
|
|
// 计算缩放中心点
|
|
const centerX = mouseX / rect.width;
|
|
const centerY = mouseY / rect.height;
|
|
|
|
// 计算新的偏移量,使缩放中心点保持在鼠标位置
|
|
const oldScale = currentScale;
|
|
currentScale = newScale;
|
|
|
|
translateX += (mouseX - rect.width * centerX) * (1 - currentScale / oldScale);
|
|
translateY += (mouseY - rect.height * centerY) * (1 - currentScale / oldScale);
|
|
|
|
// 应用变换
|
|
applyTransform(img);
|
|
updateZoomValue();
|
|
});
|
|
}
|
|
|
|
// 添加拖拽事件
|
|
function addDragEvents(img) {
|
|
// 鼠标按下开始拖拽
|
|
img.addEventListener('mousedown', function(e) {
|
|
if (currentScale === 1) return; // 只有在缩放后才能拖拽
|
|
|
|
isDragging = true;
|
|
startX = e.clientX - translateX;
|
|
startY = e.clientY - translateY;
|
|
});
|
|
|
|
// 鼠标移动时拖拽
|
|
document.addEventListener('mousemove', function(e) {
|
|
if (!isDragging) return;
|
|
|
|
translateX = e.clientX - startX;
|
|
translateY = e.clientY - startY;
|
|
|
|
applyTransform(img);
|
|
});
|
|
|
|
// 鼠标释放结束拖拽
|
|
document.addEventListener('mouseup', function() {
|
|
isDragging = false;
|
|
});
|
|
|
|
// 鼠标离开窗口结束拖拽
|
|
document.addEventListener('mouseleave', function() {
|
|
isDragging = false;
|
|
});
|
|
}
|
|
|
|
// 设置缩放控制按钮
|
|
function setupZoomControls() {
|
|
const zoomInBtn = document.getElementById('zoomInBtn');
|
|
const zoomOutBtn = document.getElementById('zoomOutBtn');
|
|
const resetZoomBtn = document.getElementById('resetZoomBtn');
|
|
|
|
// 放大按钮
|
|
zoomInBtn.addEventListener('click', function() {
|
|
if (currentScale < maxScale) {
|
|
currentScale += 0.1;
|
|
applyTransform(currentMedia);
|
|
updateZoomValue();
|
|
}
|
|
});
|
|
|
|
// 缩小按钮
|
|
zoomOutBtn.addEventListener('click', function() {
|
|
if (currentScale > minScale) {
|
|
currentScale -= 0.1;
|
|
applyTransform(currentMedia);
|
|
updateZoomValue();
|
|
}
|
|
});
|
|
|
|
// 重置按钮
|
|
resetZoomBtn.addEventListener('click', function() {
|
|
currentScale = 1;
|
|
translateX = 0;
|
|
translateY = 0;
|
|
applyTransform(currentMedia);
|
|
updateZoomValue();
|
|
});
|
|
}
|
|
|
|
// 应用变换
|
|
function applyTransform(media) {
|
|
if (!media || media.tagName === 'VIDEO') return;
|
|
|
|
media.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
|
|
}
|
|
|
|
// 更新缩放值显示
|
|
function updateZoomValue() {
|
|
const zoomValue = document.getElementById('zoomValue');
|
|
if (zoomValue) {
|
|
zoomValue.textContent = `${Math.round(currentScale * 100)}%`;
|
|
}
|
|
}
|
|
|
|
// 关闭媒体预览
|
|
function closePreviewModal() {
|
|
previewModal.classList.remove('active');
|
|
// 清空预览内容,释放资源
|
|
previewMedia.innerHTML = '';
|
|
currentMedia = null;
|
|
}
|
|
|
|
// 初始化事件监听器
|
|
function initEventListeners() {
|
|
// 返回按钮点击事件
|
|
backBtn.addEventListener('click', () => {
|
|
window.location.href = 'SupplierReview.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();
|
|
}
|
|
});
|
|
|
|
// 为预览模态框添加鼠标滚轮事件监听器,阻止页面滚动
|
|
previewModal.addEventListener('wheel', (e) => {
|
|
// 只在模态框激活时阻止滚动
|
|
if (previewModal.classList.contains('active')) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
|
|
// 滚动事件,用于显示/隐藏回到顶部按钮
|
|
window.addEventListener('scroll', () => {
|
|
const backToTopBtn = document.getElementById('backToTop');
|
|
// 当滚动距离超过300px时显示按钮
|
|
if (window.pageYOffset > 300) {
|
|
backToTopBtn.style.display = 'flex';
|
|
} else {
|
|
backToTopBtn.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
// 回到supplies-section顶部
|
|
function scrollToSuppliesSection() {
|
|
const suppliesSection = document.getElementById('suppliesSection');
|
|
suppliesSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
// 设置筛选条件
|
|
function setFilter(filter) {
|
|
currentFilter = filter;
|
|
|
|
// 重置当前卖家ID,确保切换时间筛选时显示所有货源
|
|
currentSellerId = null;
|
|
|
|
// 更新按钮状态
|
|
[todayBtn, yesterdayBtn, beforeYesterdayBtn, weekBtn, monthBtn, allBtn].forEach(btn => btn.classList.remove('active'));
|
|
document.getElementById(filter + 'Btn').classList.add('active');
|
|
|
|
// 清空自定义日期输入
|
|
startDateInput.value = '';
|
|
endDateInput.value = '';
|
|
|
|
// 重新加载数据
|
|
loadStats(filter);
|
|
}
|
|
|
|
// 应用自定义时间筛选
|
|
function applyCustomFilter() {
|
|
const startDate = startDateInput.value;
|
|
const endDate = endDateInput.value;
|
|
|
|
if (!startDate && !endDate) {
|
|
// 没有选择日期,默认显示全部
|
|
setFilter('all');
|
|
return;
|
|
}
|
|
|
|
// 更新当前筛选条件为自定义
|
|
currentFilter = 'custom';
|
|
|
|
// 重置当前卖家ID,确保切换时间筛选时显示所有货源
|
|
currentSellerId = null;
|
|
|
|
// 清除预设筛选的active状态
|
|
[todayBtn, yesterdayBtn, beforeYesterdayBtn, weekBtn, monthBtn, allBtn].forEach(btn => btn.classList.remove('active'));
|
|
|
|
// 重新加载数据,传递自定义日期范围
|
|
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);
|
|
}
|
|
}
|
|
|
|
// 封装API调用函数
|
|
async function fetchSuppliesData(filter, startDate = '', endDate = '', sellerId = null) {
|
|
// 构建缓存键
|
|
const cacheKey = sellerId ?
|
|
`seller_${sellerId}_${filter}_${startDate}_${endDate}` :
|
|
`${filter}_${startDate}_${endDate}`;
|
|
|
|
// 检查缓存
|
|
if (suppliesCache[cacheKey] && (Date.now() - suppliesCache[cacheKey].timestamp < cacheExpiry)) {
|
|
console.log('使用内存缓存数据:', cacheKey);
|
|
return suppliesCache[cacheKey].data;
|
|
}
|
|
|
|
try {
|
|
// 构建API请求URL
|
|
let url = `/api/admin/supplies?filter=${filter}`;
|
|
if (sellerId) url += `&sellerId=${sellerId}`;
|
|
if (filter === 'custom') {
|
|
if (startDate) url += `&startDate=${startDate}`;
|
|
if (endDate) url += `&endDate=${endDate}`;
|
|
}
|
|
|
|
console.log('fetchSuppliesData - 正在请求API:', url);
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
timeout: 10000,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP错误! 状态: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// 保存到内存缓存
|
|
suppliesCache[cacheKey] = {
|
|
data: data.data.supplies || [],
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
return suppliesCache[cacheKey].data;
|
|
} else {
|
|
console.error('加载货源数据失败:', data.message);
|
|
return [];
|
|
}
|
|
} catch (error) {
|
|
console.error('fetchSuppliesData - 加载货源数据出错:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// 加载统计数据
|
|
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);
|
|
|
|
// 只有在当前没有显示特定卖家的货源时才显示当天所有货源
|
|
if (!currentSellerId) {
|
|
showAllSupplies();
|
|
}
|
|
} 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;
|
|
|
|
// 只有在当前没有显示特定卖家的货源时才显示当天所有货源
|
|
if (!currentSellerId && suppliesSection.style.display === 'block') {
|
|
showAllSupplies();
|
|
}
|
|
}
|
|
}
|
|
} 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;
|
|
|
|
// 只有在当前没有显示特定卖家的货源时才显示当天所有货源
|
|
if (!currentSellerId) {
|
|
showAllSupplies();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 更新统计信息卡片
|
|
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 + 2; // 增加Y轴最大高度,为数值标签留出更多空间
|
|
}
|
|
},
|
|
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 showAllSupplies() {
|
|
// 保存当前滚动位置和货源列表高度
|
|
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
const originalHeight = suppliesGrid.clientHeight;
|
|
|
|
// 清空当前正在显示的卖家ID
|
|
currentSellerId = null;
|
|
|
|
// 重置页码
|
|
currentPage = 1;
|
|
|
|
// 构建标题
|
|
let title = '当天所有货源';
|
|
|
|
// 根据当前筛选条件更新标题
|
|
switch (currentFilter) {
|
|
case 'today':
|
|
title = '今日所有货源';
|
|
break;
|
|
case 'yesterday':
|
|
title = '昨日所有货源';
|
|
break;
|
|
case 'beforeYesterday':
|
|
title = '前日所有货源';
|
|
break;
|
|
case 'week':
|
|
title = '本周所有货源';
|
|
break;
|
|
case 'month':
|
|
title = '本月所有货源';
|
|
break;
|
|
case 'all':
|
|
title = '全部货源';
|
|
break;
|
|
case 'custom':
|
|
const startDate = startDateInput.value;
|
|
const endDate = endDateInput.value;
|
|
if (startDate && endDate) {
|
|
title = `${startDate} 至 ${endDate} 所有货源`;
|
|
} else if (startDate) {
|
|
title = `${startDate} 至今所有货源`;
|
|
} else if (endDate) {
|
|
title = `截至 ${endDate} 所有货源`;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 只有在标题变化时才更新,减少不必要的DOM操作
|
|
if (suppliesTitle.textContent !== title) {
|
|
suppliesTitle.textContent = title;
|
|
}
|
|
|
|
// 只有在suppliesSection的display属性不是block时才设置,避免不必要的布局重排
|
|
if (suppliesSection.style.display !== 'block') {
|
|
suppliesSection.style.display = 'block';
|
|
}
|
|
|
|
try {
|
|
// 使用封装的API调用函数获取数据
|
|
const startDate = currentFilter === 'custom' ? startDateInput.value : '';
|
|
const endDate = currentFilter === 'custom' ? endDateInput.value : '';
|
|
|
|
const supplies = await fetchSuppliesData(currentFilter, startDate, endDate);
|
|
|
|
// 更新总数据量和总页数
|
|
totalItems = supplies.length;
|
|
totalPages = Math.ceil(totalItems / pageSize);
|
|
|
|
console.log('showAllSupplies - 数据请求成功,总数量:', totalItems, '总页数:', totalPages);
|
|
|
|
// 渲染当前页数据
|
|
renderSuppliesWithPagination(supplies);
|
|
|
|
// 添加分页控件
|
|
addPaginationControls();
|
|
} catch (error) {
|
|
console.error('加载货源数据出错:', error);
|
|
// 只在当前不是错误状态时才显示错误信息
|
|
if (!suppliesGrid.innerHTML.includes('加载失败') && !suppliesGrid.innerHTML.includes('网络异常')) {
|
|
suppliesGrid.innerHTML = '<p style="text-align: center; color: #666; grid-column: 1 / -1;">网络异常,请检查网络连接后重试</p>';
|
|
}
|
|
} finally {
|
|
// 恢复滚动位置
|
|
window.scrollTo(0, scrollPosition);
|
|
}
|
|
}
|
|
|
|
// 显示指定卖家的货源
|
|
async function showSuppliesBySeller(sellerId) {
|
|
// 保存当前正在显示的卖家ID
|
|
currentSellerId = sellerId;
|
|
|
|
// 重置页码
|
|
currentPage = 1;
|
|
|
|
// 获取当前筛选条件和日期范围
|
|
let startDate = '';
|
|
let endDate = '';
|
|
let filter = currentFilter;
|
|
|
|
// 如果是自定义筛选,获取日期输入值
|
|
if (filter === 'custom') {
|
|
startDate = startDateInput.value;
|
|
endDate = endDateInput.value;
|
|
}
|
|
|
|
// 显示加载状态
|
|
suppliesGrid.innerHTML = '<p style="text-align: center; color: #666; grid-column: 1 / -1;">加载中...</p>';
|
|
|
|
try {
|
|
// 使用封装的API调用函数获取数据
|
|
const supplies = await fetchSuppliesData(filter, startDate, endDate, sellerId);
|
|
|
|
// 更新总数据量和总页数
|
|
totalItems = supplies.length;
|
|
totalPages = Math.ceil(totalItems / pageSize);
|
|
|
|
console.log('showSuppliesBySeller - 数据请求成功,总数量:', totalItems, '总页数:', totalPages);
|
|
|
|
// 获取创建人的姓名
|
|
let creatorName = '';
|
|
|
|
// 从chartData中查找对应的nickName
|
|
const chartItem = chartData.find(item => item.sellerId === sellerId);
|
|
if (chartItem && chartItem.nickName) {
|
|
creatorName = chartItem.nickName;
|
|
}
|
|
// 如果chartData中没有,从返回的supplies中获取
|
|
else if (supplies.length > 0 && supplies[0].nickName) {
|
|
creatorName = supplies[0].nickName;
|
|
}
|
|
|
|
// 构建标题
|
|
let title = `${sellerId} 创建的货源`;
|
|
if (creatorName) {
|
|
title = `${creatorName} (${sellerId}) 创建的货源`;
|
|
}
|
|
|
|
// 添加"查看全部货源"按钮
|
|
suppliesTitle.innerHTML = `
|
|
${title}
|
|
<button id="showAllBtn" style="
|
|
margin-left: 10px;
|
|
padding: 4px 12px;
|
|
background-color: #1677ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-weight: normal;
|
|
">查看全部货源</button>
|
|
`;
|
|
|
|
// 为"查看全部货源"按钮添加点击事件
|
|
document.getElementById('showAllBtn').addEventListener('click', showAllSupplies);
|
|
|
|
// 渲染当前页数据
|
|
renderSuppliesWithPagination(supplies);
|
|
|
|
// 添加分页控件
|
|
addPaginationControls();
|
|
|
|
// 只有在suppliesSection的display属性不是block时才设置,避免不必要的布局重排
|
|
if (suppliesSection.style.display !== 'block') {
|
|
suppliesSection.style.display = 'block';
|
|
// 只有在第一次显示suppliesSection时才滚动到该区域
|
|
suppliesSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
} catch (error) {
|
|
console.error('加载货源数据出错:', error);
|
|
suppliesGrid.innerHTML = '<p style="text-align: center; color: #666; grid-column: 1 / -1;">网络异常,请检查网络连接后重试</p>';
|
|
|
|
// 只有在suppliesSection的display属性不是block时才设置,避免不必要的布局重排
|
|
if (suppliesSection.style.display !== 'block') {
|
|
suppliesSection.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 渲染货源卡片
|
|
function renderSupplies(supplies) {
|
|
// 不过滤任何状态的货源,显示所有货源
|
|
const filteredSupplies = supplies;
|
|
|
|
if (filteredSupplies.length === 0) {
|
|
suppliesGrid.innerHTML = '<p class="empty-state">暂无货源数据</p>';
|
|
return;
|
|
}
|
|
|
|
// 保存当前滚动位置和容器高度
|
|
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
const containerHeight = suppliesGrid.clientHeight;
|
|
|
|
// 设置容器固定高度,防止清空内容时页面高度变化
|
|
suppliesGrid.style.height = `${containerHeight}px`;
|
|
suppliesGrid.style.overflow = 'hidden';
|
|
|
|
// 创建DocumentFragment,用于构建DOM结构,减少页面重排
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
filteredSupplies.forEach((supply) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'supply-card';
|
|
card.dataset.productId = supply.productId; // 添加产品ID作为数据属性
|
|
card.innerHTML = '';
|
|
|
|
// 解析媒体URL
|
|
let mediaUrl = '';
|
|
try {
|
|
// 使用更快的JSON.parse替代复杂的字符串处理
|
|
const imageUrls = JSON.parse(supply.imageUrls || '[]');
|
|
if (imageUrls && Array.isArray(imageUrls) && imageUrls.length > 0) {
|
|
mediaUrl = imageUrls[0];
|
|
}
|
|
} catch (e) {
|
|
// 解析失败时不打印错误,避免性能影响
|
|
}
|
|
|
|
// 状态文本映射,避免switch语句
|
|
const statusMap = {
|
|
'published': '已发布',
|
|
'sold_out': '已下架',
|
|
'hidden': '已隐藏'
|
|
};
|
|
const statusClass = `status-${supply.status}`;
|
|
const statusText = statusMap[supply.status] || '待审核';
|
|
|
|
// 检查是否为视频文件(使用更简单的正则表达式)
|
|
let mediaElement = '';
|
|
if (mediaUrl) {
|
|
const isVideo = /\.(mp4|mov|avi|wmv|flv|webm|mkv)$/i.test(mediaUrl) || mediaUrl.startsWith('data:video/');
|
|
if (isVideo) {
|
|
// 视频文件
|
|
mediaElement = `<video src="${mediaUrl}" alt="${supply.productName}" controls class="supply-media"></video>`;
|
|
} else {
|
|
// 图片文件
|
|
mediaElement = `<img src="${mediaUrl}" alt="${supply.productName}" class="supply-media">`;
|
|
}
|
|
}
|
|
|
|
// 第一行展示:种类|蛋黄|货源类型|产品包装|新鲜程度
|
|
const 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);
|
|
const firstLineHTML = firstLineParts.length > 0 ?
|
|
`<p class="supply-meta">${firstLineParts.join(' | ')}</p>` : '';
|
|
|
|
// 构建详细信息HTML
|
|
const detailsParts = [];
|
|
|
|
// 产品ID
|
|
if (supply.productId) {
|
|
detailsParts.push(`<p><strong>产品ID:</strong> ${supply.productId}</p>`);
|
|
}
|
|
|
|
// 规格+件数+采购价+销售价组合展示
|
|
const specifications = [];
|
|
if (supply.specification) {
|
|
if (typeof supply.specification === 'string') {
|
|
// 规格可能用逗号或中文逗号分隔
|
|
specifications.push(...supply.specification.split(/[,,]/).filter(spec => spec.trim()));
|
|
} else if (Array.isArray(supply.specification)) {
|
|
specifications.push(...supply.specification);
|
|
} else {
|
|
specifications.push(String(supply.specification));
|
|
}
|
|
}
|
|
|
|
// 处理数量、采购价、销售价
|
|
const quantities = Array.isArray(supply.quantity) ? supply.quantity :
|
|
(typeof supply.quantity === 'string' ? supply.quantity.split(',').filter(qty => qty.trim()) :
|
|
(supply.quantity ? [String(supply.quantity)] : []));
|
|
|
|
const costprices = Array.isArray(supply.costprice) ? supply.costprice :
|
|
(typeof supply.costprice === 'string' ? supply.costprice.split(',').filter(cp => cp.trim()) :
|
|
(supply.costprice ? [String(supply.costprice)] : []));
|
|
|
|
const prices = Array.isArray(supply.price) ? supply.price :
|
|
(typeof supply.price === 'string' ? supply.price.split(',').filter(p => p.trim()) :
|
|
(supply.price ? [String(supply.price)] : []));
|
|
|
|
// 计算最大长度,确保每个规格都有对应的数据
|
|
const maxLength = Math.max(specifications.length, quantities.length, costprices.length, prices.length);
|
|
|
|
// 生成组合展示HTML
|
|
if (maxLength > 0) {
|
|
const specDetails = [];
|
|
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) {
|
|
const parts = [];
|
|
if (spec) parts.push(`规格${i+1}: ${spec}`);
|
|
if (quantity) parts.push(`件数: ${quantity}件`);
|
|
if (costprice) parts.push(`采购价: ¥${costprice}`);
|
|
if (price) parts.push(`销售价: ¥${price}`);
|
|
|
|
if (parts.length > 0) {
|
|
specDetails.push(`<div class="spec-item">${parts.join(' - ')}</div>`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (specDetails.length > 0) {
|
|
detailsParts.push(`<div class="spec-section">${specDetails.join('')}</div>`);
|
|
}
|
|
}
|
|
|
|
// 商品地区
|
|
if (supply.region) {
|
|
detailsParts.push(`<p><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;
|
|
}
|
|
}
|
|
detailsParts.push(`<p>${contactText}</p>`);
|
|
}
|
|
|
|
// 创建时间
|
|
detailsParts.push(`<p><strong>创建时间:</strong> ${new Date(supply.created_at).toLocaleString()}</p>`);
|
|
|
|
// 修改时间
|
|
if (supply.updated_at) {
|
|
detailsParts.push(`<p><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;
|
|
detailsParts.push(`<div class="description-section"><strong>货源描述:</strong><div>${shortDesc}</div></div>`);
|
|
}
|
|
|
|
// 价格变更日志
|
|
let logHTML = `<div class="log-section"><h4>价格变更日志</h4>`;
|
|
|
|
try {
|
|
let logs = [];
|
|
let hasLogs = false;
|
|
|
|
// 检查product_log是否有值
|
|
if (supply.product_log != null && supply.product_log != undefined && supply.product_log != '') {
|
|
// 有值,处理日志
|
|
if (typeof supply.product_log === 'string') {
|
|
// 清理JSON字符串中的特殊字符
|
|
const cleanLogStr = supply.product_log.replace(/\s+/g, ' ').trim();
|
|
|
|
// 检查是否为JSON格式
|
|
if (cleanLogStr.startsWith('[') && cleanLogStr.endsWith(']')) {
|
|
try {
|
|
logs = JSON.parse(cleanLogStr);
|
|
} catch (parseError) {
|
|
// 解析失败,作为单个日志处理
|
|
logs = [supply.product_log];
|
|
}
|
|
} else {
|
|
// 不是JSON数组,作为单个日志处理
|
|
logs = [supply.product_log];
|
|
}
|
|
} else if (Array.isArray(supply.product_log)) {
|
|
logs = supply.product_log;
|
|
} else {
|
|
// 其他类型,转换为字符串数组
|
|
logs = [String(supply.product_log)];
|
|
}
|
|
|
|
hasLogs = logs.length > 0;
|
|
}
|
|
|
|
// 显示日志
|
|
if (hasLogs) {
|
|
logs.forEach((log, index) => {
|
|
logHTML += `<div class="log-item"><span class="log-badge">日志${index + 1}</span><span>${log}</span></div>`;
|
|
});
|
|
} else {
|
|
// 没有日志,显示提示
|
|
logHTML += `<div class="log-item"><span>暂无价格变更日志</span></div>`;
|
|
}
|
|
} catch (error) {
|
|
// 如果解析失败,直接显示日志内容
|
|
logHTML += `<div class="log-item"><span>${supply.product_log || '暂无价格变更日志'}</span></div>`;
|
|
}
|
|
|
|
logHTML += `</div>`;
|
|
detailsParts.push(logHTML);
|
|
|
|
// 状态
|
|
detailsParts.push(`<span class="supply-status ${statusClass}">${statusText}</span>`);
|
|
|
|
// 构建完整卡片HTML
|
|
card.innerHTML = `
|
|
<div class="supply-header">
|
|
${mediaElement}
|
|
<div class="supply-title">${supply.productName}</div>
|
|
${firstLineHTML}
|
|
</div>
|
|
<div class="supply-info">
|
|
${detailsParts.join('')}
|
|
</div>
|
|
`;
|
|
|
|
// 添加到DocumentFragment
|
|
fragment.appendChild(card);
|
|
});
|
|
|
|
// 一次性替换旧内容,减少页面重排
|
|
suppliesGrid.innerHTML = '';
|
|
suppliesGrid.appendChild(fragment);
|
|
|
|
// 恢复容器高度和滚动位置
|
|
suppliesGrid.style.height = '';
|
|
suppliesGrid.style.overflow = '';
|
|
window.scrollTo(0, scrollPosition);
|
|
}
|
|
|
|
// 创建货源卡片
|
|
function createSupplyCard(supply) {
|
|
const card = document.createElement('div');
|
|
card.className = 'supply-card';
|
|
card.dataset.productId = supply.productId; // 添加产品ID作为数据属性
|
|
|
|
// 解析媒体URL
|
|
let mediaUrl = '';
|
|
try {
|
|
// 使用更快的JSON.parse替代复杂的字符串处理
|
|
const imageUrls = JSON.parse(supply.imageUrls || '[]');
|
|
if (imageUrls && Array.isArray(imageUrls) && imageUrls.length > 0) {
|
|
mediaUrl = imageUrls[0];
|
|
}
|
|
} catch (e) {
|
|
// 解析失败时不打印错误,避免性能影响
|
|
}
|
|
|
|
// 状态文本映射,避免switch语句
|
|
const statusMap = {
|
|
'published': '已发布',
|
|
'sold_out': '已下架',
|
|
'hidden': '已隐藏'
|
|
};
|
|
const statusClass = `status-${supply.status}`;
|
|
const statusText = statusMap[supply.status] || '待审核';
|
|
|
|
// 检查是否为视频文件(使用更简单的正则表达式)
|
|
let mediaElement = '';
|
|
if (mediaUrl) {
|
|
const isVideo = /\.(mp4|mov|avi|wmv|flv|webm|mkv)$/i.test(mediaUrl) || mediaUrl.startsWith('data:video/');
|
|
if (isVideo) {
|
|
// 视频文件
|
|
mediaElement = `<video src="${mediaUrl}" alt="${supply.productName}" controls class="supply-media"></video>`;
|
|
} else {
|
|
// 图片文件
|
|
mediaElement = `<img src="${mediaUrl}" alt="${supply.productName}" class="supply-media" onclick="showPreview('${mediaUrl}')">`;
|
|
}
|
|
}
|
|
|
|
// 第一行展示:种类|蛋黄|货源类型|产品包装|新鲜程度
|
|
const 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);
|
|
const firstLineHTML = firstLineParts.length > 0 ?
|
|
`<p class="supply-meta">${firstLineParts.join(' | ')}</p>` : '';
|
|
|
|
// 构建详细信息HTML
|
|
const detailsParts = [];
|
|
|
|
// 产品ID
|
|
if (supply.productId) {
|
|
detailsParts.push(`<p><strong>产品ID:</strong> ${supply.productId}</p>`);
|
|
}
|
|
|
|
// 规格+件数+采购价+销售价组合展示
|
|
const specifications = [];
|
|
if (supply.specification) {
|
|
if (typeof supply.specification === 'string') {
|
|
// 规格可能用逗号或中文逗号分隔
|
|
specifications.push(...supply.specification.split(/[,,]/).filter(spec => spec.trim()));
|
|
} else if (Array.isArray(supply.specification)) {
|
|
specifications.push(...supply.specification);
|
|
} else {
|
|
specifications.push(String(supply.specification));
|
|
}
|
|
}
|
|
|
|
// 处理数量、采购价、销售价
|
|
const quantities = Array.isArray(supply.quantity) ? supply.quantity :
|
|
(typeof supply.quantity === 'string' ? supply.quantity.split(',').filter(qty => qty.trim()) :
|
|
(supply.quantity ? [String(supply.quantity)] : []));
|
|
|
|
const costprices = Array.isArray(supply.costprice) ? supply.costprice :
|
|
(typeof supply.costprice === 'string' ? supply.costprice.split(',').filter(cp => cp.trim()) :
|
|
(supply.costprice ? [String(supply.costprice)] : []));
|
|
|
|
const prices = Array.isArray(supply.price) ? supply.price :
|
|
(typeof supply.price === 'string' ? supply.price.split(',').filter(p => p.trim()) :
|
|
(supply.price ? [String(supply.price)] : []));
|
|
|
|
// 计算最大长度,确保每个规格都有对应的数据
|
|
const maxLength = Math.max(specifications.length, quantities.length, costprices.length, prices.length);
|
|
|
|
// 生成组合展示HTML
|
|
if (maxLength > 0) {
|
|
const specDetails = [];
|
|
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) {
|
|
const parts = [];
|
|
if (spec) parts.push(`规格${i+1}: ${spec}`);
|
|
if (quantity) parts.push(`件数: ${quantity}件`);
|
|
if (costprice) parts.push(`采购价: ¥${costprice}`);
|
|
if (price) parts.push(`销售价: ¥${price}`);
|
|
|
|
if (parts.length > 0) {
|
|
specDetails.push(`<div class="spec-item">${parts.join(' - ')}</div>`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (specDetails.length > 0) {
|
|
detailsParts.push(`<div class="spec-section">${specDetails.join('')}</div>`);
|
|
}
|
|
}
|
|
|
|
// 商品地区
|
|
if (supply.region) {
|
|
detailsParts.push(`<p><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;
|
|
}
|
|
}
|
|
detailsParts.push(`<p>${contactText}</p>`);
|
|
}
|
|
|
|
// 创建时间
|
|
detailsParts.push(`<p><strong>创建时间:</strong> ${new Date(supply.created_at).toLocaleString()}</p>`);
|
|
|
|
// 修改时间
|
|
if (supply.updated_at) {
|
|
detailsParts.push(`<p><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;
|
|
detailsParts.push(`<div class="description-section"><strong>货源描述:</strong><div>${shortDesc}</div></div>`);
|
|
}
|
|
|
|
// 价格变更日志
|
|
let logHTML = `<div class="log-section"><h4>价格变更日志</h4>`;
|
|
|
|
try {
|
|
let logs = [];
|
|
let hasLogs = false;
|
|
|
|
// 检查product_log是否有值
|
|
if (supply.product_log != null && supply.product_log != undefined && supply.product_log != '') {
|
|
// 有值,处理日志
|
|
if (typeof supply.product_log === 'string') {
|
|
// 清理JSON字符串中的特殊字符
|
|
const cleanLogStr = supply.product_log.replace(/\s+/g, ' ').trim();
|
|
|
|
// 检查是否为JSON格式
|
|
if (cleanLogStr.startsWith('[') && cleanLogStr.endsWith(']')) {
|
|
try {
|
|
logs = JSON.parse(cleanLogStr);
|
|
} catch (parseError) {
|
|
// 解析失败,作为单个日志处理
|
|
logs = [supply.product_log];
|
|
}
|
|
} else {
|
|
// 不是JSON数组,作为单个日志处理
|
|
logs = [supply.product_log];
|
|
}
|
|
} else if (Array.isArray(supply.product_log)) {
|
|
logs = supply.product_log;
|
|
} else {
|
|
// 其他类型,转换为字符串数组
|
|
logs = [String(supply.product_log)];
|
|
}
|
|
|
|
hasLogs = logs.length > 0;
|
|
}
|
|
|
|
// 显示日志
|
|
if (hasLogs) {
|
|
logs.forEach((log, index) => {
|
|
logHTML += `<div class="log-item"><span class="log-badge">日志${index + 1}</span><span>${log}</span></div>`;
|
|
});
|
|
} else {
|
|
// 没有日志,显示提示
|
|
logHTML += `<div class="log-item"><span>暂无价格变更日志</span></div>`;
|
|
}
|
|
} catch (error) {
|
|
// 如果解析失败,直接显示日志内容
|
|
logHTML += `<div class="log-item"><span>${supply.product_log || '暂无价格变更日志'}</span></div>`;
|
|
}
|
|
|
|
logHTML += `</div>`;
|
|
detailsParts.push(logHTML);
|
|
|
|
// 状态
|
|
detailsParts.push(`<span class="supply-status ${statusClass}">${statusText}</span>`);
|
|
|
|
// 构建完整卡片HTML
|
|
card.innerHTML = `
|
|
<div class="supply-header">
|
|
${mediaElement}
|
|
<div class="supply-title">${supply.productName}</div>
|
|
${firstLineHTML}
|
|
</div>
|
|
<div class="supply-info">
|
|
${detailsParts.join('')}
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|
|
|
|
// 渲染带分页的货源卡片
|
|
function renderSuppliesWithPagination(supplies) {
|
|
// 计算当前页的数据范围
|
|
const startIndex = (currentPage - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const currentPageSupplies = supplies.slice(startIndex, endIndex);
|
|
|
|
if (currentPageSupplies.length === 0) {
|
|
suppliesGrid.innerHTML = '<p class="empty-state">暂无货源数据</p>';
|
|
return;
|
|
}
|
|
|
|
// 保存当前滚动位置
|
|
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
|
|
// 检查是否启用虚拟滚动
|
|
if (virtualScrollEnabled && currentPageSupplies.length > visibleCount + bufferCount * 2) {
|
|
// 使用虚拟滚动
|
|
renderWithVirtualScroll(currentPageSupplies);
|
|
} else {
|
|
// 使用增量渲染
|
|
const containerHeight = suppliesGrid.clientHeight;
|
|
suppliesGrid.style.height = `${containerHeight}px`;
|
|
suppliesGrid.style.overflow = 'hidden';
|
|
incrementalRender(currentPageSupplies);
|
|
suppliesGrid.style.height = '';
|
|
suppliesGrid.style.overflow = '';
|
|
}
|
|
|
|
// 恢复滚动位置
|
|
window.scrollTo(0, scrollPosition);
|
|
}
|
|
|
|
// 增量渲染函数
|
|
function incrementalRender(supplies) {
|
|
// 创建一个Map来存储当前页的货源
|
|
const currentSuppliesMap = new Map();
|
|
supplies.forEach(supply => {
|
|
currentSuppliesMap.set(supply.productId, supply);
|
|
});
|
|
|
|
// 存储已存在的卡片
|
|
const existingCards = new Map();
|
|
Array.from(suppliesGrid.children).forEach(child => {
|
|
if (child.classList.contains('supply-card')) {
|
|
const productId = child.dataset.productId;
|
|
if (productId) {
|
|
existingCards.set(productId, child);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 创建DocumentFragment,用于构建DOM结构,减少页面重排
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
// 遍历当前页的货源
|
|
supplies.forEach(supply => {
|
|
const productId = supply.productId;
|
|
|
|
if (existingCards.has(productId)) {
|
|
// 卡片已存在,检查是否需要更新
|
|
const existingCard = existingCards.get(productId);
|
|
existingCards.delete(productId); // 标记为已处理
|
|
|
|
// 简单的更新检查:比较创建时间和修改时间
|
|
const existingCreatedAt = existingCard.querySelector('.supply-info p:nth-child(6)')?.textContent;
|
|
const existingUpdatedAt = existingCard.querySelector('.supply-info p:nth-child(7)')?.textContent;
|
|
|
|
const newCreatedAt = `创建时间: ${new Date(supply.created_at).toLocaleString()}`;
|
|
const newUpdatedAt = supply.updated_at ? `修改时间: ${new Date(supply.updated_at).toLocaleString()}` : '';
|
|
|
|
if (existingCreatedAt !== newCreatedAt ||
|
|
(newUpdatedAt && existingUpdatedAt !== newUpdatedAt)) {
|
|
// 需要更新,替换为新卡片
|
|
const newCard = createSupplyCard(supply);
|
|
fragment.appendChild(newCard);
|
|
existingCard.remove();
|
|
} else {
|
|
// 不需要更新,保留原卡片
|
|
fragment.appendChild(existingCard);
|
|
}
|
|
} else {
|
|
// 卡片不存在,创建新卡片
|
|
const newCard = createSupplyCard(supply);
|
|
fragment.appendChild(newCard);
|
|
}
|
|
});
|
|
|
|
// 移除不再需要的卡片
|
|
existingCards.forEach(card => {
|
|
card.remove();
|
|
});
|
|
|
|
// 清空容器并添加新内容
|
|
suppliesGrid.innerHTML = '';
|
|
suppliesGrid.appendChild(fragment);
|
|
}
|
|
|
|
// 添加分页控件
|
|
function addPaginationControls() {
|
|
// 检查是否已经存在分页控件
|
|
const existingPagination = document.querySelector('.pagination');
|
|
if (existingPagination) {
|
|
existingPagination.remove();
|
|
}
|
|
|
|
// 如果只有一页,不需要分页控件
|
|
if (totalPages <= 1) {
|
|
return;
|
|
}
|
|
|
|
// 创建分页控件容器
|
|
const pagination = document.createElement('div');
|
|
pagination.className = 'pagination';
|
|
pagination.style.cssText = `
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 20px;
|
|
padding: 10px;
|
|
background-color: #f5f5f5;
|
|
border-radius: 8px;
|
|
`;
|
|
|
|
// 添加首页按钮
|
|
const firstBtn = document.createElement('button');
|
|
firstBtn.textContent = '首页';
|
|
firstBtn.style.cssText = `
|
|
padding: 6px 12px;
|
|
background-color: #1677ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
`;
|
|
firstBtn.disabled = currentPage === 1;
|
|
firstBtn.addEventListener('click', () => {
|
|
if (currentPage > 1) {
|
|
currentPage = 1;
|
|
showCurrentPageData();
|
|
}
|
|
});
|
|
pagination.appendChild(firstBtn);
|
|
|
|
// 添加上一页按钮
|
|
const prevBtn = document.createElement('button');
|
|
prevBtn.textContent = '上一页';
|
|
prevBtn.style.cssText = `
|
|
padding: 6px 12px;
|
|
background-color: #1677ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
`;
|
|
prevBtn.disabled = currentPage === 1;
|
|
prevBtn.addEventListener('click', () => {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
showCurrentPageData();
|
|
}
|
|
});
|
|
pagination.appendChild(prevBtn);
|
|
|
|
// 添加页码按钮
|
|
const maxVisiblePages = 5;
|
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
|
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
|
|
|
// 调整起始页码,确保显示足够的页码
|
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
|
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
const pageBtn = document.createElement('button');
|
|
pageBtn.textContent = i;
|
|
pageBtn.style.cssText = `
|
|
padding: 6px 12px;
|
|
background-color: ${i === currentPage ? '#0958d9' : '#1677ff'};
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
`;
|
|
pageBtn.addEventListener('click', () => {
|
|
if (i !== currentPage) {
|
|
currentPage = i;
|
|
showCurrentPageData();
|
|
}
|
|
});
|
|
pagination.appendChild(pageBtn);
|
|
}
|
|
|
|
// 添加下一页按钮
|
|
const nextBtn = document.createElement('button');
|
|
nextBtn.textContent = '下一页';
|
|
nextBtn.style.cssText = `
|
|
padding: 6px 12px;
|
|
background-color: #1677ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
`;
|
|
nextBtn.disabled = currentPage === totalPages;
|
|
nextBtn.addEventListener('click', () => {
|
|
if (currentPage < totalPages) {
|
|
currentPage++;
|
|
showCurrentPageData();
|
|
}
|
|
});
|
|
pagination.appendChild(nextBtn);
|
|
|
|
// 添加末页按钮
|
|
const lastBtn = document.createElement('button');
|
|
lastBtn.textContent = '末页';
|
|
lastBtn.style.cssText = `
|
|
padding: 6px 12px;
|
|
background-color: #1677ff;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
`;
|
|
lastBtn.disabled = currentPage === totalPages;
|
|
lastBtn.addEventListener('click', () => {
|
|
if (currentPage < totalPages) {
|
|
currentPage = totalPages;
|
|
showCurrentPageData();
|
|
}
|
|
});
|
|
pagination.appendChild(lastBtn);
|
|
|
|
// 添加页码信息
|
|
const pageInfo = document.createElement('span');
|
|
pageInfo.textContent = `第 ${currentPage} / ${totalPages} 页,共 ${totalItems} 条`;
|
|
pageInfo.style.cssText = `
|
|
margin-left: 15px;
|
|
font-size: 14px;
|
|
color: #666;
|
|
`;
|
|
pagination.appendChild(pageInfo);
|
|
|
|
// 添加到页面
|
|
suppliesSection.appendChild(pagination);
|
|
}
|
|
|
|
// 显示当前页数据
|
|
async function showCurrentPageData() {
|
|
try {
|
|
const startDate = currentFilter === 'custom' ? startDateInput.value : '';
|
|
const endDate = currentFilter === 'custom' ? endDateInput.value : '';
|
|
|
|
const supplies = await fetchSuppliesData(currentFilter, startDate, endDate, currentSellerId);
|
|
|
|
// 预加载图片
|
|
preloadImages(supplies);
|
|
|
|
// 渲染当前页数据
|
|
renderSuppliesWithPagination(supplies);
|
|
|
|
// 更新分页控件
|
|
addPaginationControls();
|
|
} catch (error) {
|
|
console.error('加载当前页数据出错:', error);
|
|
}
|
|
}
|
|
|
|
// 优化事件委托,减少事件监听器数量
|
|
function optimizeEventListeners() {
|
|
// 使用事件委托处理图片点击事件
|
|
suppliesGrid.addEventListener('click', function(e) {
|
|
const mediaElement = e.target.closest('.supply-media');
|
|
if (mediaElement) {
|
|
if (mediaElement.tagName === 'IMG') {
|
|
const mediaUrl = mediaElement.src;
|
|
showPreview(mediaUrl);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 预加载图片,提高用户体验
|
|
function preloadImages(supplies) {
|
|
supplies.forEach(supply => {
|
|
try {
|
|
const imageUrls = JSON.parse(supply.imageUrls || '[]');
|
|
if (imageUrls && Array.isArray(imageUrls) && imageUrls.length > 0) {
|
|
const img = new Image();
|
|
img.src = imageUrls[0];
|
|
}
|
|
} catch (e) {
|
|
// 解析失败,忽略
|
|
}
|
|
});
|
|
}
|
|
|
|
// 虚拟滚动渲染函数
|
|
function renderWithVirtualScroll(supplies) {
|
|
// 清空容器
|
|
suppliesGrid.innerHTML = '';
|
|
|
|
// 设置容器样式
|
|
suppliesGrid.style.cssText = `
|
|
position: relative;
|
|
overflow-y: auto;
|
|
max-height: 800px;
|
|
`;
|
|
|
|
// 创建一个容器来存放虚拟元素
|
|
const virtualContainer = document.createElement('div');
|
|
virtualContainer.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
`;
|
|
suppliesGrid.appendChild(virtualContainer);
|
|
|
|
// 创建占位元素,模拟整个列表的高度
|
|
const placeholder = document.createElement('div');
|
|
placeholder.style.cssText = `
|
|
height: ${supplies.length * itemHeight}px;
|
|
width: 100%;
|
|
`;
|
|
virtualContainer.appendChild(placeholder);
|
|
|
|
// 存储当前渲染的元素
|
|
const renderedItems = new Map();
|
|
|
|
// 渲染可视区域内的元素
|
|
function renderVisibleItems() {
|
|
// 获取容器的滚动位置和可视区域高度
|
|
const scrollTop = suppliesGrid.scrollTop;
|
|
const containerHeight = suppliesGrid.clientHeight;
|
|
|
|
// 计算应该渲染的元素范围
|
|
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferCount);
|
|
const endIndex = Math.min(supplies.length, Math.ceil((scrollTop + containerHeight) / itemHeight) + bufferCount);
|
|
|
|
// 移除不在可视区域内的元素
|
|
renderedItems.forEach((item, index) => {
|
|
if (index < startIndex || index >= endIndex) {
|
|
item.remove();
|
|
renderedItems.delete(index);
|
|
}
|
|
});
|
|
|
|
// 渲染可视区域内的元素
|
|
for (let i = startIndex; i < endIndex; i++) {
|
|
if (!renderedItems.has(i)) {
|
|
const supply = supplies[i];
|
|
const item = createSupplyCard(supply);
|
|
item.style.cssText = `
|
|
position: absolute;
|
|
top: ${i * itemHeight}px;
|
|
left: 0;
|
|
width: 100%;
|
|
height: ${itemHeight}px;
|
|
box-sizing: border-box;
|
|
`;
|
|
virtualContainer.appendChild(item);
|
|
renderedItems.set(i, item);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 初始渲染
|
|
renderVisibleItems();
|
|
|
|
// 监听滚动事件,动态渲染元素
|
|
suppliesGrid.addEventListener('scroll', renderVisibleItems);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|