feat(admin): 店铺重复检查导出「站点」列改「国家」中文显示;撞款抽屉按点击处定位
- 导出 CSV 列头 站点→国家,值映射中文(DE德国/UK英国/FR法国/IT意大利/ES西班牙,未知回退原值) - 撞款详情抽屉以触发元素为垂直锚点并钳制在视口内,不再贴区块/页面顶部导致看不见需上滑 - 同步 Java 静态托管副本;更新 Java/Python 相关断言
This commit is contained in:
+14
-5
@@ -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;
|
||||
|
||||
+5
-1
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user