feat(admin): 店铺重复检查导出「站点」列改「国家」中文显示;撞款抽屉按点击处定位

- 导出 CSV 列头 站点→国家,值映射中文(DE德国/UK英国/FR法国/IT意大利/ES西班牙,未知回退原值)
- 撞款详情抽屉以触发元素为垂直锚点并钳制在视口内,不再贴区块/页面顶部导致看不见需上滑
- 同步 Java 静态托管副本;更新 Java/Python 相关断言
This commit is contained in:
2026-09-05 01:53:12 +08:00
parent d6086ec7b0
commit d3b499b596
5 changed files with 59 additions and 34 deletions
@@ -183,13 +183,13 @@ public class ShopDataDuplicateCheckQueryService {
List<DuplicateItem> cropped = cropItems(view.payload().items(), visibleKeys);
List<DuplicateItem> matched = filterItems(cropped, monitor, filters);
List<List<String>> rows = new ArrayList<>();
rows.add(List.of("ASIN", "店铺数", "店铺", "分组", "站点", "上架时间", "价格", "品牌"));
rows.add(List.of("ASIN", "店铺数", "店铺", "分组", "国家", "上架时间", "价格", "品牌"));
List<Object[]> flat = new ArrayList<>();
for (DuplicateItem item : matched) {
String shopCount = String.valueOf(item.shopCount());
for (DuplicateOccurrence occ : item.occurrences()) {
flat.add(new Object[]{item.asin(), shopCount, occ.shopName(), occ.groupName(),
effectiveSite(occ), safe(occ.date()), safe(occ.price()), safe(occ.brand())});
effectiveCountryCn(occ), safe(occ.date()), safe(occ.price()), safe(occ.brand())});
}
}
flat.sort(flatExportComparator());
@@ -358,9 +358,18 @@ public class ShopDataDuplicateCheckQueryService {
source == null ? "" : source);
}
private String effectiveSite(DuplicateOccurrence occ) {
/** 站点代码 → 中文国家名(导出「国家」列,与页面 DUP_SITE_LABELS 一致),未识别回退原值。 */
private static final Map<String, String> SITE_CN_LABELS = Map.of(
"DE", "德国", "UK", "英国", "FR", "法国", "IT", "意大利", "ES", "西班牙");
private static String siteCnLabel(String code) {
String key = safe(code).trim().toUpperCase(Locale.ROOT);
return SITE_CN_LABELS.containsKey(key) ? SITE_CN_LABELS.get(key) : safe(code);
}
private String effectiveCountryCn(DuplicateOccurrence occ) {
if (!safe(occ.country()).isEmpty()) {
return occ.country().toUpperCase(Locale.ROOT);
return siteCnLabel(occ.country());
}
List<String> codes = occ.countryCodes() == null ? List.of() : occ.countryCodes();
if (codes.isEmpty()) {
@@ -371,7 +380,7 @@ public class ShopDataDuplicateCheckQueryService {
if (sb.length() > 0) {
sb.append('、');
}
sb.append(safe(code).toUpperCase(Locale.ROOT));
sb.append(siteCnLabel(code));
}
return sb.toString();
}
@@ -2084,10 +2084,10 @@
return isNaN(t) ? -1 : t;
}
// 「查看明细」按钮点击开抽屉(动态元素,事件委托)
// 「查看明细」按钮点击开抽屉(动态元素,事件委托),并把被点按钮作为抽屉的垂直锚点
function handleDuplicateDetailClick(event) {
var button = event.target.closest('[data-open-asin-detail]');
if (button) openShopDataDuplicateDrawer(button.dataset.openAsinDetail);
if (button) openShopDataDuplicateDrawer(button.dataset.openAsinDetail, button);
}
// 站点徽章:中文国家(未识别回退原值)
@@ -2197,21 +2197,26 @@
}
// 图3 样式抽屉:ASIN + 店在售徽章 / 品牌行 / 彩色徽章组 / 按时间降序明细 + 次数列
// 块内定位:抽屉贴到「撞款详情」区块顶部(anchorRect),而不是浏览器左侧顶部;
// 区块滚出视口时钳制视口内可见区域(顶部矩阵点 ASIN 打开也要能看到抽屉)
// 块内定位:抽屉横向贴到「撞款详情」区块,纵向以触发元素(被点的 ASIN/「查看明细」按钮)为锚点、
// 钳制视口内,保证弹窗出现在点击处附近而非页面/滚动区顶部(顶部弹出会被当前滚动位置盖住看不见)。
var dupDrawerTriggerEl = null;
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 block = document.getElementById('dupCheckDetailBlock');
if (!aside || !block) return;
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 blockRect = block.getBoundingClientRect();
var left = Math.max(12, Math.min(blockRect.left + 12, Math.max(12, bodyWidth() - 860)));
// 垂直锚点优先取触发元素;未传入或已脱离文档时回退到「撞款详情」区块顶部
var anchor = (dupDrawerTriggerEl && dupDrawerTriggerEl.isConnected) ? dupDrawerTriggerEl : block;
var rect = anchor.getBoundingClientRect();
var top = Math.max(padTop, rect.top - padTop);
// 区块整体在视口外(下方/上方)时回退到视口顶部,避免抽屉出现在视口外
if (rect.top >= bodyHeight - 120 || rect.bottom <= padTop) top = padTop;
// 点击处整体已滚出视口下方时,改放到视口底部上方留出可读高度
if (rect.top >= bodyHeight - padBottom) top = Math.max(padTop, bodyHeight - 300);
// 顶部再钳制一次,保证抽屉头部可见
top = Math.min(top, Math.max(padTop, bodyHeight - 260));
aside.style.left = left + 'px';
@@ -2249,8 +2254,9 @@
return mask;
}
function openShopDataDuplicateDrawer(asin) {
function openShopDataDuplicateDrawer(asin, triggerEl) {
mountDupDrawerMaskToBody();
dupDrawerTriggerEl = triggerEl || null;
var item = null;
(shopDataDuplicateDetailItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
if (!item) {
@@ -2738,10 +2744,10 @@
document.getElementById('dupCheckImportModal').onclick = function (event) {
if (event.target === this) closeDupCheckImportModal();
};
// 矩阵表格内 ASIN / 数字徽标点击打开抽屉(动态元素,事件委托)
// 矩阵表格内 ASIN / 数字徽标点击打开抽屉(动态元素,事件委托),并传入被点元素作为锚点
document.getElementById('shopDataDuplicateList').onclick = function (event) {
var target = event.target.closest('[data-open-drawer]');
if (target) openShopDataDuplicateDrawer(target.dataset.openDrawer);
if (target) openShopDataDuplicateDrawer(target.dataset.openDrawer, target);
};
// 撞款详情卡片区「查看明细」按钮(动态元素,事件委托)
document.getElementById('dupCheckDetailCards').onclick = handleDuplicateDetailClick;
@@ -147,10 +147,14 @@ class ShopDataDuplicateCheckQueryServiceTest {
assertThat(bytes[2]).isEqualTo((byte) 0xBF);
String text = new String(bytes, StandardCharsets.UTF_8);
String[] lines = text.split("\r\n");
assertThat(lines[0]).endsWith("ASIN,店铺数,店铺,分组,站点,上架时间,价格,品牌");
assertThat(lines[0]).endsWith("ASIN,店铺数,店铺,分组,国家,上架时间,价格,品牌");
// 数据行 = E(3 记录) + A(2) + F(2) = 7,非空行总数为 8(尾随 CRLF 会产生一个空元素)
long nonEmpty = java.util.Arrays.stream(lines).filter(line -> !line.isEmpty()).count();
assertThat(nonEmpty).isEqualTo(8);
assertThat(lines[1]).startsWith("E0000001,3,");
// 站点代码列导出为中文国家名(UK→英国、FR→法国),与页面「国家」展示一致
assertThat(lines[1]).startsWith("E0000001,3,ShopA,GroupA,英国,");
assertThat(text).contains(",法国,");
assertThat(lines[1]).doesNotContain(",UK,");
}
}
+19 -13
View File
@@ -2084,10 +2084,10 @@
return isNaN(t) ? -1 : t;
}
// 「查看明细」按钮点击开抽屉(动态元素,事件委托)
// 「查看明细」按钮点击开抽屉(动态元素,事件委托),并把被点按钮作为抽屉的垂直锚点
function handleDuplicateDetailClick(event) {
var button = event.target.closest('[data-open-asin-detail]');
if (button) openShopDataDuplicateDrawer(button.dataset.openAsinDetail);
if (button) openShopDataDuplicateDrawer(button.dataset.openAsinDetail, button);
}
// 站点徽章:中文国家(未识别回退原值)
@@ -2197,21 +2197,26 @@
}
// 图3 样式抽屉:ASIN + 店在售徽章 / 品牌行 / 彩色徽章组 / 按时间降序明细 + 次数列
// 块内定位:抽屉贴到「撞款详情」区块顶部(anchorRect),而不是浏览器左侧顶部;
// 区块滚出视口时钳制视口内可见区域(顶部矩阵点 ASIN 打开也要能看到抽屉)
// 块内定位:抽屉横向贴到「撞款详情」区块,纵向以触发元素(被点的 ASIN/「查看明细」按钮)为锚点、
// 钳制视口内,保证弹窗出现在点击处附近而非页面/滚动区顶部(顶部弹出会被当前滚动位置盖住看不见)。
var dupDrawerTriggerEl = null;
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 block = document.getElementById('dupCheckDetailBlock');
if (!aside || !block) return;
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 blockRect = block.getBoundingClientRect();
var left = Math.max(12, Math.min(blockRect.left + 12, Math.max(12, bodyWidth() - 860)));
// 垂直锚点优先取触发元素;未传入或已脱离文档时回退到「撞款详情」区块顶部
var anchor = (dupDrawerTriggerEl && dupDrawerTriggerEl.isConnected) ? dupDrawerTriggerEl : block;
var rect = anchor.getBoundingClientRect();
var top = Math.max(padTop, rect.top - padTop);
// 区块整体在视口外(下方/上方)时回退到视口顶部,避免抽屉出现在视口外
if (rect.top >= bodyHeight - 120 || rect.bottom <= padTop) top = padTop;
// 点击处整体已滚出视口下方时,改放到视口底部上方留出可读高度
if (rect.top >= bodyHeight - padBottom) top = Math.max(padTop, bodyHeight - 300);
// 顶部再钳制一次,保证抽屉头部可见
top = Math.min(top, Math.max(padTop, bodyHeight - 260));
aside.style.left = left + 'px';
@@ -2249,8 +2254,9 @@
return mask;
}
function openShopDataDuplicateDrawer(asin) {
function openShopDataDuplicateDrawer(asin, triggerEl) {
mountDupDrawerMaskToBody();
dupDrawerTriggerEl = triggerEl || null;
var item = null;
(shopDataDuplicateDetailItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
if (!item) {
@@ -2738,10 +2744,10 @@
document.getElementById('dupCheckImportModal').onclick = function (event) {
if (event.target === this) closeDupCheckImportModal();
};
// 矩阵表格内 ASIN / 数字徽标点击打开抽屉(动态元素,事件委托)
// 矩阵表格内 ASIN / 数字徽标点击打开抽屉(动态元素,事件委托),并传入被点元素作为锚点
document.getElementById('shopDataDuplicateList').onclick = function (event) {
var target = event.target.closest('[data-open-drawer]');
if (target) openShopDataDuplicateDrawer(target.dataset.openDrawer);
if (target) openShopDataDuplicateDrawer(target.dataset.openDrawer, target);
};
// 撞款详情卡片区「查看明细」按钮(动态元素,事件委托)
document.getElementById('dupCheckDetailCards').onclick = handleDuplicateDetailClick;
@@ -252,8 +252,8 @@ class DuplicateCheckProxyTest(unittest.TestCase):
self.assertEqual(body['items'], [])
def test_export_streams_java_csv(self):
csv_bytes = ('' + 'ASIN,店铺数,店铺,分组,站点,上架时间,价格,品牌\r\n'
'E0000001,3,ShopA,GroupA,UK,2026年8月29日 上午4:34,GBP 12.00,BrandE\r\n').encode('utf-8')
csv_bytes = ('' + 'ASIN,店铺数,店铺,分组,国家,上架时间,价格,品牌\r\n'
'E0000001,3,ShopA,GroupA,英国,2026年8月29日 上午4:34,GBP 12.00,BrandE\r\n').encode('utf-8')
class FakeResp:
status_code = 200