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
@@ -4647,6 +4647,9 @@
.drawer-mask { display: none; position: fixed; inset: 0; z-index: 1000; background: rgba(45, 66, 86, 0.36); backdrop-filter: blur(4px); }
.drawer-mask.show { display: block; animation: modal-fade-in 0.16s ease; }
.drawer { position: absolute; top: 0; right: 0; bottom: 0; display: flex; flex-direction: column; width: min(460px, 100%); background: #ffffff; border-left: 1px solid #c7d7e5; box-shadow: var(--shadow-pop); animation: drawer-slide-in 0.18s ease; }
/* 撞款明细抽屉:无遮罩图层(底层仍可点击) + 加宽 + 从撞款详情块位置开始 */
.dup-drawer-mask { background: none; backdrop-filter: none; pointer-events: none; z-index: 990; }
.dup-drawer-mask .drawer { pointer-events: auto; top: 24px; right: 24px; bottom: 24px; width: min(760px, calc(100% - 48px)); max-width: 860px; border-radius: 12px; border: 1px solid #dbe4ec; box-shadow: 0 14px 44px rgba(15, 42, 65, 0.22); }
@keyframes drawer-slide-in { from { transform: translateX(26px); opacity: 0.4; } to { transform: none; opacity: 1; } }
.drawer-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 20px 22px 14px; border-bottom: 1px solid #e6edf4; }
.drawer-header h3 { margin: 0; font-size: 15px; font-weight: 600; color: var(--c-text); }
@@ -4858,9 +4861,60 @@
<span class="admin-brand-sub">电商运营管理后台</span>
</span>
</div>
<nav class="admin-menu" id="adminMenu" aria-label="管理菜单">
<nav class="admin-menu" id="adminMenu" aria-label="管理菜单"{% if admin_menu_rendered %} data-server-rendered="true"{% endif %}>
{% if admin_menu_rendered %}
{# 服务端按当前用户权限直接渲染菜单,避免客户端二次渲染造成"全量菜单→折叠"的闪烁 #}
{% set __adm_chevron %}<span class="menu-group-chevron" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span>{% endset %}
{% set __adm_icons = {
'users': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>',
'group-manage': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>',
'columns': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="7" height="7" x="3" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="14" rx="1"></rect><rect width="7" height="7" x="3" y="14" rx="1"></rect></svg>',
'dedupe-total-data': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"></path><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"></path><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"></path></svg>',
'invalid-asin-data': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>',
'shop-keys': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"></path><path d="m21 2-9.6 9.6"></path><circle cx="7.5" cy="15.5" r="5.5"></circle></svg>',
'shop-manage': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"></path><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"></path><path d="M2 7h20"></path><path d="M22 7v3a2 2 0 0 1-2 2 2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"></path></svg>',
'skip-price-asin': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="m4.9 4.9 14.2 14.2"></path></svg>',
'query-asin': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>',
'product-categories': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"></path></svg>',
'image-video-tasks': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m22 8-6 4 6 4V8Z"></path><rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect></svg>',
'shop-data-crawl-tasks': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M3 5v14a9 3 0 0 0 18 0V5"></path><path d="M3 12a9 3 0 0 0 18 0"></path></svg>',
'shop-data-duplicate-check': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.35 11.1h-9.17a2 2 0 0 1-1.75-2.98l1.67-2.79a2 2 0 0 0-1.75-2.98l-5.98-.01a2 2 0 0 0-2 2v1.5a2 2 0 0 0 2 2h3.36l-2.38 3.97a2 2 0 0 0 1.75 2.98h11.5a2 2 0 0 0 2-2v-1.5a2 2 0 0 0-2-2Z"></path><path d="M3 21h18"></path></svg>',
'history': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M12 7v5l4 2"></path></svg>',
'version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7.5 4.27 9 5.15"></path><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"></path><path d="M3.3 7 12 12l8.7-5"></path><path d="M12 22V12"></path></svg>',
'digital-human-version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>',
} %}
{% set __adm_fallback_icon %}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 8v8"></path><path d="M8 12h8"></path></svg>{% endset %}
{% set __adm_groups = [
('account', '账号与权限', ['users', 'columns', 'group-manage']),
('data', '数据管理', ['dedupe-total-data', 'invalid-asin-data', 'query-asin', 'product-categories']),
('shop', '店铺管理', ['shop-keys', 'shop-manage', 'skip-price-asin', 'shop-data-crawl-tasks', 'shop-data-duplicate-check']),
('record', '记录与版本', ['history', 'version', 'digital-human-version', 'image-video-tasks']),
] %}
{% set __adm_allowed = admin_menu_items | map(attribute='route_path') | list %}
{% set __adm_has = namespace(v=False) %}
{% for __gk, __gt, __routes in __adm_groups %}
{% set __visible_routes = __routes | select('in', __adm_allowed) | list %}
{% if __visible_routes %}
{% set __adm_has.v = True %}
<div class="menu-group" data-menu-group="{{ __gk }}">
<button class="menu-group-title" type="button" aria-expanded="true">{{ __gt }}{{ __adm_chevron | safe }}</button>
<div class="menu-group-body">
{% for __r in __visible_routes %}
{% set __it = admin_menu_items | selectattr('route_path', 'equalto', __r) | first %}
{% set __label = ((__it.get('name') if __it and __it.get('name') else __r) | e) %}
<button class="tab" type="button" data-tab="{{ __r }}" aria-controls="panel-{{ __r }}" title="{{ __label }}"><span class="adm-icon">{{ __adm_icons.get(__r, __adm_fallback_icon) | safe }}</span><span class="adm-label">{{ __label }}</span></button>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
{% if not __adm_has.v %}
<div class="menu-empty">暂无可用菜单</div>
{% endif %}
{% else %}
{# 服务端不可用时的轻量占位:JS 加载后走接口渲染,避免闪出全量菜单 #}
<div class="menu-empty">菜单加载中...</div>
{% endif %}
</nav>
<div class="admin-sidebar-foot">数富AI · 管理控制台</div>
</aside>
@@ -5622,6 +5676,8 @@
<button class="btn shop-data-refresh-btn" id="btnRefreshShopDataDuplicates"
type="button">重新分析</button>
<button class="btn btn-secondary" id="btnExportShopDataDuplicates" type="button">导出</button>
<button class="btn btn-secondary" id="btnOpenDupCheckImportAdd" type="button">导入(新增-测试)</button>
<button class="btn btn-secondary" id="btnOpenDupCheckImportDelete" type="button">导入(删除-测试)</button>
<span class="image-video-download-progress" id="shopDataDuplicateProgress"
aria-live="polite"></span>
</div>
@@ -5638,7 +5694,7 @@
<div class="dup-check-detail-cards" id="dupCheckDetailCards"></div>
<div class="pagination" id="dupCheckDetailPagination" style="margin-top:12px;"></div>
</div>
<div class="drawer-mask" id="dupCheckDrawerMask">
<div class="drawer-mask dup-drawer-mask" id="dupCheckDrawerMask" data-drawer-anchor="dupCheckDetailBlock">
<aside class="drawer" role="dialog" aria-modal="true" aria-label="ASIN 详情">
<div class="drawer-header">
<div>
@@ -5651,6 +5707,30 @@
<div class="drawer-body" id="dupCheckDrawerBody"></div>
</aside>
</div>
<!-- 撞款:导入(新增-测试)/导入(删除-测试)弹窗 -->
<div class="modal-mask" id="dupCheckImportModal">
<div class="modal">
<h3 id="dupCheckImportTitle">导入(新增-测试)</h3>
<div class="form-group">
<label>店铺名称(新增=以导入文件替换该店记录;删除=从基线移除该店)</label>
<input type="text" id="dupCheckImportShopName" placeholder="如:小曾">
</div>
<div class="form-group">
<label>国家代码(可选,逗号分隔,如 UK,DE。缺失时按 Excel 的 sheet 名推断)</label>
<input type="text" id="dupCheckImportCountryCodes" placeholder="如:UK,DE">
</div>
<div class="form-group">
<label>上传 Excel 文件(读取 ASIN 列,可含 日期/价格/品牌 列)</label>
<input type="file" id="dupCheckImportFile" accept=".xlsx,.xls">
</div>
<p class="msg" id="msgDupCheckImport"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnDupCheckImportSubmit">上传并导入</button>
<button class="btn btn-secondary" id="btnCloseDupCheckImportModal" type="button">取消</button>
</div>
</div>
</div>
</div>
</div>
</div>