修复商品风险多店铺问题

This commit is contained in:
super
2026-04-30 21:31:11 +08:00
parent 955a6439a0
commit 11f0d2c745
18 changed files with 18927 additions and 46 deletions

View File

@@ -227,6 +227,65 @@
color: #666;
}
.request-loading-bar {
position: fixed;
left: 0;
top: 0;
width: 0;
height: 3px;
background: #667eea;
opacity: 0;
z-index: 3000;
transition: width 0.2s ease, opacity 0.2s ease;
}
.request-loading-bar.show {
width: 74%;
opacity: 1;
}
.request-loading-bar.finishing {
width: 100%;
opacity: 0;
}
.request-loading {
position: fixed;
right: 24px;
top: 72px;
display: none;
align-items: center;
gap: 10px;
padding: 10px 14px;
color: #333;
background: rgba(255, 255, 255, 0.96);
border: 1px solid #e6eaf2;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
font-size: 13px;
z-index: 3000;
pointer-events: none;
}
.request-loading.show {
display: flex;
}
.request-spinner {
width: 16px;
height: 16px;
border: 2px solid #dce2ff;
border-top-color: #667eea;
border-radius: 50%;
animation: request-spin 0.8s linear infinite;
}
@keyframes request-spin {
to {
transform: rotate(360deg);
}
}
table {
width: 100%;
border-collapse: collapse;
@@ -661,6 +720,11 @@
<body>
<!-- <div class="nav"><a href="/home">返回首页</a></div> -->
<div class="request-loading-bar" id="requestLoadingBar"></div>
<div class="request-loading" id="requestLoading">
<span class="request-spinner"></span>
<span>请求处理中...</span>
</div>
<h1>管理后台</h1>
<div class="admin-user-box" style="position:absolute;top:24px;right:24px;z-index:10;">
@@ -1042,6 +1106,42 @@
<button class="btn" id="btnCreateSkipPriceAsin">新增 ASIN</button>
</div>
<p class="msg" id="msgSkipPriceAsin"></p>
<div class="form-row" style="align-items:flex-start;gap:16px;margin-top:18px;border-top:1px solid #f0f0f0;padding-top:16px;">
<div style="flex:1;min-width:320px;">
<h3 style="margin-bottom:12px;font-size:14px;">导入文件新增</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel先选择分组文件名必须是该分组下的店铺名表头为 国家 / 删除ASIN / 最低价)</label>
<input type="file" id="skipPriceAsinImportFile" accept=".xlsx,.xls">
</div>
<button class="btn" id="btnImportSkipPriceAsin" type="button">上传并新增</button>
</div>
<p class="msg" id="msgSkipPriceAsinImport"></p>
<div class="progress-wrap" id="skipPriceAsinImportProgressWrap" style="display:none;">
<div class="progress-bar">
<div class="progress-fill" id="skipPriceAsinImportProgressFill"></div>
</div>
<div class="progress-text" id="skipPriceAsinImportProgressText"></div>
</div>
</div>
<div style="flex:1;min-width:320px;">
<h3 style="margin-bottom:12px;font-size:14px;">导入文件删除</h3>
<div class="form-row">
<div class="form-group" style="min-width:320px;">
<label>上传 Excel先选择分组文件名必须是该分组下的店铺名按各国家删除ASIN列匹配删除</label>
<input type="file" id="skipPriceAsinDeleteImportFile" accept=".xlsx,.xls">
</div>
<button class="btn btn-danger" id="btnDeleteImportSkipPriceAsin" type="button">上传并删除</button>
</div>
<p class="msg" id="msgSkipPriceAsinDeleteImport"></p>
<div class="progress-wrap" id="skipPriceAsinDeleteImportProgressWrap" style="display:none;">
<div class="progress-bar">
<div class="progress-fill" id="skipPriceAsinDeleteImportProgressFill"></div>
</div>
<div class="progress-text" id="skipPriceAsinDeleteImportProgressText"></div>
</div>
</div>
</div>
</div>
<div class="panel-box">
<h3 style="margin-bottom:16px;font-size:15px;">店铺列表</h3>
@@ -1578,6 +1678,100 @@
<script>
(function () {
// 全局请求动画:拦截本页所有 fetch 和 XMLHttpRequest 请求。
(function installRequestLoadingInterceptor() {
if (window.__adminRequestLoadingInstalled) return;
window.__adminRequestLoadingInstalled = true;
var activeRequests = 0;
var showTimer = null;
var hideTimer = null;
var showDelayMs = 180;
var loadingEl = document.getElementById('requestLoading');
var loadingBarEl = document.getElementById('requestLoadingBar');
function showRequestLoading() {
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
if (!loadingEl || !loadingBarEl) return;
loadingBarEl.classList.remove('finishing');
loadingBarEl.classList.add('show');
loadingEl.classList.add('show');
}
function hideRequestLoading() {
if (showTimer) {
clearTimeout(showTimer);
showTimer = null;
}
if (!loadingEl || !loadingBarEl) return;
loadingBarEl.classList.remove('show');
loadingBarEl.classList.add('finishing');
loadingEl.classList.remove('show');
hideTimer = setTimeout(function () {
loadingBarEl.classList.remove('finishing');
}, 220);
}
function beginRequest() {
activeRequests += 1;
if (activeRequests === 1) {
showTimer = setTimeout(showRequestLoading, showDelayMs);
}
}
function endRequest() {
activeRequests = Math.max(0, activeRequests - 1);
if (activeRequests === 0) {
hideRequestLoading();
}
}
if (window.fetch) {
var nativeFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
var options = init || {};
var skipLoading = !!options.__skipLoading;
if (skipLoading) {
options = Object.assign({}, options);
delete options.__skipLoading;
} else {
beginRequest();
}
try {
return nativeFetch(input, options).finally(function () {
if (!skipLoading) endRequest();
});
} catch (err) {
if (!skipLoading) endRequest();
throw err;
}
};
}
if (window.XMLHttpRequest) {
var nativeOpen = XMLHttpRequest.prototype.open;
var nativeSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function () {
this.__adminSkipLoading = false;
return nativeOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
if (!this.__adminSkipLoading) {
beginRequest();
this.addEventListener('loadend', endRequest, { once: true });
}
try {
return nativeSend.apply(this, arguments);
} catch (err) {
if (!this.__adminSkipLoading) endRequest();
throw err;
}
};
}
})();
// Tab 切换
var adminTabsEl = document.getElementById('adminTabs');
if (adminTabsEl) adminTabsEl.innerHTML = '';
@@ -3441,6 +3635,270 @@
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
var skipPriceAsinImportPollTimer = null;
var skipPriceAsinDeleteImportPollTimer = null;
var skipPriceAsinImportPollSeq = 0;
var skipPriceAsinDeleteImportPollSeq = 0;
function stopSkipPriceAsinImportProgress() {
skipPriceAsinImportPollSeq += 1;
if (skipPriceAsinImportPollTimer) {
clearTimeout(skipPriceAsinImportPollTimer);
skipPriceAsinImportPollTimer = null;
}
}
function stopSkipPriceAsinDeleteImportProgress() {
skipPriceAsinDeleteImportPollSeq += 1;
if (skipPriceAsinDeleteImportPollTimer) {
clearTimeout(skipPriceAsinDeleteImportPollTimer);
skipPriceAsinDeleteImportPollTimer = null;
}
}
function setSkipPriceAsinImportProgress(percent, text) {
var wrap = document.getElementById('skipPriceAsinImportProgressWrap');
var fill = document.getElementById('skipPriceAsinImportProgressFill');
var textEl = document.getElementById('skipPriceAsinImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function setSkipPriceAsinDeleteImportProgress(percent, text) {
var wrap = document.getElementById('skipPriceAsinDeleteImportProgressWrap');
var fill = document.getElementById('skipPriceAsinDeleteImportProgressFill');
var textEl = document.getElementById('skipPriceAsinDeleteImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function pollSkipPriceAsinImport(importId) {
stopSkipPriceAsinImportProgress();
var pollSeq = ++skipPriceAsinImportPollSeq;
var finished = false;
var inFlight = false;
var attempts = 0;
var maxAttempts = 300;
function isActive() {
return !finished && pollSeq === skipPriceAsinImportPollSeq;
}
function finish() {
finished = true;
if (skipPriceAsinImportPollTimer) {
clearTimeout(skipPriceAsinImportPollTimer);
skipPriceAsinImportPollTimer = null;
}
}
function schedule() {
if (isActive()) {
skipPriceAsinImportPollTimer = setTimeout(tick, 1500);
}
}
function tick() {
if (!isActive() || inFlight) {
return;
}
attempts += 1;
if (attempts > maxAttempts) {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度超时,请稍后刷新列表确认结果';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
return;
}
inFlight = true;
fetch('/api/admin/skip-price-asins/import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!isActive()) {
return;
}
if (!res.success) {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = res.error || '查询导入进度失败';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setSkipPriceAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = '导入成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgSkipPriceAsinImport').className = 'msg ok';
loadSkipPriceAsin(1);
} else if (progress.status === 'failed') {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = progress.error_message || '导入失败';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
} else {
schedule();
}
})
.catch(function () {
if (!isActive()) {
return;
}
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度失败';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
})
.finally(function () {
inFlight = false;
});
}
tick();
}
function pollSkipPriceAsinDeleteImport(importId) {
stopSkipPriceAsinDeleteImportProgress();
var pollSeq = ++skipPriceAsinDeleteImportPollSeq;
var finished = false;
var inFlight = false;
var attempts = 0;
var maxAttempts = 300;
function isActive() {
return !finished && pollSeq === skipPriceAsinDeleteImportPollSeq;
}
function finish() {
finished = true;
if (skipPriceAsinDeleteImportPollTimer) {
clearTimeout(skipPriceAsinDeleteImportPollTimer);
skipPriceAsinDeleteImportPollTimer = null;
}
}
function schedule() {
if (isActive()) {
skipPriceAsinDeleteImportPollTimer = setTimeout(tick, 1500);
}
}
function tick() {
if (!isActive() || inFlight) {
return;
}
attempts += 1;
if (attempts > maxAttempts) {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度超时,请稍后刷新列表确认结果';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
return;
}
inFlight = true;
fetch('/api/admin/skip-price-asins/delete-import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!isActive()) {
return;
}
if (!res.success) {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = res.error || '查询删除进度失败';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setSkipPriceAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg ok';
loadSkipPriceAsin(1);
} else if (progress.status === 'failed') {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = progress.error_message || '删除失败';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
} else {
schedule();
}
})
.catch(function () {
if (!isActive()) {
return;
}
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度失败';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
})
.finally(function () {
inFlight = false;
});
}
tick();
}
function uploadSkipPriceAsinImport(deleteMode) {
var fileInput = document.getElementById(deleteMode ? 'skipPriceAsinDeleteImportFile' : 'skipPriceAsinImportFile');
var msgEl = document.getElementById(deleteMode ? 'msgSkipPriceAsinDeleteImport' : 'msgSkipPriceAsinImport');
msgEl.textContent = '';
msgEl.className = 'msg';
if (deleteMode) {
stopSkipPriceAsinDeleteImportProgress();
document.getElementById('skipPriceAsinDeleteImportProgressWrap').style.display = 'none';
} else {
stopSkipPriceAsinImportProgress();
document.getElementById('skipPriceAsinImportProgressWrap').style.display = 'none';
}
var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim();
if (!groupId) {
msgEl.textContent = '请先选择分组';
msgEl.className = 'msg err';
return;
}
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 Excel 文件';
msgEl.className = 'msg err';
return;
}
var file = fileInput.files[0];
var lowerName = (file.name || '').toLowerCase();
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
msgEl.className = 'msg err';
return;
}
if (deleteMode && !confirm('确定按 Excel 中的删除ASIN批量删除该店铺跳过跟价 ASIN 吗?')) {
return;
}
var formData = new FormData();
formData.append('file', file);
formData.append('group_id', groupId);
var xhr = new XMLHttpRequest();
xhr.open('POST', deleteMode ? '/api/admin/skip-price-asins/delete-import' : '/api/admin/skip-price-asins/import', true);
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var percent = Math.round(event.loaded * 100 / event.total);
if (deleteMode) setSkipPriceAsinDeleteImportProgress(percent, '上传中:' + percent + '%');
else setSkipPriceAsinImportProgress(percent, '上传中:' + percent + '%');
}
};
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status < 200 || xhr.status >= 300) {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
return;
}
var res;
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; }
if (!res.success) {
msgEl.textContent = res.error || (deleteMode ? '删除失败' : '导入失败');
msgEl.className = 'msg err';
return;
}
if (deleteMode) {
setSkipPriceAsinDeleteImportProgress(100, '上传完成,后端处理中...');
pollSkipPriceAsinDeleteImport(res.import_id);
} else {
setSkipPriceAsinImportProgress(100, '上传完成,后端处理中...');
pollSkipPriceAsinImport(res.import_id);
}
fileInput.value = '';
};
xhr.onerror = function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
};
xhr.send(formData);
}
document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal;
document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () {
loadChooseSkipPriceAsinShops(1);
@@ -3590,6 +4048,12 @@
};
setupSkipPriceAsinShopPicker();
initDropdownMultiSelect('skipPriceAsinCountries', '请选择国家');
document.getElementById('btnImportSkipPriceAsin').onclick = function () {
uploadSkipPriceAsinImport(false);
};
document.getElementById('btnDeleteImportSkipPriceAsin').onclick = function () {
uploadSkipPriceAsinImport(true);
};
renderSkipPriceAsinInputs();
// ========== 查询 ASIN ==========