后台店铺数据重复ASIN:全选移至表头、Tab总数、独立筛选、按ASIN分组表格展示
Build Backend JAR / build (push) Has been cancelled

- 列表全选复选框从工具栏移到表格表头,事件委托处理动态元素
- 重复ASIN Tab 标题加总数(XX 为重复 ASIN 数量)
- 重复ASIN 筛选独立:ASIN/店铺/国家/上架时间(开始+结束),与原店铺数据筛选互不影响
- 后端支持 asin/shop_name/country/date_from/date_to 参数:
  行级筛选命中 ASIN 后展示完整记录(含其他店铺,跨店语义更直观)
- 日期文本归一化(支持 2026年8月19日 上午4:34 中文格式)
- 按 ASIN 分组表格展示:ASIN/店铺数/记录条数/品牌/对应店铺/国家/上架时间/操作,
  详情展开显示每条记录的店铺/分组/国家/日期/价格/品牌
- 修复 _shop_data_crawl_fetch_result_bytes 兼容 result_id 列别名(生产首个真实调用报 KeyError)
- 新增单测:筛选语义、日期归一化、record_count
This commit is contained in:
2026-09-03 09:32:25 +08:00
parent 417a2bf831
commit 672b9f9b46
5 changed files with 263 additions and 115 deletions
+115 -25
View File
@@ -1784,7 +1784,7 @@
}
return '<table>' +
'<thead><tr>' +
'<th style="width:36px;"></th>' +
'<th style="width:36px;"><input type="checkbox" id="shopDataTaskSelectAll"></th>' +
'<th style="width:18%;">店铺</th>' +
'<th style="width:14%;">分组</th>' +
'<th style="width:16%;">国家</th>' +
@@ -1870,11 +1870,11 @@
params.set('page', String(page || 1));
params.set('page_size', String(shopDataDuplicatePageSize));
var values = {
shop_name: document.getElementById('shopDataTaskFilterShop').value.trim(),
group_name: document.getElementById('shopDataTaskFilterGroup').value.trim(),
country: document.getElementById('shopDataTaskFilterCountry').value.trim(),
created_from: document.getElementById('shopDataTaskFilterFrom').value,
created_to: document.getElementById('shopDataTaskFilterTo').value
asin: document.getElementById('shopDataDupFilterAsin').value.trim(),
shop_name: document.getElementById('shopDataDupFilterShop').value.trim(),
country: document.getElementById('shopDataDupFilterCountry').value.trim(),
date_from: document.getElementById('shopDataDupFilterDateFrom').value,
date_to: document.getElementById('shopDataDupFilterDateTo').value
};
Object.keys(values).forEach(function (key) {
if (values[key]) params.set(key, values[key]);
@@ -1924,10 +1924,69 @@
function renderShopDataDuplicateList() {
var list = document.getElementById('shopDataDuplicateList');
if (!shopDataDuplicateItems.length) {
list.innerHTML = '<div class="shop-data-empty-hint">暂无重复 ASIN。请在左侧筛选条件后点击"查询"生效范围,再点击"重新分析"。</div>';
list.innerHTML = '<div class="shop-data-empty-hint">暂无重复 ASIN。请在筛选条件后点击"查询",或"重新分析"。' +
'<span id="dupEmptyHint"></span></div>';
return;
}
list.innerHTML = shopDataDuplicateItems.map(renderShopDataDuplicateCard).join('');
var header = [
{ label: 'ASIN', width: '14%' },
{ label: '店铺数', width: '8%' },
{ label: '记录条数', width: '10%' },
{ label: '品牌', width: '12%' },
{ label: '对应店铺', width: '22%' },
{ label: '国家', width: '14%' },
{ label: '上架时间', width: '12%' },
{ label: '操作', width: '8%' }
];
var tbody = shopDataDuplicateItems.map(function (item) {
return renderShopDataDuplicateRow(item);
}).join('');
list.innerHTML = '<table>' + shopDataDuplicateHeader(header) + '<tbody>' + tbody + '</tbody></table>';
}
function renderShopDataDuplicateRow(item) {
var occurrences = Array.isArray(item.occurrences) ? item.occurrences : [];
var brand = '';
var countries = {};
var shops = {};
occurrences.forEach(function (occ) {
if (occ.brand && !brand) brand = occ.brand;
(occ.country_codes || []).forEach(function (c) { countries[c] = true; });
shops[occ.shop_name || '-'] = true;
});
var countryLabel = countryListLabel(Object.keys(countries));
var shopList = Object.keys(shops).join('、');
var dateList = occurrences.map(function (occ) { return occ.date; })
.filter(function (d) { return d; }).join('、');
return '<tr data-duplicate-asin="' + escapeHtml(item.asin) + '">' +
'<td><span class="dup-asin">' + escapeHtml(item.asin) + '</span></td>' +
'<td><span class="dup-count">' + (item.shop_count || 1) + ' 家</span></td>' +
'<td>' + (item.record_count || occurrences.length) + ' 条</td>' +
'<td>' + escapeHtml(brand || '-') + '</td>' +
'<td>' + escapeHtml(shopList || '-') + '</td>' +
'<td>' + escapeHtml(countryLabel || '-') + '</td>' +
'<td>' + escapeHtml(dateList || '-') + '</td>' +
'<td><button class="btn btn-sm" type="button" data-dup-toggle>详情</button></td>' +
'</tr>' + renderShopDataDuplicateDetailRows(item);
}
function renderShopDataDuplicateDetailRows(item) {
var occurrences = Array.isArray(item.occurrences) ? item.occurrences : [];
if (!occurrences.length) return '';
var rows = occurrences.map(function (occ) {
var countries = countryListLabel(occ.country_codes);
return '<tr class="dup-detail-row" style="display:none;">' +
'<td></td>' +
'<td class="dup-shop">' + escapeHtml(occ.shop_name || '-') + '</td>' +
'<td class="dup-country">' + escapeHtml(occ.group_name || '-') + '</td>' +
'<td class="dup-country">' + escapeHtml(countries) + '</td>' +
'<td class="dup-date">' + escapeHtml(occ.date || '-') + '</td>' +
'<td class="dup-date">' + escapeHtml(occ.price || '-') + '</td>' +
'<td>' + escapeHtml(occ.brand || '-') + '</td>' +
'<td></td>' +
'</tr>';
}).join('');
return rows;
}
function loadShopDataDuplicateAsins(page) {
@@ -1950,6 +2009,8 @@
shopDataDuplicateAnalyzed.resultCount = Number(res.analyzed_result_count) || 0;
var totalEl = document.getElementById('shopDataDuplicateTotal');
totalEl.textContent = '共 ' + shopDataDuplicateTotal + ' 个重复 ASIN · 已分析 ' + shopDataDuplicateAnalyzed.shopCount + ' 家店铺';
var tabCount = document.getElementById('shopDataDuplicateTabCount');
if (tabCount) tabCount.textContent = shopDataDuplicateTotal;
renderShopDataDuplicateList();
renderPagination('shopDataDuplicatePagination', shopDataDuplicateTotal, shopDataDuplicatePage, shopDataDuplicatePageSize, loadShopDataDuplicateAsins);
})
@@ -2177,24 +2238,32 @@
document.getElementById('shopDataSubTabRecords').onclick = function () { switchShopDataSubTab('records'); };
document.getElementById('shopDataSubTabDuplicates').onclick = function () { switchShopDataSubTab('duplicates'); };
document.getElementById('btnRefreshShopDataDuplicates').onclick = function () { loadShopDataDuplicateAsins(1); };
document.getElementById('shopDataTaskSelectAll').onchange = function (event) {
shopDataTasks.forEach(function (item) {
var resultId = shopDataResultId(item);
if (!item.file_ready || !resultId) return;
if (event.target.checked) selectedShopDataResultIds.add(resultId);
else selectedShopDataResultIds.delete(resultId);
});
syncShopDataSelectionUi();
document.getElementById('btnFilterShopDataDuplicates').onclick = function () { loadShopDataDuplicateAsins(1); };
document.getElementById('btnResetShopDataDuplicates').onclick = function () {
['shopDataDupFilterAsin', 'shopDataDupFilterShop', 'shopDataDupFilterCountry', 'shopDataDupFilterDateFrom', 'shopDataDupFilterDateTo']
.forEach(function (id) { document.getElementById(id).value = ''; });
loadShopDataDuplicateAsins(1);
};
document.getElementById('btnBatchDownloadShopDataTasks').onclick = downloadShopDataTasksZip;
// 全选移到表格表头后为动态元素,用事件委托
document.getElementById('shopDataTaskGrid').onchange = function (event) {
var checkbox = event.target.closest('[data-shop-data-select]');
if (!checkbox) return;
var resultId = Number(checkbox.dataset.shopDataSelect);
if (!resultId) return;
if (checkbox.checked) selectedShopDataResultIds.add(resultId);
else selectedShopDataResultIds.delete(resultId);
syncShopDataSelectionUi();
if (checkbox) {
var resultId = Number(checkbox.dataset.shopDataSelect);
if (!resultId) return;
if (checkbox.checked) selectedShopDataResultIds.add(resultId);
else selectedShopDataResultIds.delete(resultId);
syncShopDataSelectionUi();
return;
}
if (event.target.closest('#shopDataTaskSelectAll')) {
shopDataTasks.forEach(function (item) {
var sid = shopDataResultId(item);
if (!item.file_ready || !sid) return;
if (event.target.checked) selectedShopDataResultIds.add(sid);
else selectedShopDataResultIds.delete(sid);
});
syncShopDataSelectionUi();
}
};
document.getElementById('shopDataTaskGrid').onclick = function (event) {
var downloadButton = event.target.closest('[data-shop-data-download]');
@@ -2209,6 +2278,29 @@
deleteShopDataTask(deleteItem);
}
};
document.getElementById('btnBatchDownloadShopDataTasks').onclick = downloadShopDataTasksZip;
document.getElementById('shopDataDuplicateList').onclick = function (event) {
var toggle = event.target.closest('[data-dup-toggle]');
if (!toggle) return;
var row = toggle.closest('tr');
var allDetailRows = document.querySelectorAll('.dup-detail-row');
// 若当前 ASIN 的详情行已展开,则本次点击收起
var cur = row ? row.nextElementSibling : null;
var wasExpanded = false;
while (cur && cur.classList && cur.classList.contains('dup-detail-row')) {
if (cur.style.display !== 'none') { wasExpanded = true; break; }
cur = cur.nextElementSibling;
}
allDetailRows.forEach(function (dr) { dr.style.display = 'none'; });
if (row && !wasExpanded) {
// 展开当前 ASIN 的详情行(紧随其后)
cur = row.nextElementSibling;
while (cur && cur.classList && cur.classList.contains('dup-detail-row')) {
cur.style.display = '';
cur = cur.nextElementSibling;
}
}
};
document.getElementById('btnOpenShopDataTaskPermissions').onclick = openShopDataTaskPermissions;
document.getElementById('btnCloseShopDataTaskPermissions').onclick = closeShopDataTaskPermissions;
document.getElementById('btnCancelShopDataTaskPermissions').onclick = closeShopDataTaskPermissions;
@@ -6408,8 +6500,6 @@
currentUserRole = item.role || currentUserRole;
currentUserUsername = item.username || currentUserUsername;
nameEl.textContent = item.username || '管理员';
var avatarEl = document.getElementById('adminUserAvatar');
if (avatarEl) avatarEl.textContent = (item.username || '管').charAt(0).toUpperCase();
if (item.role) {
roleEl.textContent = item.role === 'super_admin'
? '超级管理员'