feat(admin): 店铺数据重复检查撞款抽屉样式优化与导入测试按钮
Build Backend JAR / build (push) Has been cancelled

- 撞款详情抽屉:上架时间默认降序(晚到早)、去掉列头(按时间升序)小括号、加宽至 760px、
  去掉全屏遮罩(底层仍可点击)、抽屉贴到「撞款详情」区块位置而非浏览器顶部
- 修复抽屉 fixed 定位失效:.tab-panel.active 入场动画带 transform 会成为包含块,
  打开时把 drawer mask 挂到 body 下恢复视口定位
- 新增导入(新增-测试)/ 导入(删除-测试)按钮与上传弹窗,对应后端端点
  /api/admin/shop-data-crawl/duplicate-check-import(ADD=按店名替换基线记录、DELETE=移除该店记录)
- 新增 ShopDataDuplicateCheckImportService 导入服务(复用 WorkbookParser/Aggregator,与扫描共用 Redis 锁)
- 新增 3 个单测:导入替换重聚合、删除移除、无基线报错
- _preview_mock.py 加入 .gitignore(本地预览工具不入库)
This commit is contained in:
2026-09-04 12:25:31 +08:00
parent 8241cd704e
commit 05bda8915a
8 changed files with 766 additions and 130 deletions
+147 -11
View File
@@ -2196,8 +2196,61 @@
return '<span class="dup-drawer-badge' + (cls ? ' ' + cls : '') + '">' + escapeHtml(text) + '</span>';
}
// 图3 样式抽屉:ASIN + 店在售徽章 / 品牌行 / 彩色徽章组 / 按时间序明细 + 次数列
// 图3 样式抽屉:ASIN + 店在售徽章 / 品牌行 / 彩色徽章组 / 按时间序明细 + 次数列
// 块内定位:抽屉贴到「撞款详情」区块顶部(anchorRect),而不是浏览器左侧顶部;
// 区块滚出视口时钳制到视口内可见区域(顶部矩阵点 ASIN 打开也要能看到抽屉)
function positionDupDrawer() {
var mask = document.getElementById('dupCheckDrawerMask');
var aside = mask ? mask.querySelector('.drawer') : null;
var anchor = document.getElementById('dupCheckDetailBlock');
if (!aside || !anchor) return;
var rect = anchor.getBoundingClientRect();
var bodyHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
var padTop = 24, padRight = 24, padBottom = 24;
var maxHeight = Math.max(260, bodyHeight - padTop - padBottom);
var left = Math.max(12, Math.min(rect.left + 12, Math.max(12, bodyWidth() - 860)));
var top = Math.max(padTop, rect.top - padTop);
// 区块整体在视口外(下方/上方)时回退到视口顶部,避免抽屉出现在视口外
if (rect.top >= bodyHeight - 120 || rect.bottom <= padTop) top = padTop;
// 顶部再钳制一次,保证抽屉头部可见
top = Math.min(top, Math.max(padTop, bodyHeight - 260));
aside.style.left = left + 'px';
aside.style.top = top + 'px';
aside.style.bottom = 'auto';
aside.style.maxHeight = (maxHeight - (top - padTop)) + 'px';
var body = document.getElementById('dupCheckDrawerBody');
if (body) body.style.maxHeight = (maxHeight - (top - padTop) - 96) + 'px';
}
function bodyWidth() {
var w = document.documentElement.clientWidth || window.innerWidth;
return w;
}
// 上架时间降序排列(晚到早;同时间再按店铺名稳定排序)
function dupTimeDesc(a, b) {
var ta = a._dupTime || '', tb = b._dupTime || '';
if (ta !== tb) {
if (ta === '') return 1;
if (tb === '') return -1;
return ta < tb ? 1 : -1;
}
var sa = (a.shop_name || '').toLowerCase(), sb = (b.shop_name || '').toLowerCase();
return sa < sb ? -1 : (sa > sb ? 1 : 0);
}
// 打开前把 mask 挂到 body 下。.tab-panel.active 的入场动画带 transform
// 会成为 fixed 定位的包含块,导致 mask/drawer 定位错误(未移出时 top 会算到面板外面)
function mountDupDrawerMaskToBody() {
var mask = document.getElementById('dupCheckDrawerMask');
if (mask && mask.parentElement !== document.body) {
document.body.appendChild(mask);
}
return mask;
}
function openShopDataDuplicateDrawer(asin) {
mountDupDrawerMaskToBody();
var item = null;
(shopDataDuplicateDetailItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
if (!item) {
@@ -2206,19 +2259,15 @@
if (!item) return;
var occurrences = (item.occurrences || []).slice();
var brand = item.brand || '';
// 上架时间归一化并升序排列(按时间正序展示
// 上架时间归一化并按时间降序排列(晚到早
occurrences.forEach(function (occ) { occ._dupTime = normalizeDuplicateTime(occ.date); });
occurrences.sort(function (a, b) {
var ta = a._dupTime || '', tb = b._dupTime || '';
if (ta === tb) return 0;
return ta < tb ? -1 : 1;
});
var sites = {}, firstDate = '';
occurrences.sort(dupTimeDesc);
var sites = {}, lastDate = '';
occurrences.forEach(function (occ) {
if (occ.country) sites[occ.country] = true;
if (!brand && occ.brand) brand = occ.brand;
var t = occ._dupTime;
if (t && (!firstDate || t < firstDate)) firstDate = t;
if (t && t > lastDate) lastDate = t;
});
// 店铺 + 国家两级聚合:次数 = 该店铺在该国家的上架记录数
var countsMap = {};
@@ -2253,7 +2302,7 @@
return dupDrawerBadge(dupSiteLabel(site), 'dup-site-' + site.toUpperCase());
}).join('') +
dupDrawerBadge(recordCount + ' 次上架', 'dup-drawer-badge-green') +
(firstDate ? dupDrawerBadge('最上架 ' + firstDate.slice(0, 16), 'dup-drawer-badge-cyan') : '');
(lastDate ? dupDrawerBadge('最上架 ' + lastDate.slice(0, 16), 'dup-drawer-badge-cyan') : '');
document.getElementById('dupCheckDrawerAsin').textContent = item.asin;
document.getElementById('dupCheckDrawerAsin').className = 'dup-drawer-asin';
var subtitleEl = document.getElementById('dupCheckDrawerSubtitle');
@@ -2262,9 +2311,10 @@
document.getElementById('dupCheckDrawerBody').innerHTML =
'<div class="dup-drawer-brand-line">品牌:' + escapeHtml(brand || '-') + '</div>' +
'<div class="table-scroll"><table class="dup-check-drawer-table">' +
'<thead><tr><th>店铺</th><th>国家</th><th>上架时间(按时间升序)</th><th>次数</th></tr></thead>' +
'<thead><tr><th>店铺</th><th>国家</th><th>上架时间</th><th>次数</th></tr></thead>' +
'<tbody>' + (rows || '<tr><td colspan="4" class="shop-data-empty-hint">暂无明细</td></tr>') + '</tbody></table></div>';
document.getElementById('dupCheckDrawerMask').classList.add('show');
positionDupDrawer();
}
function closeShopDataDuplicateDrawer() {
@@ -2602,6 +2652,92 @@
document.getElementById('dupCheckDrawerMask').onclick = function (event) {
if (event.target === this) closeShopDataDuplicateDrawer();
};
// ========== 撞款导入(新增-测试 / 删除-测试)==========
var dupCheckImportDeleteMode = false;
function openDupCheckImport(deleteMode) {
dupCheckImportDeleteMode = !!deleteMode;
document.getElementById('dupCheckImportTitle').textContent = deleteMode ? '导入(删除-测试)' : '导入(新增-测试)';
document.getElementById('dupCheckImportShopName').value = '';
document.getElementById('dupCheckImportCountryCodes').value = '';
document.getElementById('dupCheckImportFile').value = '';
var msg = document.getElementById('msgDupCheckImport');
msg.textContent = '';
msg.className = 'msg';
document.getElementById('dupCheckImportModal').classList.add('show');
}
function closeDupCheckImportModal() {
document.getElementById('dupCheckImportModal').classList.remove('show');
}
function submitDupCheckImport() {
var shopName = document.getElementById('dupCheckImportShopName').value.trim();
var fileInput = document.getElementById('dupCheckImportFile');
var msgEl = document.getElementById('msgDupCheckImport');
var submitBtn = document.getElementById('btnDupCheckImportSubmit');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!shopName) {
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;
}
var formData = new FormData();
formData.append('file', file);
formData.append('mode', dupCheckImportDeleteMode ? 'delete' : 'add');
formData.append('shop_name', shopName);
var codes = document.getElementById('dupCheckImportCountryCodes').value.trim();
if (codes) formData.append('country_codes', codes);
submitBtn.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/admin/shop-data-crawl/duplicate-check-import', true);
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
submitBtn.disabled = false;
if (xhr.status < 200 || xhr.status >= 300) {
msgEl.textContent = '请求失败:HTTP ' + xhr.status;
msgEl.className = 'msg err';
return;
}
var res;
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, message: '返回格式错误' }; }
if (!res.success) {
msgEl.textContent = res.message || res.error || '导入失败';
msgEl.className = 'msg err';
return;
}
var data = res.data || {};
msgEl.textContent = (data.message || (dupCheckImportDeleteMode ? '删除' : '新增') + '导入成功')
+ ':解析 ' + (data.total_rows != null ? data.total_rows : '?') + ' 行';
msgEl.className = 'msg ok';
fileInput.value = '';
// 导入落库新扫描行,重新加载视图让页面立即反映
loadShopDataDuplicateCheckOverview(false);
};
xhr.onerror = function () {
submitBtn.disabled = false;
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
};
xhr.send(formData);
}
document.getElementById('btnOpenDupCheckImportAdd').onclick = function () { openDupCheckImport(false); };
document.getElementById('btnOpenDupCheckImportDelete').onclick = function () { openDupCheckImport(true); };
document.getElementById('btnCloseDupCheckImportModal').onclick = closeDupCheckImportModal;
document.getElementById('btnDupCheckImportSubmit').onclick = submitDupCheckImport;
document.getElementById('dupCheckImportModal').onclick = function (event) {
if (event.target === this) closeDupCheckImportModal();
};
// 矩阵表格内 ASIN / 数字徽标点击打开抽屉(动态元素,事件委托)
document.getElementById('shopDataDuplicateList').onclick = function (event) {
var target = event.target.closest('[data-open-drawer]');