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.

1276 lines
52 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(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">&times;</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 = '';
// 过滤掉hidden状态的货源
const filteredSupplies = supplies.filter(supply => supply.status !== 'hidden');
if (filteredSupplies.length === 0) {
suppliesGrid.innerHTML = '<p style="text-align: center; color: #666; grid-column: 1 / -1;">暂无货源数据</p>';
return;
}
filteredSupplies.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>`;
}
// 产品日志展示
if (supply.product_log) {
try {
// 解析产品日志JSON
const productLogs = JSON.parse(supply.product_log);
if (Array.isArray(productLogs) && productLogs.length > 0) {
detailsHTML += `<div style="margin: 10px 0; font-size: 14px;">
<h4 style="margin: 0 0 10px 0; padding-bottom: 5px; border-bottom: 2px solid #ff4d4f; color: #333; font-size: 16px;">价格变更日志</h4>
<div style="display: flex; flex-direction: column; gap: 8px;">
${productLogs.map((log, index) => `
<div style="display: flex; align-items: flex-start; gap: 8px; padding: 10px; background-color: #fff1f0; border-radius: 4px;">
<div style="background-color: #ff4d4f; color: white; padding: 2px 6px; border-radius: 4px; font-size: 12px; font-weight: bold; min-width: 40px; text-align: center;">日志${index + 1}</div>
<div style="font-size: 13px; color: #666; flex: 1;">${log}</div>
</div>
`).join('')}
</div>
</div>`;
}
} catch (e) {
// 解析失败,直接展示原始日志
detailsHTML += `<div style="margin: 10px 0; font-size: 14px;">
<h4 style="margin: 0 0 10px 0; padding-bottom: 5px; border-bottom: 2px solid #ff4d4f; color: #333; font-size: 16px;">价格变更日志</h4>
<div style="padding: 10px; background-color: #fff1f0; border-radius: 4px; font-size: 13px; color: #666;">
${supply.product_log}
</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>