// 全局变量
let autoRefreshInterval = null; // 保持兼容性
let onSaleRefreshInterval = null;
let soldRefreshInterval = null;
let statisticsRefreshInterval = null; // 统计数据刷新定时器
let currentOnSalePage = 1;
let currentSoldPage = 1;
let pageSize = 20; // default fallback, will be loaded from server config
let refreshInProgress = false;
let onSaleRefreshInProgress = false;
let soldRefreshInProgress = false;
let manualRefreshInProgress = false; // 手动刷新进行中标志
let lastRefreshTime = 0;
let lastOnSaleRefreshTime = 0;
let lastSoldRefreshTime = 0;
let refreshErrorCount = 0;
let onSaleRefreshErrorCount = 0;
let soldRefreshErrorCount = 0;
// Token有效性检查配置
let TOKEN_CHECK_INTERVAL = 60000; // 默认60秒
let tokenCheckTimer = null;
// 用户权限配置
let currentUserLevel = 'guest'; // 当前用户等级
let currentPermissions = null; // 当前用户权限配置
let permissionsApplied = false; // 权限是否已应用
// 详情轮询相关变量
let onSaleDetailPollingTimer = null;
let soldDetailPollingTimer = null;
const DETAIL_POLLING_INTERVAL = 5000; // 5秒轮询一次(优化:从3秒调整为5秒,减少服务器压力)
// 前端架构:前端只从数据库/缓存读取数据,不直接调用API
// - 后端爬虫独立运行,定期从X-Meta API获取数据
// - 前端自动刷新只负责更新显示,从数据库读取最新数据
// - 爬虫状态和间隔完全由后端控制
// 搜索相关变量
let isSearchMode = false;
let currentSearchKeyword = '';
let currentSearchStatus = '';
let currentSearchPage = 1;
let currentSortBy = 'time';
let currentSortOrder = 'desc';
// 商品缓存 - 用于差异更新,避免图片闪烁
let cachedOnSaleGoods = new Map(); // key: goods_id, value: goods data
let cachedSoldGoods = new Map();
let imageCache = new Set(); // 已加载的图片URL缓存
// ========== 优化:智能刷新 - 数据哈希缓存 ==========
let onSaleDataHash = null; // 在售商品数据哈希
let soldDataHash = null; // 已售商品数据哈希
let statisticsDataHash = null; // 统计数据哈希
// ========== 增量更新哈希状态(用于服务端哈希对比) ==========
let onSaleLastHash = null; // 在售商品上次哈希值
let soldLastHash = null; // 已售商品上次哈希值
// ========== 用户权限配置应用 ==========
/**
* 应用用户权限配置
* @param {object} permissions - 权限配置对象
* @param {string} userLevel - 用户等级
*/
function applyUserPermissions(permissions, userLevel) {
currentUserLevel = userLevel;
currentPermissions = permissions;
console.debug(`应用用户权限配置: 等级=${userLevel}`, permissions);
// 根据权限配置设置刷新间隔和最小值限制
if (permissions.on_sale_refresh_interval) {
$('#onSaleInterval').val(permissions.on_sale_refresh_interval);
$('#onSaleInterval').attr('min', permissions.on_sale_refresh_interval);
$('#settingsOnSaleInterval').val(permissions.on_sale_refresh_interval);
}
if (permissions.sold_refresh_interval) {
$('#soldInterval').val(permissions.sold_refresh_interval);
$('#soldInterval').attr('min', permissions.sold_refresh_interval);
$('#settingsSoldInterval').val(permissions.sold_refresh_interval);
}
// 根据权限配置设置自动刷新开关
// 注意:登录后不默认勾选自动刷新,用户需要手动开启
// 只在首次应用权限时停止自动刷新,后续调用时保持用户的选择状态
if (permissions.auto_refresh !== undefined && !permissionsApplied) {
// 不再根据权限自动勾选,始终默认不勾选
// const autoRefreshEnabled = permissions.auto_refresh;
$('#autoRefresh').prop('checked', false);
$('#mobileAutoRefresh').prop('checked', false);
$('#settingsAutoRefresh').prop('checked', false);
// 停止可能正在运行的自动刷新
stopAutoRefresh();
console.debug('用户权限配置已应用,自动刷新默认不启用,需手动开启');
}
// 禁用游客的某些功能
if (userLevel === 'guest') {
// 游客模式:禁用自动刷新
if (!permissions.auto_refresh) {
$('#autoRefresh').prop('disabled', true);
$('#mobileAutoRefresh').prop('disabled', true);
console.debug('游客模式:自动刷新已禁用');
}
// 游客模式:禁用搜索功能
if (!permissions.search_enabled) {
$('#searchInput').prop('disabled', true);
$('#mobileSearchInput').prop('disabled', true);
$('#searchBtn').prop('disabled', true);
$('#mobileSearchBtn').prop('disabled', true);
console.log('游客模式:搜索功能已禁用');
}
} else {
// 登录用户:启用所有功能
$('#autoRefresh').prop('disabled', false);
$('#mobileAutoRefresh').prop('disabled', false);
$('#searchInput').prop('disabled', false);
$('#mobileSearchInput').prop('disabled', false);
$('#searchBtn').prop('disabled', false);
$('#mobileSearchBtn').prop('disabled', false);
}
permissionsApplied = true;
}
// 导出到全局,供其他脚本调用
window.applyUserPermissions = applyUserPermissions;
// ========== PFP编号提取函数 ==========
/**
* 从字符串中提取有效的PFP编号
* 有效范围: 2-10000
* @param {string} str - 包含编号的字符串
* @returns {number|null} 提取的编号或null
*/
function extractPfpNumberFromString(str) {
if (!str) return null;
// 提取所有数字
const numbers = str.match(/\d+/g);
if (!numbers) return null;
// 返回第一个在有效范围内的数字
for (const numStr of numbers) {
const num = parseInt(numStr, 10);
if (num >= 2 && num <= 10000) {
return num;
}
}
return null;
}
// 页面加载完成后初始化
$(document).ready(function() {
// 配置全局Ajax设置,自动在所有请求中添加Authorization header
$.ajaxSetup({
beforeSend: function(xhr) {
const token = webAuth.getToken();
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
},
error: function(xhr, status, error) {
// 全局401错误处理:Token失效时自动登出
if (xhr.status === 401) {
const responseData = xhr.responseJSON;
const errorDetail = responseData ? responseData.detail : '';
// 检查是否是密码修改或权限变更导致的Token失效
if (errorDetail && (
errorDetail.includes('密码已修改') ||
errorDetail.includes('密码修改成功') ||
errorDetail.includes('Token已失效') ||
errorDetail.includes('Token无效')
)) {
console.log('检测到Token失效,自动登出:', errorDetail);
// 清除本地Token和用户信息
webAuth.clearToken();
// 更新UI状态为游客模式
if (typeof updateUIState === 'function') {
updateUIState();
}
// 显示提示信息
if (typeof showToast === 'function') {
showToast('您的登录已失效,请重新登录', 'warning');
}
// 延迟500ms后弹出登录框
setTimeout(() => {
const loginModalElement = document.getElementById('loginModal');
if (loginModalElement) {
const loginModal = new bootstrap.Modal(loginModalElement);
loginModal.show();
}
}, 500);
// 刷新页面数据(以游客身份)
if (typeof loadGoods === 'function') {
setTimeout(() => {
loadGoods();
}, 800);
}
}
}
}
});
// 加载Token检查配置
loadTokenCheckConfig();
initializePage();
setupEventListeners();
loadInitialData();
// 如果用户已登录,启动Token有效性检查
const userInfo = webAuth.getUserInfo();
if (userInfo) {
startTokenValidityCheck();
}
// 启动健康检查机制
startHealthCheck();
});
// ========== 优化:数据哈希计算函数 ==========
function hashData(data) {
return JSON.stringify(data);
}
// 加载分页配置
async function loadPaginationConfig() {
try {
const response = await fetch('/api/v1/system/config/pagination');
const result = await response.json();
if (result.success && result.data) {
pageSize = result.data.page_size;
console.log(`分页配置已加载: ${pageSize} 条/页`);
}
} catch (error) {
console.warn('加载分页配置失败,使用默认值:', error);
}
}
/**
* 从后端获取Token检查间隔配置
*/
async function loadTokenCheckConfig() {
try {
const response = await fetch('/api/v1/web-auth/config');
if (response.ok) {
const config = await response.json();
if (config.token_check_interval) {
TOKEN_CHECK_INTERVAL = config.token_check_interval;
console.debug('[Token检查] 配置已加载,间隔:', TOKEN_CHECK_INTERVAL / 1000, '秒');
}
}
} catch (error) {
console.error('[Token检查] 配置加载失败,使用默认值:', error);
}
}
/**
* 启动Token有效性定期检查
*/
function startTokenValidityCheck() {
// 清除已存在的定时器
if (tokenCheckTimer) {
clearInterval(tokenCheckTimer);
}
// 只有登录用户才需要检查
const userInfo = webAuth.getUserInfo();
if (!userInfo) {
return;
}
console.debug('[Token检查] 启动定期检查,间隔:', TOKEN_CHECK_INTERVAL / 1000, '秒');
tokenCheckTimer = setInterval(async () => {
const isValid = await webAuth.checkTokenValidity();
if (!isValid) {
console.log('[Token检查] Token已失效,执行自动登出');
// 停止定期检查
stopTokenValidityCheck();
// 清除本地Token和用户信息
webAuth.clearToken();
// 更新UI状态为游客模式
if (typeof updateUIState === 'function') {
updateUIState();
}
// 显示提示信息
if (typeof showToast === 'function') {
showToast('您的账号已在其他设备登录,请重新登录', 'warning');
}
// 延迟500ms后弹出登录框
setTimeout(() => {
const loginModalElement = document.getElementById('loginModal');
if (loginModalElement) {
const loginModal = new bootstrap.Modal(loginModalElement);
loginModal.show();
}
}, 500);
// 刷新页面数据(以游客身份)
if (typeof loadGoods === 'function') {
setTimeout(() => {
loadGoods();
}, 800);
}
}
}, TOKEN_CHECK_INTERVAL);
}
/**
* 停止Token有效性定期检查
*/
function stopTokenValidityCheck() {
if (tokenCheckTimer) {
clearInterval(tokenCheckTimer);
tokenCheckTimer = null;
console.log('[Token检查] 已停止定期检查');
}
}
// 初始化页面
function initializePage() {
console.debug('山海·精卫PFP监控系统初始化...');
// 加载分页配置
loadPaginationConfig();
// 智能同步后端间隔设置
syncBackendIntervals();
// 初始化同步信息显示
updateSyncInfo();
// 初始化排序图标
updateSortOrderIcon();
// 初始化搜索类型选项
updateSearchTypeOptions();
}
// 更新同步信息显示(优化版:减少闪动)
function updateSyncInfo() {
const soldInterval = getSoldInterval();
const newText = `间隔: ${soldInterval}秒`;
const element = $('#soldSyncInterval');
if (element.text() !== newText) {
element.text(newText);
}
}
// 智能同步后端间隔设置
function syncBackendIntervals() {
$.get('/api/monitor/status')
.done(function(response) {
if (response.success && response.data) {
const backendOnSaleInterval = response.data.on_sale_interval || 1;
const backendSoldInterval = response.data.sold_interval || 5;
// 建议前端间隔为后端间隔的2-3倍,确保能及时看到更新
const recommendedOnSaleInterval = Math.max(3, backendOnSaleInterval * 2);
const recommendedSoldInterval = Math.max(5, backendSoldInterval * 2);
// 如果当前设置的间隔过长,自动调整
const currentOnSaleInterval = getOnSaleInterval();
const currentSoldInterval = getSoldInterval();
if (currentOnSaleInterval > recommendedOnSaleInterval * 3) {
$('#onSaleInterval').val(recommendedOnSaleInterval);
console.log(`自动优化在售商品刷新间隔: ${currentOnSaleInterval}s -> ${recommendedOnSaleInterval}s`);
}
if (currentSoldInterval > recommendedSoldInterval * 3) {
$('#soldInterval').val(recommendedSoldInterval);
console.log(`自动优化已售商品刷新间隔: ${currentSoldInterval}s -> ${recommendedSoldInterval}s`);
}
}
})
.fail(function() {
console.warn('无法获取后端间隔设置');
});
}
// 更新上次更新时间(优化版:减少闪动)
function updateLastUpdateTime(type = 'both') {
const now = new Date();
const timeString = now.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const newText = `上次更新: ${timeString}`;
if (type === 'both' || type === 'onSale') {
const element = $('#onSaleLastUpdate');
if (element.text() !== newText) {
element.text(newText);
}
}
if (type === 'both' || type === 'sold') {
const element = $('#soldLastUpdate');
if (element.text() !== newText) {
element.text(newText);
}
}
}
// ==================== 详情轮询机制 ====================
/**
* 检查是否需要启动详情轮询
* 当API返回的数据包含缺少详情的商品时,启动轮询
* @param {string} type - 'onSale' 或 'sold'
* @param {object} response - API响应
*/
function checkAndStartDetailPolling(type, response) {
const missingDetails = response.data?.missing_details || 0;
if (missingDetails > 0) {
// 显示详情加载提示
showDetailLoadingTip(type, missingDetails);
// 启动轮询(如果未启动)
if (type === 'onSale' && !onSaleDetailPollingTimer) {
console.log(`📦 在售商品有 ${missingDetails} 个缺少详情,${DETAIL_POLLING_INTERVAL/1000}秒后自动刷新`);
onSaleDetailPollingTimer = setTimeout(() => {
onSaleDetailPollingTimer = null;
loadOnSaleGoods(currentOnSalePage);
}, DETAIL_POLLING_INTERVAL);
} else if (type === 'sold' && !soldDetailPollingTimer) {
console.log(`📦 已售商品有 ${missingDetails} 个缺少详情,${DETAIL_POLLING_INTERVAL/1000}秒后自动刷新`);
soldDetailPollingTimer = setTimeout(() => {
soldDetailPollingTimer = null;
loadSoldGoods(currentSoldPage, true);
}, DETAIL_POLLING_INTERVAL);
}
} else {
// 所有商品都有详情,停止轮询
stopDetailPolling(type);
hideDetailLoadingTip(type);
}
}
/**
* 停止详情轮询
* @param {string} type - 'onSale' 或 'sold',不传则停止全部
*/
function stopDetailPolling(type) {
if (!type || type === 'onSale') {
if (onSaleDetailPollingTimer) {
clearTimeout(onSaleDetailPollingTimer);
onSaleDetailPollingTimer = null;
}
}
if (!type || type === 'sold') {
if (soldDetailPollingTimer) {
clearTimeout(soldDetailPollingTimer);
soldDetailPollingTimer = null;
}
}
}
/**
* 显示详情加载提示
* @param {string} type - 'onSale' 或 'sold'
* @param {number} count - 缺少详情的商品数量
*/
function showDetailLoadingTip(type, count) {
const tipId = type === 'onSale' ? 'onSaleDetailTip' : 'soldDetailTip';
let tipElement = $(`#${tipId}`);
if (tipElement.length === 0) {
// 创建提示元素
const containerSelector = type === 'onSale' ? '#onSaleList' : '#soldList';
const tip = $(`
正在加载 ${count} 个商品的详情...
`);
$(containerSelector).before(tip);
tipElement = tip;
} else {
tipElement.find('.tip-text').text(`正在加载 ${count} 个商品的详情...`);
}
tipElement.show();
}
/**
* 隐藏详情加载提示
* @param {string} type - 'onSale' 或 'sold'
*/
function hideDetailLoadingTip(type) {
const tipId = type === 'onSale' ? 'onSaleDetailTip' : 'soldDetailTip';
$(`#${tipId}`).hide();
}
// ==================== 同步状态管理 ====================
// 更新同步状态显示(优化版:区分正常过程和真正异常,减少闪动)
function updateSyncStatus(type, status, message = '') {
const statusElement = type === 'onSale' ? $('#onSaleSyncStatus') : $('#soldSyncStatus');
// 获取当前状态
const currentText = statusElement.text().trim();
const currentClasses = statusElement.attr('class') || '';
// 优化状态逻辑:只有真正的错误才显示异常
let newText, newClass;
if (status === 'success') {
newText = '同步正常';
newClass = 'bg-success';
} else if (status === 'loading') {
// loading状态保持为正常状态,只是显示为正常同步过程
newText = '同步正常';
newClass = 'bg-success';
} else if (status === 'error') {
// 只有网络失败、接口报错等严重问题才显示异常
newText = '同步异常';
newClass = 'bg-warning';
} else {
// warning 等其他状态也归为异常
newText = '同步异常';
newClass = 'bg-warning';
}
// 只在状态真正改变时才更新DOM
if (currentText !== newText || !currentClasses.includes(newClass)) {
statusElement.removeClass('bg-success bg-warning bg-danger bg-secondary bg-info');
statusElement.addClass(newClass).text(newText);
}
}
// 设置事件监听器
function setupEventListeners() {
// 自动刷新复选框
$('#autoRefresh').change(function() {
if (this.checked) {
startAutoRefresh();
} else {
stopAutoRefresh();
}
});
// 在售商品刷新间隔设置
$('#onSaleInterval').on('input change', function() {
let newInterval = parseInt(this.value);
// 获取用户等级的最小间隔限制
const minInterval = currentPermissions ? (currentPermissions.on_sale_refresh_interval || 1) : 1;
// 验证输入值(不能小于用户等级允许的最小间隔)
if (isNaN(newInterval) || newInterval < minInterval) {
newInterval = minInterval;
this.value = minInterval;
// 提示用户最小值限制
if (currentUserLevel !== 'guest') {
console.log(`当前等级(${currentUserLevel})的在售商品刷新间隔最小值为 ${minInterval} 秒`);
}
}
// 更新同步间隔显示
updateSyncInfo();
if ($('#autoRefresh').is(':checked')) {
// 如果自动刷新正在运行,重新启动以应用新的间隔
stopAutoRefresh();
startAutoRefresh();
console.log(`在售商品刷新间隔已更改为 ${newInterval} 秒`);
}
});
// 已售商品刷新间隔设置
$('#soldInterval').on('input change', function() {
let newInterval = parseInt(this.value);
// 获取用户等级的最小间隔限制
const minInterval = currentPermissions ? (currentPermissions.sold_refresh_interval || 1) : 1;
// 验证输入值(不能小于用户等级允许的最小间隔)
if (isNaN(newInterval) || newInterval < minInterval) {
newInterval = minInterval;
this.value = minInterval;
// 提示用户最小值限制
if (currentUserLevel !== 'guest') {
console.log(`当前等级(${currentUserLevel})的已售商品刷新间隔最小值为 ${minInterval} 秒`);
}
}
// 更新同步间隔显示
updateSyncInfo();
if ($('#autoRefresh').is(':checked')) {
// 如果自动刷新正在运行,重新启动以应用新的间隔
stopAutoRefresh();
startAutoRefresh();
console.log(`已售商品刷新间隔已更改为 ${newInterval} 秒`);
}
});
// 立即刷新按钮
$('#refreshBtn').click(function() {
// 停止自动刷新并取消勾选
if ($('#autoRefresh').is(':checked')) {
$('#autoRefresh').prop('checked', false);
$('#mobileAutoRefresh').prop('checked', false);
stopAutoRefresh();
}
refreshAllData();
});
// 成交分析按钮
$('#analysisBtn').click(function() {
showAnalysisModal();
});
// 刷新分析数据按钮
$('#refreshAnalysisBtn').click(function() {
const timeRange = $('input[name="timeRange"]:checked').val() || '30d';
loadAnalysisData(timeRange);
});
// 时间周期选择器
$(document).on('change', 'input[name="timeRange"]', function() {
const timeRange = $(this).val();
loadAnalysisData(timeRange);
});
// 设置按钮(已移除)
// $('#settingsBtn').click(function() {
// loadSettingsModal();
// });
// 保存设置按钮(已移除)
// $('#saveSettingsBtn').click(function() {
// saveSettings();
// });
// 设置模态框中的间隔输入验证
$('#settingsOnSaleInterval').on('input', function() {
let value = parseInt(this.value);
if (isNaN(value) || value < 1) {
this.value = 1;
}
});
$('#settingsSoldInterval').on('input', function() {
let value = parseInt(this.value);
if (isNaN(value) || value < 1) {
this.value = 1;
}
});
// 商品项点击事件(事件委托)
$(document).on('click', '.goods-item', function() {
const goodsData = $(this).data('goods');
if (goodsData) {
showGoodsModal(goodsData);
}
});
// 搜索相关事件
$('#searchBtn').click(function() {
performSearch();
});
$('#clearSearchBtn').click(function() {
clearSearch();
});
// 搜索框回车键搜索
$('#searchInput').keypress(function(e) {
if (e.which === 13) { // Enter键
performSearch();
}
});
// 状态选择改变时自动搜索(如果有关键词)
$('#searchStatus').change(function() {
if ($('#searchInput').val().trim()) {
performSearch();
}
});
// 排序字段改变时自动搜索(如果有关键词)
$('#searchSortBy').change(function() {
currentSortBy = $(this).val();
if ($('#searchInput').val().trim()) {
performSearch();
}
});
// 排序方向切换
$('#sortOrderBtn').click(function() {
currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
updateSortOrderIcon();
if ($('#searchInput').val().trim()) {
performSearch();
}
});
// 设置模态框标签页切换事件
setupSettingsTabSwitching();
// 移动端功能
setupMobileEventListeners();
}
// 设置模态框标签页切换事件监听
function setupSettingsTabSwitching() {
// 监听主要标签页切换 - 使用Bootstrap原生事件
$('#settingsTabs button[data-bs-toggle="tab"]').on('show.bs.tab', function (e) {
const targetId = $(e.target).attr('data-bs-target');
console.log('准备切换到标签页:', targetId);
// 手动隐藏所有其他标签页
$('#settingsTabContent .tab-pane').each(function() {
if (!$(this).is(targetId)) {
$(this).removeClass('show active');
}
});
});
$('#settingsTabs button[data-bs-toggle="tab"]').on('shown.bs.tab', function (e) {
const targetId = $(e.target).attr('data-bs-target');
const previousId = $(e.relatedTarget) ? $(e.relatedTarget).attr('data-bs-target') : null;
console.log('标签页切换完成:', previousId, '->', targetId);
// 确保目标标签页显示,其他隐藏
$('#settingsTabContent .tab-pane').removeClass('show active');
$(targetId).addClass('show active');
// 根据标签页执行特殊处理
switch(targetId) {
case '#general':
console.log('显示基础设置');
// 显示底部的取消和保存按钮(基础设置需要保存)
$('#cancelSettingsBtn').show();
$('#saveSettingsBtn').show();
break;
case '#traits':
console.log('显示特征管理');
// 隐藏底部的取消和保存按钮(特征显示只读,不需要保存)
$('#cancelSettingsBtn').hide();
$('#saveSettingsBtn').hide();
handleTraitsTabActivation();
break;
}
});
// 监听特征管理子标签页切换
$('#traitsSubTabs button[data-bs-toggle="pill"]').on('shown.bs.tab', function (e) {
const targetId = $(e.target).attr('data-bs-target');
console.log('特征管理子标签页切换到:', targetId);
});
// 设置模态框打开时重置状态
$('#settingsModal').on('show.bs.modal', function() {
console.log('设置模态框即将打开');
// 重置所有标签页状态
setTimeout(function() {
// 确保基础设置标签页被激活
$('#settingsTabs .nav-link').removeClass('active');
$('#general-tab').addClass('active');
// 确保只有基础设置内容显示
$('#settingsTabContent .tab-pane').removeClass('show active');
$('#general').addClass('show active');
// 显示底部按钮(默认显示基础设置)
$('#cancelSettingsBtn').show();
$('#saveSettingsBtn').show();
console.log('标签页状态已重置到基础设置');
}, 50);
});
}
// 处理特征管理标签页激活
function handleTraitsTabActivation() {
// 检查是否已经验证过密码
if (!$('#traitsPasswordPanel').is(':visible')) {
// 已验证过密码,确保上传功能已设置
setTimeout(setupTraitsUpload, 100);
return;
}
// 如果密码面板可见,说明需要验证密码
$('#traitsAdminPassword').focus();
}
// 加载初始数据
function loadInitialData() {
console.log('开始加载初始数据...');
showLoading(true);
// 创建一个20秒的超时Promise
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('初始数据加载超时 (20秒)')), 20000);
});
// 包装每个数据加载函数,添加5秒超时
function withTimeout(promise, name, timeoutMs = 5000) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`${name} 加载超时 (${timeoutMs/1000}秒)`)), timeoutMs);
});
return Promise.race([promise, timeout]).catch((e) => {
console.error(`${name} 加载失败:`, e.message);
return null;
});
}
// 使用Promise.race确保整体超时控制
const dataLoadingPromise = Promise.all([
withTimeout(loadStatistics(), '统计数据'),
withTimeout(loadOnSaleGoods(1), '在售商品'),
withTimeout(loadSoldGoods(1), '已售商品'),
withTimeout(loadMarketData(), '行情数据'),
withTimeout(checkMonitorStatus(), '监控状态')
]).then(() => {
console.log('所有数据加载完成');
return true;
}).catch((e) => {
console.error('数据加载失败:', e);
showError('数据加载失败,请刷新页面重试');
return false;
});
// 使用超时控制
Promise.race([dataLoadingPromise, timeoutPromise])
.then((result) => {
if (result === true) {
console.log('数据加载成功完成');
}
})
.catch((e) => {
console.error('加载超时或失败:', e.message);
showError('数据加载超时,请检查网络连接并刷新页面');
})
.finally(() => {
console.log('隐藏加载界面');
showLoading(false);
});
}
// 显示/隐藏加载状态
function showLoading(show) {
console.debug('showLoading called:', show);
const overlay = document.getElementById('loadingOverlay');
if (!overlay) {
console.error('loadingOverlay元素未找到!');
return;
}
if (show) {
overlay.style.display = 'block';
// 添加10秒强制超时,防止永久卡住
setTimeout(() => {
if (overlay.style.display === 'block') {
console.warn('强制超时:隐藏loading overlay');
showError('加载超时,如果页面未正常显示,请刷新页面');
overlay.style.display = 'none';
}
}, 10000);
} else {
overlay.style.display = 'none';
}
}
// ========== Phase 2优化:启用API缓存 ==========
// 加载统计信息
function loadStatistics() {
return $.ajax({
url: '/api/stats',
method: 'GET',
timeout: 10000, // 10秒超时
cache: true, // ✓ 启用浏览器缓存(配合后端Cache-Control: 30秒)
headers: {
'Cache-Control': 'max-age=30' // 客户端建议缓存时间
}
}).done(function(response) {
if (response.success) {
updateStatistics(response.data);
} else {
console.error('统计信息API错误:', response.error);
showError('加载统计信息失败: ' + response.error);
}
}).fail(function(xhr, status, error) {
console.error('统计信息请求失败:', status, error);
if (status === 'timeout') {
showError('加载统计信息超时,请稍后重试');
} else {
showError('网络连接失败,请稍后重试');
}
});
}
// 更新统计信息
function updateStatistics(stats) {
// ========== 优化:智能刷新 - 数据未变化时跳过更新 ==========
const currentHash = hashData(stats);
if (statisticsDataHash === currentHash) {
console.log('⏭️ 统计数据未变化,跳过更新');
return;
}
statisticsDataHash = currentHash;
console.log('✅ 统计数据已更新');
const onSaleStats = stats.on_sale || {};
const soldStats = stats.sold || {};
$('#onSaleCount').text(onSaleStats.count || 0);
$('#soldCount').text(soldStats.count || 0);
$('#avgPrice').text('¥' + (onSaleStats.avg_price ? Math.round(onSaleStats.avg_price) : 0));
$('#todayNew').text(stats.today_new || 0);
// 更新最后更新时间
if (stats.last_update_time) {
$('#lastUpdateTime').html('最后更新: ' + stats.last_update_time);
}
}
// 加载在售商品
function loadOnSaleGoods(page) {
// 翻页时清空哈希,确保获取完整数据
if (page !== currentOnSalePage) {
onSaleLastHash = null;
}
currentOnSalePage = page;
// 更新同步状态为加载中
updateSyncStatus('onSale', 'loading', '正在加载在售数据...');
return $.ajax({
url: '/api/v1/goods/list',
method: 'GET',
data: {
page: page,
limit: pageSize,
status: 1,
sort_by: 'publish_time',
sort_order: 'desc',
fetch_details: true,
incremental: true, // 启用增量更新
last_hash: onSaleLastHash // 上次哈希值
},
timeout: 10000, // 10秒超时
cache: false // 禁用浏览器缓存,始终从服务器获取
})
.done(function(response) {
if (response.success) {
const data = response.data;
// ========== 增量更新:检查服务端数据是否变化 ==========
if (data.changed === false) {
// 数据未变化,跳过更新
console.debug('⏭️ 在售商品数据未变化(服务端确认),跳过更新');
updateSyncStatus('onSale', 'success', '数据无变化');
// 始终更新分页(即使数据未变化)
updatePagination('onSale', data);
return;
}
// 数据已变化,更新哈希值
onSaleLastHash = data.hash;
console.debug(`✅ 在售商品数据已更新`);
// 处理用户权限配置
if (data.permissions) {
applyUserPermissions(data.permissions, data.user_level);
}
// ========== 优化:display函数内部已集成数据哈希检测 ==========
// 数据未变化时,displayOnSaleGoods会提前返回,下面的代码不会执行
const dataChanged = displayOnSaleGoods(data);
// 始终更新分页(即使数据未变化,也要确保分页显示)
updatePagination('onSale', data);
// 仅在数据变化时执行以下操作
if (dataChanged !== false) {
// 更新最后更新时间和同步状态
updateLastUpdateTime('onSale');
updateSyncStatus('onSale', 'success', '在售数据同步正常');
// 检查是否需要轮询获取详情
checkAndStartDetailPolling('onSale', response);
// 同步到移动端(仅在数据变化时)
syncMobileDesktopData();
syncMobileOnSaleList();
} else {
// 数据未变化,仅更新同步状态
updateSyncStatus('onSale', 'success', '在售数据同步正常');
}
} else {
showError('加载在售商品失败: ' + response.error);
updateSyncStatus('onSale', 'error', '加载失败: ' + response.error);
}
})
.fail(function() {
// 失败时清空哈希,下次全量刷新
onSaleLastHash = null;
showError('网络连接失败,请稍后重试');
updateSyncStatus('onSale', 'error', '网络连接失败');
});
}
// 加载已售商品
function loadSoldGoods(page, isAutoRefresh = false) {
// 翻页时清空哈希,确保获取完整数据
if (!isAutoRefresh && page !== currentSoldPage) {
soldLastHash = null;
}
// 只在手动翻页时更新当前页码
if (!isAutoRefresh) {
currentSoldPage = page;
}
// 更新同步状态为加载中
const loadingMessage = isAutoRefresh ? '正在刷新最新数据...' : '正在加载已售数据...';
updateSyncStatus('sold', 'loading', loadingMessage);
return $.ajax({
url: '/api/v1/goods/list',
method: 'GET',
data: {
page: page,
limit: pageSize,
status: 2,
sort_by: 'sell_time',
sort_order: 'desc',
fetch_details: true,
incremental: true, // 启用增量更新
last_hash: soldLastHash // 上次哈希值
},
timeout: 10000, // 10秒超时
cache: false // 禁用浏览器缓存,始终从服务器获取
})
.done(function(response) {
if (response.success) {
const data = response.data;
// ========== 增量更新:检查服务端数据是否变化 ==========
if (data.changed === false) {
// 数据未变化,跳过更新
console.debug('⏭️ 已售商品数据未变化(服务端确认),跳过更新');
updateSyncStatus('sold', 'success', '数据无变化');
// 始终更新分页(即使数据未变化)
updatePagination('sold', data);
return;
}
// 数据已变化,更新哈希值
soldLastHash = data.hash;
console.debug(`✅ 已售商品数据已更新`);
// 处理用户权限配置
if (data.permissions) {
applyUserPermissions(data.permissions, data.user_level);
}
// ========== 优化:display函数内部已集成数据哈希检测 ==========
// 数据未变化时,displaySoldGoods会返回false
const dataChanged = displaySoldGoods(data);
// 始终更新分页(即使数据未变化,也要确保分页显示)
updatePagination('sold', data);
// 仅在数据变化时执行以下操作
if (dataChanged !== false) {
// 更新最后更新时间和同步状态
updateLastUpdateTime('sold');
updateSyncStatus('sold', 'success', '已售数据同步正常');
// 检查是否需要轮询获取详情
checkAndStartDetailPolling('sold', response);
// 在控制台显示数据更新信息
const refreshType = isAutoRefresh ? '自动刷新' : '手动加载';
console.log(`✅ 已售商品列表已更新 (${refreshType}) - 第${page}页, ${data.goods?.length || 0}个商品, 总共${data.total || 0}个`);
// 同步到移动端(仅在数据变化时)
syncMobileDesktopData();
syncMobileSoldList();
} else {
// 数据未变化,仅更新同步状态
updateSyncStatus('sold', 'success', '已售数据同步正常');
}
} else {
showError('加载已售商品失败: ' + response.error);
updateSyncStatus('sold', 'error', '加载失败: ' + response.error);
}
})
.fail(function() {
// 失败时清空哈希,下次全量刷新
soldLastHash = null;
showError('网络连接失败,请稍后重试');
updateSyncStatus('sold', 'error', '网络连接失败');
});
}
// 显示在售商品列表(优化版:差异更新,避免图片闪烁)
// 返回值:true表示数据已更新,false表示数据未变化
function displayOnSaleGoods(data) {
const goods = data.goods || [];
const container = $('#onSaleList');
// ========== 优化:智能刷新 - 数据未变化时跳过更新 ==========
const currentHash = hashData(goods);
if (onSaleDataHash === currentHash) {
console.debug('⏭️ 在售商品数据未变化,跳过更新');
return false; // 返回false表示数据未变化
}
onSaleDataHash = currentHash;
console.debug('✅ 在售商品数据已更新');
$('#onSaleTotal').text('总计: ' + (data.total || 0));
if (goods.length === 0) {
container.html('');
cachedOnSaleGoods.clear();
return;
}
// 获取新数据的ID集合
const newGoodsMap = new Map();
goods.forEach(item => {
const id = item.goods_id || item.id;
newGoodsMap.set(id, item);
});
// 检查是否需要完全重建(页码变化或首次加载)
const existingItems = container.find('.goods-item');
const needFullRebuild = existingItems.length === 0 ||
cachedOnSaleGoods.size === 0 ||
goods.length !== existingItems.length;
if (needFullRebuild) {
// 保存现有的溢价率值
const existingPremiumRates = {};
$('#onSaleList .premium-rate').each(function() {
const $this = $(this);
const priceText = $this.closest('.goods-price').text();
const priceMatch = priceText.match(/¥(\d+(?:\.\d+)?)/);
if (priceMatch) {
const price = parseFloat(priceMatch[1]);
existingPremiumRates[price] = {
text: $this.text().trim(),
classes: $this.attr('class')
};
}
});
let html = '';
goods.forEach(function(item) {
html += createGoodsItemHtml(item, 'on-sale');
});
container.html(html).addClass('fade-in');
// 恢复已有的溢价率值
$('#onSaleList .premium-rate').each(function() {
const $this = $(this);
const priceText = $this.closest('.goods-price').text();
const priceMatch = priceText.match(/¥(\d+(?:\.\d+)?)/);
if (priceMatch) {
const price = parseFloat(priceMatch[1]);
const existing = existingPremiumRates[price];
if (existing && existing.text !== '--') {
$this.text(existing.text);
$this.attr('class', existing.classes);
}
}
});
} else {
// 差异更新:只更新变化的商品
let hasChanges = false;
existingItems.each(function(index) {
const $item = $(this);
const oldData = $item.data('goods');
const oldId = oldData ? (oldData.goods_id || oldData.id) : null;
const newItem = goods[index];
const newId = newItem ? (newItem.goods_id || newItem.id) : null;
// 如果ID不同或数据变化,更新该项
if (oldId !== newId || (newItem && hasGoodsChanged(oldData, newItem))) {
const newHtml = createGoodsItemHtml(newItem, 'on-sale');
$item.replaceWith(newHtml);
hasChanges = true;
}
});
if (hasChanges) {
console.log('差异更新:部分商品已更新');
}
}
// 更新缓存
cachedOnSaleGoods.clear();
goods.forEach(item => {
const id = item.goods_id || item.id;
cachedOnSaleGoods.set(id, item);
});
// 仅对在售商品计算溢价率
if (data.goods && data.goods.length > 0) {
// console.debug(`准备计算溢价率,商品数量: ${data.goods.length}`);
setTimeout(() => {
calculateAndDisplayPremiumRates(data.goods);
}, 100);
}
// ========== Phase 2优化:触发图片懒加载 ==========
triggerLazyLoad();
// 更新分页导航
updatePagination('onSale', data);
return true; // 返回true表示数据已更新
}
// 检查商品数据是否变化
function hasGoodsChanged(oldData, newData) {
if (!oldData || !newData) return true;
return oldData.price !== newData.price ||
oldData.sell_status !== newData.sell_status ||
oldData.seller_name !== newData.seller_name;
}
// 显示已售商品列表(优化版:差异更新)
// 返回值:true表示数据已更新,false表示数据未变化
function displaySoldGoods(data) {
const goods = data.goods || [];
const container = $('#soldList');
// ========== 优化:智能刷新 - 数据未变化时跳过更新 ==========
const currentHash = hashData(goods);
if (soldDataHash === currentHash) {
console.debug('⏭️ 已售商品数据未变化,跳过更新');
return false; // 返回false表示数据未变化
}
soldDataHash = currentHash;
console.debug('✅ 已售商品数据已更新');
$('#soldTotal').text('总计: ' + (data.total || 0));
if (goods.length === 0) {
container.html('');
cachedSoldGoods.clear();
return;
}
// 检查是否需要完全重建
const existingItems = container.find('.goods-item');
const needFullRebuild = existingItems.length === 0 ||
cachedSoldGoods.size === 0 ||
goods.length !== existingItems.length;
if (needFullRebuild) {
let html = '';
goods.forEach(function(item) {
html += createGoodsItemHtml(item, 'sold');
});
container.html(html).addClass('fade-in');
} else {
// 差异更新:只更新变化的商品
let hasChanges = false;
existingItems.each(function(index) {
const $item = $(this);
const oldData = $item.data('goods');
const oldId = oldData ? (oldData.goods_id || oldData.id) : null;
const newItem = goods[index];
const newId = newItem ? (newItem.goods_id || newItem.id) : null;
if (oldId !== newId || (newItem && hasGoodsChanged(oldData, newItem))) {
const newHtml = createGoodsItemHtml(newItem, 'sold');
$item.replaceWith(newHtml);
hasChanges = true;
}
});
if (hasChanges) {
console.log('已售列表差异更新:部分商品已更新');
}
}
// 更新缓存
cachedSoldGoods.clear();
goods.forEach(item => {
const id = item.goods_id || item.id;
cachedSoldGoods.set(id, item);
});
// ========== Phase 2优化:触发图片懒加载 ==========
triggerLazyLoad();
// 更新分页导航
updatePagination('sold', data);
return true; // 返回true表示数据已更新
}
// 创建商品项HTML
function createGoodsItemHtml(item, type) {
const priceClass = type === 'sold' ? 'sold' : '';
const statusBadge = type === 'sold' ?
'已售' :
(item.is_presale ? '预售' : '在售');
// 特征和OCR状态指示器
let indicators = '';
if (item.traits_info) {
try {
let traits = typeof item.traits_info === 'string' ? JSON.parse(item.traits_info) : item.traits_info;
if (traits && Object.keys(traits).length > 0) {
indicators += ' ';
}
} catch (e) {}
}
if (item.ocr_status === 2) {
indicators += ' ';
} else if (item.ocr_status === 1) {
indicators += ' ';
}
const publishTime = type === 'sold' ?
(item.sell_time ? '售出时间: ' + item.sell_time : '售出时间: 未知') :
(item.publish_time ? '上架时间: ' + item.publish_time : '上架时间: 未知');
const description = item.description || item.introduce || '';
// 处理商品编号和设定集标记
let collectionNumber = '';
let shedingJiBadge = '';
let nameClass = '';
let pfpNumber = item.pfp_number;
let isInferredNumber = false;
// 如果没有pfp_number,尝试从collection_number提取
if (!pfpNumber && item.collection_number) {
const extracted = extractPfpNumberFromString(item.collection_number);
if (extracted) {
pfpNumber = extracted;
}
}
// 如果没有pfp_number但有traits_info,尝试反推编号
if (!pfpNumber && item.traits_info && pfpFeatureModal && pfpFeatureModal.allPFPData) {
try {
let traitsObj = item.traits_info;
if (typeof traitsObj === 'string') {
traitsObj = JSON.parse(traitsObj);
}
const matchResult = pfpFeatureModal.matchPfpNumberByTraits(traitsObj);
if (matchResult && matchResult.pfpNumber) {
pfpNumber = matchResult.pfpNumber;
isInferredNumber = true;
item._inferredPfpNumber = true;
item._matchResult = matchResult;
}
} catch (e) {
console.warn('特征反推编号失败:', e);
}
}
if (pfpNumber) {
// 验证编号在有效范围内 [2, 10000]
if (pfpNumber >= 2 && pfpNumber <= 10000) {
// 检查是否为设定集
const isShedingJi = pfpFeatureModal && pfpFeatureModal.isShedingJi(pfpNumber);
if (isShedingJi) {
nameClass = 'text-danger fw-bold';
shedingJiBadge = '设定集';
}
// 创建可点击的编号链接
const ocrResultParam = item.ocr_result ? JSON.stringify(item.ocr_result) : 'null';
const inferredBadge = isInferredNumber ? '推测' : '';
collectionNumber = `
#${pfpNumber}
${shedingJiBadge}
${inferredBadge}
`;
}
} else if (item.collection_number) {
collectionNumber = `#${item.collection_number}`;
}
const goodsName = item.name || '未知商品';
// 生成deal_link(如果数据库中没有,则使用模板生成)
const goodsId = item.goods_id || item.id;
const dealLink = item.deal_link && item.deal_link.trim() !== ''
? item.deal_link
: `https://xmeta.x-metash.cn/prod/xmeta_mall/index.html#/pages/goodsDetail/index?id=${goodsId}`;
// 存储生成的deal_link到item对象,供模态框使用
item.deal_link = dealLink;
// 检查是否有多张图片,准备显示最多2张
let allImages = [];
let imageCount = 1;
let imageIndicator = '';
let imagesToShow = [item.image_url]; // 默认只有主图
if (item.all_images) {
try {
// 支持数组或字符串格式
if (Array.isArray(item.all_images)) {
allImages = item.all_images;
} else if (typeof item.all_images === 'string') {
allImages = JSON.parse(item.all_images);
}
if (allImages && Array.isArray(allImages) && allImages.length > 0) {
imageCount = allImages.length;
imagesToShow = allImages.slice(0, 2); // 最多显示2张图
if (imageCount > 2) {
imageIndicator = `${imageCount}图`;
}
}
} catch (e) {
// JSON解析失败,使用主图
imagesToShow = [item.image_url];
}
}
// ========== Phase 2优化:图片懒加载 + 省流缓存 ==========
// 生成图片HTML
let imagesHtml = '';
imagesToShow.forEach((imageUrl, index) => {
const imageClass = imagesToShow.length === 1 ? 'goods-image single-image lazyload' : 'goods-image dual-image lazyload';
// 如果图片已在缓存中,直接使用src;否则使用data-src懒加载
const isCached = imageCache.has(imageUrl);
imagesHtml += `
`;
});
return `
${imagesHtml}
${imageIndicator}
${indicators}
${statusBadge}
${type === 'sold' ?
`
` :
(currentUserLevel === 'vip' || currentUserLevel === 'pro' ?
`
购买
` :
`
查看
`)
}
¥${item.price}
${type !== 'sold' ? `--` : ''}
卖家: ${item.seller_name || '未知'} | ${publishTime}
${description ? `
${description}
` : ''}
`;
}
// 更新分页导航
function updatePagination(type, data) {
const paginationId = type === 'onSale' ? 'onSalePagination' : 'soldPagination';
const currentPage = data.page || 1;
const limit = data.limit || 20;
const total = data.total || 0;
// 优先使用后端返回的pages,如果没有则自己计算
const totalPages = data.pages || Math.ceil(total / limit) || 1;
const hasPrev = data.has_prev !== undefined ? data.has_prev : (currentPage > 1);
const hasNext = data.has_next !== undefined ? data.has_next : (currentPage < totalPages);
console.log(`📄 分页: type=${type}, page=${currentPage}/${totalPages}, total=${total}, limit=${limit}`);
if (totalPages <= 1) {
console.log(`📄 totalPages <= 1, 清空分页`);
$('#' + paginationId).html('');
return;
}
let html = '';
// 上一页
if (hasPrev) {
html += `上一页`;
}
// 页码
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(totalPages, currentPage + 2);
if (startPage > 1) {
html += '1';
if (startPage > 2) {
html += '...';
}
}
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === currentPage ? ' active' : '';
html += `${i}`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
html += '...';
}
html += `${totalPages}`;
}
// 下一页
if (hasNext) {
html += `下一页`;
}
$('#' + paginationId).html(html);
}
// 显示商品详情模态框
function showGoodsModal(goods) {
const modal = $('#goodsModal');
const modalTitle = $('#goodsModalTitle');
const modalBody = $('#goodsModalBody');
const modalLink = $('#goodsModalLink');
const modalViewLink = $('#goodsModalViewLink');
modalTitle.text(goods.name);
// 设置"查看"按钮链接(始终显示原始商品详情链接)
modalViewLink.attr('href', goods.deal_link);
// 根据商品状态设置主按钮文字和链接
const isOnSale = goods.sell_status !== 2; // 不是已售状态就是在售状态
if (isOnSale) {
// 在售商品:根据用户权限显示不同按钮
if (currentUserLevel === 'vip' || currentUserLevel === 'pro') {
// VIP和PRO用户:显示"购买"按钮,使用购买链接
const purchaseLink = `https://xmeta.x-metash.cn/prod/xmeta_mall/index.html#/pages/confirmOrder/index?goodsId=${goods.goods_id}&platformId=${goods.platform_id}&amount=${goods.price}`;
modalLink.attr('href', purchaseLink);
modalLink.html('购买');
modalLink.removeClass('btn-outline-primary').addClass('btn-primary');
} else {
// 其他用户:显示"查看"按钮,使用原始链接
modalLink.attr('href', goods.deal_link);
modalLink.html('查看');
modalLink.removeClass('btn-primary').addClass('btn-outline-primary');
}
} else {
// 已售商品:显示"查看原链接"按钮,使用原始链接
modalLink.attr('href', goods.deal_link);
modalLink.html('查看原链接');
modalLink.removeClass('btn-primary').addClass('btn-outline-primary');
}
const statusText = goods.sell_status === 2 ? '已售' : (goods.is_presale ? '预售' : '在售');
const statusClass = goods.sell_status === 2 ? 'success' : (goods.is_presale ? 'warning' : 'primary');
// 处理多张图片
let imagesHtml = '';
if (goods.all_images) {
try {
// 支持数组或字符串格式
let allImages;
if (Array.isArray(goods.all_images)) {
allImages = goods.all_images;
} else if (typeof goods.all_images === 'string') {
allImages = JSON.parse(goods.all_images);
}
if (allImages && allImages.length > 0) {
imagesHtml = '';
allImages.forEach((imageUrl, index) => {
imagesHtml += `
`;
});
imagesHtml += '
';
} else {
imagesHtml = `
`;
}
} catch (e) {
imagesHtml = `
`;
}
} else {
imagesHtml = `
`;
}
// 处理特征信息显示
let traitsHtml = '';
// 提取PFP编号
let modalPfpNumber = goods.pfp_number;
if (!modalPfpNumber && goods.collection_number) {
modalPfpNumber = extractPfpNumberFromString(goods.collection_number);
}
// 检查是否为设定集
const modalIsShedingJi = modalPfpNumber && pfpFeatureModal && pfpFeatureModal.isShedingJi(modalPfpNumber);
if (goods.traits_info) {
try {
let traits = goods.traits_info;
if (typeof traits === 'string') {
traits = JSON.parse(traits);
}
if (traits && Object.keys(traits).length > 0) {
traitsHtml = '特征属性
';
for (const [category, value] of Object.entries(traits)) {
traitsHtml += `
`;
}
traitsHtml += '
';
}
} catch (e) {
console.error('解析特征信息失败:', e);
}
}
// 如果没有traits_info但有有效编号,尝试从官方数据获取特征
if (!traitsHtml && modalPfpNumber && modalPfpNumber >= 2 && modalPfpNumber <= 10000) {
if (pfpFeatureModal && pfpFeatureModal.allPFPData) {
const pfpData = pfpFeatureModal.allPFPData.data || pfpFeatureModal.allPFPData;
const officialData = pfpData[modalPfpNumber];
if (officialData && officialData.features) {
const features = pfpFeatureModal.formatFeatures(officialData.features);
if (features && features.length > 0) {
traitsHtml = '特征属性 官方数据
';
for (const feature of features) {
traitsHtml += `
${feature.category}
${feature.name}
${feature.rarity ? `${feature.rarity}` : ''}
`;
}
traitsHtml += '
';
}
}
}
}
// 官网不显示OCR识别结果,只显示最终的特征属性
// OCR识别结果仅在后台管理系统中显示
let ocrHtml = '';
// 生成编号显示HTML
let collectionNumberHtml = goods.collection_number || '无';
if (modalPfpNumber && modalPfpNumber >= 2 && modalPfpNumber <= 10000) {
const shedingJiBadge = modalIsShedingJi ? '设定集' : '';
collectionNumberHtml = `
#${modalPfpNumber}
${shedingJiBadge}
`;
}
/* 已移除OCR识别结果显示 - 官网只显示特征属性
if (goods.ocr_status === 2 && goods.ocr_result) {
try {
let ocrData = goods.ocr_result;
if (typeof ocrData === 'string') {
ocrData = JSON.parse(ocrData);
}
if (ocrData.text) {
ocrHtml = `
OCR识别结果
${ocrData.text}
${ocrData.processed_at ? `
识别时间: ${ocrData.processed_at}
` : ''}
`;
}
} catch (e) {
console.error('解析OCR结果失败:', e);
}
} else if (goods.ocr_status === 1) {
ocrHtml = `
`;
} else if (goods.ocr_status === 3) {
ocrHtml = `
`;
}
*/
modalBody.html(`
商品信息
| 名称: | ${goods.name} |
| 价格: | ¥${goods.price} |
| 状态: | ${statusText} |
| 卖家: | ${goods.seller_name || '未知'} |
| 编号: | ${collectionNumberHtml} |
| 平台: | ${goods.platform_name || '未知'} |
| 上架时间: | ${goods.publish_time || '未知'} |
${goods.sell_status === 2 ? `| 售出时间: | ${goods.sell_time || '未知'} |
` : ''}
${traitsHtml}
${ocrHtml}
${goods.description || goods.introduce ? `
商品描述
${goods.description || goods.introduce}
` : ''}
`);
modal.modal('show');
}
// 显示图片预览
function showImagePreview(imageUrl, title) {
const previewModal = `
`;
// 移除已存在的预览模态框
$('#imagePreviewModal').remove();
// 添加新的预览模态框
$('body').append(previewModal);
// 显示预览模态框
$('#imagePreviewModal').modal('show');
// 在模态框关闭后移除DOM元素
$('#imagePreviewModal').on('hidden.bs.modal', function () {
$(this).remove();
});
}
// 获取在售商品刷新间隔(秒,最少1秒)
function getOnSaleInterval() {
const intervalInput = $('#onSaleInterval');
let interval = parseInt(intervalInput.val()) || 3;
// 确保间隔至少为1秒
if (interval < 1) {
interval = 1;
intervalInput.val(1);
}
return interval;
}
// 获取已售商品刷新间隔(秒,最少1秒)
function getSoldInterval() {
const intervalInput = $('#soldInterval');
let interval = parseInt(intervalInput.val()) || 30;
// 确保间隔至少为1秒
if (interval < 1) {
interval = 1;
intervalInput.val(1);
}
return interval;
}
// 获取刷新间隔(秒转毫秒,最少1秒) - 保持向后兼容
function getRefreshInterval() {
// 为了向后兼容,返回在售商品的间隔
return getOnSaleInterval() * 1000; // 转换为毫秒
}
// 开始自动刷新
function startAutoRefresh() {
// 先停止所有现有的刷新(不显示消息,避免alert重复移除错误)
stopAutoRefresh(false);
const onSaleInterval = getOnSaleInterval() * 1000; // 转换为毫秒
const soldInterval = getSoldInterval() * 1000; // 转换为毫秒
console.debug(`启动自动刷新 - 在售商品:${onSaleInterval/1000}秒, 已售商品:${soldInterval/1000}秒, 统计数据:60秒`);
// 启动在售商品自动刷新
startOnSaleAutoRefresh(onSaleInterval);
// 启动已售商品自动刷新
startSoldAutoRefresh(soldInterval);
// ========== 优化:启动统计数据定期刷新 ==========
startStatisticsRefresh();
// 保持兼容性设置
autoRefreshInterval = onSaleRefreshInterval;
updateMonitorStatus(`监控状态: 自动刷新中 (在售:${onSaleInterval/1000}s, 已售:${soldInterval/1000}s)`, 'success');
showMessage(`自动刷新已启动 - 在售:${onSaleInterval/1000}秒, 已售:${soldInterval/1000}秒`, 'success');
}
// 启动在售商品自动刷新(仅从数据库读取数据)
function startOnSaleAutoRefresh(interval) {
if (onSaleRefreshInterval) {
clearInterval(onSaleRefreshInterval);
}
onSaleRefreshInterval = setInterval(function() {
try {
// 检查开关状态
if (!$('#autoRefresh').prop('checked')) {
console.log('自动刷新开关已关闭,停止在售商品刷新');
stopOnSaleAutoRefresh();
return;
}
// 检查页面可见性
if (document.hidden) {
console.log('页面隐藏,跳过在售商品刷新');
return;
}
// 防重叠检查
if (onSaleRefreshInProgress || manualRefreshInProgress) {
console.log('在售商品刷新仍在进行中或手动刷新中,跳过本次刷新');
return;
}
const now = Date.now();
// 执行数据库数据刷新
console.log(`执行在售商品数据刷新 (间隔${interval/1000}秒)`);
onSaleRefreshInProgress = true;
lastOnSaleRefreshTime = now;
lastRefreshTime = now; // 保持兼容性
updateSyncStatus('onSale', 'loading', '正在刷新数据...');
loadOnSaleGoods(currentOnSalePage)
.then(() => {
// 刷新成功,重置错误计数
onSaleRefreshErrorCount = 0;
})
.catch((error) => {
console.error('在售商品刷新出错:', error);
onSaleRefreshErrorCount++;
updateSyncStatus('onSale', 'error', '刷新失败: ' + (error.message || '未知错误'));
if (onSaleRefreshErrorCount >= 5) {
console.error('在售商品连续刷新失败过多,停止刷新');
stopOnSaleAutoRefresh();
showError('在售商品自动刷新连续失败,已停止');
}
})
.always(() => {
onSaleRefreshInProgress = false;
});
} catch (error) {
console.error('在售商品自动刷新机制出错:', error);
onSaleRefreshInProgress = false;
}
}, interval);
}
// 启动已售商品自动刷新(仅从数据库读取数据)
function startSoldAutoRefresh(interval) {
if (soldRefreshInterval) {
clearInterval(soldRefreshInterval);
}
soldRefreshInterval = setInterval(function() {
try {
// 检查开关状态
if (!$('#autoRefresh').prop('checked')) {
console.log('自动刷新开关已关闭,停止已售商品刷新');
stopSoldAutoRefresh();
return;
}
// 检查页面可见性
if (document.hidden) {
console.log('页面隐藏,跳过已售商品刷新');
return;
}
// 防重叠检查
if (soldRefreshInProgress || manualRefreshInProgress) {
console.log('已售商品刷新仍在进行中或手动刷新中,跳过本次刷新');
return;
}
const now = Date.now();
// 执行数据库数据刷新 - 只刷新第一页最新数据
console.log(`执行已售商品数据刷新 (间隔${interval/1000}秒) - 仅更新第一页数据`);
soldRefreshInProgress = true;
lastSoldRefreshTime = now;
updateSyncStatus('sold', 'loading', '正在刷新最新数据...');
// 自动刷新策略:只刷新第一页最新数据,减少网络负载和后端耦合
const targetPage = 1; // 自动刷新始终只获取第一页最新数据
loadSoldGoods(targetPage, true)
.then(() => {
// 刷新成功,重置错误计数
soldRefreshErrorCount = 0;
})
.catch((error) => {
console.error('已售商品刷新出错:', error);
soldRefreshErrorCount++;
updateSyncStatus('sold', 'error', '刷新失败: ' + (error.message || '未知错误'));
if (soldRefreshErrorCount >= 5) {
console.error('已售商品连续刷新失败过多,停止刷新');
stopSoldAutoRefresh();
showError('已售商品自动刷新连续失败,已停止');
}
})
.always(() => {
soldRefreshInProgress = false;
});
} catch (error) {
console.error('已售商品自动刷新机制出错:', error);
soldRefreshInProgress = false;
}
}, interval);
}
// 停止自动刷新
function stopAutoRefresh(showMsg = true) {
stopOnSaleAutoRefresh();
stopSoldAutoRefresh();
stopStatisticsRefresh(); // 停止统计数据刷新
// 保持兼容性
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
// 只在真正停止时才显示消息,避免在启动时显示重复消息
if (showMsg) {
updateMonitorStatus('监控状态: 已停止', 'secondary');
showMessage('自动刷新已停止', 'info');
}
console.log('自动刷新已停止');
}
// 停止在售商品自动刷新
function stopOnSaleAutoRefresh() {
if (onSaleRefreshInterval) {
clearInterval(onSaleRefreshInterval);
onSaleRefreshInterval = null;
console.log('在售商品自动刷新已停止');
}
onSaleRefreshInProgress = false;
onSaleRefreshErrorCount = 0;
}
// 停止已售商品自动刷新
function stopSoldAutoRefresh() {
if (soldRefreshInterval) {
clearInterval(soldRefreshInterval);
soldRefreshInterval = null;
console.log('已售商品自动刷新已停止');
}
soldRefreshInProgress = false;
soldRefreshErrorCount = 0;
}
// ========== 优化:统计数据定期刷新 ==========
// 启动统计数据定期刷新(每60秒)
function startStatisticsRefresh() {
if (statisticsRefreshInterval) {
clearInterval(statisticsRefreshInterval);
}
statisticsRefreshInterval = setInterval(() => {
// 检查页面可见性
if (document.hidden) {
console.log('页面隐藏,跳过统计数据刷新');
return;
}
console.log('执行统计数据定期刷新');
loadStatistics().catch(error => {
console.error('统计数据刷新失败:', error);
});
}, 60000); // 每60秒刷新一次
console.log('✅ 统计数据定期刷新已启动(60秒间隔)');
}
// 停止统计数据定期刷新
function stopStatisticsRefresh() {
if (statisticsRefreshInterval) {
clearInterval(statisticsRefreshInterval);
statisticsRefreshInterval = null;
console.log('统计数据定期刷新已停止');
}
}
// 刷新显示数据(轻量级,仅更新前端显示)
function refreshDisplayData(silent = false) {
if (!silent) {
console.log('刷新显示数据...');
manualRefreshInProgress = true;
}
// 使用jQuery.when确保返回jQuery Deferred对象,保证.always()兼容性
return $.when(
loadStatistics(),
loadOnSaleGoods(currentOnSalePage),
loadSoldGoods(currentSoldPage),
loadMarketData(),
checkMonitorStatus()
).then(() => {
if (!silent) {
console.log('显示数据刷新完成');
// 更新刷新时间戳,防止健康检查误判
const now = Date.now();
lastOnSaleRefreshTime = now;
lastSoldRefreshTime = now;
lastRefreshTime = now;
}
}).fail((error) => {
if (!silent) {
console.error('显示数据刷新出错:', error);
}
}).always(() => {
if (!silent) {
manualRefreshInProgress = false;
}
});
}
// 刷新显示数据(前端只从数据库读取最新数据)
function forceRefreshAllData() {
const refreshBtn = $('#refreshBtn');
const refreshStatus = $('#refreshStatus');
refreshBtn.addClass('refreshing').prop('disabled', true);
refreshStatus.html('正在刷新显示数据...');
console.log('刷新显示数据...');
return refreshDisplayData(false)
.then(() => {
console.log('显示数据刷新成功');
showMessage('显示数据刷新成功', 'success');
refreshStatus.html('刷新成功');
})
.catch((error) => {
console.error('显示数据刷新失败:', error);
showError('显示数据刷新失败,请稍后重试');
refreshStatus.html('刷新失败');
})
.always(() => {
refreshBtn.removeClass('refreshing').prop('disabled', false);
setTimeout(function() {
refreshStatus.html('');
}, 3000);
});
}
// 保持向后兼容的函数名
function refreshAllData(silent = false) {
if (silent) {
// 自动刷新时使用轻量级刷新
return refreshDisplayData(true);
} else {
// 手动刷新时使用强制刷新
return forceRefreshAllData();
}
}
// 检查监控状态
function checkMonitorStatus() {
return $.ajax({
url: '/api/monitor/status',
method: 'GET',
timeout: 10000, // 10秒超时
cache: false
})
.done(function(response) {
if (response.success) {
const data = response.data;
const isMonitoring = data.is_monitoring;
const crawlerStatus = data.crawler_status;
const intervals = {
on_sale_interval: data.on_sale_interval,
sold_interval: data.sold_interval,
crawler_interval: data.crawler_interval // 向后兼容
};
const syncTimes = {
lastOnSale: data.last_on_sale_update,
lastSold: data.last_sold_update
};
if (isMonitoring) {
updateMonitorStatus('监控状态: 运行中', 'success');
} else {
updateMonitorStatus('监控状态: 已停止', 'secondary');
}
// 更新爬虫状态显示
updateCrawlerStatus(crawlerStatus, intervals, syncTimes);
}
})
.fail(function() {
updateMonitorStatus('监控状态: 连接失败', 'danger');
updateCrawlerStatus('unknown', null, {lastOnSale: null, lastSold: null});
});
}
// 更新监控状态显示(优化版:减少闪动)
function updateMonitorStatus(text, type) {
const statusElement = $('#monitorStatus');
const currentText = statusElement.text().trim();
const currentClasses = statusElement.attr('class') || '';
const newClass = `bg-${type}`;
// 只在内容或样式真正改变时才更新DOM
if (currentText !== text || !currentClasses.includes(newClass)) {
statusElement.removeClass('bg-success bg-warning bg-danger bg-secondary')
.addClass(newClass)
.text(text);
}
}
// 更新爬虫状态显示
function updateCrawlerStatus(status, intervals, syncTimes) {
const crawlerElement = $('#crawlerStatus');
// 不显示任何后端相关信息
crawlerElement.html('');
}
// 显示错误消息
function showError(message) {
showMessage(message, 'error');
}
// 显示消息
function showMessage(message, type = 'info') {
const alertClass = type === 'error' ? 'alert-danger' :
type === 'success' ? 'alert-success' :
type === 'warning' ? 'alert-warning' : 'alert-info';
const alertHtml = `
${message}
`;
$('body').append(alertHtml);
// 自动消失
setTimeout(function() {
$('.alert').alert('close');
}, 5000);
}
// 格式化时间
function formatTime(timeString) {
if (!timeString || timeString === '1970-01-01 08:00:00') {
return '未知';
}
const date = new Date(timeString);
const now = new Date();
const diff = now - date;
// 如果是今天,显示具体时间
if (diff < 24 * 60 * 60 * 1000) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}
// 否则显示日期
return date.toLocaleDateString('zh-CN');
}
// 格式化完整时间(用于价格排行榜等需要显示完整时间的场景)
function formatFullTime(timeString) {
if (!timeString || timeString === '1970-01-01 08:00:00') {
return '未知';
}
const date = new Date(timeString);
// 显示完整的日期和时间
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
}
// 页面可见性变化处理
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
// 页面隐藏时暂停自动刷新以节省资源
if ((onSaleRefreshInterval || soldRefreshInterval) && $('#autoRefresh').prop('checked')) {
console.log('页面隐藏,暂停自动刷新');
stopAutoRefresh();
}
} else {
// 页面显示时恢复自动刷新
if ($('#autoRefresh').prop('checked') && !onSaleRefreshInterval && !soldRefreshInterval) {
console.log('页面显示,恢复自动刷新');
// 重置时间戳,避免触发"长时间无响应"误判
lastOnSaleRefreshTime = Date.now();
lastSoldRefreshTime = Date.now();
startAutoRefresh();
}
}
});
// 搜索功能相关函数
// 验证搜索输入(SQL注入防护)
function validateSearchInput(keyword) {
// 检查是否为空
if (!keyword || keyword.trim() === '') {
return {
valid: false,
message: '请输入搜索关键词'
};
}
// 检查长度限制(最大100字符)
if (keyword.length > 100) {
return {
valid: false,
message: '搜索关键词过长,最多支持100个字符'
};
}
// SQL注入可疑模式检测(与后端保持一致)
const suspiciousPatterns = [
/;\s*DROP/i,
/;\s*DELETE/i,
/;\s*UPDATE/i,
/UNION\s+SELECT/i,
/--/,
/\/\*.*\*\//
];
// 检测每个可疑模式
for (const pattern of suspiciousPatterns) {
if (pattern.test(keyword)) {
return {
valid: false,
message: '输入内容包含非法字符'
};
}
}
// 验证通过
return {
valid: true,
message: ''
};
}
// 执行搜索
function performSearch() {
const keyword = $('#searchInput').val().trim();
const status = $('#searchStatus').val();
// 验证搜索输入(SQL注入防护)
const validationResult = validateSearchInput(keyword);
if (!validationResult.valid) {
showError(validationResult.message);
return;
}
currentSearchKeyword = keyword;
currentSearchStatus = status;
currentSearchPage = 1;
currentSortBy = $('#searchSortBy').val();
console.log(`搜索: 关键词="${keyword}", 状态="${status}", 排序="${currentSortBy}", 方向="${currentSortOrder}"`);
// 显示搜索结果区域,隐藏默认列表
switchToSearchMode();
// 执行搜索
loadSearchResults(currentSearchPage);
}
// 清空搜索
function clearSearch() {
$('#searchInput').val('');
$('#searchStatus').val('');
$('#searchSortBy').val('time');
currentSearchKeyword = '';
currentSearchStatus = '';
currentSearchPage = 1;
currentSortBy = 'time';
currentSortOrder = 'desc';
// 重置排序UI
updateSortOrderIcon();
// 隐藏搜索结果区域,显示默认列表
switchToDefaultMode();
console.log('搜索已清空,返回默认视图');
}
// 更新搜索类型选项(根据用户权限)
function updateSearchTypeOptions() {
const userInfo = webAuth.getUserInfo();
const userLevel = userInfo ? (userInfo.user_level || 'guest') : 'guest';
const searchTypeSelect = $('#searchTypeSelect');
// 定义搜索类型选项
const allOptions = {
'cumulative': { label: '智能搜索', levels: ['guest', 'normal', 'vip', 'pro'] },
'name': { label: '名称/编号', levels: ['guest', 'normal', 'vip', 'pro'] },
'description': { label: '商品描述', levels: ['normal', 'vip', 'pro'] },
'trait': { label: '特征属性', levels: ['vip', 'pro'] },
'seller': { label: '卖家昵称', levels: ['pro'] }
};
// 清空现有选项
searchTypeSelect.empty();
// 添加用户可用的选项
for (const [value, config] of Object.entries(allOptions)) {
if (config.levels.includes(userLevel)) {
const option = $('')
.attr('value', value)
.text(config.label);
searchTypeSelect.append(option);
}
}
// 默认选择cumulative
searchTypeSelect.val('cumulative');
console.debug('搜索类型选项已更新');
}
// 更新排序方向图标
function updateSortOrderIcon() {
const icon = $('#sortOrderIcon');
if (currentSortOrder === 'desc') {
icon.removeClass('fa-sort-amount-up').addClass('fa-sort-amount-down');
$('#sortOrderBtn').attr('title', '降序排列 (点击切换为升序)');
} else {
icon.removeClass('fa-sort-amount-down').addClass('fa-sort-amount-up');
$('#sortOrderBtn').attr('title', '升序排列 (点击切换为降序)');
}
}
// 获取排序显示文本
function getSortDisplayText(sortBy, sortOrder) {
const sortName = sortBy === 'price' ? '价格' : '时间';
const orderName = sortOrder === 'desc' ? '降序' : '升序';
return `按${sortName}${orderName}排列`;
}
// 切换到搜索模式
function switchToSearchMode() {
if (!isSearchMode) {
isSearchMode = true;
$('#searchResultsSection').show();
$('#defaultListsSection').hide();
}
}
// 切换到默认模式
function switchToDefaultMode() {
if (isSearchMode) {
isSearchMode = false;
$('#searchResultsSection').hide();
$('#defaultListsSection').show();
// 刷新默认列表
refreshDisplayData(false);
}
}
// 加载搜索结果
function loadSearchResults(page = 1) {
currentSearchPage = page;
// 从下拉菜单获取用户选择的搜索类型
const searchType = $('#searchTypeSelect').val() || 'cumulative';
const params = {
keyword: currentSearchKeyword,
search_type: searchType, // 使用用户选择的搜索类型
page: page,
limit: pageSize,
sort_by: currentSortBy,
sort_order: currentSortOrder
};
if (currentSearchStatus) {
params.status = currentSearchStatus;
}
console.log('加载搜索结果:', params);
// 使用 $.ajax 并禁用缓存,避免搜索结果被缓存
$.ajax({
url: '/api/v1/goods/search',
type: 'GET',
data: params,
cache: false, // 禁用缓存
success: function(response) {
if (response.success) {
displaySearchResults(response.data);
} else {
showError('搜索失败: ' + response.error);
}
},
error: function(xhr, status, error) {
console.error('搜索API调用失败:', error);
showError('搜索请求失败,请稍后重试');
}
});
}
// 显示搜索结果
function displaySearchResults(data) {
const { goods, total, page, pages, keyword, status, sort_by, sort_order } = data;
// 更新搜索信息显示
let statusText = '';
if (status == 1) statusText = ' (在售)';
else if (status == 2) statusText = ' (已售)';
$('#searchResultInfo').text(`搜索 "${keyword}"${statusText}: 找到 ${total} 件商品`);
// 更新排序信息显示
const sortText = getSortDisplayText(sort_by, sort_order);
$('#searchSortInfo').text(sortText);
// 显示商品列表
const listContainer = $('#searchResultsList');
listContainer.empty();
if (goods.length === 0) {
listContainer.html(`
`);
} else {
goods.forEach(item => {
// 根据商品的sell_status确定类型
const type = item.sell_status === 2 ? 'sold' : 'on-sale';
const goodsHtml = createGoodsItemHtml(item, type);
listContainer.append(goodsHtml);
});
}
// 更新分页
updateSearchPagination(data);
// ========== Phase 2优化:触发图片懒加载 ==========
triggerLazyLoad();
console.log(`显示搜索结果: ${goods.length}/${total} 件商品`);
}
// 更新搜索分页
function updateSearchPagination(data) {
const pagination = $('#searchPagination');
pagination.empty();
if (data.pages <= 1) {
return;
}
const { page, pages } = data;
// 上一页
if (data.has_prev) {
pagination.append(`上一页`);
} else {
pagination.append('上一页');
}
// 页码
const startPage = Math.max(1, page - 2);
const endPage = Math.min(pages, page + 2);
if (startPage > 1) {
pagination.append('1');
if (startPage > 2) {
pagination.append('...');
}
}
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === page ? ' active' : '';
pagination.append(`${i}`);
}
if (endPage < pages) {
if (endPage < pages - 1) {
pagination.append('...');
}
pagination.append(`${pages}`);
}
// 下一页
if (data.has_next) {
pagination.append(`下一页`);
} else {
pagination.append('下一页');
}
}
// 健康检查机制
function startHealthCheck() {
// 每20秒检查一次自动刷新状态
setInterval(function() {
try {
const shouldBeRunning = $('#autoRefresh').prop('checked');
const onSaleRunning = onSaleRefreshInterval !== null;
const soldRunning = soldRefreshInterval !== null;
const timeSinceLastOnSaleRefresh = Date.now() - lastOnSaleRefreshTime;
const timeSinceLastSoldRefresh = Date.now() - lastSoldRefreshTime;
console.log(`健康检查 - 开关:${shouldBeRunning}, 在售定时器:${onSaleRunning}, 已售定时器:${soldRunning}`);
console.log(`距上次刷新 - 在售:${Math.round(timeSinceLastOnSaleRefresh/1000)}s, 已售:${Math.round(timeSinceLastSoldRefresh/1000)}s`);
// 检查定时器是否意外停止
if (shouldBeRunning && (!onSaleRunning || !soldRunning)) {
console.warn('检测到自动刷新定时器意外停止,尝试重启');
startAutoRefresh();
return;
}
// 检查在售商品刷新是否长时间没有响应
const onSaleInterval = getOnSaleInterval() * 1000;
if (shouldBeRunning && onSaleRunning && timeSinceLastOnSaleRefresh > onSaleInterval * 10) {
console.warn('检测到在售商品刷新长时间无响应,尝试重启');
stopOnSaleAutoRefresh();
onSaleRefreshInProgress = false;
onSaleRefreshErrorCount = 0;
setTimeout(() => startOnSaleAutoRefresh(onSaleInterval), 1000);
}
// 检查已售商品刷新是否长时间没有响应
const soldInterval = getSoldInterval() * 1000;
if (shouldBeRunning && soldRunning && timeSinceLastSoldRefresh > soldInterval * 10) {
console.warn('检测到已售商品刷新长时间无响应,尝试重启');
stopSoldAutoRefresh();
soldRefreshInProgress = false;
soldRefreshErrorCount = 0;
setTimeout(() => startSoldAutoRefresh(soldInterval), 1000);
}
// 检查是否卡在刷新状态
if (onSaleRefreshInProgress && timeSinceLastOnSaleRefresh > 30000) {
console.warn('检测到在售商品刷新进程可能卡死,重置状态');
onSaleRefreshInProgress = false;
}
if (soldRefreshInProgress && timeSinceLastSoldRefresh > 30000) {
console.warn('检测到已售商品刷新进程可能卡死,重置状态');
soldRefreshInProgress = false;
}
// 保持向后兼容性
refreshInProgress = onSaleRefreshInProgress || soldRefreshInProgress;
lastRefreshTime = Math.max(lastOnSaleRefreshTime, lastSoldRefreshTime);
autoRefreshInterval = onSaleRefreshInterval;
} catch (error) {
console.error('健康检查出错:', error);
}
}, 20000);
console.log('自动刷新健康检查机制已启动');
}
// 显示成交分析模态框
function showAnalysisModal() {
const modal = $('#analysisModal');
modal.modal('show');
// 加载分析数据
loadAnalysisData();
}
// 加载成交分析数据
function loadAnalysisData(timeRange) {
// 如果没有传入timeRange,使用当前选中的值,默认为30d
if (!timeRange) {
timeRange = $('input[name="timeRange"]:checked').val() || '30d';
}
const modalBody = $('#analysisModalBody');
// 显示加载状态
modalBody.html(`
`);
// 使用time_range参数调用API
$.get(`/api/v1/analytics/transactions?time_range=${timeRange}&limit=200`)
.done(function(response) {
if (response.success) {
displayAnalysisData(response.data);
} else {
modalBody.html(`
加载分析数据失败: ${response.error || '未知错误'}
`);
}
})
.fail(function(xhr, status, error) {
console.error('加载分析数据失败:', error);
let errorMsg = '网络连接失败,请稍后重试';
if (xhr.responseJSON && xhr.responseJSON.detail) {
errorMsg = xhr.responseJSON.detail;
}
modalBody.html(`
${errorMsg}
`);
});
}
// 全局变量存储价格排行榜数据
let globalPriceRankingData = [];
// 显示分析数据
function displayAnalysisData(data) {
const modalBody = $('#analysisModalBody');
// 存储价格排行榜数据到全局变量
globalPriceRankingData = data.price_ranking || [];
// 获取时间范围标签
const timeRangeLabels = {
'7d': '近7天',
'30d': '近30天',
'90d': '近90天',
'all': '全部历史'
};
const timeRangeLabel = timeRangeLabels[data.time_range] || '近30天';
let html = '';
// 1. 价格排行榜(支持点击查看详情)
html += `
| 排名 |
商品名称 |
价格 |
卖家 |
售出时间 |
`;
data.price_ranking.forEach((item, index) => {
const rankClass = index < 3 ? 'text-warning fw-bold' : '';
const rankIcon = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '';
html += `
| ${rankIcon}${index + 1} |
${item.name}
|
¥${item.price} |
${item.seller_name || '未知'} |
${formatFullTime(item.sell_time)} |
`;
});
html += `
`;
// 2. 卖家排行榜
html += `
| 排名 |
卖家 |
成交数 |
总收入 |
均价 |
`;
data.seller_ranking.slice(0, 15).forEach((item, index) => {
html += `
| ${index + 1} |
${item.seller_name} |
${item.sold_count} |
¥${Math.round(item.total_revenue)} |
¥${Math.round(item.avg_price)} |
`;
});
html += `
`;
// 3. 价格分布
html += `
`;
data.price_distribution.forEach(item => {
const percentage = (item.count / data.price_ranking.length * 100).toFixed(1);
html += `
${item.price_range}
${item.count}件 (${percentage}%)
平均价格: ¥${Math.round(item.avg_price)}
`;
});
html += `
`;
// 4. 每日成交统计
html += `
| 日期 |
成交数量 |
平均价格 |
总成交额 |
`;
data.daily_transactions.forEach(item => {
html += `
| ${item.sell_date} |
${item.daily_count} |
¥${Math.round(item.daily_avg_price || 0)} |
¥${Math.round(item.daily_total_value || 0)} |
`;
});
html += `
`;
// 5. 稀有度统计
if (data.rarity_stats && data.rarity_stats.length > 0) {
html += `
| 类型/稀有度 |
成交数 |
均价 |
价格区间 |
`;
data.rarity_stats.forEach(item => {
html += `
| ${item.collection_number} |
${item.count} |
¥${Math.round(item.avg_price)} |
¥${Math.round(item.min_price)} - ¥${Math.round(item.max_price)} |
`;
});
html += `
`;
}
// 6. 成交时间分布
if (data.time_distribution && data.time_distribution.length > 0) {
html += `
`;
data.time_distribution.forEach(item => {
const maxCount = Math.max(...data.time_distribution.map(d => d.count));
const percentage = (item.count / maxCount * 100).toFixed(1);
html += `
${item.time_period}
${item.count}件 | 均价¥${Math.round(item.avg_price)}
`;
});
html += `
`;
}
html += '
';
modalBody.html(html);
}
// 显示价格排行榜商品详情
function showPriceRankingDetails(index) {
if (!globalPriceRankingData || index >= globalPriceRankingData.length) {
console.error('价格排行榜数据不存在或索引超出范围');
return;
}
const item = globalPriceRankingData[index];
// 转换数据格式以匹配 showGoodsModal 的期望
const goodsId = item.goods_id || item.id;
// 生成正确的详情链接,优先使用数据库中的链接,如果为空则使用模板生成
let dealLink = item.deal_link;
if (!dealLink || dealLink.trim() === '') {
dealLink = `https://xmeta.x-metash.cn/prod/xmeta_mall/index.html#/pages/goodsDetail/index?id=${goodsId}`;
}
const goodsData = {
id: goodsId, // 数据库返回的是 goods_id
name: item.name,
description: item.description || item.introduce || '', // 支持两个字段
introduce: item.introduce || item.description || '',
price: item.price,
original_price: item.original_price || item.price,
seller_name: item.seller_name,
sell_status: 2, // 价格排行榜都是已售商品
sell_time: item.sell_time,
image_url: item.image_url,
all_images: item.all_images || '[]', // 数据库直接返回的字段名
deal_link: dealLink, // 使用生成或修正后的链接
collection_number: item.collection_number, // 编号
platform_name: item.platform_name || '未知', // 平台名称
publish_time: item.publish_time || '未知', // 上架时间
is_presale: false // 已售商品不是预售
};
// 使用现有的商品详情模态框
showGoodsModal(goodsData);
}
// 执行重新获取已售数据(需要密码)
function executeRefreshSoldData(password) {
const statusDiv = $('#refreshSoldStatus');
const btn = $('#refreshSoldDataBtn');
// 显示执行状态
statusDiv.show().html(`
正在验证管理员密码...
`);
// 禁用按钮防止重复点击
btn.prop('disabled', true).html('执行中...');
// 发送密码和执行请求
$.ajax({
url: '/api/refresh/sold',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ password: password }),
success: function(response) {
if (response.success) {
statusDiv.html(`
执行成功!
${response.message || '已售数据重新获取完成'}
${response.processed_pages ? `
处理页数: ${response.processed_pages}` : ''}
${response.total_goods ? `
总商品数: ${response.total_goods}` : ''}
`);
// 清空密码输入框并重置按钮状态
$('#adminPassword').val('');
$('#refreshSoldDataBtn').prop('disabled', true).removeClass('btn-warning').addClass('btn-secondary');
// 刷新已售商品列表
setTimeout(() => {
loadSoldGoods(currentSoldPage);
loadStatistics();
}, 1000);
showMessage('已售数据重新获取成功', 'success');
} else {
throw new Error(response.error || '操作失败');
}
},
error: function(xhr, status, error) {
let errorMessage = '操作失败';
if (xhr.status === 401) {
errorMessage = '密码错误,无权执行此操作';
statusDiv.html(`
密码错误
请检查管理员密码是否正确
`);
// 清空错误的密码
$('#adminPassword').val().select();
} else {
try {
const response = JSON.parse(xhr.responseText);
errorMessage = response.error || errorMessage;
} catch (e) {
errorMessage = `网络错误 (${xhr.status})`;
}
statusDiv.html(`
执行失败
${errorMessage}
`);
}
showMessage(errorMessage, 'error');
},
complete: function() {
// 重新启用按钮
btn.prop('disabled', $('#adminPassword').val().trim().length === 0)
.html('执行重新获取');
}
});
}
// 设置模态框相关功能
function loadSettingsModal() {
// 加载当前设置到模态框
const currentOnSaleInterval = $('#onSaleInterval').val() || 3;
const currentSoldInterval = $('#soldInterval').val() || 30;
const isAutoRefreshEnabled = $('#autoRefresh').is(':checked');
$('#settingsOnSaleInterval').val(currentOnSaleInterval);
$('#settingsSoldInterval').val(currentSoldInterval);
$('#settingsAutoRefresh').prop('checked', isAutoRefreshEnabled);
// 重置管理员功能区域
$('#adminPassword').val('');
$('#refreshSoldDataBtn').prop('disabled', true).removeClass('btn-warning').addClass('btn-secondary')
.html('执行重新获取');
$('#refreshSoldStatus').hide();
}
function saveSettings() {
let newOnSaleInterval = parseInt($('#settingsOnSaleInterval').val());
let newSoldInterval = parseInt($('#settingsSoldInterval').val());
const enableAutoRefresh = $('#settingsAutoRefresh').is(':checked');
// 验证输入值
if (isNaN(newOnSaleInterval) || newOnSaleInterval < 1) {
newOnSaleInterval = 1;
$('#settingsOnSaleInterval').val(1);
showMessage('在售商品刷新间隔已自动修正为最小值1秒', 'warning');
}
if (isNaN(newSoldInterval) || newSoldInterval < 1) {
newSoldInterval = 1;
$('#settingsSoldInterval').val(1);
showMessage('已售商品刷新间隔已自动修正为最小值1秒', 'warning');
}
// 应用设置到主界面
$('#onSaleInterval').val(newOnSaleInterval);
$('#soldInterval').val(newSoldInterval);
$('#autoRefresh').prop('checked', enableAutoRefresh);
// 更新同步间隔显示
updateSyncInfo();
// 如果自动刷新状态发生变化,相应地启动或停止
if (enableAutoRefresh) {
if (autoRefreshInterval) {
stopAutoRefresh(); // 先停止旧的
}
startAutoRefresh(); // 启动新的
} else {
stopAutoRefresh();
}
// 关闭模态框
$('#settingsModal').modal('hide');
// 显示保存成功消息
showMessage(`设置已保存:在售商品${newOnSaleInterval}秒,已售商品${newSoldInterval}秒,自动刷新${enableAutoRefresh ? '已启用' : '已禁用'}`, 'success');
console.log(`设置已保存:在售商品${newOnSaleInterval}秒,已售商品${newSoldInterval}秒,自动刷新${enableAutoRefresh ? '已启用' : '已禁用'}`);
}
// 检查已售数据同步状态
function checkSoldDataSyncStatus() {
console.log('检查已售数据同步状态...');
return $.get('/api/sold/sync-status')
.done(function(response) {
if (response.success) {
const data = response;
const syncRate = data.sync_rate;
console.log(`已售数据同步状态: ${data.message}`);
if (syncRate < 90) {
showMessage(`已售数据同步异常: ${data.message},正在自动修复...`, 'warning');
// 自动尝试修复同步问题
autoFixSoldDataSync();
} else {
console.log(`已售数据同步正常: ${data.message}`);
}
return data;
} else {
console.error('检查同步状态失败:', response.error);
return null;
}
})
.fail(function(xhr, status, error) {
console.error('检查已售数据同步状态失败:', error);
return null;
});
}
// 窗口关闭前清理
window.addEventListener('beforeunload', function() {
stopAutoRefresh();
});
// ========== 优化:禁用定期同步检查机制 ==========
// 原因:当前修复任务(fetch_sold_goods_details)不存在,检查机制无效
// 每次检查都会触发警告,但无法真正修复,浪费资源且影响用户体验
// 如需启用,请先实现真正的修复任务
/*
let syncCheckInterval = setInterval(function() {
checkSoldDataSyncStatus().then(data => {
if (data && data.sync_rate < 90) {
// 如果同步率过低,增加检查频率
clearInterval(syncCheckInterval);
syncCheckInterval = setInterval(() => checkSoldDataSyncStatus(), 1 * 60 * 1000); // 每分钟检查
logger.warning('已售数据同步异常,增加检查频率');
}
});
}, 3 * 60 * 1000);
*/
// 自动修复已售数据同步问题
function autoFixSoldDataSync() {
console.log('尝试自动修复已售数据同步问题...');
return $.post('/api/sold/force-sync')
.done(function(response) {
if (response.success) {
if (response.action === 'forced_refresh') {
showMessage(`同步修复成功,同步率从${response.sync_before}%提升`, 'success');
// 刷新已售商品列表
loadSoldGoods(1);
} else {
showMessage('已售数据同步正常,无需修复', 'info');
}
} else {
showMessage('自动修复失败: ' + response.error, 'error');
}
})
.fail(function(xhr, status, error) {
console.error('自动修复已售数据同步失败:', error);
showMessage('自动修复网络请求失败,请手动检查', 'error');
});
}
// ========== 优化:禁用页面加载时的同步检查 ==========
// 原因:当前修复任务(fetch_sold_goods_details)不存在,检查机制无效
// 每次打开页面都会显示警告,但无法真正修复,影响用户体验和首页加载速度
// 如需启用,请先实现真正的修复任务
/*
$(document).ready(function() {
setTimeout(() => {
checkSoldDataSyncStatus();
}, 5000); // 页面加载5秒后检查
});
*/
// ==================== 管理员功能 ====================
// 管理员功能变量
let currentAdminAction = '';
let batchUpdateInProgress = false;
// 设置事件监听器 - 在 setupEventListeners 函数中添加
function setupAdminEventListeners() {
// 管理员密码输入监听 - 控制设置界面中的按钮启用状态
$('#adminPassword').on('input', function() {
const password = $(this).val().trim();
const isPasswordValid = password.length > 0;
// 注意:原有的重新获取数据和批量更新功能已集成到数据校验功能中
// 这些按钮已从界面移除,相关功能通过数据校验模块实现
// $('#refreshOnSaleDataBtn').prop('disabled', !isPasswordValid);
// $('#refreshSoldDataBtn').prop('disabled', !isPasswordValid);
// $('#batchUpdateBtn').prop('disabled', !isPasswordValid);
});
// 注意:以下功能已集成到数据校验模块中,相关按钮已从界面移除
/*
// 重新获取在售数据按钮点击
$('#refreshOnSaleDataBtn').click(function(e) {
e.preventDefault();
const password = $('#adminPassword').val().trim();
if (!password) {
showMessage('请先输入管理员密码', 'warning');
return;
}
if (confirm('确认要重新获取所有在售数据吗?这个操作可能需要较长时间。')) {
executeRefreshOnSaleData(password);
}
});
// 批量更新按钮点击(设置界面中的)
$('#batchUpdateBtn').click(function(e) {
e.preventDefault();
const password = $('#adminPassword').val().trim();
if (!password) {
showMessage('请先输入管理员密码', 'warning');
return;
}
showAdminConfirmModal('批量更新商品详情信息', 'batchUpdate');
});
*/
// 管理员密码确认输入监听
$('#adminConfirmPassword').on('input', function() {
const password = $(this).val().trim();
$('#confirmAdminAction').prop('disabled', password.length === 0);
});
// 密码显示/隐藏切换
$('#toggleConfirmPassword').click(function() {
const input = $('#adminConfirmPassword');
const icon = $(this).find('i');
if (input.attr('type') === 'password') {
input.attr('type', 'text');
icon.removeClass('fa-eye').addClass('fa-eye-slash');
} else {
input.attr('type', 'password');
icon.removeClass('fa-eye-slash').addClass('fa-eye');
}
});
// 设置界面密码显示/隐藏切换
$('#toggleAdminPassword').click(function() {
const input = $('#adminPassword');
const icon = $(this).find('i');
if (input.attr('type') === 'password') {
input.attr('type', 'text');
icon.removeClass('fa-eye').addClass('fa-eye-slash');
} else {
input.attr('type', 'password');
icon.removeClass('fa-eye-slash').addClass('fa-eye');
}
});
// 确认执行按钮
$('#confirmAdminAction').click(function() {
executeAdminAction();
});
// 模态框关闭时重置状态
$('#adminConfirmModal').on('hidden.bs.modal', function() {
resetAdminModal();
});
}
// 显示管理员确认模态框
function showAdminConfirmModal(actionName, actionType) {
currentAdminAction = actionType;
$('#adminActionName').text(actionName);
$('#adminConfirmPassword').val('');
$('#confirmAdminAction').prop('disabled', true);
// 根据操作类型显示/隐藏相关选项
if (actionType === 'batchUpdate') {
$('#batchUpdateOptions').show();
} else {
$('#batchUpdateOptions').hide();
}
// 重置进度显示
$('#batchUpdateProgress').hide();
resetProgressDisplay();
// 显示模态框
const modal = new bootstrap.Modal($('#adminConfirmModal')[0]);
modal.show();
}
// 执行管理员操作
function executeAdminAction() {
const password = $('#adminConfirmPassword').val().trim();
if (!password) {
showMessage('请输入管理员密码', 'warning');
return;
}
if (batchUpdateInProgress) {
showMessage('批量更新正在进行中,请等待完成', 'warning');
return;
}
// 显示进度区域
$('#batchUpdateProgress').show();
$('#confirmAdminAction').prop('disabled', true);
batchUpdateInProgress = true;
updateProgressDisplay(0, '正在验证密码...');
if (currentAdminAction === 'batchUpdate') {
executeBatchUpdate(password);
}
}
// 执行批量更新
function executeBatchUpdate(password) {
const statusFilter = $('input[name="updateScope"]:checked').val();
const limit = parseInt($('#batchLimit').val());
const statusText = statusFilter === '1' ? '在售' : '所有';
updateProgressDisplay(0, `开始批量更新${statusText}商品信息...`);
// 构建请求数据
const requestData = {
password: password,
limit: limit,
status_filter: statusFilter || null
};
// 发送批量更新请求
$.ajax({
url: '/api/admin/batch_fetch_details',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(requestData),
timeout: 300000, // 5分钟超时
xhr: function() {
const xhr = new window.XMLHttpRequest();
// 监听进度(如果服务器支持的话)
xhr.addEventListener('progress', function(evt) {
if (evt.lengthComputable) {
const percentComplete = evt.loaded / evt.total;
updateProgressDisplay(percentComplete * 100, '正在处理...');
}
}, false);
return xhr;
}
})
.done(function(response) {
if (response.success) {
const processed = response.processed || 0;
const updated = response.updated || 0;
const errors = response.errors || 0;
updateProgressDisplay(100, `批量更新完成!处理 ${processed} 个商品,成功更新 ${updated} 个,失败 ${errors} 个`);
// 显示详细结果
const resultHtml = `
批量更新完成
- 处理商品数量: ${processed}
- 成功更新: ${updated}
- 更新失败: ${errors}
`;
$('#batchUpdateStatus').html(resultHtml).show();
// 同时在设置界面显示结果
const settingsResultHtml = `
批量更新完成
处理 ${processed} 个商品,成功更新 ${updated} 个,失败 ${errors} 个
`;
// 在设置界面显示结果
const settingsStatus = $('#batchUpdateStatus');
if (settingsStatus.length) {
settingsStatus.html(settingsResultHtml).show();
}
// 刷新商品列表
setTimeout(function() {
if (statusFilter === '1') {
loadOnSaleGoods(1);
} else {
loadOnSaleGoods(1);
loadSoldGoods(1);
}
loadStats(); // 刷新统计信息
}, 1000);
showMessage(`批量更新完成:成功更新 ${updated} 个商品`, 'success');
// 3秒后自动关闭模态框
setTimeout(function() {
if (!batchUpdateInProgress) return;
$('#adminConfirmModal').modal('hide');
}, 3000);
} else {
updateProgressDisplay(0, '批量更新失败:' + (response.error || '未知错误'));
showMessage('批量更新失败:' + (response.error || '未知错误'), 'error');
const errorHtml = `
更新失败
${response.error || '未知错误'}
`;
$('#batchUpdateStatus').html(errorHtml);
}
})
.fail(function(xhr, status, error) {
let errorMessage = '网络请求失败';
if (xhr.status === 403) {
errorMessage = '管理员密码错误';
} else if (xhr.status === 408 || status === 'timeout') {
errorMessage = '请求超时,操作可能仍在后台进行';
} else if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
}
updateProgressDisplay(0, '批量更新失败:' + errorMessage);
showMessage('批量更新失败:' + errorMessage, 'error');
const errorHtml = `
`;
$('#batchUpdateStatus').html(errorHtml);
})
.finally(function() {
batchUpdateInProgress = false;
$('#confirmAdminAction').prop('disabled', false);
});
}
// 更新进度显示
function updateProgressDisplay(percentage, statusText) {
$('#progressBar').css('width', percentage + '%')
.attr('aria-valuenow', percentage);
$('#progressText').text(Math.round(percentage) + '%');
if (statusText) {
$('#batchUpdateStatus').html(`${statusText}`);
}
}
// 重置进度显示
function resetProgressDisplay() {
updateProgressDisplay(0, '');
$('#batchUpdateStatus').html('');
}
// 重置管理员模态框
function resetAdminModal() {
currentAdminAction = '';
batchUpdateInProgress = false;
$('#adminConfirmPassword').val('');
$('#confirmAdminAction').prop('disabled', true);
$('#batchUpdateProgress').hide();
resetProgressDisplay();
// 重置密码输入框类型
$('#adminConfirmPassword').attr('type', 'password');
$('#toggleConfirmPassword').find('i').removeClass('fa-eye-slash').addClass('fa-eye');
}
// 在页面初始化时设置管理员事件监听器
$(document).ready(function() {
setupAdminEventListeners();
setupTraitsManagement();
});
// =================== 特征管理功能 ===================
let traitsData = []; // 初始化为数组,支持虚拟滚动
let currentEditingTrait = null;
// 初始化特征管理
function setupTraitsManagement() {
// 设置事件监听器
$('#loadTraitsBtn').on('click', function() {
loadTraitsData(false);
});
$('#traitsStatsBtn').on('click', toggleTraitsStats);
$('#traitCategoryFilter').on('change', filterTraitsByCategory);
$('#saveTraitEditBtn').on('click', saveTraitEdit);
$('#removeTraitOverrideBtn').on('click', removeTraitOverride);
// 模态框重置
$('#traitEditModal').on('hidden.bs.modal', function() {
resetTraitEditModal();
});
}
// 加载特征数据 - 支持虚拟滚动
// 全局变量:分页状态
let traitsCurrentPage = 1;
let traitsPageSize = 30;
let traitsTotalCount = 0;
let traitsIsLoading = false;
let traitsHasMore = true;
let traitsCurrentCategory = '';
// 加载特征数据(只读模式,支持分页和虚拟滚动)
function loadTraitsData(isAppend = false) {
// 防止重复加载
if (traitsIsLoading) return;
// 如果已经没有更多数据,不再加载
if (isAppend && !traitsHasMore) {
console.log('已加载全部数据');
return;
}
traitsIsLoading = true;
if (!isAppend) {
// 初始加载
$('#loadTraitsBtn').prop('disabled', true).html('加载中...');
traitsCurrentPage = 1;
traitsHasMore = true;
traitsData = [];
} else {
// 追加加载 - 显示底部加载指示器
showLoadingIndicator();
}
// 构建API URL
let url = `/api/v1/traits/list?page=${traitsCurrentPage}&limit=${traitsPageSize}`;
if (traitsCurrentCategory) {
url += `&category=${encodeURIComponent(traitsCurrentCategory)}`;
}
$.get(url)
.done(function(response) {
try {
if (response.success) {
// 确保获取到的是数组
let newTraits = response.data.traits || response.data;
// 如果不是数组,尝试转换或使用空数组
if (!Array.isArray(newTraits)) {
console.warn('API返回的数据不是数组格式:', newTraits);
newTraits = [];
}
traitsTotalCount = response.data.total || 0;
// 验证数据格式
if (!newTraits || newTraits.length === 0) {
if (!isAppend) {
showMessage('暂无特征数据', 'warning');
}
traitsHasMore = false;
hideLoadingIndicator();
return;
}
// 确保 traitsData 是数组
if (!Array.isArray(traitsData)) {
console.warn('traitsData不是数组,重置为空数组');
traitsData = [];
}
// 合并或替换数据
if (isAppend) {
traitsData = traitsData.concat(newTraits);
} else {
traitsData = newTraits;
}
// 检查是否还有更多数据
traitsHasMore = traitsData.length < traitsTotalCount;
// 渲染表格
renderTraitsTable(traitsData, isAppend);
if (!isAppend) {
showMessage(`特征数据加载成功,共${traitsTotalCount}条,已显示${traitsData.length}条`, 'success');
}
// 准备加载下一页
traitsCurrentPage++;
} else {
const errorMsg = response.error || response.message || '未知错误';
showMessage('加载失败:' + String(errorMsg), 'error');
traitsHasMore = false;
}
} catch (e) {
console.error('处理特征数据失败:', e);
showMessage('处理数据失败:' + e.message, 'error');
traitsHasMore = false;
}
})
.fail(function(xhr, status, error) {
let errorMessage = '加载失败';
try {
if (xhr.responseJSON) {
if (xhr.responseJSON.error) {
errorMessage = String(xhr.responseJSON.error);
} else if (xhr.responseJSON.detail) {
// 处理Pydantic验证错误(数组格式)
if (Array.isArray(xhr.responseJSON.detail)) {
const firstError = xhr.responseJSON.detail[0];
if (firstError && firstError.msg) {
errorMessage = String(firstError.msg);
} else {
errorMessage = '参数验证失败';
}
} else {
errorMessage = String(xhr.responseJSON.detail);
}
} else if (xhr.responseJSON.message) {
errorMessage = String(xhr.responseJSON.message);
}
} else if (xhr.responseText) {
errorMessage = '请求失败: ' + xhr.status + ' ' + xhr.statusText;
} else {
errorMessage = '网络错误: ' + String(error || status);
}
} catch (e) {
console.error('解析错误信息失败:', e);
errorMessage = '请求失败,状态码: ' + xhr.status;
}
console.error('加载特征数据失败:', {xhr, status, error, responseJSON: xhr.responseJSON});
showMessage(errorMessage, 'error');
traitsHasMore = false;
})
.finally(function() {
traitsIsLoading = false;
hideLoadingIndicator();
$('#loadTraitsBtn').prop('disabled', false).html('加载特征数据');
});
}
// 显示底部加载指示器
function showLoadingIndicator() {
if ($('#traitsLoadingIndicator').length === 0) {
$('#traitsTableContainer').after(`
加载更多数据...
`);
}
$('#traitsLoadingIndicator').show();
}
// 隐藏底部加载指示器
function hideLoadingIndicator() {
$('#traitsLoadingIndicator').hide();
}
// 渲染特征表格(只读模式,支持追加模式)
function renderTraitsTable(data, isAppend = false) {
// 追加模式:只添加新行,不重建整个表格
if (isAppend && $('#traitsTableContainer table tbody').length > 0) {
let rowsHtml = '';
if (Array.isArray(data)) {
// 计算需要追加的新数据(当前页的数据)
const existingRowCount = $('#traitsTableContainer table tbody tr').length;
const newData = data.slice(existingRowCount);
newData.forEach(trait => {
const rarityBadge = getRarityBadge(trait.rarity_level);
const rarityPercent = (trait.rarity_rate * 100).toFixed(2);
rowsHtml += `
| ${trait.category} |
${trait.trait_name} |
${rarityPercent}% |
${rarityBadge} |
`;
});
}
if (rowsHtml) {
$('#traitsTableContainer table tbody').append(rowsHtml);
}
return;
}
// 初始加载:重建整个表格
let html = `
`;
$('#traitsTableContainer').html(html);
// 绑定滚动事件监听
setupTraitsScrollListener();
}
// 设置滚动事件监听(无限滚动)
function setupTraitsScrollListener() {
const scrollContainer = $('#traitsTableScroll');
if (scrollContainer.length === 0) return;
// 移除旧的监听器,避免重复绑定
scrollContainer.off('scroll');
// 添加滚动监听
scrollContainer.on('scroll', function() {
const container = $(this);
const scrollTop = container.scrollTop();
const scrollHeight = container[0].scrollHeight;
const clientHeight = container.innerHeight();
// 当滚动到距离底部100px时,加载更多数据
if (scrollTop + clientHeight >= scrollHeight - 100) {
if (!traitsIsLoading && traitsHasMore) {
console.log('触发加载更多数据');
loadTraitsData(true);
}
}
});
}
// 根据稀有度分数获取等级信息
function getRarityLevel(score) {
if (score >= 90) {
return { level: 'legendary', label: '传说级', class: 'primary' };
} else if (score >= 75) {
return { level: 'super_rare', label: '超稀有', class: 'danger' };
} else if (score >= 60) {
return { level: 'rare', label: '稀有', class: 'warning' };
} else {
return { level: 'common', label: '普通', class: 'secondary' };
}
}
// 获取稀有度徽章
function getRarityBadge(level) {
const badges = {
'common': '普通',
'rare': '稀有',
'super_rare': '超稀有',
'legendary': '传说'
};
return badges[level] || '未知';
}
// 按类别筛选特征
function filterTraitsByCategory() {
const selectedCategory = $('#traitCategoryFilter').val();
// 更新当前筛选的类别
traitsCurrentCategory = selectedCategory;
// 重新加载数据(从第一页开始)
loadTraitsData(false);
// 如果统计面板正在显示,重新加载统计数据(带上类别筛选)
const statsPanel = $('#traitsStatsPanel');
if (statsPanel.is(':visible')) {
loadTraitsStats(selectedCategory);
}
}
// 编辑特征
function editTrait(category, name, rarity, currentLevel, hasOverride, manualLevel) {
currentEditingTrait = { category, name, rarity };
// 填充模态框信息
$('#traitEditCategory').text(category);
$('#traitEditName').text(name);
$('#traitEditOriginalRarity').text(rarity.toFixed(2));
$('#traitEditLevel').val(manualLevel || currentLevel);
$('#traitEditReason').val('');
$('#traitEditPassword').val('');
// 显示/隐藏恢复按钮
if (hasOverride) {
$('#removeTraitOverrideBtn').show();
} else {
$('#removeTraitOverrideBtn').hide();
}
// 显示模态框
$('#traitEditModal').modal('show');
}
// 快速移除覆盖
function quickRemoveOverride(category, name) {
if (!confirm(`确认要恢复 "${category} - ${name}" 为自动计算吗?`)) {
return;
}
const password = $('#adminPassword').val();
if (!password) {
showMessage('请先在管理员功能页面输入密码', 'warning');
return;
}
removeTraitOverrideAPI(category, name, password);
}
// 保存特征编辑
function saveTraitEdit() {
if (!currentEditingTrait) return;
const level = $('#traitEditLevel').val();
const reason = $('#traitEditReason').val();
const password = $('#traitEditPassword').val();
if (!level || !password) {
showMessage('请填写完整信息', 'warning');
return;
}
const data = {
category: currentEditingTrait.category,
name: currentEditingTrait.name,
manual_level: level,
reason: reason,
password: password
};
$('#saveTraitEditBtn').prop('disabled', true).html('保存中...');
$.ajax({
url: '/api/admin/traits/override',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(data)
})
.done(function(response) {
if (response.success) {
showMessage(response.message, 'success');
$('#traitEditModal').modal('hide');
loadTraitsData(); // 重新加载数据
} else {
showMessage('保存失败:' + response.error, 'error');
}
})
.fail(function(xhr) {
let errorMessage = '保存失败';
if (xhr.status === 401) {
errorMessage = '管理员密码错误';
} else if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
}
showMessage(errorMessage, 'error');
})
.finally(function() {
$('#saveTraitEditBtn').prop('disabled', false).html('保存');
});
}
// 移除特征覆盖
function removeTraitOverride() {
if (!currentEditingTrait) return;
const password = $('#traitEditPassword').val();
if (!password) {
showMessage('请输入管理员密码', 'warning');
return;
}
removeTraitOverrideAPI(currentEditingTrait.category, currentEditingTrait.name, password);
}
// 移除特征覆盖API调用
function removeTraitOverrideAPI(category, name, password) {
const data = {
category: category,
name: name,
password: password
};
$.ajax({
url: '/api/admin/traits/override',
method: 'DELETE',
contentType: 'application/json',
data: JSON.stringify(data)
})
.done(function(response) {
if (response.success) {
showMessage(response.message, 'success');
if ($('#traitEditModal').hasClass('show')) {
$('#traitEditModal').modal('hide');
}
loadTraitsData(); // 重新加载数据
} else {
showMessage('操作失败:' + response.error, 'error');
}
})
.fail(function(xhr) {
let errorMessage = '操作失败';
if (xhr.status === 401) {
errorMessage = '管理员密码错误';
} else if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
}
showMessage(errorMessage, 'error');
});
}
// 加载特征统计数据(支持按类别筛选)
function loadTraitsStats(category) {
const panel = $('#traitsStatsPanel');
// 禁用按钮并显示加载状态
$('#traitsStatsBtn').prop('disabled', true).html('加载中...');
// 构建API URL(如果有类别筛选则添加参数)
let apiUrl = '/api/v1/traits/stats';
if (category) {
apiUrl += '?category=' + encodeURIComponent(category);
}
$.get(apiUrl)
.done(function(response) {
if (response.success && response.data) {
// 使用API返回的统计数据
const stats = response.data;
const totalTraits = stats.total || 0;
const categoriesCount = Object.keys(stats.by_category || {}).length;
// 计算稀有特征数量(稀有+超稀有+传说级)
const rarityStats = stats.by_rarity_level || {};
const rareTraitsCount = (rarityStats.rare || 0) +
(rarityStats.super_rare || 0) +
(rarityStats.legendary || 0);
$('#totalTraitsCount').text(totalTraits);
$('#categoriesCount').text(categoriesCount);
$('#rareTraitsCount').text(rareTraitsCount);
// 显示各个稀有度等级的详细数量
console.log('稀有度统计数据:', rarityStats, '筛选类别:', category || '全部');
$('#legendaryCount').text(rarityStats.legendary || 0);
$('#superRareCount').text(rarityStats.super_rare || 0);
$('#rareCount').text(rarityStats.rare || 0);
$('#commonCount').text(rarityStats.common || 0);
console.log('已更新稀有度数量显示');
panel.show();
$('#traitsStatsBtn').html('隐藏统计');
} else {
showMessage('加载统计失败:' + (response.error || '未知错误'), 'error');
}
})
.fail(function(xhr, status, error) {
let errorMessage = '加载统计失败';
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
} else if (xhr.responseJSON && xhr.responseJSON.detail) {
errorMessage = String(xhr.responseJSON.detail);
} else if (error) {
errorMessage = '网络错误: ' + error;
}
console.error('加载统计失败:', {xhr, status, error});
showMessage(errorMessage, 'error');
})
.finally(function() {
$('#traitsStatsBtn').prop('disabled', false);
});
}
// 切换特征统计面板(只读模式)
function toggleTraitsStats() {
const panel = $('#traitsStatsPanel');
if (panel.is(':visible')) {
panel.hide();
$('#traitsStatsBtn').html('查看统计');
return;
}
// 获取当前选择的类别
const selectedCategory = $('#traitCategoryFilter').val();
// 加载统计数据(带上类别筛选)
loadTraitsStats(selectedCategory);
}
// 重置特征编辑模态框
function resetTraitEditModal() {
currentEditingTrait = null;
$('#traitEditForm')[0].reset();
$('#removeTraitOverrideBtn').hide();
}
// =================== 在售数据重新获取功能 ===================
// 执行重新获取在售数据
function executeRefreshOnSaleData(password) {
const btn = $('#refreshOnSaleDataBtn');
const statusDiv = $('#refreshOnSaleStatus');
// 禁用按钮
btn.prop('disabled', true).html('获取中...');
statusDiv.show().html('正在重新获取在售数据,请稍候...
');
$.ajax({
url: '/api/refresh/on_sale',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ password: password }),
timeout: 600000 // 10分钟超时
})
.done(function(response) {
if (response.success) {
const stats = response.stats;
const successHtml = `
在售数据获取完成
- 处理页数: ${stats.pages_processed}
- 获取商品: ${stats.items_fetched} 件
- 新增商品: ${stats.new_items} 件
- 获取详情: ${stats.details_fetched} 个
`;
statusDiv.html(successHtml);
showMessage('在售数据获取完成', 'success');
// 刷新页面数据
setTimeout(() => {
if (!isSearchMode) {
loadOnSaleData(currentOnSalePage);
} else {
performSearch();
}
}, 1000);
} else {
statusDiv.html(`
`);
showMessage('在售数据获取失败:' + response.error, 'error');
}
})
.fail(function(xhr, status, error) {
let errorMessage = '网络请求失败';
if (xhr.status === 401) {
errorMessage = '管理员密码错误';
} else if (xhr.status === 408 || status === 'timeout') {
errorMessage = '请求超时,操作可能仍在后台进行';
} else if (xhr.responseJSON && xhr.responseJSON.error) {
errorMessage = xhr.responseJSON.error;
}
const errorHtml = `
`;
statusDiv.html(errorHtml);
showMessage('在售数据获取失败:' + errorMessage, 'error');
})
.finally(function() {
// 恢复按钮状态
btn.prop('disabled', false).html('重新获取在售数据');
});
}
// ================== 特征管理功能 ==================
let traitsAdminPassword = '';
let isTraitsPasswordVerified = false;
let traitsVersions = [];
// 特征管理密码验证
function verifyTraitsPassword() {
const password = $('#traitsAdminPassword').val();
const statusSpan = $('#traitsPasswordStatus');
if (!password) {
showTraitsPasswordStatus('请输入密码', 'danger');
return;
}
traitsAdminPassword = password;
// 通过尝试获取版本列表来验证密码
$.ajax({
url: '/admin/traits/versions',
method: 'POST',
data: { admin_password: password },
timeout: 10000
})
.done(function(response) {
if (response.success) {
isTraitsPasswordVerified = true;
showTraitsPasswordStatus('密码验证成功', 'success');
$('#traitsPasswordPanel').hide();
$('#traitsAdminPanel').show();
logTraitsMessage('管理员密码验证成功,特征管理面板已激活');
setupTraitsUpload();
loadVersions();
} else {
showTraitsPasswordStatus('密码验证失败', 'danger');
}
})
.fail(function() {
showTraitsPasswordStatus('验证请求失败', 'danger');
});
}
function showTraitsPasswordStatus(message, type) {
const statusSpan = $('#traitsPasswordStatus');
const className = type === 'success' ? 'text-success' : 'text-danger';
statusSpan.html(`${message}`)
.removeClass('text-success text-danger')
.addClass(className);
}
// 设置文件上传
function setupTraitsUpload() {
const uploadArea = $('#uploadArea');
const fileInput = $('#excelFile');
// 检查元素是否存在
if (uploadArea.length === 0) {
console.error('找不到uploadArea元素');
return;
}
if (fileInput.length === 0) {
console.error('找不到excelFile元素');
return;
}
uploadArea.off('click').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
fileInput.trigger('click');
});
uploadArea.off('dragover drop dragleave').on({
'dragover': function(e) {
e.preventDefault();
$(this).addClass('border-primary bg-primary bg-opacity-10');
},
'drop': function(e) {
e.preventDefault();
$(this).removeClass('border-primary bg-primary bg-opacity-10');
const files = e.originalEvent.dataTransfer.files;
if (files.length > 0 && files[0].name.match(/\.(xlsx?|xls)$/i)) {
fileInput[0].files = files;
updateImportButton();
const fileName = files[0].name;
logTraitsMessage(`选择了文件: ${fileName}`);
autoFillVersionInfo(fileName);
} else {
alert('请选择Excel文件(.xlsx或.xls格式)');
}
},
'dragleave': function() {
$(this).removeClass('border-primary bg-primary bg-opacity-10');
}
});
fileInput.off('change').on('change', function() {
updateImportButton();
if (this.files.length > 0) {
const fileName = this.files[0].name;
logTraitsMessage(`选择了文件: ${fileName}`);
autoFillVersionInfo(fileName);
}
});
}
// 自动填充版本信息
function autoFillVersionInfo(fileName) {
// 自动填充版本名称为文件名(去除扩展名)
const versionName = fileName.replace(/\.(xlsx?|xls)$/i, '');
$('#versionName').val(versionName);
// 自动填充版本描述
const currentDate = new Date().toLocaleDateString('zh-CN');
$('#versionDescription').val(`从文件 ${fileName} 导入 (${currentDate})`);
}
function updateImportButton() {
const fileInput = $('#excelFile')[0];
const importBtn = $('#importBtn');
if (fileInput.files.length > 0 && isTraitsPasswordVerified) {
importBtn.prop('disabled', false);
} else {
importBtn.prop('disabled', true);
}
}
// 导入Excel文件
function importExcel() {
const fileInput = $('#excelFile')[0];
const versionName = $('#versionName').val();
const versionDescription = $('#versionDescription').val();
if (!fileInput.files.length) {
alert('请选择Excel文件');
return;
}
if (!versionName.trim()) {
alert('请输入版本名称');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('version_name', versionName);
formData.append('version_description', versionDescription);
formData.append('admin_password', traitsAdminPassword);
const importBtn = $('#importBtn');
const originalText = importBtn.html();
importBtn.prop('disabled', true).html('导入中...');
logTraitsMessage(`开始导入: ${fileInput.files[0].name}`);
$.ajax({
url: '/admin/traits/import',
method: 'POST',
data: formData,
processData: false,
contentType: false,
timeout: 60000
})
.done(function(response) {
if (response.success) {
logTraitsMessage(`导入成功: 版本"${versionName}"已创建,包含${response.data_count}条数据`);
$('#versionName').val('');
$('#versionDescription').val('');
$('#excelFile').val('');
updateImportButton();
loadVersions();
alert('Excel文件导入成功!');
} else {
logTraitsMessage(`导入失败: ${response.error}`);
alert('导入失败:' + response.error);
}
})
.fail(function(xhr) {
const errorMessage = xhr.responseJSON ? xhr.responseJSON.error : '网络错误';
logTraitsMessage(`导入失败: ${errorMessage}`);
alert('导入失败:' + errorMessage);
})
.finally(function() {
importBtn.prop('disabled', false).html(originalText);
});
}
// 导出Excel文件
function exportExcel() {
const versionId = $('#exportVersionSelect').val();
const exportData = { admin_password: traitsAdminPassword };
if (versionId) {
exportData.version_id = versionId;
}
logTraitsMessage('开始导出数据...');
$.ajax({
url: '/admin/traits/export',
method: 'POST',
data: JSON.stringify(exportData),
contentType: 'application/json',
timeout: 30000
})
.done(function(response) {
if (response.success) {
logTraitsMessage(`导出成功: ${response.filename || '特征数据.xlsx'}`);
alert('导出成功!文件已保存到服务器doc目录');
} else {
logTraitsMessage(`导出失败: ${response.error}`);
alert('导出失败:' + response.error);
}
})
.fail(function() {
logTraitsMessage('导出失败: 网络错误');
alert('导出失败:网络错误');
});
}
// 加载版本列表
function loadVersions() {
$.ajax({
url: '/admin/traits/versions',
method: 'POST',
data: { admin_password: traitsAdminPassword },
timeout: 10000
})
.done(function(response) {
if (response.success) {
traitsVersions = response.data;
renderVersionsList();
updateExportVersionOptions();
}
});
}
function renderVersionsList() {
const container = $('#versionsList');
if (!traitsVersions.length) {
container.html('暂无版本数据
');
return;
}
let html = '';
traitsVersions.forEach(version => {
const isActive = version.is_active;
const activeClass = isActive ? ' bg-light border-primary' : '';
const activeTag = isActive ? '活跃' : '';
html += `
${version.version_name}${activeTag}
${version.description || '无描述'}
${version.created_at}
${version.data_count}条数据
${!isActive ? `` : ''}
${!isActive ? `` : ''}
`;
});
container.html(html);
}
function updateExportVersionOptions() {
const select = $('#exportVersionSelect');
select.empty().append('');
traitsVersions.forEach(version => {
const activeText = version.is_active ? ' (当前活跃)' : '';
select.append(``);
});
}
// 切换版本
function switchVersion(versionId) {
if (!confirm('确定要切换到此版本吗?这将影响特征识别功能。')) {
return;
}
$.ajax({
url: '/admin/traits/switch_version',
method: 'POST',
data: {
version_id: versionId,
admin_password: traitsAdminPassword
},
timeout: 10000
})
.done(function(response) {
if (response.success) {
logTraitsMessage(`版本切换成功: ${response.message}`);
loadVersions();
alert('版本切换成功!');
} else {
alert('切换失败:' + response.error);
}
})
.fail(function() {
alert('切换失败:网络错误');
});
}
// 删除版本
function deleteVersion(versionId) {
if (!confirm('确定要删除此版本吗?此操作不可恢复!')) {
return;
}
$.ajax({
url: '/admin/traits/delete_version',
method: 'POST',
data: {
version_id: versionId,
admin_password: traitsAdminPassword
},
timeout: 10000
})
.done(function(response) {
if (response.success) {
logTraitsMessage(`版本删除成功: ${response.message}`);
loadVersions();
alert('版本删除成功!');
} else {
alert('删除失败:' + response.error);
}
})
.fail(function() {
alert('删除失败:网络错误');
});
}
// 备份当前版本
function backupCurrentVersion() {
const backupName = prompt('请输入备份名称:');
if (!backupName) return;
$.ajax({
url: '/admin/traits/backup',
method: 'POST',
data: {
backup_name: backupName,
admin_password: traitsAdminPassword
},
timeout: 30000
})
.done(function(response) {
if (response.success) {
logTraitsMessage(`备份成功: ${response.backup_name}`);
loadVersions();
alert('备份成功!');
} else {
alert('备份失败:' + response.error);
}
})
.fail(function() {
alert('备份失败:网络错误');
});
}
// 刷新版本列表
function refreshVersions() {
loadVersions();
logTraitsMessage('版本列表已刷新');
}
// 清空日志
function clearTraitsLog() {
$('#traitsLogContainer').empty();
}
// 记录日志消息
function logTraitsMessage(message) {
const logContainer = $('#traitsLogContainer');
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] ${message}
`;
logContainer.append(logEntry);
logContainer.scrollTop(logContainer[0].scrollHeight);
}
// ================================ 移动端功能 ================================
// 设置移动端事件监听器
function setupMobileEventListeners() {
console.log('设置移动端事件监听器...');
// 移动端菜单切换
$('#mobileMenuBtn').click(function() {
$('#mobileSearchCollapse').collapse('toggle');
});
// 移动端自动刷新同步
$('#mobileAutoRefresh').change(function() {
const isChecked = $(this).is(':checked');
$('#autoRefresh').prop('checked', isChecked);
$('#autoRefresh').trigger('change');
});
// 移动端刷新按钮
$('#mobileRefreshBtn').click(function() {
// 停止自动刷新并取消勾选
if ($('#autoRefresh').is(':checked')) {
$('#autoRefresh').prop('checked', false);
$('#mobileAutoRefresh').prop('checked', false);
stopAutoRefresh();
}
refreshAllData();
});
// 移动端分析按钮
$('#mobileAnalysisBtn').click(function() {
showAnalysisModal();
});
// 移动端设置按钮(已移除)
// $('#mobileSettingsBtn').click(function() {
// $('#settingsModal').modal('show');
// $('#mobileSearchCollapse').collapse('hide');
// });
// 移动端搜索功能
$('#mobileSearchBtn').click(function() {
performMobileSearch();
});
$('#mobileClearSearchBtn').click(function() {
clearMobileSearch();
});
// 移动端搜索输入框回车事件
$('#mobileSearchInput').keypress(function(e) {
if (e.which === 13) {
performMobileSearch();
}
});
// 移动端排序方向切换
$('#mobileSortOrderBtn').click(function() {
toggleMobileSortOrder();
});
// 移动端搜索状态和排序改变时
$('#mobileSearchStatus, #mobileSearchSortBy').change(function() {
if (isSearchMode) {
performMobileSearch();
}
});
// 移动端标签页切换事件
$('#mobileGoodsTabs button').on('shown.bs.tab', function (e) {
const targetTab = $(this).data('bs-target');
console.log('移动端标签页切换:', targetTab);
// 根据切换的标签页更新对应的商品列表
if (targetTab === '#mobile-on-sale') {
syncMobileOnSaleList();
} else if (targetTab === '#mobile-sold') {
syncMobileSoldList();
}
});
// 窗口大小变化时同步数据
$(window).resize(debounce(function() {
syncMobileDesktopData();
}, 300));
console.log('移动端事件监听器设置完成');
}
// 移动端搜索功能
function performMobileSearch() {
const keyword = $('#mobileSearchInput').val().trim();
const status = $('#mobileSearchStatus').val();
const sortBy = $('#mobileSearchSortBy').val();
// 验证搜索输入(SQL注入防护)
const validationResult = validateSearchInput(keyword);
if (!validationResult.valid) {
showError(validationResult.message);
return;
}
// 同步到桌面端搜索
$('#searchInput').val(keyword);
$('#searchStatus').val(status);
$('#searchSortBy').val(sortBy);
// 执行搜索
performSearch();
}
// 清空移动端搜索
function clearMobileSearch() {
$('#mobileSearchInput').val('');
$('#mobileSearchStatus').val('');
$('#mobileSearchSortBy').val('time');
$('#mobileSortOrderIcon').removeClass('fa-sort-amount-up').addClass('fa-sort-amount-down');
// 同步到桌面端并清空搜索
clearSearch();
}
// 切换移动端排序方向
function toggleMobileSortOrder() {
currentSortOrder = currentSortOrder === 'desc' ? 'asc' : 'desc';
updateMobileSortOrderIcon();
// 同步到桌面端
updateSortOrderIcon();
if ($('#mobileSearchInput').val().trim()) {
performMobileSearch();
}
}
// 更新移动端排序图标
function updateMobileSortOrderIcon() {
const icon = $('#mobileSortOrderIcon');
icon.removeClass('fa-sort-amount-up fa-sort-amount-down');
icon.addClass(currentSortOrder === 'desc' ? 'fa-sort-amount-down' : 'fa-sort-amount-up');
}
// 同步移动端在售商品列表
function syncMobileOnSaleList() {
const desktopList = $('#onSaleList');
const mobileList = $('#mobileOnSaleList');
// 复制桌面端内容到移动端
mobileList.html(desktopList.html());
// 同步分页
const desktopPagination = $('#onSalePagination');
const mobilePagination = $('#mobileOnSalePagination');
mobilePagination.html(desktopPagination.html());
// 更新计数
const total = $('#onSaleTotal').text();
$('#mobileOnSaleCount').text(total.replace('总计: ', ''));
}
// 同步移动端已售商品列表
function syncMobileSoldList() {
const desktopList = $('#soldList');
const mobileList = $('#mobileSoldList');
// 复制桌面端内容到移动端
mobileList.html(desktopList.html());
// 同步分页
const desktopPagination = $('#soldPagination');
const mobilePagination = $('#mobileSoldPagination');
mobilePagination.html(desktopPagination.html());
// 更新计数
const total = $('#soldTotal').text();
$('#mobileSoldCount').text(total.replace('总计: ', ''));
}
// 同步移动端和桌面端数据
function syncMobileDesktopData() {
// 同步统计数据
$('#mobileOnSaleCount').text($('#onSaleCount').text());
$('#mobileSoldCount').text($('#soldCount').text());
// 同步自动刷新状态
const autoRefreshChecked = $('#autoRefresh').is(':checked');
$('#mobileAutoRefresh').prop('checked', autoRefreshChecked);
// 同步监控状态
const monitorStatus = $('#monitorStatus').text();
$('#mobileMonitorStatus').text(monitorStatus.replace('监控状态: ', ''));
// 同步上次更新时间
const onSaleUpdate = $('#onSaleLastUpdate').text();
$('#mobileOnSaleLastUpdate').text(onSaleUpdate.replace('上次更新: ', ''));
const soldUpdate = $('#soldLastUpdate').text();
$('#mobileSoldLastUpdate').text(soldUpdate.replace('上次更新: ', ''));
// 同步同步状态
const onSaleSync = $('#onSaleSyncStatus').text();
$('#mobileOnSaleSyncStatus').text(onSaleSync);
const soldSync = $('#soldSyncStatus').text();
$('#mobileSoldSyncStatus').text(soldSync);
// 同步地板价、涨跌幅和在售总数(防止遗漏)
const floorPrice = $('#floorPrice').text();
if (floorPrice && floorPrice !== '¥--') {
$('#mobileFloorPrice').text(floorPrice);
}
const trendHtml = $('#trendPercentage').html();
if (trendHtml) {
$('#mobileTrendPercentage').html(trendHtml);
}
const onSaleTotal = $('#onSaleTotal').text();
if (onSaleTotal) {
$('#mobileOnSaleTotal').text(onSaleTotal);
}
}
// 防抖函数
function debounce(func, wait, immediate) {
let timeout;
return function executedFunction() {
const context = this;
const args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
// ====== 数据校验功能 ======
// 校验状态变量
let validationInProgress = false;
let validationStatusInterval = null;
let marketDataInterval = null;
// 初始化校验功能
// 更新市场数据
function updateMarketData() {
// 检查管理员密码
const password = $('#adminPassword').val();
if (!password) {
showError('请先在管理员功能页面输入密码');
return;
}
const btn = $('#updateMarketDataBtn');
const originalText = btn.text();
btn.prop('disabled', true).text('更新中...');
$.ajax({
url: '/api/market/update',
method: 'POST',
data: JSON.stringify({
admin_password: password
}),
contentType: 'application/json',
success: function(response) {
if (response.success) {
showMessage('市场数据已更新', 'success');
loadMarketData(); // 重新加载市场数据
} else {
showError('市场数据更新失败: ' + (response.error || '未知错误'));
}
},
error: function(xhr, status, error) {
console.error('市场数据更新失败:', error);
showError('市场数据更新失败: ' + error);
},
complete: function() {
btn.prop('disabled', false).text(originalText);
}
});
}
// 获取校验类型的中文显示名称
function getValidationTypeText(type) {
const typeMap = {
'manual': '手动数据校验',
'auto': '自动数据校验',
'active_consistency': '在售商品一致性校验',
'sold_integrity': '已售数据完整性校验'
};
return typeMap[type] || type;
}
// 加载市场数据
function loadMarketData() {
return $.ajax({
url: '/api/market/data',
method: 'GET',
timeout: 10000, // 10秒超时
cache: true, // ✓ 启用缓存
headers: {
'Cache-Control': 'max-age=60' // 市场数据缓存60秒
}
}).done(function(response) {
if (response.success && response.data) {
displayMarketData(response.data);
} else {
console.error('行情数据API错误:', response.error);
}
}).fail(function(xhr, status, error) {
console.error('加载行情数据失败:', status, error);
if (status === 'timeout') {
console.warn('行情数据加载超时,将显示默认值');
}
});
}
// 全局变量存储当前地板价
let currentFloorPrice = 0;
// 显示市场数据
function displayMarketData(data) {
console.log('更新行情数据:', data);
// 更新地板价(PC端和手机端同步)
if (data.floor_price !== undefined && data.floor_price > 0) {
currentFloorPrice = data.floor_price;
$('#floorPrice').text(`¥${data.floor_price.toFixed(0)}`);
$('#mobileFloorPrice').text(`¥${data.floor_price.toFixed(0)}`);
} else {
currentFloorPrice = 0;
$('#floorPrice').text('¥--');
$('#mobileFloorPrice').text('¥--');
}
// 更新涨跌幅(PC端和手机端同步)
if (data.trend_percentage !== undefined) {
const trend = data.trend_percentage;
const trendElement = $('#trendPercentage');
const mobileTrendElement = $('#mobileTrendPercentage');
// 清除之前的样式
trendElement.find('span').removeClass('trend-up trend-down trend-neutral');
mobileTrendElement.find('span').removeClass('trend-up trend-down trend-neutral');
let trendHtml;
if (trend > 0) {
trendHtml = `+${trend.toFixed(2)}%`;
} else if (trend < 0) {
trendHtml = `${trend.toFixed(2)}%`;
} else {
trendHtml = `0.00%`;
}
trendElement.html(trendHtml);
mobileTrendElement.html(trendHtml);
} else {
$('#trendPercentage').html(`--`);
$('#mobileTrendPercentage').html(`--`);
}
// 更新在售总数(PC端和手机端同步,使用API数据)
if (data.total_listings !== undefined && data.total_listings > 0) {
$('#onSaleTotal').text(`总计: ${data.total_listings}`);
$('#mobileOnSaleTotal').text(`总计: ${data.total_listings}`);
}
// 当地板价更新时,重新计算所有在售商品的溢价率
updateAllPremiumRates();
}
// 计算并显示指定商品列表的溢价率
function calculateAndDisplayPremiumRates(goods) {
// console.debug(`溢价率计算开始`);
if (!goods || goods.length === 0) {
// console.debug('商品列表为空,跳过溢价率计算');
return;
}
// 检查地板价是否有效,如果无效则先加载地板价再计算
if (!currentFloorPrice || currentFloorPrice <= 0) {
console.log('地板价无效,先加载地板价数据');
loadMarketData().then(() => {
// console.debug(`地板价加载完成,重新计算溢价率`);
// 地板价加载完成后重新计算
calculateAndDisplayPremiumRatesSync(goods);
}).catch((error) => {
console.error('加载地板价失败,使用默认显示:', error);
calculateAndDisplayPremiumRatesSync(goods);
});
return;
}
// 直接计算溢价率
// console.debug('地板价有效,直接计算溢价率');
calculateAndDisplayPremiumRatesSync(goods);
}
// 同步计算并显示溢价率(内部函数)
function calculateAndDisplayPremiumRatesSync(goods) {
if (!goods || goods.length === 0) {
// console.debug('同步计算溢价率: 商品列表为空');
return;
}
// console.debug(`开始同步计算溢价率`);
let calculatedCount = 0;
let foundElementCount = 0;
// 同时更新PC端和移动端的溢价率
$('#onSaleList .premium-rate, #mobileOnSaleList .premium-rate').each(function(index) {
const $premiumElement = $(this);
const $priceElement = $premiumElement.closest('.goods-price');
const priceText = $priceElement.text();
const priceMatch = priceText.match(/¥(\d+(?:\.\d+)?)/);
if (priceMatch) {
const price = parseFloat(priceMatch[1]);
// 在商品列表中查找匹配的价格
const matchingItem = goods.find(item =>
parseFloat(item.price) === price
);
if (matchingItem) {
foundElementCount++;
const premiumRate = calculatePremiumRate(price, currentFloorPrice);
updatePremiumRateDisplay($premiumElement, premiumRate);
calculatedCount++;
// 只在值发生变化时输出日志,并限制日志数量
if (calculatedCount <= 3) {
// console.debug(`溢价率元素${index + 1} 溢价率: ${premiumRate !== null ? premiumRate.toFixed(1) + '%' : '--'}`);
}
}
}
});
if (calculatedCount > 0) {
// console.debug(`溢价率计算完成`);
}
}
// 更新所有在售商品的溢价率(当地板价变化时调用)
function updateAllPremiumRates() {
// 确保地板价有效
if (!currentFloorPrice || currentFloorPrice <= 0) {
// console.debug('地板价无效,跳过溢价率更新');
return;
}
// 同时更新PC端和移动端的溢价率
$('#onSaleList .premium-rate, #mobileOnSaleList .premium-rate').each(function() {
const $this = $(this);
const priceText = $this.closest('.goods-price').text();
const priceMatch = priceText.match(/¥(\d+(?:\.\d+)?)/);
if (priceMatch) {
const price = parseFloat(priceMatch[1]);
const premiumRate = calculatePremiumRate(price, currentFloorPrice);
updatePremiumRateDisplay($this, premiumRate);
}
});
}
// 计算溢价率:(当前价格 / 地板价 - 1) * 100
function calculatePremiumRate(currentPrice, floorPrice) {
if (!floorPrice || floorPrice <= 0 || !currentPrice || currentPrice <= 0) {
return null;
}
return ((currentPrice / floorPrice - 1) * 100);
}
// 更新溢价率显示(增量更新,避免不必要的闪动)
function updatePremiumRateDisplay(element, premiumRate) {
// 获取当前显示的内容
const currentText = element.text().trim();
const currentClasses = element.attr('class') || '';
let newText, newClass;
if (premiumRate === null) {
newText = '--';
newClass = 'premium-rate premium-unknown';
} else {
newText = premiumRate >= 0 ? `+${premiumRate.toFixed(1)}%` : `${premiumRate.toFixed(1)}%`;
// 根据溢价率确定样式类
let styleClass;
if (premiumRate <= 5) {
styleClass = 'premium-floor'; // 接近地板价 (≤5%)
} else if (premiumRate <= 20) {
styleClass = 'premium-low'; // 低溢价 (5-20%)
} else if (premiumRate <= 50) {
styleClass = 'premium-medium'; // 中溢价 (20-50%)
} else {
styleClass = 'premium-high'; // 高溢价 (>50%)
}
newClass = `premium-rate ${styleClass}`;
}
// 只在内容或样式真正改变时才更新DOM
if (currentText !== newText || !currentClasses.includes(newClass.split(' ')[1])) {
element.text(newText);
element.removeClass('premium-high premium-medium premium-low premium-floor premium-unknown');
// 添加新样式
if (premiumRate === null) {
element.addClass('premium-unknown');
} else if (premiumRate <= 5) {
element.addClass('premium-floor');
} else if (premiumRate <= 20) {
element.addClass('premium-low');
} else if (premiumRate <= 50) {
element.addClass('premium-medium');
} else {
element.addClass('premium-high');
}
}
}
// 显示校验结果
function displayValidationResult(result) {
if (!result) return;
let resultHtml = '';
resultHtml += '
校验结果:
';
if (result.checked_items !== undefined) {
resultHtml += `
检查项目: ${result.checked_items}
`;
}
if (result.updated_items !== undefined) {
resultHtml += `
更新项目: ${result.updated_items}
`;
}
if (result.errors !== undefined && result.errors > 0) {
resultHtml += `
错误: ${result.errors}
`;
}
if (result.details) {
resultHtml += `
${result.details}
`;
}
resultHtml += '
';
// 显示结果(可以添加到消息区域或专门的结果区域)
showMessage(resultHtml, 'info', 10000); // 显示10秒
}
// 设置校验相关的定期更新
// 停止校验相关的定期更新
function stopValidationUpdates() {
if (validationStatusInterval) {
clearInterval(validationStatusInterval);
validationStatusInterval = null;
}
if (marketDataInterval) {
clearInterval(marketDataInterval);
marketDataInterval = null;
}
console.log('校验定期更新已停止');
}
// ===========================================
// 管理员功能模块
// ===========================================
// 管理员状态管理
let isAdminAuthenticated = false;
// 初始化管理员功能
function initAdminFunctions() {
console.log('初始化管理员功能...');
// 绑定密码验证按钮事件
$('#verifyAdminPasswordBtn').on('click', function() {
verifyAdminPassword();
});
// 绑定管理员密码输入框回车事件
$('#adminPassword').on('keypress', function(e) {
if (e.which === 13) { // 回车键
verifyAdminPassword();
}
});
// 绑定密码显示/隐藏按钮
$('#toggleAdminPassword').on('click', function() {
togglePasswordVisibility('adminPassword', 'toggleAdminPassword');
});
// 绑定退出登录按钮
$('#logoutAdminBtn').on('click', function() {
logoutAdmin();
});
console.log('管理员功能初始化完成');
}
// 验证管理员密码
function verifyAdminPassword() {
const password = $('#adminPassword').val();
if (!password) {
showAdminPasswordStatus('请输入管理员密码', 'danger');
return;
}
console.log('正在验证管理员密码...');
showAdminPasswordStatus('正在验证密码...', 'info');
// 禁用验证按钮防止重复提交
$('#verifyAdminPasswordBtn').prop('disabled', true);
// 通过校验API测试密码
$.ajax({
url: '/api/validation/manual',
method: 'POST',
data: JSON.stringify({
admin_password: password,
test_auth: true
}),
contentType: 'application/json',
success: function(response) {
if (response.success || response.error === '无效的请求参数') {
loginAdmin();
showAdminPasswordStatus('密码验证成功!', 'success');
} else {
showAdminPasswordStatus('密码验证失败', 'danger');
$('#adminPassword').val('').focus();
}
$('#verifyAdminPasswordBtn').prop('disabled', false);
},
error: function(xhr) {
if (xhr.status === 403) {
showAdminPasswordStatus('管理员密码错误', 'danger');
$('#adminPassword').val('').focus();
} else {
showAdminPasswordStatus('网络错误,请稍后重试', 'danger');
}
$('#verifyAdminPasswordBtn').prop('disabled', false);
}
});
}
// 管理员登录成功
function loginAdmin() {
isAdminAuthenticated = true;
// 隐藏密码验证面板
$('#adminPasswordPanel').fadeOut(300);
// 显示管理员功能面板
setTimeout(() => {
$('#adminFunctionsPanel').fadeIn(300);
}, 300);
// 清空密码字段
$('#adminPassword').val('');
console.log('管理员登录成功');
}
// 管理员退出登录
function logoutAdmin() {
isAdminAuthenticated = false;
// 隐藏管理员功能面板
$('#adminFunctionsPanel').fadeOut(300);
// 显示密码验证面板
setTimeout(() => {
$('#adminPasswordPanel').fadeIn(300);
$('#adminPasswordStatus').hide();
}, 300);
console.log('管理员已退出登录');
}
// 显示管理员密码验证状态
function showAdminPasswordStatus(message, type) {
const statusDiv = $('#adminPasswordStatus');
const alertClass = {
'success': 'alert-success',
'danger': 'alert-danger',
'warning': 'alert-warning',
'info': 'alert-info'
}[type] || 'alert-info';
statusDiv.html(`
${message}
`).show();
// 自动隐藏成功和错误消息
if (type === 'success' || type === 'danger') {
setTimeout(() => {
statusDiv.fadeOut();
}, 3000);
}
}
// 切换密码显示/隐藏
function togglePasswordVisibility(passwordFieldId, toggleButtonId) {
const passwordField = $('#' + passwordFieldId);
const toggleButton = $('#' + toggleButtonId);
const icon = toggleButton.find('i');
if (passwordField.attr('type') === 'password') {
passwordField.attr('type', 'text');
icon.removeClass('fa-eye').addClass('fa-eye-slash');
} else {
passwordField.attr('type', 'password');
icon.removeClass('fa-eye-slash').addClass('fa-eye');
}
}
// ========== Phase 2优化:图片懒加载实现 ==========
/**
* 图片懒加载功能
* 使用Intersection Observer API实现
* 只加载可视区域内的图片,节省流量和提高加载速度
*/
// 全局懒加载观察器
let lazyloadObserver = null;
// 初始化懒加载观察器
function initLazyLoad() {
// 检查浏览器是否支持Intersection Observer
if ('IntersectionObserver' in window) {
// 创建观察器实例
lazyloadObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
// 当图片进入可视区域时加载
if (entry.isIntersecting) {
const img = entry.target;
const src = img.getAttribute('data-src');
if (src) {
// 检查图片是否已在缓存中
if (imageCache.has(src)) {
// 已缓存,直接设置src(浏览器会从缓存加载)
img.src = src;
img.classList.remove('lazyload');
observer.unobserve(img);
} else {
// 未缓存,加载并添加到缓存
img.onload = function() {
imageCache.add(src);
};
img.src = src;
img.classList.remove('lazyload');
observer.unobserve(img);
}
}
}
});
}, {
// 配置选项
rootMargin: '100px', // 提前100px开始加载(优化体验)
threshold: 0.01 // 至少1%可见时触发
});
console.log('✓ 图片懒加载已启用(带缓存优化)');
} else {
// 不支持Intersection Observer时的降级方案
console.warn('浏览器不支持Intersection Observer,使用降级方案');
// 直接加载所有图片
loadAllImagesImmediately();
}
}
// 观察所有懒加载图片
function observeLazyImages() {
if (!lazyloadObserver) {
initLazyLoad();
}
// 查找所有带lazyload类的图片
const lazyImages = document.querySelectorAll('img.lazyload');
if (lazyloadObserver) {
lazyImages.forEach(function(img) {
lazyloadObserver.observe(img);
});
if (lazyImages.length > 0) {
console.log(`正在观察 ${lazyImages.length} 张图片的懒加载`);
}
}
}
// 降级方案:直接加载所有图片
function loadAllImagesImmediately() {
const lazyImages = document.querySelectorAll('img.lazyload');
lazyImages.forEach(function(img) {
const src = img.getAttribute('data-src');
if (src) {
img.src = src;
img.classList.remove('lazyload');
}
});
}
// 在商品列表更新后调用懒加载
function triggerLazyLoad() {
// 使用setTimeout确保DOM已更新
setTimeout(function() {
observeLazyImages();
}, 100);
}
// 页面加载时初始化懒加载
$(document).ready(function() {
initLazyLoad();
});
console.debug('✓ Phase 2优化已加载:API缓存 + 图片懒加载');