From bc5d5b3d2e5d02dce585e60f7a3bdf0ebbc2f69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Fri, 4 Sep 2026 10:21:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E7=AE=A1=E7=90=86=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E9=A1=B5=E9=9D=A2=E5=90=8C=E6=BA=90=E6=89=98=E7=AE=A1?= =?UTF-8?q?=E5=88=B0=20Java(classpath:/static)=E5=B9=B6=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=20Java=20=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 Flask web_source/admin.html 与 static/* 拷入 backend-java resources/static, 经 Spring 默认静态映射同源服务于 api.aishufu.top:18080;/static 根相对引用通过 static/static 子目录镜像零改动保持。页面登录改直连 Java POST /login(cookie+ 浏览器持久 deviceId),登出走 Java POST /logout。新增 AdminConsoleController: GET /、/admin、/login、/logout 页面转发,及 /api/admin/current-user、 /current-user/menus(管理员角色,返回旧前端契约的 item/items 形状)。admin.js 增加全局 fetch 响应解包(ApiResponse data 展开且保留 data 键,401 跳登录)与 URL 兼容映射层(把 Flask 别名统一到 Java 规范路径)。 --- .../controller/AdminConsoleController.java | 88 + .../src/main/resources/static/admin.html | 6579 +++++++++++++++ .../src/main/resources/static/login.html | 887 ++ .../static/static/admin-interactions.js | 333 + .../src/main/resources/static/static/admin.js | 7145 +++++++++++++++++ .../src/main/resources/static/static/bg.jpg | Bin 0 -> 164259 bytes .../src/main/resources/static/static/logo.jpg | Bin 0 -> 10772 bytes .../resources/static/static/logo_thumb.png | Bin 0 -> 6360 bytes 8 files changed, 15032 insertions(+) create mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminConsoleController.java create mode 100644 backend-java/src/main/resources/static/admin.html create mode 100644 backend-java/src/main/resources/static/login.html create mode 100644 backend-java/src/main/resources/static/static/admin-interactions.js create mode 100644 backend-java/src/main/resources/static/static/admin.js create mode 100644 backend-java/src/main/resources/static/static/bg.jpg create mode 100644 backend-java/src/main/resources/static/static/logo.jpg create mode 100644 backend-java/src/main/resources/static/static/logo_thumb.png diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminConsoleController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminConsoleController.java new file mode 100644 index 00000000..b8e775f1 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/controller/AdminConsoleController.java @@ -0,0 +1,88 @@ +package com.nanri.aiimage.modules.admin.controller; + +import com.nanri.aiimage.common.api.ApiResponse; +import com.nanri.aiimage.modules.admin.support.AdminAuthSupport; +import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity; +import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo; +import com.nanri.aiimage.modules.permission.service.PermissionMenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 管理后台页面与"当前管理员"自描述接口(Java 收敛自 Flask 侧 web_source/admin.html 的依赖)。 + * + *

页面经静态资源托管于同源(classpath:/static 下的 admin.html / login.html), + * 登录/登出沿用 auth 模块(POST /login、POST /logout)。这里提供 GET 页面转发与 + * /api/admin/current-user、/current-user/menus,保持与旧前端契约一致({item}、{items})。 + */ +@RestController +@RequiredArgsConstructor +@Tag(name = "管理后台页面", description = "管理后台静态页路由与当前管理员信息") +public class AdminConsoleController { + + private final AdminAuthSupport adminAuthSupport; + private final PermissionMenuService permissionMenuService; + + @GetMapping("/") + @Operation(summary = "根路径跳管理后台", hidden = true) + public String root() { + return "redirect:/admin.html"; + } + + @GetMapping("/admin") + @Operation(summary = "管理后台入口", hidden = true) + public String adminPage() { + return "redirect:/admin.html"; + } + + @GetMapping("/login") + @Operation(summary = "登录页入口", hidden = true) + public String loginPage() { + return "forward:/login.html"; + } + + @GetMapping("/logout") + @Operation(summary = "登出 GET 兜底(POST 走 auth 模块)", hidden = true) + public String logoutPage() { + return "redirect:/login.html"; + } + + @GetMapping("/api/admin/current-user") + @Operation(summary = "当前登录管理员概要") + public ApiResponse> currentUser(HttpServletRequest request) { + AdminUserEntity operator = adminAuthSupport.requireAdminOrInternal(request); + Map item = new LinkedHashMap<>(); + item.put("id", operator.getId()); + item.put("username", operator.getUsername() == null ? "" : operator.getUsername()); + item.put("role", adminAuthSupport.currentRole(operator)); + return ApiResponse.success(Map.of("item", item)); + } + + @GetMapping("/api/admin/current-user/menus") + @Operation(summary = "当前登录管理员的可见菜单(扁平 route_path/name 列表)") + public ApiResponse> currentUserMenus(HttpServletRequest request) { + AdminUserEntity operator = adminAuthSupport.requireAdminOrInternal(request); + List menus = permissionMenuService.getUserColumnPermissions( + operator, operator.getId(), PermissionMenuService.MENU_TYPE_ADMIN); + List> items = new ArrayList<>(menus.size()); + for (PermissionMenuItemVo menu : menus) { + if (menu.getRoutePath() == null || menu.getRoutePath().isBlank()) { + continue; + } + Map item = new LinkedHashMap<>(); + item.put("route_path", menu.getRoutePath()); + item.put("name", menu.getName() == null ? menu.getRoutePath() : menu.getName()); + items.add(item); + } + return ApiResponse.success(Map.of("items", items)); + } +} diff --git a/backend-java/src/main/resources/static/admin.html b/backend-java/src/main/resources/static/admin.html new file mode 100644 index 00000000..d9f1fc19 --- /dev/null +++ b/backend-java/src/main/resources/static/admin.html @@ -0,0 +1,6579 @@ + + + + + + + + 管理后台 - 数富AI + + + + + + +

+
+ + 请求处理中... +
+ +
+ +
+
+
+ 运营管理控制台 +
+ 管理后台 + +

用户管理

+
+
+
+ 加载中... + + +
+
+
+
+
+ +
+
当前页面操作提示
+

先查看数据,再使用上方筛选快速定位所需记录。

+
    +
    +
    + +
    + + + +
    +
    +
    +

    用户列表

    + +
    +

    + 当前没有开放注册入口,仅管理员可在此创建用户。层级关系:超级管理员 -> 管理员 -> 普通账号。

    +
    +
    + + +
    + + +
    +
    + + + + + + + + + + + + +
    ID用户名角色所属管理员创建时间操作
    +
    + +
    +
    + + +
    +
    +
    +

    菜单列表

    + +
    +

    拖动每行左侧的手柄可调整同级菜单的显示顺序,松开后自动保存。

    +
    + + + + + + + + + + + + + +
    排序ID菜单名称菜单类型上级菜单创建时间操作
    +
    +
    +
    + + +
    +
    +
    +

    版本列表

    + +
    +
    + + + + + + + + + + +
    版本号下载链接创建时间操作
    +
    + +
    +
    + + +
    +
    +
    +

    版本列表

    + +
    +
    + + + + + + + + + + + + + + +
    版本号状态最新文件大小MD5更新日志发布时间操作
    +
    + +
    +
    + + +
    +
    +
    +
    + 数据权限分组 +
    正在加载...
    +
    +
    +
    +
    +
    +

    分组列表

    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    序号分组名称组长组员数量组员创建时间修改时间操作
    +
    +
    +
    + + + + + +
    +
    +
    +
    + 数据权限分组 +
    正在加载...
    +
    +
    +
    +
    +
    +

    ASIN列表

    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + + + + + + + + + + + + + +
    IDASIN国家用户名分组创建时间操作
    +
    + +
    +
    + + +
    +
    +
    +

    品牌数据列表

    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    + + + + + + + + + + + + + +
    IDASIN品牌分组来源创建时间操作
    +
    + +
    +
    + + +
    +
    +
    +

    店铺密钥列表

    + +
    +
    + + + + + + + + + + + + + + +
    序号备注名紫鸟账号名称紫鸟令牌白名单状态 创建时间修改时间操作
    +
    + +
    +
    + + +
    +
    +
    +

    店铺列表

    + +
    +
    +
    + + +
    +
    + + +
    + +
    +
    + + + + + + + + + + + + + + + + +
    序号分组店铺名店铺商城名自动化账号账号密码创建时间修改时间操作
    +
    + +
    +
    + + +
    +
    +
    +

    店铺列表

    +
    + + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + + - + +
    +
    + + +
    +
    + + + + + + + + + + + + + +
    序号分组店铺名ASIN国家最低价操作
    +
    + +
    +
    + +
    +
    +
    +

    店铺列表

    +
    + + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    +
    + + + + + + + + + + + + +
    序号分组店铺名ASIN国家操作
    +
    + +
    +
    + +
    +
    +
    +

    商品类目

    +
    +
    +
    + +
    + + + +
    + +
    +
    +
    + + + + + + + + + + + + +
    类目层级路径排序来源备注操作
    +
    +
    +
    + +
    +
    +

    视频任务记录筛选

    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + + + +
    + +
    +
    +
    + +
    +
    +
    +
    +

    店铺数据记录

    +
    +

    筛选条件

    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    + + + + +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +

    店铺数据重复检查

    +
    +

    筛选条件

    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + 店铺上架分布 + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    + + + +
    + +
    +
    + +
    +
    +
    + 撞款详情 + 同一 ASIN 在多条店铺的上架明细 +
    +
    + +
    +
    + +
    +
    +
    +
    + + + +
    +
    +

    筛选条件

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +

    生成记录

    +
    + + + + + + + + + + + +
    ID用户类型创建时间结果预览
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + + +
    + + + + + + + diff --git a/backend-java/src/main/resources/static/login.html b/backend-java/src/main/resources/static/login.html new file mode 100644 index 00000000..ed97bfd5 --- /dev/null +++ b/backend-java/src/main/resources/static/login.html @@ -0,0 +1,887 @@ + + + + + + + + 登录 - 数富AI + + + + 跳转到登录表单 +
    + + +
    + +
    +
    + + + diff --git a/backend-java/src/main/resources/static/static/admin-interactions.js b/backend-java/src/main/resources/static/static/admin-interactions.js new file mode 100644 index 00000000..a6c79d27 --- /dev/null +++ b/backend-java/src/main/resources/static/static/admin-interactions.js @@ -0,0 +1,333 @@ +(function () { + 'use strict'; + + if (window.__adminInteractionLayerInstalled) return; + window.__adminInteractionLayerInstalled = true; + + var toastRegion = document.getElementById('adminToastRegion'); + var confirmMask = document.getElementById('adminConfirmModal'); + var confirmTitle = document.getElementById('adminConfirmTitle'); + var confirmMessage = document.getElementById('adminConfirmMessage'); + var confirmAccept = document.getElementById('adminConfirmAccept'); + var confirmCancel = document.getElementById('adminConfirmCancel'); + var guide = document.getElementById('adminOperationGuide'); + var guideText = document.getElementById('adminOperationGuideText'); + var guideSteps = document.getElementById('adminOperationGuideSteps'); + var guideToggle = document.getElementById('adminOperationGuideToggle'); + var pendingConfirmButton = null; + var previousFocus = null; + var busyButton = null; + var busyWasDisabled = false; + var activeFetches = 0; + var activeXhrs = 0; + var mainContent = document.getElementById('adminContent'); + + var guides = { + users: { text: '先用筛选定位账号,再编辑角色和菜单权限;删除账号会要求二次确认。', steps: ['筛选账号', '编辑权限', '确认保存'] }, + columns: { text: '菜单会影响后台和软件端的可见范围。先填写名称并选择对应页面,再设置上级菜单;顺序直接拖动列表左侧手柄调整。', steps: ['新增或调整菜单', '设置层级', '拖动排序'] }, + 'dedupe-total-data': { text: '支持按关键词、用户、分组和国家筛选;导入前先确认所选分组,导出会按日期范围生成文件。', steps: ['选择分组', '筛选或导入', '核对并导出'] }, + 'invalid-asin-data': { text: '维护不符合规则的 ASIN 或品牌。添加后可使用上方筛选快速回查。', steps: ['填写 ASIN/品牌', '选择分组', '保存并回查'] }, + 'shop-keys': { text: '紫鸟令牌属于敏感配置。白名单状态可悬停查看检测详情,编辑前请先核对账号名称。', steps: ['新增或筛选密钥', '查看白名单状态', '编辑或删除'] }, + 'shop-manage': { text: '店铺信息按分组管理。长商城名会自动缩略,悬停即可查看完整内容。', steps: ['选择分组', '维护店铺信息', '筛选核对结果'] }, + 'skip-price-asin': { text: '最低价 ASIN 以店铺为单位维护。列表里点击 ASIN 即可复制,点右侧「配置」可一次维护该店铺所有站点的 ASIN 与最低价。', steps: ['筛选店铺', '打开配置抽屉', '保存或批量导入'] }, + 'query-asin': { text: '查询 ASIN 以店铺为单位维护。列表里点击 ASIN 即可复制,点右侧「配置」可一次维护该店铺所有站点的 ASIN。', steps: ['筛选店铺', '打开配置抽屉', '保存或导出'] }, + 'product-categories': { text: '类目树支持展开查看层级。搜索、编辑和删除都在同一列表中完成。', steps: ['搜索类目', '展开层级', '新增或编辑'] }, + 'image-video-tasks': { text: '可先使用筛选缩小任务范围,再查看任务状态、结果和权限范围。', steps: ['设置筛选', '查看任务结果', '按需处理任务'] }, + 'shop-data-crawl-tasks': { text: '店铺数据任务按状态和时间筛选。批量操作前请核对已选任务。', steps: ['筛选任务', '检查状态', '执行批量操作'] }, + history: { text: '生成记录可按用户和时间范围回溯,用于核对结果文件和执行时间。', steps: ['设置时间范围', '筛选记录', '查看结果预览'] }, + version: { text: '上传版本后请核对版本号和下载链接,再通知用户更新。', steps: ['上传压缩包', '检查版本记录', '维护历史版本'] }, + 'digital-human-version': { text: '数字人版本需先上传草稿,再发布并标记最新版本。', steps: ['上传草稿', '确认更新日志', '发布或设为最新'] } + }; + + function cleanText(value) { + return String(value == null ? '' : value).replace(/\s+/g, ' ').trim(); + } + + function showToast(message, type) { + var value = cleanText(message); + if (!value || !toastRegion) return; + var item = document.createElement('div'); + item.className = 'admin-toast' + (type === 'error' ? ' is-error' : type === 'info' ? ' is-info' : ''); + var content = document.createElement('span'); + content.className = 'admin-toast__text'; + content.textContent = value; + item.appendChild(content); + toastRegion.appendChild(item); + window.setTimeout(function () { + item.style.opacity = '0'; + item.style.transform = 'translateY(-6px)'; + item.style.transition = 'opacity 160ms ease, transform 160ms ease'; + window.setTimeout(function () { item.remove(); }, 180); + }, type === 'error' ? 5200 : 3200); + } + + window.__adminToast = showToast; + + function activeTabName() { + var tab = document.querySelector('#adminMenu .tab.active'); + return tab ? (tab.dataset.tab || '') : ''; + } + + function updateGuide(tabName) { + if (!guide || !guideText || !guideSteps) return; + var config = guides[tabName] || { text: '先使用筛选定位记录,再进行新增、编辑、导出等操作。涉及删除的数据会要求二次确认。', steps: ['选择筛选条件', '处理记录', '核对反馈'] }; + guideText.textContent = config.text; + guideSteps.innerHTML = (config.steps || []).map(function (step) { return '
  1. ' + step + '
  2. '; }).join(''); + guide.dataset.tab = tabName || ''; + } + + window.__adminUpdateOperationGuide = updateGuide; + + function setGuideCollapsed(collapsed) { + if (!guide || !guideToggle) return; + guide.classList.toggle('is-collapsed', collapsed); + guideToggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); + guideToggle.textContent = collapsed ? '展开提示' : '收起提示'; + try { localStorage.setItem('shufuAdminGuideCollapsed', collapsed ? '1' : '0'); } catch (error) {} + } + + if (guideToggle) { + var collapsed = false; + try { collapsed = localStorage.getItem('shufuAdminGuideCollapsed') === '1'; } catch (error) {} + setGuideCollapsed(collapsed); + guideToggle.addEventListener('click', function () { + setGuideCollapsed(!guide.classList.contains('is-collapsed')); + }); + } + + function hashTabName() { + var raw = (window.location.hash || '').replace(/^#/, ''); + var match = raw.match(/(?:^|&)tab=([^&]+)/); + return match ? decodeURIComponent(match[1]) : ''; + } + + function syncTabHash(tabName) { + if (!tabName || !window.history || !window.history.replaceState) return; + var next = '#tab=' + encodeURIComponent(tabName); + if (window.location.hash !== next) window.history.replaceState(null, '', next); + } + + function navigateToHash(attemptsLeft) { + var tabName = hashTabName(); + if (!tabName) { + updateGuide(activeTabName()); + return; + } + var tab = document.querySelector('#adminMenu .tab[data-tab="' + tabName + '"]'); + if (tab && typeof tab.onclick === 'function') { + if (!tab.classList.contains('active')) tab.click(); + else updateGuide(tabName); + return; + } + if (attemptsLeft > 0) { + window.setTimeout(function () { navigateToHash(attemptsLeft - 1); }, 80); + } else { + updateGuide(activeTabName()); + } + } + + function closeConfirm() { + if (!confirmMask) return; + confirmMask.classList.remove('show'); + confirmMask.setAttribute('aria-hidden', 'true'); + document.body.classList.remove('admin-confirm-open'); + var focus = previousFocus; + pendingConfirmButton = null; + previousFocus = null; + if (focus && focus.isConnected) focus.focus(); + } + + function openConfirm(button) { + if (!confirmMask || !confirmMessage || !confirmAccept) return; + pendingConfirmButton = button; + previousFocus = document.activeElement; + var customMessage = cleanText(button.dataset.confirmMessage); + var label = cleanText(button.getAttribute('aria-label') || button.textContent || '删除'); + var subject = cleanText(button.dataset.name || button.dataset.value || button.dataset.shopManageName || button.dataset.shopName || button.dataset.ziniaoAccountName || ''); + var country = cleanText(button.dataset.country || ''); + if (!customMessage && country && subject) subject += '(' + country + ')'; + if (!customMessage && subject) customMessage = (button.classList.contains('btn-danger') ? '确认删除“' : '确认执行“') + subject + '”吗?此操作可能影响已有数据。'; + if (confirmTitle) confirmTitle.textContent = button.classList.contains('btn-danger') ? '删除前确认' : '请确认操作'; + confirmMessage.textContent = customMessage || ('确认执行“' + label + '”吗?此操作可能影响已有数据。'); + confirmAccept.textContent = button.dataset.confirmActionLabel || (button.classList.contains('btn-danger') ? '确认删除' : '确认操作'); + confirmMask.classList.add('show'); + confirmMask.setAttribute('aria-hidden', 'false'); + document.body.classList.add('admin-confirm-open'); + window.setTimeout(function () { confirmAccept.focus(); }, 0); + } + + function keepConfirmFocus(event) { + if (!confirmMask || !confirmMask.classList.contains('show') || event.key !== 'Tab') return; + var focusable = Array.prototype.filter.call(confirmMask.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'), function (el) { + return !el.disabled && el.offsetParent !== null; + }); + if (!focusable.length) return; + var first = focusable[0]; + var last = focusable[focusable.length - 1]; + if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } + else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } + } + if (confirmCancel) confirmCancel.addEventListener('click', closeConfirm); + if (confirmMask) confirmMask.addEventListener('click', function (event) { + if (event.target === confirmMask) closeConfirm(); + }); + if (confirmAccept) confirmAccept.addEventListener('click', function () { + var target = pendingConfirmButton; + closeConfirm(); + if (!target) return; + window.__adminConfirmBypass = true; + try { target.click(); } + finally { window.setTimeout(function () { window.__adminConfirmBypass = false; }, 0); } + }); + document.addEventListener('keydown', function (event) { + if (event.key === 'Enter' && event.target && event.target.matches && event.target.matches('input:not([type="file"]), select') && !event.target.closest('textarea')) { + var searchScope = event.target.closest('.form-row, .form-box'); + var searchButton = searchScope && searchScope.querySelector('button[id^="btnSearch"], button[id*="Search"]'); + if (searchButton && !searchButton.disabled) { + event.preventDefault(); + searchButton.click(); + return; + } + } + if (event.key === 'Escape' && confirmMask && confirmMask.classList.contains('show')) { + event.preventDefault(); + closeConfirm(); + return; + } + keepConfirmFocus(event); + }); + + var nativeAlert = window.alert ? window.alert.bind(window) : null; + window.alert = function (message) { + showToast(message, /失败|错误|无权|不能为空|不正确|异常/.test(String(message || '')) ? 'error' : 'info'); + }; + var nativeConfirm = window.confirm ? window.confirm.bind(window) : null; + window.confirm = function (message) { + if (window.__adminConfirmBypass) return true; + return nativeConfirm ? nativeConfirm(message) : false; + }; + + function updatePageBusy() { + if (!mainContent) return; + mainContent.setAttribute('aria-busy', activeFetches || activeXhrs ? 'true' : 'false'); + } + + function startBusy() { + var button = window.__adminLastActionButton; + if (!button || !button.isConnected || button.disabled || button.classList.contains('tab') || button.classList.contains('menu-group-title') || button === confirmAccept) return; + busyButton = button; + busyWasDisabled = button.disabled; + button.classList.add('is-busy'); + button.setAttribute('aria-busy', 'true'); + button.disabled = true; + } + + function finishBusy() { + var finishedButton = busyButton; + if (finishedButton && finishedButton.isConnected) { + finishedButton.classList.remove('is-busy'); + finishedButton.removeAttribute('aria-busy'); + if (!busyWasDisabled) finishedButton.disabled = false; + } + if (window.__adminLastActionButton === finishedButton) window.__adminLastActionButton = null; + busyButton = null; + busyWasDisabled = false; + } + + if (window.fetch) { + var nativeFetch = window.fetch.bind(window); + window.fetch = function () { + activeFetches += 1; + updatePageBusy(); + if (activeFetches === 1) startBusy(); + var request; + try { request = nativeFetch.apply(window, arguments); } + catch (error) { activeFetches = Math.max(0, activeFetches - 1); if (!activeFetches && !activeXhrs) finishBusy(); throw error; } + return Promise.resolve(request).finally(function () { + activeFetches = Math.max(0, activeFetches - 1); + updatePageBusy(); + if (!activeFetches && !activeXhrs) finishBusy(); + }); + }; + } + + if (window.XMLHttpRequest) { + var nativeSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.send = function () { + activeXhrs += 1; + updatePageBusy(); + if (activeXhrs === 1) startBusy(); + this.addEventListener('loadend', function () { + activeXhrs = Math.max(0, activeXhrs - 1); + updatePageBusy(); + if (!activeFetches && !activeXhrs) finishBusy(); + }, { once: true }); + try { return nativeSend.apply(this, arguments); } + catch (error) { activeXhrs = Math.max(0, activeXhrs - 1); if (!activeFetches && !activeXhrs) finishBusy(); throw error; } + }; + } + + function addButtonHint(button) { + if (!button || button.title) return; + var label = cleanText(button.textContent); + if (label === '编辑') button.title = '编辑当前记录'; + else if (label === '删除') button.title = '删除当前记录,需二次确认'; + else if (label === '查询') button.title = '按当前筛选条件查询'; + else if (/^导出/.test(label)) button.title = '导出当前筛选结果'; + else if (/^上传并/.test(label)) button.title = '上传文件并执行相应操作'; + else if (label === '管理分组') button.title = '新增、编辑或删除分组'; + else if (label === '选择店铺') button.title = '从店铺列表选择并回填'; + } + + function enhance(root) { + var scope = root && root.querySelectorAll ? root : document; + scope.querySelectorAll('button').forEach(addButtonHint); + scope.querySelectorAll('.table-ellipsis').forEach(function (element) { + if (!element.title) element.title = cleanText(element.textContent); + }); + scope.querySelectorAll('.empty-tip').forEach(function (element) { element.setAttribute('role', 'status'); }); + scope.querySelectorAll('.msg').forEach(function (element) { + var value = cleanText(element.textContent); + if (!value || (!element.classList.contains('ok') && !element.classList.contains('err'))) return; + var key = value + '|' + element.className; + if (element.dataset.adminToastKey === key) return; + element.dataset.adminToastKey = key; + showToast(value, element.classList.contains('err') ? 'error' : 'success'); + }); + } + + document.addEventListener('click', function (event) { + var button = event.target && event.target.closest ? event.target.closest('button') : null; + if (!button || button.disabled) return; + if (button.classList.contains('tab') || button.classList.contains('menu-group-title')) { + window.__adminLastActionButton = null; + } else if (button !== confirmAccept) { + window.__adminLastActionButton = button; + } + if (button.classList.contains('tab')) { + window.setTimeout(function () { + var tabName = activeTabName(); + updateGuide(tabName); + syncTabHash(tabName); + }, 0); + } + if ((!button.matches('.btn-danger') && !button.hasAttribute('data-admin-confirm')) || button === confirmAccept || window.__adminConfirmBypass) return; + event.preventDefault(); + event.stopImmediatePropagation(); + openConfirm(button); + }, true); + + var observer = new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + enhance(mutation.target && mutation.target.nodeType === 1 ? mutation.target : document); + if (mutation.type === 'attributes' && mutation.target.matches && mutation.target.matches('#adminMenu .tab')) { + window.setTimeout(function () { updateGuide(activeTabName()); }, 0); + } + }); + }); + enhance(document); + observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class'] }); + + window.addEventListener('hashchange', function () { navigateToHash(0); }); + navigateToHash(25); +})(); diff --git a/backend-java/src/main/resources/static/static/admin.js b/backend-java/src/main/resources/static/static/admin.js new file mode 100644 index 00000000..1149d199 --- /dev/null +++ b/backend-java/src/main/resources/static/static/admin.js @@ -0,0 +1,7145 @@ +(function () { + // 全局请求动画:拦截本页所有 fetch 和 XMLHttpRequest 请求。 + (function installRequestLoadingInterceptor() { + if (window.__adminRequestLoadingInstalled) return; + window.__adminRequestLoadingInstalled = true; + var activeRequests = 0; + var showTimer = null; + var hideTimer = null; + var showDelayMs = 180; + var loadingEl = document.getElementById('requestLoading'); + var loadingBarEl = document.getElementById('requestLoadingBar'); + + function showRequestLoading() { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + if (!loadingEl || !loadingBarEl) return; + loadingBarEl.classList.remove('finishing'); + loadingBarEl.classList.add('show'); + loadingEl.classList.add('show'); + } + + function hideRequestLoading() { + if (showTimer) { + clearTimeout(showTimer); + showTimer = null; + } + if (!loadingEl || !loadingBarEl) return; + loadingBarEl.classList.remove('show'); + loadingBarEl.classList.add('finishing'); + loadingEl.classList.remove('show'); + hideTimer = setTimeout(function () { + loadingBarEl.classList.remove('finishing'); + }, 220); + } + + function beginRequest() { + activeRequests += 1; + if (activeRequests === 1) { + showTimer = setTimeout(showRequestLoading, showDelayMs); + } + } + + function endRequest() { + activeRequests = Math.max(0, activeRequests - 1); + if (activeRequests === 0) { + hideRequestLoading(); + } + } + + // Java 统一响应体 ApiResponse{success,message,data,code} 转成旧前端形状: + // 成功 -> {success:true, ...data};失败 -> error=message;code 401 跳登录页。 + function normalizeJavaResponseBody(body) { + if (!body || typeof body !== 'object') return body; + if (body.success === false) { + if (!('error' in body) && body.message) body.error = body.message; + if (body.code === 401 && window.location.pathname.indexOf('/login') !== 0) { + var now = Date.now(); + if (!normalizeJavaResponseBody.__last401 || now - normalizeJavaResponseBody.__last401 > 1500) { + normalizeJavaResponseBody.__last401 = now; + window.location.replace('/login'); + } + } + return body; + } + if (body.success === true && body.data && typeof body.data === 'object') { + // 保留 data 键兼容 res.data.xxx 读取,同时把对象 data 展开到顶层兼容 res.xxx; + // 数组 data 原样保留在 res.data(不展开)。 + var merged = {}; + Object.keys(body).forEach(function (k) { merged[k] = body[k]; }); + if (!Array.isArray(body.data)) { + Object.keys(body.data).forEach(function (k) { merged[k] = body.data[k]; }); + } + return merged; + } + return body; + } + + // 后台历史 URL 兼容映射:把 Flask 时代的别名/单复数字段统一到 Java 规范路径, + // GET 查询参数原样透传。命中规则外的 /api/admin 路径保持不动。 + function adminApiUrlRewrite(input) { + if (typeof input !== 'string') return input; + if (input.indexOf('/api/admin') !== 0) return input; + var qIndex = input.indexOf('?'); + var path = qIndex >= 0 ? input.slice(0, qIndex) : input; + var suffix = qIndex >= 0 ? input.slice(qIndex) : ''; + var rest = path.slice('/api/admin'.length); + var out = null; + function map(root, tail) { out = '/api/admin' + root + tail; } + if (rest === '/column/reorder') { + // 已在 Java 原生提供 + } else if (rest === '/columns' || rest === '/column') { + map('/permission-menus', ''); + } else if (rest.indexOf('/column/') === 0) { + map('/permission-menus', rest.slice('/column'.length)); + } else if (/^\/user\/[^/]+\/columns$/.test(rest)) { + map('/permission-users', rest.slice('/user'.length)); + } else if (/^\/user\/[^/]+\/column-permissions$/.test(rest)) { + map('/permission-users', rest.slice('/user'.length)); + } else if (rest.indexOf('/digital-human-versions') === 0) { + out = '/api/digital-human/versions' + rest.slice('/digital-human-versions'.length); + } else if (rest.indexOf('/shop-manage-groups') === 0) { + map('/shop-manages/groups', rest.slice('/shop-manage-groups'.length)); + } else if (rest.indexOf('/shop-manage-group') === 0) { + map('/shop-manages/groups', rest.slice('/shop-manage-group'.length)); + } else if (rest === '/shop-data-crawl-tasks' + || rest.indexOf('/shop-data-crawl-tasks/download-zip') === 0) { + // Java 已同路径提供 + } else if (/^\/shop-data-crawl-tasks\/[^/]+\/download$/.test(rest)) { + map('/shop-data-crawl/results', rest.slice('/shop-data-crawl-tasks'.length)); + } else if (/^\/shop-data-crawl-tasks\/[^/]+$/.test(rest)) { + map('/shop-data-crawl/history', rest.slice('/shop-data-crawl-tasks'.length)); + } else if (rest === '/shop-key' || rest.indexOf('/shop-key/') === 0) { + map('/shop-keys', rest.slice('/shop-key'.length)); + } else if (rest === '/shop-manage' || rest.indexOf('/shop-manage/') === 0) { + map('/shop-manages', rest.slice('/shop-manage'.length)); + } else if (/^\/skip-price-asin($|\/)/.test(rest)) { + map('/skip-price-asins', rest.slice('/skip-price-asin'.length).replace('/country/', '/countries/')); + } else if (/^\/query-asin($|\/)/.test(rest)) { + map('/query-asins', rest.slice('/query-asin'.length).replace('/country/', '/countries/')); + } else { + return input; + } + if (out === null) return input; + return out + suffix; + } + + if (window.fetch) { + var nativeFetch = window.fetch.bind(window); + window.fetch = function (input, init) { + var options = init || {}; + var skipLoading = !!options.__skipLoading; + if (skipLoading) { + options = Object.assign({}, options); + delete options.__skipLoading; + } else { + beginRequest(); + } + try { + var urlInput = (typeof input === 'string') ? adminApiUrlRewrite(input) : input; + var pending = nativeFetch(urlInput, options).then(function (resp) { + if (!resp || !resp.headers) return resp; + var contentType = String(resp.headers.get('content-type') || ''); + if (contentType.indexOf('json') < 0) return resp; + var nativeJson = resp.json.bind(resp); + resp.json = function () { + return nativeJson().then(normalizeJavaResponseBody); + }; + return resp; + }); + return pending.finally(function () { + if (!skipLoading) endRequest(); + }); + } catch (err) { + if (!skipLoading) endRequest(); + throw err; + } + }; + } + + if (window.XMLHttpRequest) { + var nativeOpen = XMLHttpRequest.prototype.open; + var nativeSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function () { + var args = Array.prototype.slice.call(arguments); + if (typeof args[1] === 'string' && args[1].indexOf('/api/admin') === 0) { + args[1] = adminApiUrlRewrite(args[1]); + } + this.__adminSkipLoading = false; + return nativeOpen.apply(this, args); + }; + XMLHttpRequest.prototype.send = function () { + if (!this.__adminSkipLoading) { + beginRequest(); + this.addEventListener('loadend', endRequest, { once: true }); + } + try { + return nativeSend.apply(this, arguments); + } catch (err) { + if (!this.__adminSkipLoading) endRequest(); + throw err; + } + }; + } + })(); + + // ===== 菜单分组(一级分类 + 二级菜单)===== + var ADMIN_MENU_GROUPS = [ + { key: 'account', title: '账号与权限', items: ['users', 'columns', 'group-manage'] }, + { key: 'data', title: '数据管理', items: ['dedupe-total-data', 'invalid-asin-data', 'query-asin', 'product-categories'] }, + { key: 'shop', title: '店铺管理', items: ['shop-keys', 'shop-manage', 'skip-price-asin', 'shop-data-crawl-tasks', 'shop-data-duplicate-check'] }, + { key: 'record', title: '记录与版本', items: ['history', 'version', 'digital-human-version', 'image-video-tasks'] } + ]; + var ADMIN_MENU_ICONS = { + 'users': '', + 'group-manage': '', + 'columns': '', + 'dedupe-total-data': '', + 'invalid-asin-data': '', + 'shop-keys': '', + 'shop-manage': '', + 'skip-price-asin': '', + 'query-asin': '', + 'product-categories': '', + 'image-video-tasks': '', + 'shop-data-crawl-tasks': '', + 'shop-data-duplicate-check': '', + 'history': '', + 'version': '', + 'digital-human-version': '' + }; + var ADMIN_MENU_FALLBACK_ICON = ''; + function adminMenuIcon(route) { + return ADMIN_MENU_ICONS[route] || ADMIN_MENU_FALLBACK_ICON; + } + + // Tab 切换 + var adminMenuEl = document.getElementById('adminMenu'); + var activeAdminTabName = ''; + var adminMenuByRoute = {}; + var ADMIN_PANEL_MAP = { + 'users': 'panel-users', + 'columns': 'panel-columns', + 'group-manage': 'panel-group-manage', + 'dedupe-total-data': 'panel-dedupe-total-data', + 'invalid-asin-data': 'panel-invalid-asin-data', + 'shop-keys': 'panel-shop-keys', + 'shop-manage': 'panel-shop-manage', + 'skip-price-asin': 'panel-skip-price-asin', + 'query-asin': 'panel-query-asin', + 'product-categories': 'panel-product-categories', + 'image-video-tasks': 'panel-image-video-tasks', + 'shop-data-crawl-tasks': 'panel-shop-data-crawl-tasks', + 'shop-data-duplicate-check': 'panel-shop-data-duplicate-check', + 'history': 'panel-history', + 'version': 'panel-version', + 'digital-human-version': 'panel-digital-human-version' + }; + function runTabLoader(tabName) { + if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); } + else if (tabName === 'columns') loadColumns(); + else if (tabName === 'group-manage') initShopManageGroupPanel(); + else if (tabName === 'dedupe-total-data') { + loadDedupeTotalData(1); + loadDedupeGroupSummary(); + } + else if (tabName === 'invalid-asin-data') { + loadShopManageGroups(); + loadInvalidAsinData(1); + } + else if (tabName === 'shop-keys') loadShopKeys(1); + else if (tabName === 'shop-manage') loadShopManage(1); + else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1); + else if (tabName === 'query-asin') loadQueryAsin(1); + else if (tabName === 'product-categories') loadProductCategories(); + else if (tabName === 'image-video-tasks') loadImageVideoTasks(1); + else if (tabName === 'shop-data-crawl-tasks') loadShopDataCrawlTasks(1); + else if (tabName === 'shop-data-duplicate-check') loadShopDataDuplicateCheckOverview(); + else if (tabName === 'history') loadHistory(1); + else if (tabName === 'version') loadSoftwareVersions(); + else if (tabName === 'digital-human-version') loadDigitalHumanVersions(); + } + function hideAllAdminPanels() { + document.querySelectorAll('.tab-panel').forEach(function (panel) { + panel.classList.remove('active'); + panel.style.display = 'none'; + }); + } + function getActiveAdminTabName() { + return activeAdminTabName; + } + function activateAdminTab(tabName) { + var panelId = ADMIN_PANEL_MAP[tabName]; + var panel = panelId ? document.getElementById(panelId) : null; + if (!panel) return; + activeAdminTabName = tabName; + document.querySelectorAll('#adminMenu .tab').forEach(function (tab) { + var isActive = tab.dataset.tab === tabName; + tab.classList.toggle('active', isActive); + if (isActive) tab.setAttribute('aria-current', 'page'); + else tab.removeAttribute('aria-current'); + }); + // 激活页面时自动展开所属分组(折叠由用户显式控制) + if (adminMenuEl) { + adminMenuEl.querySelectorAll('.menu-group').forEach(function (group) { + if (group.querySelector('.tab[data-tab="' + tabName + '"]')) { + group.classList.remove('is-collapsed'); + var groupTitle = group.querySelector('.menu-group-title'); + if (groupTitle) groupTitle.setAttribute('aria-expanded', 'true'); + } + }); + } + hideAllAdminPanels(); + panel.style.display = ''; + panel.classList.add('active'); + var nameItem = adminMenuByRoute[tabName]; + var titleEl = document.getElementById('adminPageTitle'); + if (titleEl) titleEl.textContent = (nameItem && nameItem.name) || tabName; + runTabLoader(tabName); + } + function renderAdminTabs(items) { + var knownItems = (items || []).filter(function (item) { + return !!ADMIN_PANEL_MAP[item.route_path]; + }); + if (!knownItems.length) { + activeAdminTabName = ''; + if (adminMenuEl) adminMenuEl.innerHTML = ''; + hideAllAdminPanels(); + return; + } + adminMenuByRoute = {}; + knownItems.forEach(function (item) { + adminMenuByRoute[item.route_path] = item; + }); + var groupsHtml = ''; + ADMIN_MENU_GROUPS.forEach(function (group) { + var groupRoutes = group.items.filter(function (route) { + return !!adminMenuByRoute[route]; + }); + if (!groupRoutes.length) return; + var itemsHtml = groupRoutes.map(function (route) { + var item = adminMenuByRoute[route]; + var label = (item.name || route).replace(/"/g, '"'); + return ''; + }).join(''); + groupsHtml += ''; + }); + if (!groupsHtml) { + activeAdminTabName = ''; + if (adminMenuEl) adminMenuEl.innerHTML = ''; + hideAllAdminPanels(); + return; + } + if (adminMenuEl) adminMenuEl.innerHTML = groupsHtml; + document.querySelectorAll('#adminMenu .tab').forEach(function (tab) { + tab.onclick = function () { + activateAdminTab(tab.dataset.tab); + }; + }); + if (window.__initAdminMenuCollapse) window.__initAdminMenuCollapse(); + var fallbackTab = activeAdminTabName && knownItems.some(function (item) { return item.route_path === activeAdminTabName; }) + ? activeAdminTabName + : knownItems[0].route_path; + activateAdminTab(fallbackTab); + } + function syncAdminMenuGroups() { + if (!adminMenuEl) return; + adminMenuEl.querySelectorAll('.menu-group').forEach(function (group) { + var visible = false; + group.querySelectorAll('.tab').forEach(function (tab) { + if (tab.style.display !== 'none') visible = true; + }); + group.style.display = visible ? '' : 'none'; + }); + } + function bindAdminMenuTabs(serverRendered) { + adminMenuByRoute = {}; + var routes = []; + adminMenuEl.querySelectorAll('#adminMenu .tab').forEach(function (tab) { + var route = tab.dataset.tab; + if (!route) return; + routes.push(route); + if (!adminMenuByRoute[route]) { + adminMenuByRoute[route] = { route_path: route, name: tab.getAttribute('title') || route }; + } + tab.onclick = function () { + activateAdminTab(tab.dataset.tab); + }; + }); + return routes; + } + function loadAdminMenus(preferredTab) { + if (preferredTab) activeAdminTabName = preferredTab; + // 服务端已按权限渲染菜单(data-server-rendered=true):直接绑定事件并激活, + // 不再请求接口重渲染,避免"全量菜单→折叠"的闪烁。 + if (!adminMenuEl || adminMenuEl.getAttribute('data-server-rendered') !== 'true') { + renderAdminTabsFromApi(); + return; + } + var serverRoutes = bindAdminMenuTabs(true); + if (!serverRoutes.length) { + activeAdminTabName = ''; + hideAllAdminPanels(); + return; + } + if (window.__initAdminMenuCollapse) window.__initAdminMenuCollapse(); + var fallbackTab = activeAdminTabName && serverRoutes.indexOf(activeAdminTabName) >= 0 + ? activeAdminTabName + : serverRoutes[0]; + activateAdminTab(fallbackTab); + } + function renderAdminTabsFromApi() { + fetch('/api/admin/current-user/menus') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + if (adminMenuEl) adminMenuEl.innerHTML = ''; + hideAllAdminPanels(); + return; + } + renderAdminTabs(res.items || []); + }) + .catch(function () { + if (adminMenuEl) adminMenuEl.innerHTML = ''; + hideAllAdminPanels(); + }); + } + hideAllAdminPanels(); + + // ========== 用户管理 ========== + var userPage = 1, userPageSize = 15; + var currentUserId = null; + var currentUserRole = 'admin'; + var currentUserUsername = ''; + var adminsList = []; + function roleLabel(role) { + if (role === 'super_admin') return '超级管理员'; + if (role === 'admin') return '管理员'; + return '普通账号'; + } + function buildUserListQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + userPageSize; + var kw = (document.getElementById('searchUsername').value || '').trim(); + if (kw) q += '&username=' + encodeURIComponent(kw); + var cby = document.getElementById('filterCreatedBy').value; + if (cby) q += '&created_by_id=' + encodeURIComponent(cby); + return q; + } + function loadUsers(page) { + userPage = page || 1; + fetch('/api/admin/users?' + buildUserListQuery(userPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('userListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + currentUserId = res.current_user_id || null; + currentUserRole = res.current_user_role || 'admin'; + currentUserUsername = (res.current_user_username || '').trim(); + adminsList = res.admins || []; + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无用户'; + } else { + tbody.innerHTML = items.map(function (u) { + return '' + u.id + '' + (u.username || '') + '' + + roleLabel(u.role || 'normal') + '' + (u.creator_username || '-') + '' + (u.created_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers); + bindUserActions(); + updateCreateFormByRole(); + updateUserFilterByRole(); + }) + .catch(function () { + document.getElementById('userListBody').innerHTML = '请求失败'; + }); + } + function updateUserFilterByRole() { + var grp = document.getElementById('filterCreatedByGroup'); + var sel = document.getElementById('filterCreatedBy'); + if (currentUserRole === 'super_admin') { + grp.style.display = 'block'; + var cur = sel.value; + sel.innerHTML = ''; + adminsList.forEach(function (a) { + var opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.username; + sel.appendChild(opt); + }); + sel.value = cur || ''; + } else { + grp.style.display = 'none'; + } + } + + function updateDedupeTotalDataAccess() { + var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]'); + var panel = document.getElementById('panel-dedupe-total-data'); + var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === ''); + if (tab) tab.style.display = canUse ? '' : 'none'; + if (panel) panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab && tab.classList.contains('active')) { + tab.classList.remove('active'); + + tab.removeAttribute('aria-current'); + if (panel) panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) { usersTab.classList.add('active'); usersTab.setAttribute('aria-current', 'page'); } + if (usersPanel) usersPanel.classList.add('active'); + } + syncAdminMenuGroups(); + } + function updateShopManageAccess() { + var tab = document.querySelector('.tab[data-tab="shop-manage"]'); + var panel = document.getElementById('panel-shop-manage'); + if (!tab || !panel) return; + var canUse = currentUserRole === 'super_admin' || + !!currentUserAdminPermissionKeys['admin_shop_manage'] || + !!currentUserAdminPermissionRoutes['shop-manage']; + tab.style.display = canUse ? '' : 'none'; + panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab.classList.contains('active')) { + tab.classList.remove('active'); + + tab.removeAttribute('aria-current'); + panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) { usersTab.classList.add('active'); usersTab.setAttribute('aria-current', 'page'); } + if (usersPanel) usersPanel.classList.add('active'); + } + syncAdminMenuGroups(); + } + function loadCurrentUserAdminPermissions() { + currentUserAdminPermissionKeys = {}; + currentUserAdminPermissionRoutes = {}; + if (!currentUserId || currentUserRole === 'super_admin') { + updateShopManageAccess(); + return; + } + fetch('/api/admin/user/' + currentUserId + '/column-permissions') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + updateShopManageAccess(); + return; + } + (res.items || []).forEach(function (item) { + var columnKey = (item.column_key || '').trim(); + var routePath = (item.route_path || '').trim(); + if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; + if (routePath) currentUserAdminPermissionRoutes[routePath] = true; + }); + updateShopManageAccess(); + }) + .catch(function () { + updateShopManageAccess(); + }); + } + function applyTabAccess(tabName, canUse) { + var tab = document.querySelector('.tab[data-tab="' + tabName + '"]'); + var panel = document.getElementById('panel-' + tabName); + if (tab) tab.style.display = canUse ? '' : 'none'; + if (panel) panel.style.display = canUse ? '' : 'none'; + if (!canUse && tab && tab.classList.contains('active')) { + tab.classList.remove('active'); + + tab.removeAttribute('aria-current'); + if (panel) panel.classList.remove('active'); + var usersTab = document.querySelector('.tab[data-tab="users"]'); + var usersPanel = document.getElementById('panel-users'); + if (usersTab) { usersTab.classList.add('active'); usersTab.setAttribute('aria-current', 'page'); } + if (usersPanel) usersPanel.classList.add('active'); + } + syncAdminMenuGroups(); + } + function hasAdminTabAccess(tabName) { + var config = ADMIN_TAB_ACCESS_CONFIG[tabName]; + if (!config) return true; + if (currentUserRole === 'super_admin') return true; + if (config.superAdminOnly) return false; + return !!currentUserAdminPermissionKeys[config.columnKey] || + !!currentUserAdminPermissionRoutes[config.routePath]; + } + function refreshAdminTabAccess() { + Object.keys(ADMIN_TAB_ACCESS_CONFIG).forEach(function (tabName) { + applyTabAccess(tabName, hasAdminTabAccess(tabName)); + }); + } + function loadCurrentUserAdminPermissions() { + currentUserAdminPermissionKeys = {}; + currentUserAdminPermissionRoutes = {}; + if (!currentUserId || currentUserRole === 'super_admin') { + refreshAdminTabAccess(); + return; + } + fetch('/api/admin/user/' + currentUserId + '/column-permissions') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + refreshAdminTabAccess(); + return; + } + (res.items || []).forEach(function (item) { + var columnKey = (item.column_key || '').trim(); + var routePath = (item.route_path || '').trim(); + if (columnKey) currentUserAdminPermissionKeys[columnKey] = true; + if (routePath) currentUserAdminPermissionRoutes[routePath] = true; + }); + refreshAdminTabAccess(); + }) + .catch(function () { + refreshAdminTabAccess(); + }); + } + var allColumnsList = []; + var columnPermissionCatalogReady = false; + var columnPermissionCatalogState = 'idle'; // idle | loading | ready | failed + var columnPermissionCatalogError = ''; + var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' }; + function fetchCatalogWithRetry(url, attempts) { + return fetch(url).then(function (r) { + if (!r.ok) throw new Error('请求失败(' + r.status + ')'); + return r.json(); + }).catch(function (error) { + if (attempts <= 1) throw error; + return new Promise(function (resolve) { setTimeout(resolve, 700); }) + .then(function () { return fetchCatalogWithRetry(url, attempts - 1); }); + }); + } + function renderPermissionCatalogStatus() { + var wrap = document.getElementById('createColumnPermissionWrap'); + if (!wrap) return; + if (columnPermissionCatalogState === 'loading') { + wrap.textContent = '菜单权限加载中…'; + } else if (columnPermissionCatalogState === 'failed') { + wrap.innerHTML = ''; + var tip = document.createElement('div'); + tip.style.color = '#e5484d'; + tip.style.fontSize = '13px'; + tip.style.padding = '8px 0'; + tip.textContent = '菜单权限加载失败' + + (columnPermissionCatalogError ? '(' + columnPermissionCatalogError + ')' : '') + + ':'; + var retryBtn = document.createElement('button'); + retryBtn.type = 'button'; + retryBtn.className = 'col-perm-retry'; + retryBtn.textContent = '重新加载'; + retryBtn.style.marginLeft = '6px'; + retryBtn.style.padding = '2px 10px'; + retryBtn.onclick = loadColumnsForPermission; + tip.appendChild(retryBtn); + wrap.appendChild(tip); + } + } + // Hierarchical permission editor: only direct IDs are submitted; descendants are visual inheritance. + function columnParentId(item) { + var value = item && item.parent_id != null ? item.parent_id : item && item.parentId; + var parsed = value == null || value === '' ? null : Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; + } + function columnId(item) { return Number(item && item.id); } + function columnMenuType(item) { + return String(item && item.menu_type || 'app').toLowerCase() === 'admin' ? 'admin' : 'app'; + } + // 数据层一级分组(不映射真实页面),只用于权限树层级与「上级菜单」候选。 + function isAdminMenuGroup(item) { + return String(item && item.column_key || '').indexOf('admin_group_') === 0; + } + function columnDescendantIds(id) { + var result = [], pending = [Number(id)]; + while (pending.length) { + var parent = pending.shift(); + allColumnsList.forEach(function (item) { + if (columnParentId(item) !== parent) return; + var childId = columnId(item); + if (result.indexOf(childId) < 0) { + result.push(childId); + pending.push(childId); + } + }); + } + return result; + } + function columnTreeHasDirectDescendant(id, directIds) { + return columnDescendantIds(id).some(function (descendantId) { return !!directIds[descendantId]; }); + } + function columnTreeFullyCovered(id, directIds) { + if (directIds[id]) return true; + var children = allColumnsList.filter(function (item) { return columnParentId(item) === Number(id); }); + return children.length > 0 && children.every(function (child) { + return columnTreeFullyCovered(columnId(child), directIds); + }); + } + var columnPermissionCatalogPending = null; + function loadColumnsForPermission() { + if (columnPermissionCatalogPending) return columnPermissionCatalogPending; + columnPermissionCatalogReady = false; + columnPermissionCatalogState = 'loading'; + columnPermissionCatalogError = ''; + renderPermissionCatalogStatus(); + var ownedKeys = {}, ownedRoutes = {}; + var ownPermissionRequest = (currentUserRole === 'admin' && currentUserId) + ? fetchCatalogWithRetry('/api/admin/user/' + currentUserId + '/column-permissions', 3) + .then(function (res) { + if (!res.success) throw new Error(res.error || '当前用户权限加载失败'); + (res.items || []).forEach(function (item) { + if (item.column_key) ownedKeys[String(item.column_key).trim()] = true; + if (item.route_path) ownedRoutes[String(item.route_path).trim()] = true; + }); + }) + : Promise.resolve(); + var catalogChain = ownPermissionRequest.then(function () { return fetchCatalogWithRetry('/api/admin/columns', 3); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '菜单列表加载失败'); + var availableColumns = (res.items || []).filter(function (item) { + if (item.column_key === 'admin_image_video_task_data' || + item.column_key === 'admin_shop_data_crawl_task_data') return false; + return true; + }); + if (currentUserRole === 'admin') { + var columnsById = {}, grantableIds = {}, visibleIds = {}; + availableColumns.forEach(function (item) { + var id = columnId(item); + columnsById[id] = item; + if (ownedKeys[String(item.column_key || '').trim()] || ownedRoutes[String(item.route_path || '').trim()]) { + grantableIds[id] = true; + } + }); + Object.keys(grantableIds).forEach(function (id) { + var currentId = Number(id), visited = {}; + while (currentId > 0 && !visited[currentId] && columnsById[currentId]) { + visited[currentId] = true; + visibleIds[currentId] = true; + currentId = columnParentId(columnsById[currentId]); + } + }); + allColumnsList = availableColumns.filter(function (item) { + return !!visibleIds[columnId(item)]; + }).map(function (item) { + item._structureOnly = !grantableIds[columnId(item)]; + return item; + }); + } else { + allColumnsList = availableColumns.map(function (item) { + item._structureOnly = false; + return item; + }); + } + columnPermissionCatalogReady = true; + columnPermissionCatalogState = 'ready'; + renderColumnPermissionWrap('createColumnPermissionWrap'); + renderColumnPermissionWrap('editColumnPermissionWrap'); + populateColumnParentSelects(); + }).catch(function (error) { + columnPermissionCatalogReady = false; + columnPermissionCatalogState = 'failed'; + columnPermissionCatalogError = (error && error.message) ? error.message : ''; + allColumnsList = []; + var editPermissionWrap = document.getElementById('editColumnPermissionWrap'); + if (editPermissionWrap) editPermissionWrap._permissionsReady = false; + renderColumnPermissionWrap('createColumnPermissionWrap'); + renderColumnPermissionWrap('editColumnPermissionWrap'); + renderPermissionCatalogStatus(); + }).finally(function () { + columnPermissionCatalogPending = null; + }); + columnPermissionCatalogPending = catalogChain; + return catalogChain; + } + function renderColumnCards(wrapId) { + var wrap = document.getElementById(wrapId); + var cardsEl = document.getElementById(columnCardsContainerMap[wrapId]); + if (!wrap || !cardsEl) return; + var directIds = wrap._directIds || {}; + cardsEl.innerHTML = ''; + allColumnsList.forEach(function (item) { + var id = columnId(item); + if (!directIds[id] || item._structureOnly) return; + var card = document.createElement('span'); + card.className = 'column-permission-card'; + card.textContent = item.name || '未命名'; + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'col-card-remove'; + btn.setAttribute('aria-label', '移除'); + btn.textContent = '×'; + btn.onclick = function () { + delete directIds[id]; + renderColumnPermissionWrap(wrapId); + }; + card.appendChild(btn); + cardsEl.appendChild(card); + }); + } + function renderColumnPermissionWrap(wrapId) { + var wrap = document.getElementById(wrapId); + if (!wrap) return; + var directIds = wrap._directIds || {}; + var collapsedIds = wrap._collapsedIds || {}; + var collapsedMenuTypes = wrap._collapsedMenuTypes || {}; + var previousScrollTop = wrap.scrollTop; + wrap._directIds = directIds; + wrap._collapsedIds = collapsedIds; + wrap._collapsedMenuTypes = collapsedMenuTypes; + wrap.innerHTML = ''; + if (!allColumnsList.length) { + wrap.textContent = '暂无可分配菜单'; + renderColumnCards(wrapId); + return; + } + var byParent = {}; + allColumnsList.forEach(function (item) { + var parent = columnParentId(item); + var key = parent == null ? 'root' : String(parent); + (byParent[key] || (byParent[key] = [])).push(item); + }); + if (!wrap._collapseStateInitialized) { + allColumnsList.forEach(function (item) { + var id = columnId(item); + if ((byParent[String(id)] || []).length) collapsedIds[id] = true; + }); + wrap._collapseStateInitialized = true; + } + if (!wrap._menuTypeCollapseInitialized) { + collapsedMenuTypes.admin = true; + collapsedMenuTypes.app = true; + wrap._menuTypeCollapseInitialized = true; + } + function renderNode(item, depth, inherited) { + var id = columnId(item); + var itemMenuType = columnMenuType(item); + var children = (byParent[String(id)] || []).filter(function (child) { + return columnMenuType(child) === itemMenuType; + }); + var structureOnly = item._structureOnly === true; + var effective = inherited || !!directIds[id]; + var covered = effective || columnTreeFullyCovered(id, directIds); + var node = document.createElement('div'); + node.className = 'column-tree-node'; + node.setAttribute('data-depth', String(depth)); + var row = document.createElement('div'); + row.className = 'column-tree-row'; + if (children.length) { + var toggle = document.createElement('button'); + var collapsed = !!collapsedIds[id]; + toggle.type = 'button'; + toggle.className = 'column-tree-toggle'; + toggle.setAttribute('aria-expanded', String(!collapsed)); + toggle.setAttribute('aria-label', (collapsed ? '展开' : '收起') + (item.name || '菜单')); + toggle.title = collapsed ? '展开子菜单' : '收起子菜单'; + toggle.onclick = function () { + if (collapsedIds[id]) delete collapsedIds[id]; + else collapsedIds[id] = true; + renderColumnPermissionWrap(wrapId); + }; + row.appendChild(toggle); + } else { + var spacer = document.createElement('span'); + spacer.className = 'column-tree-toggle-spacer'; + spacer.setAttribute('aria-hidden', 'true'); + row.appendChild(spacer); + } + var label = document.createElement('label'); + var checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = covered; + checkbox.indeterminate = !covered && columnTreeHasDirectDescendant(id, directIds); + checkbox.disabled = inherited || structureOnly; + checkbox.onchange = function () { + if (structureOnly) return; + if (checkbox.checked) { + directIds[id] = true; + columnDescendantIds(id).forEach(function (descendantId) { delete directIds[descendantId]; }); + } else { + delete directIds[id]; + columnDescendantIds(id).forEach(function (descendantId) { delete directIds[descendantId]; }); + } + renderColumnPermissionWrap(wrapId); + }; + var text = document.createElement('span'); + text.textContent = item.name || '未命名'; + if (inherited || structureOnly) text.className = 'inherited-label'; + label.appendChild(checkbox); + label.appendChild(text); + row.appendChild(label); + node.appendChild(row); + if (children.length) { + var childrenWrap = document.createElement('div'); + childrenWrap.className = 'column-tree-children'; + childrenWrap.hidden = !!collapsedIds[id]; + children.forEach(function (child) { + childrenWrap.appendChild(renderNode(child, depth + 1, effective)); + }); + node.appendChild(childrenWrap); + } + return node; + } + [ + { key: 'admin', label: '后台菜单' }, + { key: 'app', label: '软件菜单' } + ].forEach(function (group) { + var groupItems = allColumnsList.filter(function (item) { + return columnMenuType(item) === group.key; + }); + if (!groupItems.length) return; + var section = document.createElement('div'); + section.className = 'column-tree-group'; + var groupToggle = document.createElement('button'); + var groupCollapsed = !!collapsedMenuTypes[group.key]; + groupToggle.type = 'button'; + groupToggle.className = 'column-tree-group-toggle'; + groupToggle.setAttribute('aria-expanded', String(!groupCollapsed)); + groupToggle.setAttribute('aria-label', (groupCollapsed ? '展开' : '收起') + group.label); + var groupName = document.createElement('span'); + groupName.className = 'column-tree-group-name'; + groupName.textContent = group.label; + var groupCount = document.createElement('span'); + groupCount.className = 'column-tree-group-count'; + groupCount.textContent = groupItems.length + ' 项'; + groupToggle.appendChild(groupName); + groupToggle.appendChild(groupCount); + groupToggle.onclick = function () { + if (collapsedMenuTypes[group.key]) delete collapsedMenuTypes[group.key]; + else collapsedMenuTypes[group.key] = true; + renderColumnPermissionWrap(wrapId); + }; + section.appendChild(groupToggle); + var groupBody = document.createElement('div'); + groupBody.className = 'column-tree-group-body'; + groupBody.hidden = groupCollapsed; + groupItems.filter(function (item) { + var parentId = columnParentId(item); + if (parentId == null) return true; + var parent = allColumnsList.find(function (candidate) { + return columnId(candidate) === parentId; + }); + return !parent || columnMenuType(parent) !== group.key; + }).forEach(function (item) { + groupBody.appendChild(renderNode(item, 0, false)); + }); + section.appendChild(groupBody); + wrap.appendChild(section); + }); + renderColumnCards(wrapId); + wrap.scrollTop = previousScrollTop; + } + function getSelectedColumnIds(wrapId) { + var wrap = document.getElementById(wrapId); + return wrap && wrap._directIds ? Object.keys(wrap._directIds).map(Number).filter(function (id) { + var item = allColumnsList.find(function (column) { return columnId(column) === id; }); + return id > 0 && item && !item._structureOnly; + }) : []; + } + function setColumnPermissionCheckboxes(wrapId, columnIds) { + var wrap = document.getElementById(wrapId); + if (!wrap) return; + wrap._directIds = {}; + (columnIds || []).forEach(function (id) { + var parsed = Number(id); + if (Number.isFinite(parsed) && parsed > 0) wrap._directIds[parsed] = true; + }); + wrap._permissionsReady = true; + renderColumnPermissionWrap(wrapId); + } + function populateColumnParentSelects() { + ['columnParentId', 'editColumnParentId'].forEach(function (selectId) { + var select = document.getElementById(selectId); + if (!select) return; + var current = select.value; + var typeSelect = document.getElementById(selectId === 'editColumnParentId' ? 'editColumnMenuType' : 'columnMenuType'); + var menuType = typeSelect ? (typeSelect.value || 'app') : 'app'; + var editingId = selectId === 'editColumnParentId' ? Number(document.getElementById('editColumnId').value || 0) : 0; + var blockedIds = editingId > 0 ? [editingId].concat(columnDescendantIds(editingId)) : []; + select.innerHTML = ''; + allColumnsList.filter(function (item) { + // 只有一级分组可以作为上级菜单;分组行可能对管理员不可直接授予(structureOnly), + // 但作为父级仍然合法,因此不做 _structureOnly 过滤。 + return isAdminMenuGroup(item) + && String(item.menu_type || 'app') === String(menuType) + && blockedIds.indexOf(columnId(item)) < 0; + }).forEach(function (item) { + var option = document.createElement('option'); + option.value = item.id; + option.textContent = item.name || '未命名'; + select.appendChild(option); + }); + // 兜底:编辑旧菜单时当前父级可能不是分组行,原值保留在选项里避免误改层级。 + if (current && !Array.prototype.some.call(select.options, function (option) { + return option.value === String(current); + })) { + var legacy = allColumnsList.find(function (candidate) { return String(columnId(candidate)) === String(current); }); + var fallback = document.createElement('option'); + fallback.value = current; + fallback.textContent = (legacy && legacy.name ? legacy.name : '原上级菜单') + '(原上级)'; + select.appendChild(fallback); + } + select.value = current; + }); + } + function updateCreateFormByRole() { + var roleSel = document.getElementById('createRole'); + var optAdmin = document.getElementById('optAdmin'); + var formCreatedBy = document.getElementById('formGroupCreatedBy'); + var selCreatedBy = document.getElementById('createCreatedBy'); + if (currentUserRole === 'super_admin') { + if (optAdmin) optAdmin.style.display = ''; + formCreatedBy.style.display = (roleSel.value === 'normal') ? 'block' : 'none'; + selCreatedBy.innerHTML = ''; + adminsList.forEach(function (a) { + var opt = document.createElement('option'); + opt.value = a.id; + opt.textContent = a.username; + selCreatedBy.appendChild(opt); + }); + } else { + if (optAdmin) optAdmin.style.display = 'none'; + roleSel.value = 'normal'; + formCreatedBy.style.display = 'none'; + } + } + function bindUserActions() { + document.querySelectorAll('[data-edit]').forEach(function (btn) { + btn.onclick = function () { + var raw = (btn.getAttribute('data-user') || '{}').replace(/"/g, '"'); + var u; + try { u = JSON.parse(raw); } catch (e) { u = {}; } + document.getElementById('editUserId').value = u.id || ''; + document.getElementById('editUsername').value = u.username || ''; + document.getElementById('editPassword').value = ''; + var editRole = document.getElementById('editRole'); + var editFormGroupRole = document.getElementById('editFormGroupRole'); + var editFormGroupCreator = document.getElementById('editFormGroupCreator'); + var editCreatorName = document.getElementById('editCreatorName'); + editFormGroupRole.style.display = (currentUserRole === 'super_admin' && u.role !== 'super_admin') ? 'block' : 'none'; + editFormGroupCreator.style.display = (u.role === 'normal' && u.creator_username) ? 'block' : 'none'; + editCreatorName.value = u.creator_username || ''; + if (u.role !== 'super_admin') { editRole.value = u.role || 'normal'; } + document.getElementById('msgEdit').textContent = ''; + var editPermissionWrap = document.getElementById('editColumnPermissionWrap'); + editPermissionWrap._directIds = {}; + editPermissionWrap._permissionsReady = false; + renderColumnPermissionWrap('editColumnPermissionWrap'); + if (!columnPermissionCatalogReady && (columnPermissionCatalogState === 'failed' || columnPermissionCatalogState === 'idle')) { + loadColumnsForPermission(); + } + fetch('/api/admin/user/' + (u.id) + '/columns') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (String(document.getElementById('editUserId').value || '') !== String(u.id || '')) return; + if (!res.success || !Array.isArray(res.column_ids)) { + throw new Error(res.error || '权限加载失败'); + } + setColumnPermissionCheckboxes('editColumnPermissionWrap', res.column_ids); + }) + .catch(function (error) { + if (String(document.getElementById('editUserId').value || '') !== String(u.id || '')) return; + var msgEl = document.getElementById('msgEdit'); + msgEl.textContent = error.message || '权限加载失败'; + msgEl.className = 'msg err'; + }); + document.getElementById('editUserModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-delete]').forEach(function (btn) { + btn.onclick = function () { + if (!confirm('确定删除用户 "' + (btn.dataset.name || '') + '" 吗?')) return; + fetch('/api/admin/user/' + btn.dataset.delete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadUsers(userPage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + + document.getElementById('btnSearchUsers').onclick = function () { loadUsers(1); }; + document.getElementById('btnOpenCreateUser').onclick = function () { + updateCreateFormByRole(); + document.getElementById('createUserModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateUserModal').onclick = function () { + document.getElementById('createUserModal').classList.remove('show'); + }; + document.getElementById('createRole').onchange = function () { updateCreateFormByRole(); }; + document.getElementById('btnCreate').onclick = function () { + var username = (document.getElementById('username').value || '').trim(); + var password = document.getElementById('password').value || ''; + var role = document.getElementById('createRole').value || 'normal'; + var createdById = document.getElementById('createCreatedBy').value ? parseInt(document.getElementById('createCreatedBy').value, 10) : null; + var msgEl = document.getElementById('msgCreate'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!username || username.length < 2) { + msgEl.textContent = '用户名至少 2 个字符'; + msgEl.classList.add('err'); + return; + } + if (!password || password.length < 6) { + msgEl.textContent = '密码至少 6 个字符'; + msgEl.classList.add('err'); + return; + } + if (!columnPermissionCatalogReady) { + if (columnPermissionCatalogState === 'failed' || columnPermissionCatalogState === 'idle') { + loadColumnsForPermission(); + msgEl.textContent = '菜单权限加载失败,已重新加载,请稍后重试'; + msgEl.classList.add('err'); + return; + } + msgEl.textContent = '菜单权限尚未加载(加载中),请稍后重试'; + msgEl.classList.add('err'); + return; + } + var body = { username: username, password: password, role: role }; + if (role === 'normal' && currentUserRole === 'super_admin' && createdById) body.created_by_id = createdById; + body.column_ids = getSelectedColumnIds('createColumnPermissionWrap'); + fetch('/api/admin/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '创建成功'; + msgEl.classList.add('ok'); + document.getElementById('username').value = ''; + document.getElementById('password').value = ''; + loadUsers(1); + } else { + msgEl.textContent = res.error || '创建失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + + document.getElementById('btnSaveUser').onclick = function () { + var uid = document.getElementById('editUserId').value; + var password = document.getElementById('editPassword').value; + var editRoleEl = document.getElementById('editRole'); + var msgEl = document.getElementById('msgEdit'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + var editPermissionWrap = document.getElementById('editColumnPermissionWrap'); + if (!columnPermissionCatalogReady || !editPermissionWrap._permissionsReady) { + msgEl.textContent = '用户权限尚未加载,不能保存'; + msgEl.classList.add('err'); + return; + } + var body = {}; + if (password) body.password = password; + if (currentUserRole === 'super_admin' && editRoleEl && editRoleEl.offsetParent !== null) + body.role = editRoleEl.value || 'normal'; + body.column_ids = getSelectedColumnIds('editColumnPermissionWrap'); + fetch('/api/admin/user/' + uid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '保存成功'; + msgEl.classList.add('ok'); + document.getElementById('editUserModal').classList.remove('show'); + loadUsers(userPage); + if (String(uid || '') === String(currentUserId || '')) { + loadAdminMenus(getActiveAdminTabName()); + } + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + document.getElementById('btnCloseEdit').onclick = function () { + document.getElementById('editUserModal').classList.remove('show'); + }; + + // ========== 视频任务管理 ========== + var imageVideoTaskPage = 1, imageVideoTaskPageSize = 20; + var imageVideoCards = []; + var selectedImageVideoKeys = new Set(); + var imageVideoDownloadInProgress = false; + var imageVideoPermissionUsers = []; + var imageVideoPermissionInitialUserIds = new Set(); + var selectedImageVideoPermissionUserIds = new Set(); + var imageVideoPermissionView = 'granted'; + function updateImageVideoPermissionAccess() { + var button = document.getElementById('btnOpenImageVideoPermissions'); + if (button) button.style.display = currentUserRole === 'super_admin' ? 'inline-flex' : 'none'; + var shopButton = document.getElementById('btnOpenShopDataTaskPermissions'); + if (shopButton) shopButton.style.display = currentUserRole === 'super_admin' ? 'inline-flex' : 'none'; + } + function imageVideoPermissionUsersForView() { + if (imageVideoPermissionView === 'granted') { + return imageVideoPermissionUsers.filter(function (user) { + return imageVideoPermissionInitialUserIds.has(Number(user.id)); + }); + } + return imageVideoPermissionUsers; + } + function syncImageVideoPermissionTabs() { + document.getElementById('imageVideoPermissionGrantedCount').textContent = '(' + imageVideoPermissionInitialUserIds.size + ')'; + document.getElementById('imageVideoPermissionAllCount').textContent = '(' + imageVideoPermissionUsers.length + ')'; + document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) { + var active = tab.dataset.imageVideoPermissionView === imageVideoPermissionView; + tab.classList.toggle('active', active); + tab.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + } + function filteredImageVideoPermissionUsers() { + var users = imageVideoPermissionUsersForView(); + var keyword = (document.getElementById('imageVideoPermissionSearch').value || '').trim().toLowerCase(); + if (!keyword) return users; + return users.filter(function (user) { + return String(user.username || '').toLowerCase().indexOf(keyword) >= 0; + }); + } + function syncImageVideoPermissionSelectAll() { + var visibleUsers = filteredImageVideoPermissionUsers(); + var selectedCount = visibleUsers.filter(function (user) { + return selectedImageVideoPermissionUserIds.has(Number(user.id)); + }).length; + var selectAll = document.getElementById('imageVideoPermissionSelectAll'); + selectAll.checked = visibleUsers.length > 0 && selectedCount === visibleUsers.length; + selectAll.indeterminate = selectedCount > 0 && selectedCount < visibleUsers.length; + selectAll.disabled = visibleUsers.length === 0; + } + function renderImageVideoPermissionUsers() { + var list = document.getElementById('imageVideoPermissionList'); + var summary = document.getElementById('imageVideoPermissionSummary'); + var visibleUsers = filteredImageVideoPermissionUsers(); + syncImageVideoPermissionTabs(); + var pendingCount = imageVideoPermissionUsers.filter(function (user) { + return imageVideoPermissionInitialUserIds.has(Number(user.id)) !== selectedImageVideoPermissionUserIds.has(Number(user.id)); + }).length; + summary.textContent = imageVideoPermissionView === 'granted' + ? '当前显示已保存分配用户,共 ' + visibleUsers.length + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '') + : '当前显示全部用户,已保存分配 ' + imageVideoPermissionInitialUserIds.size + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : ''); + list.innerHTML = visibleUsers.length ? visibleUsers.map(function (user) { + var userId = Number(user.id); + var persistedGranted = imageVideoPermissionInitialUserIds.has(userId); + var pendingGranted = selectedImageVideoPermissionUserIds.has(userId); + var pendingChanged = persistedGranted !== pendingGranted; + var actionText = pendingChanged + ? (pendingGranted ? '待保存分配' : '待保存取消') + : (persistedGranted ? '取消分配' : '分配'); + return '
    ' + + '' + + '' + + '' + escapeHtml(user.username || '-') + '' + + '' + escapeHtml(roleLabel(user.role)) + '' + + '' + + '' + + '
    '; + }).join('') : '
    暂无匹配用户
    '; + syncImageVideoPermissionSelectAll(); + } + function setImageVideoPermissionLoading(loading) { + document.getElementById('btnSaveImageVideoPermissions').disabled = loading; + document.getElementById('imageVideoPermissionSearch').disabled = loading; + if (loading) document.getElementById('imageVideoPermissionSelectAll').disabled = true; + } + function openImageVideoPermissions() { + if (currentUserRole !== 'super_admin') return; + var modal = document.getElementById('imageVideoPermissionModal'); + var message = document.getElementById('imageVideoPermissionMessage'); + modal.classList.add('show'); + message.textContent = ''; + message.className = 'msg'; + imageVideoPermissionView = 'granted'; + syncImageVideoPermissionTabs(); + document.getElementById('imageVideoPermissionSearch').value = ''; + document.getElementById('imageVideoPermissionList').innerHTML = '
    加载中...
    '; + setImageVideoPermissionLoading(true); + fetch('/api/admin/image-video-task-permissions') + .then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '权限加载失败'); + imageVideoPermissionUsers = res.items || []; + imageVideoPermissionInitialUserIds = new Set(imageVideoPermissionUsers.filter(function (user) { + return !!user.granted; + }).map(function (user) { return Number(user.id); })); + selectedImageVideoPermissionUserIds = new Set(imageVideoPermissionInitialUserIds); + renderImageVideoPermissionUsers(); + }) + .catch(function (error) { + imageVideoPermissionUsers = []; + selectedImageVideoPermissionUserIds.clear(); + document.getElementById('imageVideoPermissionList').innerHTML = '
    ' + escapeHtml(error.message || '权限加载失败') + '
    '; + }) + .finally(function () { setImageVideoPermissionLoading(false); }); + } + function closeImageVideoPermissions() { + document.getElementById('imageVideoPermissionModal').classList.remove('show'); + } + function saveImageVideoPermissions() { + var message = document.getElementById('imageVideoPermissionMessage'); + message.textContent = '保存中...'; + message.className = 'msg'; + setImageVideoPermissionLoading(true); + fetch('/api/admin/image-video-task-permissions', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_ids: Array.from(selectedImageVideoPermissionUserIds).sort(function (a, b) { return a - b; }) }) + }) + .then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '权限保存失败'); + imageVideoPermissionUsers.forEach(function (user) { + user.granted = selectedImageVideoPermissionUserIds.has(Number(user.id)); + }); + imageVideoPermissionInitialUserIds = new Set(selectedImageVideoPermissionUserIds); + renderImageVideoPermissionUsers(); + message.textContent = res.msg || '保存成功'; + message.className = 'msg ok'; + }) + .catch(function (error) { + message.textContent = error.message || '权限保存失败'; + message.className = 'msg err'; + }) + .finally(function () { + setImageVideoPermissionLoading(false); + syncImageVideoPermissionSelectAll(); + }); + } + function buildImageVideoTaskQuery(page) { + var params = new URLSearchParams(); + params.set('page', String(page || 1)); + params.set('page_size', String(imageVideoTaskPageSize)); + var values = { + username: document.getElementById('imageVideoFilterUsername').value.trim(), + submitted_from: document.getElementById('imageVideoFilterFrom').value, + submitted_to: document.getElementById('imageVideoFilterTo').value + }; + Object.keys(values).forEach(function (key) { if (values[key]) params.set(key, values[key]); }); + return params.toString(); + } + var SHOP_DATA_STATUS_LABELS = { + 'PENDING': '待执行', 'WAITING': '排队中', 'RUNNING': '执行中', 'POLLING': '处理中', + 'SUCCESS': '已完成', 'COMPLETED': '已完成', 'DONE': '已完成', + 'FAILED': '失败', 'ERROR': '失败', 'CANCELLED': '已取消', 'CANCELED': '已取消', + 'DELETED': '已删除', 'STOPPED': '已停止' + }; + function imageVideoStatusLabel(value) { + var status = String(value || '-').toUpperCase(); + var label = SHOP_DATA_STATUS_LABELS[status]; + if (label) return label; + if (status.indexOf('SUCCEED') === 0) return '已完成'; + if (status.indexOf('FAIL') === 0 || status.indexOf('ERROR') === 0) return '失败'; + if (status.indexOf('CANCEL') === 0) return '已取消'; + if (status.indexOf('RUN') === 0 || status.indexOf('WAIT') === 0 || status.indexOf('PROCESS') === 0 || status.indexOf('POLL') === 0) return '处理中'; + return status === '-' ? '-' : status; + } + function countryCodeLabel(value) { + var labels = { DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙' }; + return labels[String(value || '').toUpperCase()] || value || '-'; + } + function countryListLabel(codes) { + if (Array.isArray(codes)) { + return codes.map(countryCodeLabel).join('、') || '-'; + } + return String(codes || '').split(/[,,]/).map(function (code) { + return countryCodeLabel(code.trim()); + }).filter(Boolean).join('、') || '-'; + } + function renderImageVideoStatus(value) { + var status = String(value || '-').toUpperCase(); + return '' + escapeHtml(imageVideoStatusLabel(status)) + ''; + } + function safeAdminUrl(value) { + var url = String(value || '').trim(); + return /^https?:\/\//i.test(url) ? url : ''; + } + function imageVideoDownloadIcon() { + return ''; + } + function imageVideoUnavailableHtml(message) { + return '
    ' + + '' + + '' + escapeHtml(message || '暂无可用视频') + '' + + '
    '; + } + function imageVideoCardKey(taskId, videoIndex) { + return String(taskId) + ':' + String(videoIndex); + } + function flattenImageVideoTasks(items) { + var cards = []; + (items || []).forEach(function (task) { + var videos = Array.isArray(task.videos) ? task.videos : []; + if (!videos.length) { + cards.push({ key: imageVideoCardKey(task.task_id, 'empty'), task: task, video: null, videoIndex: 0 }); + return; + } + videos.forEach(function (video, index) { + cards.push({ + key: imageVideoCardKey(task.task_id, index), + task: task, + video: { + sourceUrl: safeAdminUrl(video.source_url), + archivedUrl: safeAdminUrl(video.archived_url), + displayUrl: safeAdminUrl(video.display_url), + objectKey: String(video.object_key || ''), + archiveStatus: String(video.archive_status || ''), + archiveError: String(video.archive_error || '') + }, + videoIndex: index + }); + }); + }); + return cards; + } + function renderImageVideoCard(card) { + var task = card.task; + var video = card.video; + var hasVideo = !!(video && video.displayUrl); + var debugUrl = safeAdminUrl(task.debug_url); + var generatedAt = task.completed_at || task.submitted_at || '-'; + var mediaHtml = hasVideo + ? '' + + '' + : imageVideoUnavailableHtml(task.status === 'FAILED' ? '任务失败,未生成视频' : '视频生成中或暂无结果'); + return '
    ' + + '
    ' + mediaHtml + '
    ' + + '
    ' + + '
    ' + + '
    任务 ' + escapeHtml(task.task_id) + (video ? ' · 视频 ' + (card.videoIndex + 1) : '') + '
    ' + + renderImageVideoStatus(task.status) + + '
    ' + + '
    ' + + '
    ' + escapeHtml(task.username || '-') + '
    ' + + '
    ' + escapeHtml(task.group_name || '-') + '
    ' + + '
    ' + escapeHtml(task.mode || '-') + '
    ' + + '
    ' + escapeHtml(generatedAt) + '
    ' + + '
    ' + (debugUrl + ? '' + : '-') + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    '; + } + function syncImageVideoSelectionUi() { + var selectable = imageVideoCards.filter(function (card) { return !!(card.video && card.video.displayUrl); }); + document.querySelectorAll('[data-image-video-card]').forEach(function (cardEl) { + var selected = selectedImageVideoKeys.has(cardEl.dataset.imageVideoCard); + cardEl.classList.toggle('selected', selected); + var checkbox = cardEl.querySelector('[data-image-video-select]'); + if (checkbox) checkbox.checked = selected; + }); + var selectedCount = selectable.filter(function (card) { return selectedImageVideoKeys.has(card.key); }).length; + var selectAll = document.getElementById('imageVideoSelectAll'); + selectAll.checked = selectable.length > 0 && selectedCount === selectable.length; + selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length; + selectAll.disabled = imageVideoDownloadInProgress || selectable.length === 0; + var batchButton = document.getElementById('btnBatchDownloadImageVideos'); + batchButton.disabled = imageVideoDownloadInProgress || selectedCount === 0; + batchButton.innerHTML = imageVideoDownloadIcon() + (imageVideoDownloadInProgress ? '处理中' : '批量下载' + (selectedCount ? ' (' + selectedCount + ')' : '')); + document.querySelectorAll('[data-image-video-download]').forEach(function (button) { + var card = imageVideoCards.find(function (item) { return item.key === button.dataset.imageVideoDownload; }); + button.disabled = imageVideoDownloadInProgress || !card || !card.video || !card.video.displayUrl; + }); + } + function renderImageVideoCards() { + var grid = document.getElementById('imageVideoTaskGrid'); + grid.innerHTML = imageVideoCards.length + ? imageVideoCards.map(renderImageVideoCard).join('') + : '
    暂无符合条件的视频任务
    '; + grid.querySelectorAll('video').forEach(function (video) { + video.addEventListener('error', function () { + var media = video.closest('.image-video-media'); + if (!media || media.querySelector('.image-video-unavailable')) return; + video.remove(); + media.insertAdjacentHTML('beforeend', imageVideoUnavailableHtml('视频加载失败,可尝试下载')); + }, { once: true }); + }); + syncImageVideoSelectionUi(); + } + function resetImageVideoSelection() { + selectedImageVideoKeys.clear(); + document.getElementById('imageVideoDownloadProgress').textContent = ''; + syncImageVideoSelectionUi(); + } + function loadImageVideoTasks(page) { + imageVideoTaskPage = page || 1; + selectedImageVideoKeys.clear(); + var grid = document.getElementById('imageVideoTaskGrid'); + grid.innerHTML = '
    加载中...
    '; + document.getElementById('imageVideoDownloadProgress').textContent = ''; + fetch('/api/admin/image-video-tasks?' + buildImageVideoTaskQuery(imageVideoTaskPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + imageVideoCards = []; + grid.innerHTML = '
    加载失败:' + escapeHtml(res.error || '') + '
    '; + document.getElementById('imageVideoTaskTotal').textContent = ''; + syncImageVideoSelectionUi(); + return; + } + var items = res.items || []; + imageVideoCards = flattenImageVideoTasks(items); + var videoCount = imageVideoCards.filter(function (card) { return !!card.video; }).length; + document.getElementById('imageVideoTaskTotal').textContent = '共 ' + (res.total || 0) + ' 个任务 · 本页 ' + videoCount + ' 个视频'; + renderImageVideoCards(); + renderPagination('imageVideoTaskPagination', res.total, res.page, res.page_size, loadImageVideoTasks); + }) + .catch(function () { + imageVideoCards = []; + grid.innerHTML = '
    请求失败,请稍后重试
    '; + document.getElementById('imageVideoTaskTotal').textContent = ''; + syncImageVideoSelectionUi(); + }); + } + function imageVideoFilename(card) { + var url = card.video.displayUrl || ''; + var path = url.split(/[?#]/)[0]; + var match = path.match(/\.([a-z0-9]{2,5})$/i); + return 'task-' + card.task.task_id + '-video-' + (card.videoIndex + 1) + '.' + (match ? match[1].toLowerCase() : 'mp4'); + } + function triggerImageVideoLink(url, filename, newWindow) { + var link = document.createElement('a'); + link.href = url; + link.download = filename; + if (newWindow) { + link.target = '_blank'; + link.rel = 'noreferrer'; + } + document.body.appendChild(link); + link.click(); + link.remove(); + } + function downloadImageVideo(card) { + var url = card && card.video ? card.video.displayUrl : ''; + if (!url) return Promise.resolve({ fallback: false, skipped: true }); + var filename = imageVideoFilename(card); + return fetch(url, { __skipLoading: true }) + .then(function (response) { + if (!response.ok) throw new Error('HTTP ' + response.status); + return response.blob(); + }) + .then(function (blob) { + var objectUrl = URL.createObjectURL(blob); + triggerImageVideoLink(objectUrl, filename, false); + setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000); + return { fallback: false, skipped: false }; + }) + .catch(function () { + triggerImageVideoLink(url, filename, true); + return { fallback: true, skipped: false }; + }); + } + function runImageVideoDownloads(cards) { + if (imageVideoDownloadInProgress || !cards.length) return; + imageVideoDownloadInProgress = true; + syncImageVideoSelectionUi(); + var progress = document.getElementById('imageVideoDownloadProgress'); + progress.textContent = '正在下载'; + downloadImageVideo(cards[0]).then(function (result) { + progress.textContent = result.fallback ? '已在新窗口打开视频' : '下载已开始'; + }).finally(function () { + imageVideoDownloadInProgress = false; + syncImageVideoSelectionUi(); + }); + } + function imageVideoZipFilename(response) { + var disposition = response.headers.get('Content-Disposition') || ''; + var encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i); + if (encoded) { + try { return decodeURIComponent(encoded[1]); } catch (error) { } + } + var plain = disposition.match(/filename="?([^";]+)"?/i); + return plain ? plain[1] : 'video-tasks.zip'; + } + function downloadImageVideoZip(cards) { + if (imageVideoDownloadInProgress || !cards.length) return; + imageVideoDownloadInProgress = true; + syncImageVideoSelectionUi(); + var progress = document.getElementById('imageVideoDownloadProgress'); + progress.textContent = '正在打包 ' + cards.length + ' 个视频...'; + fetch('/api/admin/image-video-tasks/download-zip', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + items: cards.map(function (card) { + return { task_id: Number(card.task.task_id), video_index: card.videoIndex }; + }) + }), + __skipLoading: true + }).then(function (response) { + if (!response.ok) { + return response.json().catch(function () { return {}; }).then(function (data) { + throw new Error(data.error || '压缩包生成失败'); + }); + } + var filename = imageVideoZipFilename(response); + var errorCount = Number(response.headers.get('X-Archive-Error-Count') || 0); + return response.blob().then(function (blob) { + return { blob: blob, filename: filename, errorCount: errorCount }; + }); + }).then(function (result) { + var objectUrl = URL.createObjectURL(result.blob); + triggerImageVideoLink(objectUrl, result.filename, false); + setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000); + progress.textContent = result.errorCount + ? '压缩包已下载,' + result.errorCount + ' 个视频失败,详见包内错误清单' + : '压缩包下载已开始'; + }).catch(function (error) { + progress.textContent = error.message || '压缩包下载失败'; + }).finally(function () { + imageVideoDownloadInProgress = false; + syncImageVideoSelectionUi(); + }); + } + function copyImageVideoDebugUrl(card, button) { + var url = card ? safeAdminUrl(card.task.debug_url) : ''; + if (!url) return; + var copyPromise; + if (navigator.clipboard && navigator.clipboard.writeText) { + copyPromise = navigator.clipboard.writeText(url); + } else { + copyPromise = new Promise(function (resolve, reject) { + var textarea = document.createElement('textarea'); + textarea.value = url; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + var copied = document.execCommand('copy'); + textarea.remove(); + if (copied) resolve(); + else reject(new Error('copy failed')); + }); + } + copyPromise.then(function () { + button.textContent = '已复制'; + button.classList.add('copied'); + setTimeout(function () { + button.textContent = '复制链接'; + button.classList.remove('copied'); + }, 1600); + }).catch(function () { + button.textContent = '复制失败'; + setTimeout(function () { button.textContent = '复制链接'; }, 1600); + }); + } + document.getElementById('btnFilterImageVideoTasks').onclick = function () { loadImageVideoTasks(1); }; + document.getElementById('btnOpenImageVideoPermissions').onclick = openImageVideoPermissions; + document.getElementById('btnCloseImageVideoPermissions').onclick = closeImageVideoPermissions; + document.getElementById('btnCancelImageVideoPermissions').onclick = closeImageVideoPermissions; + document.getElementById('btnSaveImageVideoPermissions').onclick = saveImageVideoPermissions; + document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) { + tab.onclick = function () { + imageVideoPermissionView = tab.dataset.imageVideoPermissionView || 'granted'; + document.getElementById('imageVideoPermissionSearch').value = ''; + renderImageVideoPermissionUsers(); + }; + }); + document.getElementById('imageVideoPermissionSearch').oninput = renderImageVideoPermissionUsers; + document.getElementById('imageVideoPermissionSelectAll').onchange = function (event) { + filteredImageVideoPermissionUsers().forEach(function (user) { + var userId = Number(user.id); + if (event.target.checked) selectedImageVideoPermissionUserIds.add(userId); + else selectedImageVideoPermissionUserIds.delete(userId); + }); + renderImageVideoPermissionUsers(); + }; + document.getElementById('imageVideoPermissionList').onclick = function (event) { + var toggle = event.target.closest('[data-image-video-permission-toggle]'); + if (!toggle) return; + var userId = Number(toggle.dataset.imageVideoPermissionToggle); + if (selectedImageVideoPermissionUserIds.has(userId)) selectedImageVideoPermissionUserIds.delete(userId); + else selectedImageVideoPermissionUserIds.add(userId); + renderImageVideoPermissionUsers(); + }; + document.getElementById('imageVideoPermissionList').onchange = function (event) { + var checkbox = event.target.closest('[data-image-video-permission-user]'); + if (!checkbox) return; + var userId = Number(checkbox.dataset.imageVideoPermissionUser); + if (checkbox.checked) selectedImageVideoPermissionUserIds.add(userId); + else selectedImageVideoPermissionUserIds.delete(userId); + renderImageVideoPermissionUsers(); + }; + document.getElementById('imageVideoPermissionModal').onclick = function (event) { + if (event.target === event.currentTarget) closeImageVideoPermissions(); + }; + document.getElementById('btnResetImageVideoTasks').onclick = function () { + ['imageVideoFilterUsername', 'imageVideoFilterFrom', 'imageVideoFilterTo'] + .forEach(function (id) { document.getElementById(id).value = ''; }); + loadImageVideoTasks(1); + }; + document.getElementById('imageVideoSelectAll').onchange = function (event) { + imageVideoCards.forEach(function (card) { + if (!card.video || !card.video.displayUrl) return; + if (event.target.checked) selectedImageVideoKeys.add(card.key); + else selectedImageVideoKeys.delete(card.key); + }); + syncImageVideoSelectionUi(); + }; + document.getElementById('btnBatchDownloadImageVideos').onclick = function () { + downloadImageVideoZip(imageVideoCards.filter(function (card) { return selectedImageVideoKeys.has(card.key); })); + }; + document.getElementById('imageVideoTaskGrid').onchange = function (event) { + var checkbox = event.target.closest('[data-image-video-select]'); + if (!checkbox) return; + if (checkbox.checked) selectedImageVideoKeys.add(checkbox.dataset.imageVideoSelect); + else selectedImageVideoKeys.delete(checkbox.dataset.imageVideoSelect); + syncImageVideoSelectionUi(); + }; + document.getElementById('imageVideoTaskGrid').onclick = function (event) { + var copyButton = event.target.closest('[data-image-video-copy-debug]'); + if (copyButton) { + var copyCard = imageVideoCards.find(function (item) { return item.key === copyButton.dataset.imageVideoCopyDebug; }); + if (copyCard) copyImageVideoDebugUrl(copyCard, copyButton); + } + var downloadButton = event.target.closest('[data-image-video-download]'); + if (downloadButton) { + var card = imageVideoCards.find(function (item) { return item.key === downloadButton.dataset.imageVideoDownload; }); + if (card) runImageVideoDownloads([card]); + } + }; + + // ========== 店铺数据任务管理 ========== + var shopDataTaskPage = 1, shopDataTaskPageSize = 20; + var shopDataTaskGroups = []; + var shopDataTasks = []; + var selectedShopDataResultIds = new Set(); + var shopDataDownloadInProgress = false; + var shopDataPermissionUsers = []; + var shopDataPermissionInitialUserIds = new Set(); + var selectedShopDataPermissionUserIds = new Set(); + var shopDataPermissionView = 'granted'; + + function buildShopDataTaskQuery(page) { + var params = new URLSearchParams(); + params.set('page', String(page || 1)); + params.set('page_size', String(shopDataTaskPageSize)); + 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 + }; + Object.keys(values).forEach(function (key) { + if (values[key]) params.set(key, values[key]); + }); + return params.toString(); + } + + function shopDataResultId(item) { + if (!item) return 0; + var value = item.result_id != null ? item.result_id : item.resultId; + var id = Number(value); + return isFinite(id) && id > 0 ? id : 0; + } + + function shopDataBoolean(value) { + if (typeof value === 'string') { + return ['1', 'true', 'yes', 'y'].indexOf(value.toLowerCase()) >= 0; + } + return !!value; + } + + function shopDataDateValue(value) { + if (!value) return 0; + var timestamp = Date.parse(String(value).replace(' ', 'T')); + return isNaN(timestamp) ? 0 : timestamp; + } + + function shopDataResultSort(a, b) { + var dateDiff = shopDataDateValue(b.created_at || b.finished_at) - shopDataDateValue(a.created_at || a.finished_at); + if (dateDiff) return dateDiff; + return shopDataResultId(b) - shopDataResultId(a); + } + + function shopDataGroupKey(item) { + var name = String((item && (item.shop_name || item.shop || item.source_filename)) || '').trim(); + // Keep the same trimmed, case-insensitive key as the Flask grouping query. + return 'name:' + name.toLowerCase(); + } + + function shopDataNormalizeResult(group, raw) { + var result = {}; + Object.keys(group || {}).forEach(function (key) { + if (key !== 'results' && key !== 'group_results') result[key] = group[key]; + }); + Object.keys(raw || {}).forEach(function (key) { + if (key !== 'results' && key !== 'group_results') result[key] = raw[key]; + }); + result.shop_name = result.shop_name || result.shop || result.source_filename || ''; + result.shop_id = result.shop_id || result.shopId || result.source_file_url || ''; + result.group_name = result.group_name || result.group || ''; + result.status = result.status || result.task_status || result.file_status || ''; + result.error = result.error || result.result_error || result.error_message || result.task_error || result.file_error || ''; + result.output_filename = result.output_filename || result.result_filename || result.filename || ''; + result.country_codes = result.country_codes || result.countryCodes || []; + if (result.file_size == null) result.file_size = result.result_file_size; + if (result.row_count == null) result.row_count = result.rows; + if (result.finished_at == null) result.finished_at = result.completed_at; + if (result.result_id == null && raw && raw.resultId != null) result.result_id = raw.resultId; + if (result.result_id == null && raw && raw.id != null && !Array.isArray(raw.results) && !Array.isArray(raw.group_results)) result.result_id = raw.id; + if (result.file_ready == null) { + result.file_ready = !!String(result.result_file_url || result.resultFileUrl || '').trim(); + } else { + result.file_ready = shopDataBoolean(result.file_ready); + } + return result; + } + + // The admin API now returns one group per shop. Keep a flat result list + // for selection/actions while rendering the grouped view. + function normalizeShopDataTaskGroups(items) { + var groups = []; + var byKey = Object.create(null); + (Array.isArray(items) ? items : []).forEach(function (rawGroup) { + if (!rawGroup || typeof rawGroup !== 'object') return; + var rawResults = Array.isArray(rawGroup.results) + ? rawGroup.results + : (Array.isArray(rawGroup.group_results) ? rawGroup.group_results : [rawGroup]); + var groupBase = {}; + Object.keys(rawGroup).forEach(function (key) { + if (key !== 'results' && key !== 'group_results') groupBase[key] = rawGroup[key]; + }); + if (!groupBase.shop_name && rawResults.length) { + groupBase.shop_name = rawResults[0].shop_name || rawResults[0].shop || rawResults[0].source_filename || ''; + } + if (!groupBase.shop_id && rawResults.length) { + groupBase.shop_id = rawResults[0].shop_id || rawResults[0].shopId || rawResults[0].source_file_url || ''; + } + var key = shopDataGroupKey(groupBase); + var group = byKey[key]; + if (!group) { + group = { + key: key, + shop_name: groupBase.shop_name || '', + shop_id: groupBase.shop_id || '', + group_name: groupBase.group_name || groupBase.group || '', + latest_created_at: groupBase.latest_created_at || '', + results: [] + }; + byKey[key] = group; + groups.push(group); + } + rawResults.forEach(function (rawResult) { + if (!rawResult || typeof rawResult !== 'object') return; + var result = shopDataNormalizeResult(groupBase, rawResult); + var resultId = shopDataResultId(result); + // 失败/进行中的任务没有结果文件也保留显示(禁用下载、可删除) + if (!resultId) return; + if (resultId && group.results.some(function (existing) { return shopDataResultId(existing) === resultId; })) return; + group.results.push(result); + if (!group.shop_name) group.shop_name = result.shop_name || ''; + if (!group.shop_id) group.shop_id = result.shop_id || ''; + if (!group.group_name) group.group_name = result.group_name || ''; + }); + }); + groups.forEach(function (group) { + group.results.sort(shopDataResultSort); + group.results = group.results.slice(0, 1); + if (!group.latest_created_at && group.results.length) { + group.latest_created_at = group.results[0].created_at || group.results[0].finished_at || ''; + } + }); + return groups.filter(function (group) { return group.results.length > 0; }); + } + + function shopDataDeleteIcon() { + return ''; + } + + function renderShopDataStatus(item, status) { + var normalized = String(status || '-').toUpperCase(); + var errorTitle = item && item.error ? ' title="' + escapeHtml(item.error) + '"' : ''; + var cls = normalized === 'SUCCESS' || normalized === 'COMPLETED' + ? 'success' + : (normalized === 'FAILED' || normalized === 'CANCELLED' ? 'failed' : 'running'); + return '' + escapeHtml(imageVideoStatusLabel(normalized)) + ''; + } + + function renderShopDataRecordRow(item) { + var resultId = shopDataResultId(item); + var selected = resultId > 0 && selectedShopDataResultIds.has(resultId); + var status = String(item.status || item.file_status || '').toUpperCase(); + var terminal = ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(status) >= 0; + var countryCodes = item.country_codes != null ? item.country_codes : item.countryCodes; + var updatedAt = item.updated_at || item.latest_created_at || item.created_at || item.finished_at || '-'; + var checkbox = ' 0 ? '' : ' disabled') + '>'; + return '' + + '' + checkbox + '' + + '' + escapeHtml(item.shop_name || '-') + '' + + '' + escapeHtml(item.group_name || '-') + '' + + '' + escapeHtml(countryListLabel(countryCodes)) + '' + + '' + renderShopDataStatus(item, status || item.file_status) + '' + + '' + escapeHtml(updatedAt) + '' + + '' + + '' + + '' + + '' + + ''; + } + + function renderShopDataRecordTable() { + if (!shopDataTasks.length) { + return '
    暂无符合条件的店铺数据任务
    '; + } + // 卡片网格:每店铺一张卡片,按截图样式(店铺名、当日累计文件、分组、最新时间、任务号+状态、国家、文件名、下载/删除) + var cards = shopDataTasks.map(function (item) { + var resultId = shopDataResultId(item); + var selected = resultId > 0 && selectedShopDataResultIds.has(resultId); + var status = String(item.status || item.file_status || '').toUpperCase(); + var terminal = ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(status) >= 0; + var countryCodes = item.country_codes != null ? item.country_codes : item.countryCodes; + var updatedAt = item.updated_at || item.latest_created_at || item.created_at || item.finished_at || '-'; + var taskNo = item.task_id || item.taskId || '-'; + var errorTitle = item.error ? ' title="' + escapeHtml(item.error) + '"' : ''; + return '
    ' + + '
    ' + + '' + escapeHtml(item.shop_name || '-') + '' + + '
    ' + + '
    ' + + ' 0 ? '' : ' disabled') + '>' + + '任务 ' + escapeHtml(String(taskNo)) + '' + + '' + + escapeHtml(imageVideoStatusLabel(status)) + '' + + '
    ' + + '
    分组' + + '' + escapeHtml(item.group_name || '-') + '
    ' + + '
    最新' + + '' + escapeHtml(updatedAt) + '
    ' + + '
    国家' + + '' + escapeHtml(countryListLabel(countryCodes)) + '
    ' + + '
    文件' + + '' + escapeHtml(item.output_filename || '-') + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    '; + }).join(''); + return '
    ' + cards + '
    '; + } + + function renderShopDataTasks() { + var grid = document.getElementById('shopDataTaskGrid'); + grid.innerHTML = renderShopDataRecordTable(); + syncShopDataSelectionUi(); + } + + function syncShopDataSelectionUi() { + document.querySelectorAll('[data-shop-data-card]').forEach(function (card) { + var resultId = Number(card.dataset.shopDataCard); + var selected = selectedShopDataResultIds.has(resultId); + card.classList.toggle('selected', selected); + var checkbox = card.querySelector('[data-shop-data-select]'); + if (checkbox) checkbox.checked = selected && !checkbox.disabled; + }); + var selectable = shopDataTasks.filter(function (item) { return !!item.file_ready && shopDataResultId(item) > 0; }); + var selectedCount = selectable.filter(function (item) { + return selectedShopDataResultIds.has(shopDataResultId(item)); + }).length; + var selectAll = document.getElementById('shopDataTaskSelectAll'); + if (selectAll) { + selectAll.checked = selectable.length > 0 && selectedCount === selectable.length; + selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length; + selectAll.disabled = shopDataDownloadInProgress || selectable.length === 0; + } + var batch = document.getElementById('btnBatchDownloadShopDataTasks'); + batch.disabled = shopDataDownloadInProgress || selectedCount === 0; + batch.innerHTML = imageVideoDownloadIcon() + (shopDataDownloadInProgress + ? '处理中' + : '批量下载' + (selectedCount ? ' (' + selectedCount + ')' : '')); + } + + function loadShopDataCrawlTasks(page) { + shopDataTaskPage = page || 1; + selectedShopDataResultIds.clear(); + var grid = document.getElementById('shopDataTaskGrid'); + grid.innerHTML = '
    加载中...
    '; + document.getElementById('shopDataTaskDownloadProgress').textContent = ''; + fetch('/api/admin/shop-data-crawl-tasks?' + buildShopDataTaskQuery(shopDataTaskPage)) + .then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载失败'); + var payload = res.data && typeof res.data === 'object' && !Array.isArray(res.data) ? res.data : res; + shopDataTaskGroups = normalizeShopDataTaskGroups(payload.items || []); + shopDataTasks = shopDataTaskGroups.reduce(function (all, group) { + return all.concat(group.results || []); + }, []); + var total = payload.total != null ? Number(payload.total) : shopDataTaskGroups.length; + var responsePage = payload.page || page; + var responsePageSize = payload.page_size || shopDataTaskPageSize; + document.getElementById('shopDataTaskTotal').textContent = '共 ' + (total || 0) + ' 家店铺'; + renderShopDataTasks(); + renderPagination('shopDataTaskPagination', total, responsePage, responsePageSize, loadShopDataCrawlTasks); + }) + .catch(function (error) { + shopDataTaskGroups = []; + shopDataTasks = []; + grid.innerHTML = '
    加载失败:' + escapeHtml(error.message || '') + '
    '; + document.getElementById('shopDataTaskTotal').textContent = ''; + syncShopDataSelectionUi(); + }); + } + + // ========== 店铺数据重复检查(指标卡 + 分布 + 矩阵表格 + 抽屉)========== + var shopDataDuplicatePage = 1, shopDataDuplicatePageSize = 20; + var shopDataDuplicateItems = []; + var shopDataDuplicateShops = []; + var shopDataDuplicateTotal = 0; + var shopDataDuplicateView = 'monitor'; + var shopDataDuplicateLoading = false; + var shopDataDuplicateScannedAt = ''; + var shopDataDuplicateOverviewPending = false; + var shopDataDuplicateExporting = false; + // 撞款详情卡片区(跨店重复 ASIN)独立分页 + var shopDataDuplicateDetailPage = 1; + var shopDataDuplicateDetailPageSize = 6; + var shopDataDuplicateDetailItems = []; + var shopDataDuplicateDetailTotal = 0; + var shopDataDuplicateDetailLoading = false; + + // 站点代码 → 中文国家名(撞款详情/抽屉展示用),未识别回退原值 + var DUP_SITE_LABELS = { DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙' }; + function dupSiteLabel(site) { + var key = String(site || '').trim().toUpperCase(); + return DUP_SITE_LABELS[key] || String(site || '').trim(); + } + + function buildShopDataDuplicateQuery(page) { + var params = new URLSearchParams(); + params.set('page', String(page || 1)); + params.set('page_size', String(shopDataDuplicatePageSize)); + params.set('view', shopDataDuplicateView); + var values = { + asin: document.getElementById('shopDataDupFilterAsin').value.trim(), + shop_name: document.getElementById('shopDataDupFilterShop').value.trim(), + country: document.getElementById('shopDataDupFilterCountry').value.trim(), + site: document.getElementById('shopDataDupFilterSite').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]); + }); + return params.toString(); + } + + function pad2(value) { + var n = parseInt(value, 10); + if (isNaN(n)) return String(value); + return n < 10 ? '0' + n : String(n); + } + + // 中文日期(如 2026年8月18日 上午4:34)归一化为 YYYY-MM-DD HH:MM[:SS],便于展示与排序 + function normalizeDuplicateTime(raw) { + if (!raw) return ''; + var s = String(raw).trim(); + var m = s.match(/^(\d{4})年(\d{1,2})月(\d{1,2})日\s*(.*)$/); + if (m) { + var md = m[1] + '-' + pad2(m[2]) + '-' + pad2(m[3]); + var tm = m[4].match(/(上午|下午|晚上|凌晨)?\s*(\d{1,2})[::](\d{2})(?:[::](\d{1,2}))?/); + if (!tm) return md; + var hour = parseInt(tm[2], 10); + var period = tm[1] || ''; + if (period === '下午' || period === '晚上') hour = hour % 12 + 12; + if ((period === '上午' || period === '凌晨') && hour === 12) hour = 0; + return md + ' ' + pad2(hour) + ':' + tm[3] + (tm[4] ? ':' + pad2(tm[4]) : ''); + } + var dm = s.replace(/\//g, '-').match(/^(\d{4})-(\d{1,2})-(\d{1,2})(.*)$/); + if (dm) return dm[1] + '-' + pad2(dm[2]) + '-' + pad2(dm[3]) + (dm[4] || ''); + return s; + } + + function renderShopDataDuplicateMetrics(summary) { + var metricsEl = document.getElementById('shopDataDuplicateMetrics'); + if (!summary || summary.asin_total == null) { + metricsEl.innerHTML = '
    暂无扫描结果' + + '-
    '; + return; + } + var defs = [ + { label: '唯一ASIN', value: summary.asin_total, accent: true }, + { label: '上架记录', value: summary.record_total, accent: true }, + { label: '在线店铺', value: summary.shop_count, accent: true }, + { label: '重复ASIN', value: summary.duplicate_asin_total, warn: true }, + { label: '重复店铺', value: summary.duplicate_shop_count, warn: true }, + { label: '店铺平均ASIN', value: summary.asin_per_shop } + ]; + metricsEl.innerHTML = defs.map(function (def) { + return '
    ' + + '' + def.label + '' + + '' + def.value + '
    '; + }).join(''); + } + + function renderShopDataDuplicateDistribution(shops) { + var barsEl = document.getElementById('shopDataDuplicateDistribution'); + var noteEl = document.getElementById('shopDataDuplicateDistributionNote'); + if (!shops || !shops.length) { + barsEl.innerHTML = '
    暂无店铺数据
    '; + noteEl.textContent = ''; + return; + } + var max = 0; + shops.forEach(function (shop) { if (shop.asin_count > max) max = shop.asin_count; }); + // 新样式:双列网格,每格 = 店名 + 记录数 / 进度条 / N 个 ASIN(按 ASIN 数降序) + barsEl.innerHTML = shops.map(function (shop) { + var width = max ? Math.max(2, Math.round(shop.asin_count / max * 100)) : 0; + var recordCount = Number(shop.record_count || 0); + var asinCount = Number(shop.asin_count || 0); + return '
    ' + + '
    ' + + '' + escapeHtml(shop.shop_name) + '' + + '' + recordCount.toLocaleString() + '' + + '
    ' + + '
    ' + + '
    ' + asinCount.toLocaleString() + ' 个 ASIN
    ' + + '
    '; + }).join(''); + noteEl.textContent = '各店铺在本批站点中的上架记录量 · 共 ' + shops.length + ' 家店铺'; + } + + function shopDataDuplicateCellCount(item, shopName) { + var count = 0; + (item.occurrences || []).forEach(function (occ) { + if ((occ.shop_name || '') === shopName) count++; + }); + return count; + } + + function renderShopDataDuplicateMatrix() { + var list = document.getElementById('shopDataDuplicateList'); + if (!shopDataDuplicateItems.length) { + list.innerHTML = '
    暂无数据' + + (shopDataDuplicateView === 'monitor' && !shopDataDuplicateTotal ? ':当前没有跨店铺重复的 ASIN' : '') + + '。可点击右上角「重新分析」重新扫描。
    '; + return; + } + var shops = shopDataDuplicateShops; + var thead = 'ASIN店铺数' + + shops.map(function (shop) { + return '' + escapeHtml(shop.shop_name) + ''; + }).join('') + ''; + var tbody = shopDataDuplicateItems.map(function (item) { + var cells = shops.map(function (shop) { + var count = shopDataDuplicateCellCount(item, shop.shop_name); + var cls = count === 0 ? 'zero' : (item.shop_count >= 2 ? 'danger' : 'has'); + return '' + (count === 0 ? '0' : count) + ''; + }).join(''); + return '' + + '' + escapeHtml(item.asin) + '' + + '' + item.shop_count + '' + + cells + ''; + }).join(''); + list.innerHTML = '' + thead + '' + tbody + '
    '; + } + + function renderShopDataDuplicateList() { + renderShopDataDuplicateMatrix(); + } + + // 主时间戳(转数值失败时按字符串比较) + function dupTimeRank(raw) { + var t = Date.parse(normalizeDuplicateTime(raw)); + return isNaN(t) ? -1 : t; + } + + // 「查看明细」按钮点击开抽屉(动态元素,事件委托) + function handleDuplicateDetailClick(event) { + var button = event.target.closest('[data-open-asin-detail]'); + if (button) openShopDataDuplicateDrawer(button.dataset.openAsinDetail); + } + + // 站点徽章:中文国家(未识别回退原值) + function dupSiteBadge(site) { + return '' + + escapeHtml(dupSiteLabel(site)) + ''; + } + + // 撞款详情卡片区:同一 ASIN 在多条店铺的上架明细(独立分页,仅跨店铺重复) + function renderShopDataDuplicateDetailCards() { + var block = document.getElementById('dupCheckDetailBlock'); + var cardsEl = document.getElementById('dupCheckDetailCards'); + if (!block || !cardsEl) return; + if (!shopDataDuplicateDetailItems.length) { + block.style.display = 'none'; + cardsEl.innerHTML = ''; + return; + } + block.style.display = ''; + cardsEl.innerHTML = shopDataDuplicateDetailItems.map(function (item) { + // 按店铺分组:名称 + 次数 + 国家 + 上架时间(倒序) + var shopsMap = {}; + (item.occurrences || []).forEach(function (occ) { + var key = occ.shop_name || '-'; + if (!shopsMap[key]) shopsMap[key] = { shop: key, sites: {}, times: [], count: 0 }; + var info = shopsMap[key]; + info.count++; + if (occ.country) info.sites[occ.country] = true; + var d = normalizeDuplicateTime(occ.date); + if (d) info.times.push(d); + }); + var rows = Object.keys(shopsMap).sort().map(function (key) { + var info = shopsMap[key]; + info.times.sort(function (a, b) { return (a < b ? 1 : (a > b ? -1 : 0)); }); + info.times = Array.from(new Set(info.times)); + var rowSites = Object.keys(info.sites).sort().map(function (site) { + return dupSiteBadge(site); + }).join(''); + return '' + + '' + escapeHtml(info.shop) + '' + info.count + ' 次' + + '' + (rowSites || '-') + '' + + '' + info.times.map(escapeHtml).join('、') + '' + + ''; + }).join(''); + return '
    ' + + '
    ' + + '' + escapeHtml(item.asin) + '' + + '' + item.shop_count + ' 家店铺' + + '' + escapeHtml(item.brand || '') + '' + + '' + + '
    ' + + '' + + '' + + '' + rows + '
    店铺国家上架时间
    ' + + '
    '; + }).join(''); + } + + // 撞款详情分页加载:筛选/重置时回到第 1 页,矩阵翻页不影响详情页 + function loadShopDataDuplicateDetail(page) { + if (shopDataDuplicateDetailLoading) return; + shopDataDuplicateDetailLoading = true; + var block = document.getElementById('dupCheckDetailBlock'); + var cardsEl = document.getElementById('dupCheckDetailCards'); + var params = new URLSearchParams(); + params.set('page', String(page || 1)); + params.set('page_size', String(shopDataDuplicateDetailPageSize)); + var values = { + asin: document.getElementById('shopDataDupFilterAsin').value.trim(), + shop_name: document.getElementById('shopDataDupFilterShop').value.trim(), + country: document.getElementById('shopDataDupFilterCountry').value.trim(), + site: document.getElementById('shopDataDupFilterSite').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]); + }); + cardsEl.innerHTML = '
    加载中...
    '; + fetch('/api/admin/shop-data-crawl/duplicate-check-detail?' + params.toString()) + .then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载失败'); + shopDataDuplicateDetailItems = res.items || []; + shopDataDuplicateDetailTotal = Number(res.total) || 0; + shopDataDuplicateDetailPage = res.page || 1; + renderShopDataDuplicateDetailCards(); + if (block.style.display !== 'none') { + renderPagination('dupCheckDetailPagination', shopDataDuplicateDetailTotal, + shopDataDuplicateDetailPage, shopDataDuplicateDetailPageSize, + loadShopDataDuplicateDetail); + } + }) + .catch(function (error) { + shopDataDuplicateDetailItems = []; + shopDataDuplicateDetailTotal = 0; + renderShopDataDuplicateDetailCards(); + }) + .finally(function () { + shopDataDuplicateDetailLoading = false; + }); + } + + // 抽屉徽章:基础滴状标签(按上下文附加不同配色类) + function dupDrawerBadge(text, cls) { + return '' + escapeHtml(text) + ''; + } + + // 图3 样式抽屉:ASIN + 店在售徽章 / 品牌行 / 彩色徽章组 / 按时间升序明细 + 次数列 + function openShopDataDuplicateDrawer(asin) { + var item = null; + (shopDataDuplicateDetailItems || []).forEach(function (it) { if (it.asin === asin) item = it; }); + if (!item) { + (shopDataDuplicateItems || []).forEach(function (it) { if (it.asin === asin) item = it; }); + } + 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.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; + }); + // 店铺 + 国家两级聚合:次数 = 该店铺在该国家的上架记录数 + var countsMap = {}; + occurrences.forEach(function (occ) { + var shopKey = occ.shop_name || '-'; + var siteKey = (occ.country || '').trim().toUpperCase() || '-'; + var key = shopKey + '|||' + siteKey; + countsMap[key] = (countsMap[key] || 0) + 1; + }); + var rows = occurrences.map(function (occ) { + var shopKey = occ.shop_name || '-'; + var siteKey = (occ.country || '').trim().toUpperCase() || '-'; + var times = []; + if (occ._dupTime && occ._dupTime.length >= 16) times.push(occ._dupTime.slice(0, 16)); + else if (normalizeDuplicateTime(occ.date)) times.push(normalizeDuplicateTime(occ.date)); + else occ.date || ''; + var count = countsMap[shopKey + '|||' + siteKey] || 0; + var siteBadge = siteKey === '-' ? '-' : dupSiteBadge(siteKey); + return '' + + '' + escapeHtml(shopKey) + '' + + '' + siteBadge + '' + + '' + escapeHtml(times.join('')) + '' + + '' + count + ''; + }).join(''); + // 顶部「N 店在售」红色徽章 + var shopCount = Number(item.shop_count || 0); + var recordCount = Number(item.record_count || 0); + var headerBadges = + dupDrawerBadge(shopCount + ' 店在售', 'dup-drawer-badge-danger') + + dupDrawerBadge(shopCount + ' 家店铺', 'dup-drawer-badge-blue') + + Object.keys(sites).sort().map(function (site) { + 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') : ''); + document.getElementById('dupCheckDrawerAsin').textContent = item.asin; + document.getElementById('dupCheckDrawerAsin').className = 'dup-drawer-asin'; + var subtitleEl = document.getElementById('dupCheckDrawerSubtitle'); + subtitleEl.className = 'dup-drawer-subtitle dup-drawer-inline-badges'; + subtitleEl.innerHTML = headerBadges; + document.getElementById('dupCheckDrawerBody').innerHTML = + '
    品牌:' + escapeHtml(brand || '-') + '
    ' + + '
    ' + + '' + + '' + (rows || '') + '
    店铺国家上架时间(按时间升序)次数
    暂无明细
    '; + document.getElementById('dupCheckDrawerMask').classList.add('show'); + } + + function closeShopDataDuplicateDrawer() { + document.getElementById('dupCheckDrawerMask').classList.remove('show'); + } + + function loadShopDataDuplicateCheckOverview(force) { + var progress = document.getElementById('shopDataDuplicateProgress'); + var button = document.getElementById('btnRefreshShopDataDuplicates'); + if (button) button.disabled = true; + if (force) progress.textContent = '正在全量扫描各店铺结果文件,请稍候...'; + fetch('/api/admin/shop-data-crawl/duplicate-check-overview' + (force ? '?force=1' : '')) + .then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载失败'); + shopDataDuplicateOverviewPending = !!res.pending; + shopDataDuplicateScannedAt = res.scanned_at || ''; + var summary = res.summary || {}; + renderShopDataDuplicateMetrics(summary); + renderShopDataDuplicateDistribution(res.shops || []); + var dupTabCount = document.getElementById('dupCheckMonitorCount'); + var allTabCount = document.getElementById('dupCheckAllCount'); + if (dupTabCount) dupTabCount.textContent = summary.duplicate_asin_total != null ? summary.duplicate_asin_total : 0; + if (allTabCount) allTabCount.textContent = summary.asin_total != null ? summary.asin_total : 0; + loadShopDataDuplicateCheckItems(1); + }) + .catch(function (error) { + var metricsEl = document.getElementById('shopDataDuplicateMetrics'); + if (metricsEl) { + metricsEl.innerHTML = '
    加载失败:' + + escapeHtml(error.message || '') + '
    '; + } + }) + .finally(function () { + if (button) button.disabled = false; + if (progress) progress.textContent = ''; + }); + } + + function loadShopDataDuplicateCheckItems(page) { + if (shopDataDuplicateLoading) return; + shopDataDuplicatePage = page || 1; + shopDataDuplicateLoading = true; + var list = document.getElementById('shopDataDuplicateList'); + var progress = document.getElementById('shopDataDuplicateProgress'); + var exportButton = document.getElementById('btnExportShopDataDuplicates'); + list.innerHTML = '
    加载中...
    '; + if (exportButton) exportButton.disabled = true; + fetch('/api/admin/shop-data-crawl/duplicate-check-items?' + buildShopDataDuplicateQuery(shopDataDuplicatePage)) + .then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载失败'); + shopDataDuplicateItems = res.items || []; + shopDataDuplicateShops = res.shops || []; + shopDataDuplicateTotal = Number(res.total) || 0; + if (res.scanned_at) shopDataDuplicateScannedAt = res.scanned_at; + var totalEl = document.getElementById('shopDataDuplicateTotal'); + totalEl.textContent = (shopDataDuplicateView === 'monitor' ? '重复监控' : '全部ASIN台账') + + ' · 共 ' + shopDataDuplicateTotal + ' 个 ASIN' + + (shopDataDuplicateScannedAt ? ' · 扫描时间 ' + shopDataDuplicateScannedAt + : (shopDataDuplicateOverviewPending ? ' · 尚无扫描结果(点击「重新分析」立即扫描)' : '')); + renderShopDataDuplicateMatrix(); + loadShopDataDuplicateDetail(shopDataDuplicateDetailPage || 1); + renderPagination('shopDataDuplicatePagination', shopDataDuplicateTotal, + shopDataDuplicatePage, shopDataDuplicatePageSize, loadShopDataDuplicateCheckItems); + }) + .catch(function (error) { + shopDataDuplicateItems = []; + shopDataDuplicateTotal = 0; + list.innerHTML = '
    加载失败:' + escapeHtml(error.message || '') + '
    '; + document.getElementById('shopDataDuplicateTotal').textContent = ''; + loadShopDataDuplicateDetail(1); + }) + .finally(function () { + shopDataDuplicateLoading = false; + if (progress) progress.textContent = ''; + if (exportButton) exportButton.disabled = false; + }); + } + + function switchDupCheckView(view) { + if (view === shopDataDuplicateView) return; + shopDataDuplicateView = view; + var monitorTab = document.getElementById('dupCheckTabMonitor'); + var allTab = document.getElementById('dupCheckTabAll'); + var monitorActive = view === 'monitor'; + monitorTab.classList.toggle('active', monitorActive); + monitorTab.setAttribute('aria-selected', monitorActive ? 'true' : 'false'); + allTab.classList.toggle('active', !monitorActive); + allTab.setAttribute('aria-selected', !monitorActive ? 'true' : 'false'); + shopDataDuplicateDetailPage = 1; + loadShopDataDuplicateCheckItems(1); + } + + function exportShopDataDuplicates() { + if (shopDataDuplicateExporting) return; + shopDataDuplicateExporting = true; + var progress = document.getElementById('shopDataDuplicateProgress'); + var button = document.getElementById('btnExportShopDataDuplicates'); + if (button) button.disabled = true; + if (progress) progress.textContent = '正在导出当前筛选结果...'; + var query = new URLSearchParams(buildShopDataDuplicateQuery(1)); + query.delete('page'); + query.delete('page_size'); + fetch('/api/admin/shop-data-crawl/duplicate-check-export?' + query.toString()) + .then(function (response) { + if (!response.ok) { + return response.json().then(function (data) { throw new Error(data.error || '导出失败'); }); + } + return response.blob(); + }) + .then(function (blob) { + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + var now = new Date(); + function pad(n) { return n < 10 ? '0' + n : String(n); } + a.download = '店铺数据重复检查_' + now.getFullYear() + pad(now.getMonth() + 1) + pad(now.getDate()) + + '_' + pad(now.getHours()) + pad(now.getMinutes()) + '.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(function () { URL.revokeObjectURL(url); }, 1000); + }) + .catch(function (error) { + if (progress) progress.textContent = '导出失败:' + (error.message || ''); + }) + .finally(function () { + shopDataDuplicateExporting = false; + if (button) button.disabled = false; + if (progress) progress.textContent = ''; + }); + } + + function downloadShopDataTask(item) { + var resultId = shopDataResultId(item); + if (!item || !item.file_ready || !resultId) return; + var filename = item.output_filename || ('shop-data-task-' + resultId + '.xlsx'); + triggerImageVideoLink('/api/admin/shop-data-crawl-tasks/' + resultId + '/download', filename, false); + } + + function deleteShopDataTask(item) { + var resultId = shopDataResultId(item); + if (!item || !resultId || ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(String(item.status || item.file_status || '').toUpperCase()) < 0) return; + if (!window.confirm('确认删除店铺“' + (item.shop_name || '-') + '”的任务 ' + item.task_id + ' 及结果文件?')) return; + var progress = document.getElementById('shopDataTaskDownloadProgress'); + progress.textContent = '正在删除任务 ' + item.task_id + '...'; + fetch('/api/admin/shop-data-crawl-tasks/' + resultId, { method: 'DELETE' }) + .then(function (response) { return response.json().then(function (data) { return { ok: response.ok, data: data }; }); }) + .then(function (result) { + if (!result.ok || !result.data.success) throw new Error(result.data.error || '删除失败'); + progress.textContent = result.data.msg || '删除成功'; + loadShopDataCrawlTasks(shopDataTaskPage); + }) + .catch(function (error) { + progress.textContent = error.message || '删除失败'; + }); + } + + function downloadShopDataTasksZip() { + var resultIds = Array.from(selectedShopDataResultIds); + if (shopDataDownloadInProgress || !resultIds.length) return; + shopDataDownloadInProgress = true; + syncShopDataSelectionUi(); + var progress = document.getElementById('shopDataTaskDownloadProgress'); + progress.textContent = '正在打包 ' + resultIds.length + ' 个文件...'; + fetch('/api/admin/shop-data-crawl-tasks/download-zip', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ result_ids: resultIds }), + __skipLoading: true + }).then(function (response) { + if (!response.ok) { + return response.json().catch(function () { return {}; }).then(function (data) { + throw new Error(data.error || '压缩包生成失败'); + }); + } + var filename = imageVideoZipFilename(response); + var errorCount = Number(response.headers.get('X-Archive-Error-Count') || 0); + return response.blob().then(function (blob) { + return { blob: blob, filename: filename, errorCount: errorCount }; + }); + }).then(function (result) { + var objectUrl = URL.createObjectURL(result.blob); + triggerImageVideoLink(objectUrl, result.filename, false); + setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000); + progress.textContent = result.errorCount + ? '压缩包已下载,' + result.errorCount + ' 个文件失败,详见包内清单' + : '压缩包下载已开始'; + }).catch(function (error) { + progress.textContent = error.message || '批量下载失败'; + }).finally(function () { + shopDataDownloadInProgress = false; + syncShopDataSelectionUi(); + }); + } + + function shopDataPermissionUsersForView() { + return shopDataPermissionView === 'granted' + ? shopDataPermissionUsers.filter(function (user) { + return shopDataPermissionInitialUserIds.has(Number(user.id)); + }) + : shopDataPermissionUsers; + } + + function filteredShopDataPermissionUsers() { + var users = shopDataPermissionUsersForView(); + var keyword = (document.getElementById('shopDataTaskPermissionSearch').value || '').trim().toLowerCase(); + return keyword ? users.filter(function (user) { + return String(user.username || '').toLowerCase().indexOf(keyword) >= 0; + }) : users; + } + + function renderShopDataPermissionUsers() { + var visibleUsers = filteredShopDataPermissionUsers(); + document.getElementById('shopDataTaskPermissionGrantedCount').textContent = '(' + shopDataPermissionInitialUserIds.size + ')'; + document.getElementById('shopDataTaskPermissionAllCount').textContent = '(' + shopDataPermissionUsers.length + ')'; + document.querySelectorAll('[data-shop-data-permission-view]').forEach(function (tab) { + var active = tab.dataset.shopDataPermissionView === shopDataPermissionView; + tab.classList.toggle('active', active); + tab.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + var pendingCount = shopDataPermissionUsers.filter(function (user) { + var userId = Number(user.id); + return shopDataPermissionInitialUserIds.has(userId) !== selectedShopDataPermissionUserIds.has(userId); + }).length; + document.getElementById('shopDataTaskPermissionSummary').textContent = + (shopDataPermissionView === 'granted' ? '当前显示已分配用户,共 ' + visibleUsers.length + ' 人' : '当前显示全部用户,已分配 ' + shopDataPermissionInitialUserIds.size + ' 人') + + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : ''); + document.getElementById('shopDataTaskPermissionList').innerHTML = visibleUsers.length + ? visibleUsers.map(function (user) { + var userId = Number(user.id); + var saved = shopDataPermissionInitialUserIds.has(userId); + var selected = selectedShopDataPermissionUserIds.has(userId); + var changed = saved !== selected; + return '
    ' + + '' + + '' + escapeHtml(user.username || '-') + '' + escapeHtml(roleLabel(user.role)) + '' + + '
    '; + }).join('') + : '
    暂无匹配用户
    '; + var selectedVisibleCount = visibleUsers.filter(function (user) { + return selectedShopDataPermissionUserIds.has(Number(user.id)); + }).length; + var selectAll = document.getElementById('shopDataTaskPermissionSelectAll'); + selectAll.checked = visibleUsers.length > 0 && selectedVisibleCount === visibleUsers.length; + selectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleUsers.length; + selectAll.disabled = visibleUsers.length === 0; + } + + function openShopDataTaskPermissions() { + if (currentUserRole !== 'super_admin') return; + var modal = document.getElementById('shopDataTaskPermissionModal'); + var saveButton = document.getElementById('btnSaveShopDataTaskPermissions'); + var permissionLoaded = false; + modal.classList.add('show'); + shopDataPermissionView = 'granted'; + document.getElementById('shopDataTaskPermissionSearch').value = ''; + document.getElementById('shopDataTaskPermissionMessage').textContent = ''; + document.getElementById('shopDataTaskPermissionList').innerHTML = '
    加载中...
    '; + saveButton.disabled = true; + fetch('/api/admin/shop-data-crawl-task-permissions') + .then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '权限加载失败'); + shopDataPermissionUsers = res.items || []; + shopDataPermissionInitialUserIds = new Set(shopDataPermissionUsers.filter(function (user) { return !!user.granted; }).map(function (user) { return Number(user.id); })); + selectedShopDataPermissionUserIds = new Set(shopDataPermissionInitialUserIds); + permissionLoaded = true; + renderShopDataPermissionUsers(); + }).catch(function (error) { + shopDataPermissionUsers = []; + shopDataPermissionInitialUserIds = new Set(); + selectedShopDataPermissionUserIds = new Set(); + document.getElementById('shopDataTaskPermissionList').innerHTML = '
    ' + escapeHtml(error.message || '权限加载失败') + '
    '; + document.getElementById('shopDataTaskPermissionMessage').textContent = '权限加载失败,请关闭后重试'; + }).finally(function () { + saveButton.disabled = !permissionLoaded; + }); + } + + function closeShopDataTaskPermissions() { + document.getElementById('shopDataTaskPermissionModal').classList.remove('show'); + } + + function saveShopDataTaskPermissions() { + var message = document.getElementById('shopDataTaskPermissionMessage'); + message.textContent = '保存中...'; + message.className = 'msg'; + document.getElementById('btnSaveShopDataTaskPermissions').disabled = true; + fetch('/api/admin/shop-data-crawl-task-permissions', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ user_ids: Array.from(selectedShopDataPermissionUserIds).sort(function (a, b) { return a - b; }) }) + }).then(function (response) { return response.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '权限保存失败'); + shopDataPermissionInitialUserIds = new Set(selectedShopDataPermissionUserIds); + renderShopDataPermissionUsers(); + message.textContent = res.msg || '保存成功'; + message.className = 'msg ok'; + }).catch(function (error) { + message.textContent = error.message || '权限保存失败'; + message.className = 'msg err'; + }).finally(function () { + document.getElementById('btnSaveShopDataTaskPermissions').disabled = false; + }); + } + + document.getElementById('btnFilterShopDataTasks').onclick = function () { + loadShopDataCrawlTasks(1); + }; + document.getElementById('btnResetShopDataTasks').onclick = function () { + ['shopDataTaskFilterShop', 'shopDataTaskFilterGroup', 'shopDataTaskFilterCountry', 'shopDataTaskFilterFrom', 'shopDataTaskFilterTo'] + .forEach(function (id) { document.getElementById(id).value = ''; }); + loadShopDataCrawlTasks(1); + }; + document.getElementById('btnRefreshShopDataDuplicates').onclick = function () { loadShopDataDuplicateCheckOverview(true); }; + document.getElementById('btnFilterShopDataDuplicates').onclick = function () { + shopDataDuplicateDetailPage = 1; + loadShopDataDuplicateCheckItems(1); + }; + document.getElementById('btnResetShopDataDuplicates').onclick = function () { + ['shopDataDupFilterAsin', 'shopDataDupFilterShop', 'shopDataDupFilterCountry', 'shopDataDupFilterSite', 'shopDataDupFilterDateFrom', 'shopDataDupFilterDateTo'] + .forEach(function (id) { document.getElementById(id).value = ''; }); + shopDataDuplicateDetailPage = 1; + loadShopDataDuplicateCheckItems(1); + }; + document.getElementById('dupCheckTabMonitor').onclick = function () { switchDupCheckView('monitor'); }; + document.getElementById('dupCheckTabAll').onclick = function () { switchDupCheckView('all'); }; + document.getElementById('btnExportShopDataDuplicates').onclick = exportShopDataDuplicates; + document.getElementById('btnCloseDupCheckDrawer').onclick = closeShopDataDuplicateDrawer; + document.getElementById('dupCheckDrawerMask').onclick = function (event) { + if (event.target === this) closeShopDataDuplicateDrawer(); + }; + // 矩阵表格内 ASIN / 数字徽标点击打开抽屉(动态元素,事件委托) + document.getElementById('shopDataDuplicateList').onclick = function (event) { + var target = event.target.closest('[data-open-drawer]'); + if (target) openShopDataDuplicateDrawer(target.dataset.openDrawer); + }; + // 撞款详情卡片区「查看明细」按钮(动态元素,事件委托) + document.getElementById('dupCheckDetailCards').onclick = handleDuplicateDetailClick; + // 全选移到表格表头后为动态元素,用事件委托 + document.getElementById('shopDataTaskGrid').onchange = function (event) { + var checkbox = event.target.closest('[data-shop-data-select]'); + if (checkbox) { + var resultId = Number(checkbox.dataset.shopDataSelect); + if (!resultId) return; + if (checkbox.checked) selectedShopDataResultIds.add(resultId); + else selectedShopDataResultIds.delete(resultId); + syncShopDataSelectionUi(); + } + }; + // 全选移到工具栏后不在 grid 容器内,单独绑定 change 事件 + document.getElementById('shopDataTaskSelectAll').onchange = function (event) { + 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]'); + if (downloadButton) { + var downloadItem = shopDataTasks.find(function (task) { return shopDataResultId(task) === Number(downloadButton.dataset.shopDataDownload); }); + downloadShopDataTask(downloadItem); + return; + } + var deleteButton = event.target.closest('[data-shop-data-delete]'); + if (deleteButton) { + var deleteItem = shopDataTasks.find(function (task) { return shopDataResultId(task) === Number(deleteButton.dataset.shopDataDelete); }); + deleteShopDataTask(deleteItem); + } + }; + document.getElementById('btnBatchDownloadShopDataTasks').onclick = downloadShopDataTasksZip; + document.getElementById('btnOpenShopDataTaskPermissions').onclick = openShopDataTaskPermissions; + document.getElementById('btnCloseShopDataTaskPermissions').onclick = closeShopDataTaskPermissions; + document.getElementById('btnCancelShopDataTaskPermissions').onclick = closeShopDataTaskPermissions; + document.getElementById('btnSaveShopDataTaskPermissions').onclick = saveShopDataTaskPermissions; + document.querySelectorAll('[data-shop-data-permission-view]').forEach(function (tab) { + tab.onclick = function () { + shopDataPermissionView = tab.dataset.shopDataPermissionView || 'granted'; + document.getElementById('shopDataTaskPermissionSearch').value = ''; + renderShopDataPermissionUsers(); + }; + }); + document.getElementById('shopDataTaskPermissionSearch').oninput = renderShopDataPermissionUsers; + document.getElementById('shopDataTaskPermissionSelectAll').onchange = function (event) { + filteredShopDataPermissionUsers().forEach(function (user) { + if (event.target.checked) selectedShopDataPermissionUserIds.add(Number(user.id)); + else selectedShopDataPermissionUserIds.delete(Number(user.id)); + }); + renderShopDataPermissionUsers(); + }; + document.getElementById('shopDataTaskPermissionList').onclick = function (event) { + var button = event.target.closest('[data-shop-data-permission-toggle]'); + if (!button) return; + var userId = Number(button.dataset.shopDataPermissionToggle); + if (selectedShopDataPermissionUserIds.has(userId)) selectedShopDataPermissionUserIds.delete(userId); + else selectedShopDataPermissionUserIds.add(userId); + renderShopDataPermissionUsers(); + }; + document.getElementById('shopDataTaskPermissionList').onchange = function (event) { + var checkbox = event.target.closest('[data-shop-data-permission-user]'); + if (!checkbox) return; + var userId = Number(checkbox.dataset.shopDataPermissionUser); + if (checkbox.checked) selectedShopDataPermissionUserIds.add(userId); + else selectedShopDataPermissionUserIds.delete(userId); + renderShopDataPermissionUsers(); + }; + document.getElementById('shopDataTaskPermissionModal').onclick = function (event) { + if (event.target === event.currentTarget) closeShopDataTaskPermissions(); + }; + + var historyPage = 1, historyPageSize = 15; + function toSqlDatetime(val) { + if (!val) return ''; + return val.replace('T', ' '); + } + function buildHistoryQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + historyPageSize; + var uid = document.getElementById('filterUser').value; + var start = toSqlDatetime(document.getElementById('filterTimeStart').value); + var end = toSqlDatetime(document.getElementById('filterTimeEnd').value); + if (uid) q += '&user_id=' + uid; + if (start) q += '&time_start=' + encodeURIComponent(start); + if (end) q += '&time_end=' + encodeURIComponent(end); + return q; + } + function panelTypeLabel(value) { + var panelType = String(value || ''); + var labels = { + textToImage: '反推词', productMainImage: '产品主图', buyerShow: '买家秀', + productPoster: '产品海报', clonePoster: '克隆海报', randomPoster: '随机海报', + clothingDetail: '服装详情', productDetail: '产品详情', extremeDetail: '极致详情', + cloneDetail: '克隆详情', imageEdit: '图片编辑' + }; + if (labels[panelType]) return labels[panelType]; + return panelType ? panelType : '-'; + } + function loadHistory(page) { + historyPage = page || 1; + fetch('/api/admin/history?' + buildHistoryQuery(historyPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('historyListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无记录'; + } else { + tbody.innerHTML = items.map(function (h) { + var urls = (h.result_urls || []); + var thumbUrls = (h.long_image_url ? [h.long_image_url] : []).concat(urls); + var thumbs = thumbUrls.slice(0, 3).map(function (url) { + return ''; + }).join(''); + return '' + h.id + '' + (h.username || '-') + '' + + panelTypeLabel(h.panel_type) + '' + (h.created_at || '') + '' + + '
    ' + (thumbs || '-') + '
    '; + }).join(''); + } + renderPagination('historyPagination', res.total, res.page, res.page_size, loadHistory); + }) + .catch(function () { + document.getElementById('historyListBody').innerHTML = '请求失败'; + }); + } + var shopManageAllUsers = []; + function escapeHtml(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + // 站点列改成纯展示后,点击 ASIN 文本即复制。 + function copyAsinText(button) { + var value = button && button.dataset ? (button.dataset.copyAsin || '') : ''; + if (!value) return; + var copyPromise; + if (navigator.clipboard && navigator.clipboard.writeText) { + copyPromise = navigator.clipboard.writeText(value); + } else { + copyPromise = new Promise(function (resolve, reject) { + var textarea = document.createElement('textarea'); + textarea.value = value; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + var copied = document.execCommand('copy'); + textarea.remove(); + if (copied) resolve(); + else reject(new Error('copy failed')); + }); + } + copyPromise.then(function () { + button.classList.add('is-copied'); + setTimeout(function () { button.classList.remove('is-copied'); }, 1200); + if (window.__adminToast) window.__adminToast('已复制 ' + value, 'info'); + }).catch(function () { + if (window.__adminToast) window.__adminToast('复制失败,请手动选择', 'error'); + }); + } + var dropdownMultiSelects = {}; + function initDropdownMultiSelect(selectId, placeholder) { + var select = document.getElementById(selectId); + if (!select || dropdownMultiSelects[selectId]) return; + + select.classList.add('multi-select-native'); + var wrapper = document.createElement('div'); + wrapper.className = 'multi-select-dropdown'; + wrapper.setAttribute('data-multi-select-id', selectId); + + var trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'multi-select-trigger'; + trigger.textContent = placeholder || '请选择'; + + var panel = document.createElement('div'); + panel.className = 'multi-select-panel'; + + select.parentNode.insertBefore(wrapper, select); + wrapper.appendChild(trigger); + wrapper.appendChild(panel); + wrapper.appendChild(select); + + function selectedOptions() { + return Array.from(select.options).filter(function (option) { return option.selected; }); + } + + function updateSummary() { + var selected = selectedOptions(); + if (!selected.length) { + trigger.textContent = placeholder || '请选择'; + trigger.title = ''; + return; + } + var labels = selected.map(function (option) { return option.textContent || option.value; }); + trigger.textContent = labels.join('、'); + trigger.title = labels.join('、'); + } + + function renderOptions() { + panel.innerHTML = Array.from(select.options).map(function (option, index) { + var optionId = selectId + '_multi_' + index; + return ''; + }).join(''); + panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) { + checkbox.onchange = function () { + var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))]; + if (!option) return; + option.selected = checkbox.checked; + updateSummary(); + select.dispatchEvent(new Event('change', { bubbles: true })); + }; + }); + } + + trigger.onclick = function (event) { + event.stopPropagation(); + document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) { + if (item !== wrapper) item.classList.remove('open'); + }); + wrapper.classList.toggle('open'); + }; + panel.onclick = function (event) { + event.stopPropagation(); + }; + select.addEventListener('change', function () { + panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) { + var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))]; + checkbox.checked = !!(option && option.selected); + }); + updateSummary(); + }); + + renderOptions(); + updateSummary(); + dropdownMultiSelects[selectId] = { + refresh: function () { + renderOptions(); + updateSummary(); + } + }; + } + + function refreshDropdownMultiSelect(selectId) { + if (dropdownMultiSelects[selectId]) { + dropdownMultiSelects[selectId].refresh(); + } + } + + document.addEventListener('click', function () { + document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) { + item.classList.remove('open'); + }); + }); + function getEligibleShopManageGroupUsers(leaderUserId) { + var canViewAllMembers = currentUserRole === 'super_admin'; + return shopManageAllUsers.filter(function (u) { + if (String(u.id || '') === String(leaderUserId || '')) return false; + if (canViewAllMembers) return true; + return String(u.role || '') === 'normal' && + String(u.created_by_id || '') === String(leaderUserId || ''); + }); + } + + // ========== 分组管理通讯录:按拼音分组 + 模糊搜索 + 字母索引 ========== + // 隐藏 select(shopManageGroupMemberSelect)仅作为数据交换层,保存逻辑不变。 + var shopManageGroupEligibleUsers = []; + var shopManageGroupSelectedIds = {}; + var shopManageGroupSearchKeyword = ''; + var shopManageGroupContactGroupsCache = []; + var SHOP_GROUP_AVATAR_COLORS = ['#f97316', '#0ea5e9', '#8b5cf6', '#10b981', '#ef4444', '#eab308', '#14b8a6', '#6366f1']; + + function shopGroupAvatarColor(name) { + var code = String(name || '#').charCodeAt(0) || 0; + return SHOP_GROUP_AVATAR_COLORS[code % SHOP_GROUP_AVATAR_COLORS.length]; + } + function shopGroupUserInitial(user) { + var abbr = String(user.pinyin_abbr || '').trim(); + if (abbr) { + var first = abbr.charAt(0).toUpperCase(); + return /^[A-Z0-9]$/.test(first) ? first : '#'; + } + var name = String(user.username || '').trim(); + if (!name) return '#'; + var first = name.charAt(0); + if (/[A-Za-z0-9]/.test(first)) return first.toUpperCase(); + return '#'; + } + function shopGroupMatchSearch(user, keyword) { + if (!keyword) return true; + var kw = keyword.toLowerCase(); + if (String(user.username || '').toLowerCase().indexOf(kw) !== -1) return true; + if (String(user.pinyin_abbr || '').toLowerCase().indexOf(kw) !== -1) return true; + return false; + } + function buildShopManageGroupContactGroups(users) { + var groups = {}; + users.forEach(function (u) { + var initial = shopGroupUserInitial(u); + var key = /^[A-Z]$/.test(initial) ? initial : '#'; + (groups[key] = groups[key] || []).push(u); + }); + var keys = Object.keys(groups).sort(function (a, b) { + if (a === '#') return 1; + if (b === '#') return -1; + return a < b ? -1 : 1; + }); + return keys.map(function (key) { + var list = groups[key].slice().sort(function (a, b) { + return String(a.username || '').localeCompare(String(b.username || ''), 'zh'); + }); + return { key: key, items: list }; + }); + } + function renderShopManageGroupContact() { + var contactEl = document.getElementById('shopManageGroupContact'); + var indexEl = document.getElementById('shopManageGroupMemberIndex'); + if (!contactEl) return; + var keyword = (shopManageGroupSearchKeyword || '').trim(); + var filtered = shopManageGroupEligibleUsers.filter(function (u) { + return shopGroupMatchSearch(u, keyword); + }); + if (!shopManageGroupEligibleUsers.length) { + contactEl.innerHTML = '
    当前组长名下暂无可添加的普通员工账号。
    '; + if (indexEl) indexEl.innerHTML = ''; + return; + } + if (!filtered.length) { + contactEl.innerHTML = '
    未找到与「' + escapeHtml(keyword) + '」匹配的用户,可尝试输入拼音首字母,如 zwh。
    '; + if (indexEl) indexEl.innerHTML = ''; + return; + } + shopManageGroupContactGroupsCache = buildShopManageGroupContactGroups(filtered); + contactEl.innerHTML = shopManageGroupContactGroupsCache.map(function (group) { + var itemsHtml = group.items.map(function (u) { + var selected = !!shopManageGroupSelectedIds[String(u.id)]; + var name = u.username || ''; + return '
    ' + + '' + escapeHtml(shopGroupUserInitial(u)) + '' + + '' + escapeHtml(name) + '' + + '' + + '
    '; + }).join(''); + return '
    ' + escapeHtml(group.key) + '
    ' + + '
    ' + itemsHtml + '
    '; + }).join(''); + if (indexEl) { + indexEl.innerHTML = shopManageGroupContactGroupsCache.map(function (group) { + return ''; + }).join(''); + } + } + function syncShopManageGroupMemberSelect() { + var sel = document.getElementById('shopManageGroupMemberSelect'); + if (!sel) return; + sel.innerHTML = shopManageGroupEligibleUsers.map(function (u) { + var selected = !!shopManageGroupSelectedIds[String(u.id)]; + return ''; + }).join(''); + } + function updateShopManageGroupMemberCount() { + var countEl = document.getElementById('shopManageGroupMemberCount'); + if (countEl) { + countEl.textContent = '已选 ' + Object.keys(shopManageGroupSelectedIds).length + ' 人'; + } + } + function toggleShopManageGroupMember(userId) { + var key = String(userId || ''); + if (!key) return; + var contactEl = document.getElementById('shopManageGroupContact'); + var itemEl = contactEl ? Array.prototype.find.call( + contactEl.querySelectorAll('.shop-group-contact-item'), + function (el) { return el.dataset.userId === key; } + ) : null; + if (shopManageGroupSelectedIds[key]) { + delete shopManageGroupSelectedIds[key]; + } else { + shopManageGroupSelectedIds[key] = true; + } + if (itemEl) itemEl.classList.toggle('is-selected', !!shopManageGroupSelectedIds[key]); + syncShopManageGroupMemberSelect(); + updateShopManageGroupMemberCount(); + } + function shopGroupScrollToLetter(letter) { + var contactEl = document.getElementById('shopManageGroupContact'); + if (!contactEl) return; + var groupEl = Array.prototype.find.call( + contactEl.querySelectorAll('.shop-group-contact-group'), + function (el) { return el.dataset.letter === letter; } + ); + if (groupEl) groupEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + function resetShopManageGroupSearch() { + shopManageGroupSearchKeyword = ''; + var searchInput = document.getElementById('shopManageGroupMemberSearch'); + if (searchInput) searchInput.value = ''; + } + + function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) { + var sel = document.getElementById('shopManageGroupMemberSelect'); + var helpEl = document.getElementById('shopManageGroupMemberHelp'); + if (!sel) return; + resetShopManageGroupSearch(); + shopManageGroupEligibleUsers = getEligibleShopManageGroupUsers(leaderUserId); + shopManageGroupSelectedIds = {}; + (selectedUserIds || []).forEach(function (id) { + shopManageGroupSelectedIds[String(id)] = true; + }); + syncShopManageGroupMemberSelect(); + renderShopManageGroupContact(); + updateShopManageGroupMemberCount(); + if (helpEl) { + helpEl.textContent = shopManageGroupEligibleUsers.length + ? '可添加当前组长创建的普通员工账号,点击用户即可选中 / 取消;顶部支持按用户名或拼音首字母搜索。' + : (currentUserRole === 'normal' + ? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。' + : '当前组长名下暂无可添加的普通员工账号。'); + } + } + function setShopManageGroupLeader(leaderUserId, leaderUsername, selectedUserIds) { + var leaderIdEl = document.getElementById('shopManageGroupLeaderUserId'); + var leaderNameEl = document.getElementById('shopManageGroupLeaderName'); + if (leaderIdEl) leaderIdEl.value = leaderUserId ? String(leaderUserId) : ''; + if (leaderNameEl) leaderNameEl.value = leaderUsername || ''; + refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds || []); + } + function getSelectedShopManageGroupMemberIds() { + var sel = document.getElementById('shopManageGroupMemberSelect'); + if (!sel) return []; + return Array.prototype.slice.call(sel.options || []) + .filter(function (opt) { return !!opt.selected; }) + .map(function (opt) { return Number(opt.value); }) + .filter(function (id) { return !!id; }); + } + function loadUserOptions() { + return fetch('/api/admin/users?page=1&page_size=999') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + throw new Error(res.error || '加载用户失败'); + } + var items = res.items || []; + shopManageAllUsers = items.map(function (u) { + return { + id: u.id, + username: u.username || '', + role: u.role || 'normal', + created_by_id: u.created_by_id || null, + pinyin_abbr: u.pinyin_abbr || '' + }; + }); + setShopManageGroupLeader( + document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '', + document.getElementById('shopManageGroupLeaderName') ? document.getElementById('shopManageGroupLeaderName').value : '', + getSelectedShopManageGroupMemberIds() + ); + var sel = document.getElementById('filterUser'); + var cur = sel.value; + sel.innerHTML = ''; + items.forEach(function (u) { + var opt = document.createElement('option'); + opt.value = u.id; + opt.textContent = u.username || ''; + sel.appendChild(opt); + }); + sel.value = cur || ''; + }) + .catch(function () { + shopManageAllUsers = []; + var sel = document.getElementById('filterUser'); + if (sel) { + sel.innerHTML = ''; + } + refreshShopManageGroupMemberSelect( + document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '', + [] + ); + }); + } + document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); }; + + // ========== 数据去重总数据 ========== + var dedupeTotalDataPage = 1, dedupeTotalDataPageSize = 15; + function getDedupeTotalDataDateRange() { + return { + startDate: document.getElementById('exportDedupeTotalDataStartDate').value || '', + endDate: document.getElementById('exportDedupeTotalDataEndDate').value || '' + }; + } + function validateDedupeTotalDataDateRange() { + var dateRange = getDedupeTotalDataDateRange(); + if (dateRange.startDate && dateRange.endDate && dateRange.startDate > dateRange.endDate) { + alert('开始日期不能晚于结束日期'); + return false; + } + return true; + } + function buildDedupeTotalDataQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize; + var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim(); + var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim(); + var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim(); + var country = (document.getElementById('dedupeTotalDataCountryFilter').value || '').trim(); + var dateRange = getDedupeTotalDataDateRange(); + if (keyword) q += '&keyword=' + encodeURIComponent(keyword); + if (username) q += '&username=' + encodeURIComponent(username); + if (groupId) q += '&group_id=' + encodeURIComponent(groupId); + if (country) q += '&country=' + encodeURIComponent(country); + if (dateRange.startDate) q += '&start_date=' + encodeURIComponent(dateRange.startDate); + if (dateRange.endDate) q += '&end_date=' + encodeURIComponent(dateRange.endDate); + return q; + } + function getDedupeTotalDataCountryLabel(countryCodes) { + var countryLabels = { + DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙' + }; + if (Array.isArray(countryCodes)) { + return countryCodes.map(function (code) { return countryLabels[code] || code; }).join('、'); + } + return String(countryCodes || '').split(/[,,]/).map(function (code) { + code = code.trim(); + return countryLabels[code] || code; + }).filter(Boolean).join('、'); + } + function loadDedupeTotalData(page) { + if (!validateDedupeTotalDataDateRange()) return; + dedupeTotalDataPage = page || 1; + fetch('/api/admin/dedupe-total-data?' + buildDedupeTotalDataQuery(dedupeTotalDataPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('dedupeTotalDataListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + escapeHtml(res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无总数据'; + } else { + tbody.innerHTML = items.map(function (item) { + return '' + escapeHtml(item.id) + '' + escapeHtml(item.data_value || '') + '' + escapeHtml(getDedupeTotalDataCountryLabel(item.country)) + '' + escapeHtml(item.username || '') + '' + escapeHtml(item.group_name || '未分组') + '' + escapeHtml(item.created_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData); + bindDedupeTotalDataActions(); + }) + .catch(function () { + document.getElementById('dedupeTotalDataListBody').innerHTML = '请求失败'; + }); + } + function bindDedupeTotalDataActions() { + document.querySelectorAll('[data-dedupe-total-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || ''; + document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/"/g, '"'); + document.getElementById('msgEditDedupeTotalData').textContent = ''; + document.getElementById('msgEditDedupeTotalData').className = 'msg'; + var groupId = btn.dataset.groupId || ''; + loadShopManageGroups().then(function () { + document.getElementById('editDedupeTotalDataGroupId').value = groupId; + document.getElementById('editDedupeTotalDataModal').classList.add('show'); + }); + }; + }); + document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) { + btn.onclick = function () { + var value = (btn.dataset.value || '').replace(/"/g, '"'); + if (!confirm('确定删除总数据“' + value + '”吗?')) return; + fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); }; + var dedupeTotalDataExportButton = document.getElementById('btnExportDedupeTotalData'); + var dedupeTotalDataExportWait = document.getElementById('dedupeTotalDataExportWait'); + var dedupeTotalDataExportWaitSeconds = document.getElementById('dedupeTotalDataExportWaitSeconds'); + var dedupeTotalDataExportWaitTimer = null; + function showDedupeTotalDataExportWait() { + var startedAt = Date.now(); + dedupeTotalDataExportWaitSeconds.textContent = '0'; + dedupeTotalDataExportWait.classList.add('show'); + dedupeTotalDataExportWait.setAttribute('aria-hidden', 'false'); + dedupeTotalDataExportWaitTimer = setInterval(function () { + dedupeTotalDataExportWaitSeconds.textContent = String(Math.floor((Date.now() - startedAt) / 1000)); + }, 1000); + } + function hideDedupeTotalDataExportWait() { + clearInterval(dedupeTotalDataExportWaitTimer); + dedupeTotalDataExportWaitTimer = null; + dedupeTotalDataExportWait.classList.remove('show'); + dedupeTotalDataExportWait.setAttribute('aria-hidden', 'true'); + } + dedupeTotalDataExportButton.onclick = function () { + if (dedupeTotalDataExportButton.disabled) return; + var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim(); + var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim(); + var country = (document.getElementById('dedupeTotalDataCountryFilter').value || '').trim(); + var dateRange = getDedupeTotalDataDateRange(); + if (!validateDedupeTotalDataDateRange()) return; + var params = []; + if (username) params.push('username=' + encodeURIComponent(username)); + if (groupId) params.push('group_id=' + encodeURIComponent(groupId)); + if (country) params.push('country=' + encodeURIComponent(country)); + if (dateRange.startDate) params.push('start_date=' + encodeURIComponent(dateRange.startDate)); + if (dateRange.endDate) params.push('end_date=' + encodeURIComponent(dateRange.endDate)); + var originalButtonText = dedupeTotalDataExportButton.textContent; + dedupeTotalDataExportButton.disabled = true; + dedupeTotalDataExportButton.setAttribute('aria-busy', 'true'); + dedupeTotalDataExportButton.textContent = '导出中...'; + showDedupeTotalDataExportWait(); + fetch('/api/admin/dedupe-total-data/export' + (params.length ? ('?' + params.join('&')) : '')) + .then(function (response) { + var contentType = response.headers.get('content-type') || ''; + if (!response.ok || contentType.indexOf('application/json') >= 0) { + return response.json().then(function (res) { + throw new Error((res && (res.error || res.msg)) || '导出失败'); + }); + } + return response.blob().then(function (blob) { + return { + blob: blob, + filename: extractDownloadFilename( + response.headers.get('content-disposition'), + 'dedupe-total-data.xlsx' + ) + }; + }); + }) + .then(function (payload) { + triggerBrowserDownload(payload.blob, payload.filename); + }) + .catch(function (err) { + alert((err && err.message) || '导出失败'); + }) + .finally(function () { + dedupeTotalDataExportButton.disabled = false; + dedupeTotalDataExportButton.removeAttribute('aria-busy'); + dedupeTotalDataExportButton.textContent = originalButtonText; + hideDedupeTotalDataExportWait(); + }); + }; + var dedupeImportPollTimer = null; + var dedupeDeleteImportPollTimer = null; + function stopDedupeImportProgress() { + if (dedupeImportPollTimer) { + clearInterval(dedupeImportPollTimer); + dedupeImportPollTimer = null; + } + } + function stopDedupeDeleteImportProgress() { + if (dedupeDeleteImportPollTimer) { + clearInterval(dedupeDeleteImportPollTimer); + dedupeDeleteImportPollTimer = null; + } + } + function setDedupeImportProgress(percent, text) { + var wrap = document.getElementById('dedupeTotalDataProgressWrap'); + var fill = document.getElementById('dedupeTotalDataProgressFill'); + var textEl = document.getElementById('dedupeTotalDataProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function setDedupeDeleteImportProgress(percent, text) { + var wrap = document.getElementById('dedupeTotalDataDeleteProgressWrap'); + var fill = document.getElementById('dedupeTotalDataDeleteProgressFill'); + var textEl = document.getElementById('dedupeTotalDataDeleteProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function pollDedupeImport(importId) { + stopDedupeImportProgress(); + function tick() { + fetch('/api/admin/dedupe-total-data/import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = res.error || '查询导入进度失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setDedupeImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '导入成功:总行数 ' + (progress.total_rows || 0) + ',ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgDedupeTotalData').className = 'msg ok'; + loadDedupeTotalData(1); + } else if (progress.status === 'failed') { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = progress.error_message || '导入失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + } + }) + .catch(function () { + stopDedupeImportProgress(); + document.getElementById('msgDedupeTotalData').textContent = '查询导入进度失败'; + document.getElementById('msgDedupeTotalData').className = 'msg err'; + }); + } + tick(); + dedupeImportPollTimer = setInterval(tick, 1000); + } + function pollDedupeDeleteImport(importId) { + stopDedupeDeleteImportProgress(); + function tick() { + fetch('/api/admin/dedupe-total-data/delete-import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = res.error || '查询删除进度失败'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setDedupeDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = '删除成功:总行数 ' + (progress.total_rows || 0) + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgDeleteDedupeTotalData').className = 'msg ok'; + loadDedupeTotalData(1); + } else if (progress.status === 'failed') { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = progress.error_message || '删除失败'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + } + }) + .catch(function () { + stopDedupeDeleteImportProgress(); + document.getElementById('msgDeleteDedupeTotalData').textContent = '查询删除进度失败'; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg err'; + }); + } + tick(); + dedupeDeleteImportPollTimer = setInterval(tick, 1000); + } + + document.getElementById('btnOpenCreateDedupeTotalData').onclick = function () { + document.getElementById('dedupeTotalDataFile').value = ''; + document.getElementById('dedupeTotalDataImportGroupId').value = ''; + document.getElementById('msgDedupeTotalData').textContent = ''; + document.getElementById('msgDedupeTotalData').className = 'msg'; + document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none'; + document.getElementById('createDedupeTotalDataModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateDedupeTotalDataModal').onclick = function () { + document.getElementById('createDedupeTotalDataModal').classList.remove('show'); + }; + document.getElementById('btnOpenDeleteDedupeTotalData').onclick = function () { + document.getElementById('dedupeTotalDataDeleteFile').value = ''; + document.getElementById('dedupeTotalDataDeleteGroupId').value = ''; + document.getElementById('msgDeleteDedupeTotalData').textContent = ''; + document.getElementById('msgDeleteDedupeTotalData').className = 'msg'; + document.getElementById('dedupeTotalDataDeleteProgressWrap').style.display = 'none'; + document.getElementById('deleteDedupeTotalDataModal').classList.add('show'); + }; + document.getElementById('btnCloseDeleteDedupeTotalDataModal').onclick = function () { + document.getElementById('deleteDedupeTotalDataModal').classList.remove('show'); + }; + document.getElementById('btnAddDedupeTotalData').onclick = function () { + var fileInput = document.getElementById('dedupeTotalDataFile'); + var msgEl = document.getElementById('msgDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + stopDedupeImportProgress(); + document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none'; + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '请选择 Excel 文件'; + msgEl.classList.add('err'); + return; + } + var groupId = (document.getElementById('dedupeTotalDataImportGroupId').value || '').trim(); + if (!groupId) { + msgEl.textContent = '请选择分组'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + var lowerName = (file.name || '').toLowerCase(); + if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { + msgEl.textContent = '仅支持 .xlsx 或 .xls 文件'; + msgEl.classList.add('err'); + return; + } + var formData = new FormData(); + formData.append('file', file); + formData.append('group_id', groupId); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/admin/dedupe-total-data/import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + setDedupeImportProgress(percent, '上传中:' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; } + if (!res.success) { + msgEl.textContent = res.error || '导入失败'; + msgEl.className = 'msg err'; + return; + } + setDedupeImportProgress(100, '上传完成,后端处理中...'); + pollDedupeImport(res.import_id); + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + }; + document.getElementById('btnDeleteImportDedupeTotalData').onclick = function () { + var fileInput = document.getElementById('dedupeTotalDataDeleteFile'); + var msgEl = document.getElementById('msgDeleteDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + stopDedupeDeleteImportProgress(); + document.getElementById('dedupeTotalDataDeleteProgressWrap').style.display = 'none'; + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '请选择 Excel 文件'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + var lowerName = (file.name || '').toLowerCase(); + if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { + msgEl.textContent = '仅支持 .xlsx 或 .xls 文件'; + msgEl.classList.add('err'); + return; + } + if (!confirm('确定按 Excel 中的 ASIN 批量删除匹配的总数据吗?')) { + return; + } + var groupId = (document.getElementById('dedupeTotalDataDeleteGroupId').value || '').trim(); + if (!groupId) { + msgEl.textContent = '请选择分组'; + msgEl.classList.add('err'); + return; + } + var formData = new FormData(); + formData.append('file', file); + formData.append('group_id', groupId); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/admin/dedupe-total-data/delete-import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + setDedupeDeleteImportProgress(percent, '上传中:' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; } + if (!res.success) { + msgEl.textContent = res.error || '删除失败'; + msgEl.className = 'msg err'; + return; + } + setDedupeDeleteImportProgress(100, '上传完成,后端处理中...'); + pollDedupeDeleteImport(res.import_id); + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + }; + document.getElementById('btnSaveDedupeTotalData').onclick = function () { + var itemId = document.getElementById('editDedupeTotalDataId').value; + var value = (document.getElementById('editDedupeTotalDataValue').value || '').trim(); + var groupId = (document.getElementById('editDedupeTotalDataGroupId').value || '').trim(); + var msgEl = document.getElementById('msgEditDedupeTotalData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!value) { + msgEl.textContent = '请填写总数据值'; + msgEl.classList.add('err'); + return; + } + if (!groupId) { + msgEl.textContent = '请选择分组'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/dedupe-total-data/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data_value: value, group_id: Number(groupId) }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editDedupeTotalDataModal').classList.remove('show'); + loadDedupeTotalData(dedupeTotalDataPage); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + document.getElementById('btnCloseEditDedupeTotalData').onclick = function () { + document.getElementById('editDedupeTotalDataModal').classList.remove('show'); + }; + + // ========== 品牌数据库 ========== + var invalidAsinDataPage = 1, invalidAsinDataPageSize = 15; + function buildInvalidAsinDataQuery(page) { + var q = 'page=' + (page || 1) + '&page_size=' + invalidAsinDataPageSize; + var dataValue = (document.getElementById('searchInvalidAsinData').value || '').trim(); + var brand = (document.getElementById('searchInvalidAsinDataBrand').value || '').trim(); + var groupId = (document.getElementById('invalidAsinDataFilterGroupId').value || '').trim(); + if (dataValue) q += '&data_value=' + encodeURIComponent(dataValue); + if (brand) q += '&brand=' + encodeURIComponent(brand); + if (groupId) q += '&group_id=' + encodeURIComponent(groupId); + return q; + } + function loadInvalidAsinData(page) { + invalidAsinDataPage = page || 1; + fetch('/api/admin/invalid-asin-data?' + buildInvalidAsinDataQuery(invalidAsinDataPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('invalidAsinDataListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无数据'; + } else { + tbody.innerHTML = items.map(function (item) { + var dataValueAttr = (item.data_value || '').replace(/"/g, '"'); + var brandAttr = (item.brand || '').replace(/"/g, '"'); + var groupId = item.group_id == null ? '' : String(item.group_id); + var source = String(item.record_source || 'AUTO').toUpperCase(); + var sourceLabel = source === 'MANUAL' ? '\u624b\u52a8\u65b0\u589e' : '\u81ea\u52a8\u5bfc\u5165'; + return '' + escapeHtml(item.id || '') + '' + escapeHtml(item.data_value || '') + '' + escapeHtml(item.brand || '') + '' + escapeHtml(item.group_name || '') + '' + sourceLabel + '' + escapeHtml(item.created_at || '') + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('invalidAsinDataPagination', res.total, res.page, res.page_size, loadInvalidAsinData); + bindInvalidAsinDataActions(); + }) + .catch(function () { + document.getElementById('invalidAsinDataListBody').innerHTML = '请求失败'; + }); + } + function bindInvalidAsinDataActions() { + document.querySelectorAll('[data-invalid-asin-edit]').forEach(function (btn) { + btn.onclick = function () { + document.getElementById('editInvalidAsinDataId').value = btn.dataset.invalidAsinEdit || ''; + document.getElementById('editInvalidAsinDataValue').value = (btn.dataset.value || '').replace(/"/g, '"'); + document.getElementById('editInvalidAsinDataBrand').value = (btn.dataset.brand || '').replace(/"/g, '"'); + var source = String(btn.dataset.source || 'AUTO').toUpperCase(); + var isManual = source === 'MANUAL'; + document.getElementById('editInvalidAsinDataRecordSource').value = source; + document.getElementById('editInvalidAsinDataSourceLabel').value = isManual + ? '\u624b\u52a8\u65b0\u589e' + : '\u81ea\u52a8\u5bfc\u5165'; + document.getElementById('editInvalidAsinDataGroupForm').style.display = isManual ? '' : 'none'; + if (isManual) { + loadShopManageGroups().then(function () { + document.getElementById('editInvalidAsinDataGroupSelect').value = currentUserRole === 'super_admin' + ? (btn.dataset.groupId || '') + : getInvalidAsinDataLockedGroupId(); + }); + } + document.getElementById('msgEditInvalidAsinData').textContent = ''; + document.getElementById('msgEditInvalidAsinData').className = 'msg'; + document.getElementById('editInvalidAsinDataModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-invalid-asin-delete]').forEach(function (btn) { + btn.onclick = function () { + var value = (btn.dataset.value || '').replace(/"/g, '"'); + if (!confirm('确定删除“' + value + '”吗?')) return; + fetch('/api/admin/invalid-asin-data/' + btn.dataset.invalidAsinDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadInvalidAsinData(invalidAsinDataPage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + document.getElementById('btnSearchInvalidAsinData').onclick = function () { loadInvalidAsinData(1); }; + document.getElementById('btnOpenCreateInvalidAsinData').onclick = function () { + document.getElementById('invalidAsinDataValue').value = ''; + document.getElementById('invalidAsinDataBrand').value = ''; + document.getElementById('invalidAsinDataGroupSelect').value = currentUserRole === 'super_admin' + ? '' + : getInvalidAsinDataLockedGroupId(); + document.getElementById('msgInvalidAsinData').textContent = ''; + document.getElementById('msgInvalidAsinData').className = 'msg'; + document.getElementById('createInvalidAsinDataModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateInvalidAsinData').onclick = function () { + document.getElementById('createInvalidAsinDataModal').classList.remove('show'); + }; + document.getElementById('btnAddInvalidAsinData').onclick = function () { + var dataValue = (document.getElementById('invalidAsinDataValue').value || '').trim(); + var brand = (document.getElementById('invalidAsinDataBrand').value || '').trim(); + var groupId = (document.getElementById('invalidAsinDataGroupSelect').value || '').trim(); + var msgEl = document.getElementById('msgInvalidAsinData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!dataValue) { + msgEl.textContent = '请填写 ASIN'; + msgEl.classList.add('err'); + return; + } + if (!groupId) { + msgEl.textContent = '请选择分组'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/invalid-asin-data', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data_value: dataValue, brand: brand, group_id: Number(groupId) }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '创建成功'; + msgEl.classList.add('ok'); + document.getElementById('invalidAsinDataValue').value = ''; + document.getElementById('invalidAsinDataBrand').value = ''; + document.getElementById('invalidAsinDataGroupSelect').value = currentUserRole === 'super_admin' + ? '' + : getInvalidAsinDataLockedGroupId(); + loadInvalidAsinData(1); + } else { + msgEl.textContent = res.error || '创建失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + document.getElementById('btnSaveInvalidAsinData').onclick = function () { + var itemId = document.getElementById('editInvalidAsinDataId').value; + var dataValue = (document.getElementById('editInvalidAsinDataValue').value || '').trim(); + var brand = (document.getElementById('editInvalidAsinDataBrand').value || '').trim(); + var source = String(document.getElementById('editInvalidAsinDataRecordSource').value || 'AUTO').toUpperCase(); + var groupId = (document.getElementById('editInvalidAsinDataGroupSelect').value || '').trim(); + var msgEl = document.getElementById('msgEditInvalidAsinData'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!dataValue) { + msgEl.textContent = '请填写 ASIN'; + msgEl.classList.add('err'); + return; + } + if (source === 'MANUAL' && !groupId) { + msgEl.textContent = '请选择分组'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/invalid-asin-data/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + data_value: dataValue, + brand: brand, + group_id: source === 'MANUAL' ? Number(groupId) : null + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editInvalidAsinDataModal').classList.remove('show'); + loadInvalidAsinData(invalidAsinDataPage); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + document.getElementById('btnCloseEditInvalidAsinData').onclick = function () { + document.getElementById('editInvalidAsinDataModal').classList.remove('show'); + }; + + // ========== 店铺密钥管理 ========== + var shopKeyPage = 1, shopKeyPageSize = 15; + function buildShopKeyQuery(page) { + return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize; + } + function renderShopKeyWhitelistStatus(item) { + var status = String(item.ip_whitelist_status || 'UNKNOWN').toUpperCase(); + var statusMeta = { + ALLOWED: { label: '正常', className: 'is-allowed' }, + BLOCKED: { label: '未加白名单', className: 'is-blocked' }, + UNKNOWN: { label: '未检测', className: 'is-unknown' } + }; + var meta = statusMeta[status] || statusMeta.UNKNOWN; + var details = []; + if (item.ip_whitelist_checked_at) details.push('检测时间:' + item.ip_whitelist_checked_at); + if (item.ip_whitelist_message) details.push(item.ip_whitelist_message); + var detailText = details.join('\n') || ('白名单状态:' + meta.label); + return '' + escapeHtml(meta.label) + ''; + } + function loadShopKeys(page) { + shopKeyPage = page || 1; + fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('shopKeyListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无店铺密钥'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1; + return '' + rowNo + '' + renderShopTableText(item.remark_name) + '' + renderShopTableText(item.ziniao_account_name) + '' + renderShopTableText(item.ziniao_token) + '' + renderShopKeyWhitelistStatus(item) + '' + renderShopTableText(item.created_at) + '' + renderShopTableText(item.updated_at) + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys); + bindShopKeyActions(); + }) + .catch(function () { + document.getElementById('shopKeyListBody').innerHTML = '请求失败'; + }); + } + function bindShopKeyActions() { + document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = {}; + try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; } + document.getElementById('editShopKeyId').value = item.id || ''; + document.getElementById('editShopKeyRemarkName').value = item.remark_name || ''; + document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || ''; + document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || ''; + document.getElementById('msgEditShopKey').textContent = ''; + document.getElementById('msgEditShopKey').className = 'msg'; + document.getElementById('editShopKeyModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-shop-key-delete]').forEach(function (btn) { + btn.onclick = function () { + var name = (btn.dataset.ziniaoAccountName || '').replace(/"/g, '"'); + if (!confirm('确定删除店铺密钥“' + name + '”吗?')) return; + fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadShopKeys(shopKeyPage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + document.getElementById('btnOpenCreateShopKey').onclick = function () { + document.getElementById('shopKeyRemarkName').value = ''; + document.getElementById('shopKeyZiniaoAccountName').value = ''; + document.getElementById('shopKeyZiniaoToken').value = ''; + document.getElementById('msgShopKey').textContent = ''; + document.getElementById('msgShopKey').className = 'msg'; + document.getElementById('createShopKeyModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateShopKey').onclick = function () { + document.getElementById('createShopKeyModal').classList.remove('show'); + }; + document.getElementById('btnCreateShopKey').onclick = function () { + var remarkName = (document.getElementById('shopKeyRemarkName').value || '').trim(); + var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim(); + var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim(); + var msgEl = document.getElementById('msgShopKey'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!ziniaoAccountName || !ziniaoToken) { + msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-key', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + remark_name: remarkName, + ziniao_account_name: ziniaoAccountName, + ziniao_token: ziniaoToken + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('shopKeyRemarkName').value = ''; + document.getElementById('shopKeyZiniaoAccountName').value = ''; + document.getElementById('shopKeyZiniaoToken').value = ''; + msgEl.textContent = res.msg || '创建成功'; + msgEl.className = 'msg ok'; + loadShopKeys(1); + } else { + msgEl.textContent = res.error || '创建失败'; + msgEl.className = 'msg err'; + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + document.getElementById('btnSaveShopKey').onclick = function () { + var itemId = document.getElementById('editShopKeyId').value; + var remarkName = (document.getElementById('editShopKeyRemarkName').value || '').trim(); + var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim(); + var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim(); + var msgEl = document.getElementById('msgEditShopKey'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!ziniaoAccountName || !ziniaoToken) { + msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-key/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + remark_name: remarkName, + ziniao_account_name: ziniaoAccountName, + ziniao_token: ziniaoToken + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editShopKeyModal').classList.remove('show'); + loadShopKeys(shopKeyPage); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + document.getElementById('btnCloseEditShopKey').onclick = function () { + document.getElementById('editShopKeyModal').classList.remove('show'); + }; + + // ========== 店铺管理 ========== + var shopManagePage = 1, shopManagePageSize = 15; + var shopManageGroups = []; + var shopManageGroupsLoadedAt = 0; + var shopManageLockedGroupId = null; + var currentShopManageGroupGrantRoutes = []; + + function buildShopManageQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize; + var groupId = (document.getElementById('shopManageFilterGroupId').value || '').trim(); + var shopName = (document.getElementById('shopManageFilterShopName').value || '').trim(); + if (groupId) query += '&group_id=' + encodeURIComponent(groupId); + if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); + return query; + } + + function shopPasswordIcon(revealed) { + if (revealed) { + return ''; + } + return ''; + } + + function renderShopPasswordCell(item) { + var maskedPassword = item.password || '******'; + return '' + + '' + escapeHtml(maskedPassword) + '' + + ''; + } + + function renderShopTableText(value, fallback) { + var text = String(value == null ? '' : value).trim(); + var shown = text || fallback || '-'; + return '' + escapeHtml(shown) + ''; + } + + function loadShopManage(page) { + shopManagePage = page || 1; + fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('shopManageListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + if (items.length === 0) { + tbody.innerHTML = '暂无店铺'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1; + return '' + + '' + rowNo + '' + + '' + renderShopTableText(item.group_name) + '' + + '' + renderShopTableText(item.shop_name) + '' + + '' + renderShopTableText(item.mall_name) + '' + + '' + renderShopTableText(item.zn_username) + '' + + '' + renderShopTableText(item.account) + '' + + '' + renderShopPasswordCell(item) + '' + + '' + renderShopTableText(item.created_at) + '' + + '' + renderShopTableText(item.updated_at) + '' + + '' + + ' ' + + '' + + ''; + }).join(''); + } + renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage); + bindShopManageActions(); + }) + .catch(function () { + document.getElementById('shopManageListBody').innerHTML = '请求失败'; + }); + } + + function bindShopManageActions() { + document.querySelectorAll('[data-shop-password-toggle]').forEach(function (btn) { + btn.onclick = function () { + var valueEl = btn.parentElement.querySelector('[data-shop-password-value]'); + var revealed = btn.dataset.revealed === 'true'; + if (revealed) { + valueEl.textContent = btn.dataset.maskedPassword || '******'; + valueEl.title = btn.dataset.maskedPassword || '******'; + btn.dataset.revealed = 'false'; + btn.setAttribute('aria-label', '显示密码'); + btn.setAttribute('aria-pressed', 'false'); + btn.title = '显示密码'; + btn.innerHTML = shopPasswordIcon(false); + return; + } + + btn.disabled = true; + valueEl.textContent = '读取中...'; + fetch('/api/admin/shop-manage/' + encodeURIComponent(btn.dataset.shopPasswordToggle) + '/credential?shop_name=' + encodeURIComponent(btn.dataset.shopName || '')) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '读取密码失败'); + valueEl.textContent = res.password || ''; + valueEl.title = res.password || ''; + btn.dataset.revealed = 'true'; + btn.setAttribute('aria-label', '隐藏密码'); + btn.setAttribute('aria-pressed', 'true'); + btn.title = '隐藏密码'; + btn.innerHTML = shopPasswordIcon(true); + }) + .catch(function (error) { + valueEl.textContent = btn.dataset.maskedPassword || '******'; + alert(error.message || '读取密码失败'); + }) + .finally(function () { + btn.disabled = false; + }); + }; + }); + document.querySelectorAll('[data-shop-manage-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = {}; + try { item = JSON.parse((btn.dataset.shopManage || '').replace(/"/g, '"')); } catch (e) { item = {}; } + document.getElementById('editShopManageId').value = item.id || ''; + document.getElementById('editShopManageShopName').value = item.shop_name || ''; + document.getElementById('editShopManageMallName').value = item.mall_name || ''; + document.getElementById('editShopManageZnUsername').value = item.zn_username || ''; + document.getElementById('editShopManageAccount').value = item.account || ''; + document.getElementById('editShopManagePassword').value = item.password || ''; + document.getElementById('msgEditShopManage').textContent = ''; + document.getElementById('msgEditShopManage').className = 'msg'; + loadShopManageGroups(null, item.group_id).then(function () { + document.getElementById('editShopManageModal').classList.add('show'); + }); + }; + }); + document.querySelectorAll('[data-shop-manage-delete]').forEach(function (btn) { + btn.onclick = function () { + var name = (btn.dataset.shopManageName || '').replace(/"/g, '"'); + if (!confirm('确定删除店铺“' + name + '”吗?')) return; + fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadShopManage(shopManagePage); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + } + + function getInvalidAsinDataLockedGroupId() { + if (currentUserRole === 'super_admin') return ''; + if (shopManageLockedGroupId) return String(shopManageLockedGroupId); + return shopManageGroups.length ? String(shopManageGroups[0].id || '') : ''; + } + + function refreshShopGroupSelects(selectedCreateId, selectedEditId) { + var createSel = document.getElementById('shopManageGroupSelect'); + var editSel = document.getElementById('editShopManageGroupSelect'); + var invalidCreateSel = document.getElementById('invalidAsinDataGroupSelect'); + var invalidEditSel = document.getElementById('editInvalidAsinDataGroupSelect'); + var invalidFilterSel = document.getElementById('invalidAsinDataFilterGroupId'); + var dedupeImportSel = document.getElementById('dedupeTotalDataImportGroupId'); + var dedupeDeleteSel = document.getElementById('dedupeTotalDataDeleteGroupId'); + var dedupeFilterSel = document.getElementById('dedupeTotalDataGroupFilterId'); + var dedupeEditSel = document.getElementById('editDedupeTotalDataGroupId'); + var filterSel = document.getElementById('shopManageFilterGroupId'); + var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); + var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); + var skipImportSel = document.getElementById('skipPriceAsinImportGroupId'); + var skipDeleteImportSel = document.getElementById('skipPriceAsinDeleteImportGroupId'); + var queryCreateSel = document.getElementById('queryAsinGroupSelect'); + var queryFilterSel = document.getElementById('queryAsinFilterGroupId'); + var queryImportSel = document.getElementById('queryAsinImportGroupId'); + var queryDeleteImportSel = document.getElementById('queryAsinDeleteImportGroupId'); + var selectedInvalidCreateId = invalidCreateSel ? invalidCreateSel.value : ''; + var selectedInvalidEditId = invalidEditSel ? invalidEditSel.value : ''; + var selectedInvalidFilterId = invalidFilterSel ? invalidFilterSel.value : ''; + var selectedDedupeImportId = dedupeImportSel ? dedupeImportSel.value : ''; + var selectedDedupeDeleteId = dedupeDeleteSel ? dedupeDeleteSel.value : ''; + var selectedDedupeFilterId = dedupeFilterSel ? dedupeFilterSel.value : ''; + var selectedDedupeEditId = dedupeEditSel ? dedupeEditSel.value : ''; + var selectedFilterId = filterSel ? filterSel.value : ''; + var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; + var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; + var selectedQueryCreateId = queryCreateSel ? queryCreateSel.value : ''; + var selectedQueryFilterId = queryFilterSel ? queryFilterSel.value : ''; + var createOpts = ['']; + var filterOpts = ['']; + shopManageGroups.forEach(function (g) { + var option = ''; + createOpts.push(option); + filterOpts.push(option); + }); + createSel.innerHTML = createOpts.join(''); + editSel.innerHTML = createOpts.join(''); + var lockedInvalidGroupId = getInvalidAsinDataLockedGroupId(); + var lockedGroup = shopManageGroups.find(function (group) { + return String(group.id || '') === lockedInvalidGroupId; + }); + var invalidGroupOpts = createOpts; + if (currentUserRole !== 'super_admin') { + invalidGroupOpts = lockedGroup + ? [''] + : [createOpts[0]]; + } + var invalidFilterOpts = currentUserRole === 'super_admin' + ? filterOpts + : (lockedGroup + ? [''] + : [createOpts[0]]); + if (invalidCreateSel) { + invalidCreateSel.innerHTML = invalidGroupOpts.join(''); + invalidCreateSel.disabled = currentUserRole !== 'super_admin'; + } + if (invalidEditSel) { + invalidEditSel.innerHTML = invalidGroupOpts.join(''); + invalidEditSel.disabled = currentUserRole !== 'super_admin'; + } + if (invalidFilterSel) { + invalidFilterSel.innerHTML = invalidFilterOpts.join(''); + invalidFilterSel.disabled = currentUserRole !== 'super_admin'; + } + if (dedupeImportSel) dedupeImportSel.innerHTML = createOpts.join(''); + if (dedupeDeleteSel) dedupeDeleteSel.innerHTML = createOpts.join(''); + if (dedupeFilterSel) dedupeFilterSel.innerHTML = filterOpts.join(''); + if (dedupeEditSel) dedupeEditSel.innerHTML = createOpts.join(''); + if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); + if (queryCreateSel) queryCreateSel.innerHTML = createOpts.join(''); + if (skipImportSel) skipImportSel.innerHTML = createOpts.join(''); + if (skipDeleteImportSel) skipDeleteImportSel.innerHTML = createOpts.join(''); + if (queryImportSel) queryImportSel.innerHTML = createOpts.join(''); + if (queryDeleteImportSel) queryDeleteImportSel.innerHTML = createOpts.join(''); + if (filterSel) filterSel.innerHTML = filterOpts.join(''); + if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); + if (queryFilterSel) queryFilterSel.innerHTML = filterOpts.join(''); + if (selectedCreateId != null) createSel.value = String(selectedCreateId); + if (selectedEditId != null) editSel.value = String(selectedEditId); + if (invalidCreateSel) { + invalidCreateSel.value = currentUserRole === 'super_admin' + ? selectedInvalidCreateId + : lockedInvalidGroupId; + } + if (invalidEditSel) { + invalidEditSel.value = currentUserRole === 'super_admin' + ? selectedInvalidEditId + : lockedInvalidGroupId; + } + if (invalidFilterSel) { + invalidFilterSel.value = currentUserRole === 'super_admin' + ? selectedInvalidFilterId + : lockedInvalidGroupId; + } + if (dedupeImportSel && selectedDedupeImportId) dedupeImportSel.value = selectedDedupeImportId; + if (dedupeDeleteSel && selectedDedupeDeleteId) dedupeDeleteSel.value = selectedDedupeDeleteId; + if (dedupeFilterSel && selectedDedupeFilterId) dedupeFilterSel.value = selectedDedupeFilterId; + if (dedupeEditSel && selectedDedupeEditId) dedupeEditSel.value = selectedDedupeEditId; + if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; + if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; + if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; + if (queryCreateSel && selectedQueryCreateId) queryCreateSel.value = selectedQueryCreateId; + if (queryFilterSel && selectedQueryFilterId) queryFilterSel.value = selectedQueryFilterId; + } + + function renderShopManageGroupSummary() { + var summaryEl = document.getElementById('shopManageGroupSummary'); + if (!summaryEl) return; + if (currentUserRole === 'super_admin') { + summaryEl.innerHTML = '全部分组 · ' + + escapeHtml(shopManageGroups.length) + ' 个'; + return; + } + if (!shopManageGroups.length) { + summaryEl.textContent = '未加入分组'; + return; + } + var visibleGroups = shopManageGroups.slice(0, 4); + summaryEl.innerHTML = visibleGroups.map(function (group) { + var relation = String(group.leader_user_id || '') === String(currentUserId || '') ? '组长' : '组员'; + return '' + + escapeHtml(group.group_name || '') + ' · ' + relation + ''; + }).join('') + (shopManageGroups.length > visibleGroups.length + ? '另 ' + escapeHtml(shopManageGroups.length - visibleGroups.length) + ' 个' + : ''); + } + + function loadShopManageGroups(selectedCreateId, selectedEditId, force) { + if (!force && shopManageGroups.length && Date.now() - shopManageGroupsLoadedAt < 30000) { + refreshShopGroupSelects(selectedCreateId, selectedEditId); + return Promise.resolve(shopManageGroups); + } + return fetch('/api/admin/shop-manage-groups') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载分组失败'); + shopManageGroups = res.items || []; + shopManageLockedGroupId = res.locked_group_id || null; + shopManageGroupsLoadedAt = Date.now(); + refreshShopGroupSelects(selectedCreateId, selectedEditId); + return shopManageGroups; + }) + .catch(function () { + shopManageGroups = []; + shopManageLockedGroupId = null; + shopManageGroupsLoadedAt = 0; + refreshShopGroupSelects(); + return []; + }); + } + + function renderDedupeGroupSummary() { + var summaryEl = document.getElementById('dedupeGroupSummary'); + if (!summaryEl) return; + if (currentUserRole === 'super_admin') { + summaryEl.innerHTML = '全部分组 · ' + + escapeHtml(shopManageGroups.length) + ' 个'; + return; + } + if (!shopManageGroups.length) { + summaryEl.textContent = '未加入分组'; + return; + } + var visibleGroups = shopManageGroups.slice(0, 4); + summaryEl.innerHTML = visibleGroups.map(function (group) { + var relation = String(group.leader_user_id || '') === String(currentUserId || '') ? '组长' : '组员'; + return '' + + escapeHtml(group.group_name || '') + ' · ' + relation + ''; + }).join('') + (shopManageGroups.length > visibleGroups.length + ? '另 ' + escapeHtml(shopManageGroups.length - visibleGroups.length) + ' 个' + : ''); + } + + function loadDedupeGroupSummary(force) { + var summaryEl = document.getElementById('dedupeGroupSummary'); + if (!summaryEl) return Promise.resolve([]); + summaryEl.textContent = '正在加载...'; + return loadShopManageGroups(null, null, force).then(function (groups) { + renderDedupeGroupSummary(); + return groups; + }); + } + + function resetShopManageGroupForm() { + document.getElementById('shopManageGroupEditId').value = ''; + document.getElementById('shopManageGroupLeaderUserId').value = currentUserId || ''; + document.getElementById('shopManageGroupLeaderName').value = currentUserUsername || ''; + document.getElementById('shopManageGroupInput').value = ''; + document.getElementById('msgShopManageGroup').textContent = ''; + document.getElementById('msgShopManageGroup').className = 'msg'; + refreshShopManageGroupMemberSelect(currentUserId, []); + } + + function setShopManageGroupGrantRoutes(routes) { + currentShopManageGroupGrantRoutes = Array.isArray(routes) ? routes.slice() : []; + } + + // 分组管理已升级为独立菜单(panel-group-manage),各功能面板内不再有「管理分组」按钮 + function updateShopManageGroupButtonsAccess() { + // 面板按钮已全部移除,无需控制显隐 + } + + // 分组管理独立菜单页初始化 + function initShopManageGroupPanel() { + resetShopManageGroupForm(); + loadUserOptions() + .then(function () { return loadShopManageGroups(); }) + .then(function () { + renderShopManageGroupRows(); + renderShopManageGroupSummary(); + }); + } + + function renderShopManageGroupRows() { + var tbody = document.getElementById('shopManageGroupListBody'); + if (!shopManageGroups.length) { + tbody.innerHTML = '暂无分组'; + return; + } + tbody.innerHTML = shopManageGroups.map(function (item, index) { + var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames : []; + var memberHtml = memberNames.length + ? '
    ' + + memberNames.map(function (name) { + return '' + escapeHtml(name || '') + ''; + }).join('') + '
    ' + : '-'; + var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || ''); + var actionHtml = canEdit + ? (' ' + + '') + : '-'; + return '' + + '' + (index + 1) + '' + + '' + escapeHtml(item.group_name || '') + '' + + '' + escapeHtml(item.leader_username || '') + '' + + '' + escapeHtml(item.member_count || 0) + '' + + '' + memberHtml + '' + + '' + escapeHtml(item.created_at || '') + '' + + '' + escapeHtml(item.updated_at || '') + '' + + '' + actionHtml + '' + + ''; + }).join(''); + + document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = null; + shopManageGroups.some(function (group) { + if (String(group.id || '') === String(btn.dataset.shopGroupEdit || '')) { + item = group; + return true; + } + return false; + }); + if (!item) return; + document.getElementById('shopManageGroupEditId').value = item.id || ''; + document.getElementById('shopManageGroupInput').value = item.group_name || ''; + setShopManageGroupLeader(item.leader_user_id || '', item.leader_username || '', item.member_user_ids || []); + document.getElementById('msgShopManageGroup').textContent = ''; + document.getElementById('msgShopManageGroup').className = 'msg'; + document.getElementById('shopManageGroupModalTitle').textContent = '编辑分组'; + document.getElementById('shopManageGroupModal').classList.add('show'); + }; + }); + + document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { + btn.onclick = function () { + var gid = btn.dataset.shopGroupDelete; + var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); + if (!confirm('确定删除分组“' + gname + '”吗?')) return; + fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + alert(res.error || '删除失败'); + return; + } + loadShopManageGroups(null, null, true).then(function () { + renderShopManageGroupRows(); + renderShopManageGroupSummary(); + renderDedupeGroupSummary(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + if (getActiveAdminTabName() === 'dedupe-total-data') loadDedupeTotalData(dedupeTotalDataPage); + if (getActiveAdminTabName() === 'invalid-asin-data') loadInvalidAsinData(invalidAsinDataPage); + }); + }); + }; + }); + } + + // 分组管理已升级为独立菜单(panel-group-manage via group-manage tab),新建/编辑走 shopManageGroupModal 弹窗 + setShopManageGroupGrantRoutes(['group-manage']); + document.getElementById('btnOpenCreateShopManageGroup').onclick = function () { + resetShopManageGroupForm(); + document.getElementById('shopManageGroupModalTitle').textContent = '新建分组'; + document.getElementById('shopManageGroupModal').classList.add('show'); + }; + document.getElementById('btnCloseShopManageGroupModal').onclick = function () { + document.getElementById('shopManageGroupModal').classList.remove('show'); + }; + // 组员通讯录:搜索 / 字母索引 / 点击选中(打开弹窗时 refresh 会重置搜索词) + var groupMemberSearchInput = document.getElementById('shopManageGroupMemberSearch'); + if (groupMemberSearchInput) { + groupMemberSearchInput.addEventListener('input', function () { + shopManageGroupSearchKeyword = groupMemberSearchInput.value || ''; + renderShopManageGroupContact(); + }); + } + var groupMemberIndexEl = document.getElementById('shopManageGroupMemberIndex'); + if (groupMemberIndexEl) { + groupMemberIndexEl.addEventListener('click', function (e) { + var btn = e.target.closest('button[data-letter]'); + if (btn) shopGroupScrollToLetter(btn.dataset.letter); + }); + } + var groupMemberContactEl = document.getElementById('shopManageGroupContact'); + if (groupMemberContactEl) { + groupMemberContactEl.addEventListener('click', function (e) { + var item = e.target.closest('.shop-group-contact-item'); + if (item) toggleShopManageGroupMember(item.dataset.userId); + }); + } + document.getElementById('btnSearchShopManage').onclick = function () { + loadShopManage(1); + }; + document.getElementById('shopManageFilterShopName').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadShopManage(1); + } + }); + document.getElementById('btnSaveShopManageGroup').onclick = function () { + var editId = (document.getElementById('shopManageGroupEditId').value || '').trim(); + var groupName = (document.getElementById('shopManageGroupInput').value || '').trim(); + var memberUserIds = getSelectedShopManageGroupMemberIds(); + var msgEl = document.getElementById('msgShopManageGroup'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupName) { + msgEl.textContent = '请输入分组名称'; + msgEl.className = 'msg err'; + return; + } + var method = editId ? 'PUT' : 'POST'; + var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group'; + fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + group_name: groupName, + member_user_ids: memberUserIds, + grant_menu_routes: currentShopManageGroupGrantRoutes + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '保存失败'; + msgEl.className = 'msg err'; + return; + } + resetShopManageGroupForm(); + document.getElementById('shopManageGroupModal').classList.remove('show'); + msgEl.textContent = res.msg || '保存成功'; + msgEl.className = 'msg ok'; + loadShopManageGroups(null, null, true).then(function () { + renderShopManageGroupRows(); + renderShopManageGroupSummary(); + renderDedupeGroupSummary(); + loadShopManage(shopManagePage); + loadSkipPriceAsin(skipPriceAsinPage); + if (getActiveAdminTabName() === 'dedupe-total-data') loadDedupeTotalData(dedupeTotalDataPage); + if (getActiveAdminTabName() === 'invalid-asin-data') loadInvalidAsinData(invalidAsinDataPage); + }); + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + + document.getElementById('btnOpenCreateShopManage').onclick = function () { + document.getElementById('shopManageGroupSelect').value = ''; + document.getElementById('shopManageShopName').value = ''; + document.getElementById('shopManageMallName').value = ''; + document.getElementById('shopManageZnUsername').value = ''; + document.getElementById('shopManageAccount').value = ''; + document.getElementById('shopManagePassword').value = ''; + document.getElementById('msgShopManage').textContent = ''; + document.getElementById('msgShopManage').className = 'msg'; + document.getElementById('createShopManageModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateShopManage').onclick = function () { + document.getElementById('createShopManageModal').classList.remove('show'); + }; + document.getElementById('btnCreateShopManage').onclick = function () { + var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim(); + var shopName = (document.getElementById('shopManageShopName').value || '').trim(); + var mallName = (document.getElementById('shopManageMallName').value || '').trim(); + var znUsername = (document.getElementById('shopManageZnUsername').value || '').trim(); + var account = (document.getElementById('shopManageAccount').value || '').trim(); + var password = (document.getElementById('shopManagePassword').value || '').trim(); + var msgEl = document.getElementById('msgShopManage'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !mallName || !account || !password) { + msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-manage', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, zn_username: znUsername, account: account, password: password }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('shopManageGroupSelect').value = ''; + document.getElementById('shopManageShopName').value = ''; + document.getElementById('shopManageMallName').value = ''; + document.getElementById('shopManageZnUsername').value = ''; + document.getElementById('shopManageAccount').value = ''; + document.getElementById('shopManagePassword').value = ''; + msgEl.textContent = res.msg || '创建成功'; + msgEl.className = 'msg ok'; + loadShopManage(1); + } else { + msgEl.textContent = res.error || '创建失败'; + msgEl.className = 'msg err'; + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + + document.getElementById('btnSaveShopManage').onclick = function () { + var itemId = document.getElementById('editShopManageId').value; + var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim(); + var shopName = (document.getElementById('editShopManageShopName').value || '').trim(); + var mallName = (document.getElementById('editShopManageMallName').value || '').trim(); + var znUsername = (document.getElementById('editShopManageZnUsername').value || '').trim(); + var account = (document.getElementById('editShopManageAccount').value || '').trim(); + var password = (document.getElementById('editShopManagePassword').value || '').trim(); + var msgEl = document.getElementById('msgEditShopManage'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !mallName || !account || !password) { + msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码'; + msgEl.classList.add('err'); + return; + } + fetch('/api/admin/shop-manage/' + itemId, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, zn_username: znUsername, account: account, password: password }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editShopManageModal').classList.remove('show'); + loadShopManage(shopManagePage); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.classList.add('err'); + }); + }; + document.getElementById('btnCloseEditShopManage').onclick = function () { + document.getElementById('editShopManageModal').classList.remove('show'); + }; + + // ========== 版本管理 ========== + // ========== 新增 ASIN 弹窗共用:店铺下拉 ========== + // 按分组拉取该组全部店铺名(接口单页上限 100,逐页取完) + function loadAsinShopNameOptions(selectId, groupId) { + var select = document.getElementById(selectId); + if (!select) return; + if (!groupId) { + select.innerHTML = ''; + select.disabled = true; + return; + } + select.innerHTML = ''; + select.disabled = true; + var requestGroupId = String(groupId); + select.setAttribute('data-loading-group-id', requestGroupId); + var names = []; + function fetchPage(page) { + return fetch('/api/admin/shop-manages?page=' + page + '&page_size=100&group_id=' + encodeURIComponent(requestGroupId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) throw new Error(res.error || '加载店铺失败'); + (res.items || []).forEach(function (item) { + var name = (item.shop_name || '').trim(); + if (name && names.indexOf(name) === -1) names.push(name); + }); + var pageSize = Number(res.page_size) || 100; + if (page * pageSize < Number(res.total || 0) && page < 20) return fetchPage(page + 1); + return null; + }); + } + fetchPage(1) + .then(function () { + // 期间又切了分组,丢弃这次结果 + if (select.getAttribute('data-loading-group-id') !== requestGroupId) return; + if (!names.length) { + select.innerHTML = ''; + return; + } + select.innerHTML = [''].concat(names.map(function (name) { + return ''; + })).join(''); + select.disabled = false; + }) + .catch(function () { + if (select.getAttribute('data-loading-group-id') !== requestGroupId) return; + select.innerHTML = ''; + }); + } + + // ========== 跳过跟价 ASIN ========== + var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15; + var skipPriceAsinItemsById = {}; + var skipPriceCountryColumns = [ + { code: 'DE', field: 'asin_de', minimumPriceField: 'minimum_price_de', label: '德国' }, + { code: 'UK', field: 'asin_uk', minimumPriceField: 'minimum_price_uk', label: '英国' }, + { code: 'FR', field: 'asin_fr', minimumPriceField: 'minimum_price_fr', label: '法国' }, + { code: 'IT', field: 'asin_it', minimumPriceField: 'minimum_price_it', label: '意大利' }, + { code: 'ES', field: 'asin_es', minimumPriceField: 'minimum_price_es', label: '西班牙' } + ]; + function formatSkipPriceMinimumPrice(value) { + if (value === null || value === undefined || value === '') return ''; + var num = Number(value); + if (!isFinite(num)) return String(value); + return num.toFixed(2); + } + function buildSkipPriceAsinFilterQuery() { + var query = ''; + var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim(); + var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim(); + var country = (document.getElementById('skipPriceAsinFilterCountry').value || '').trim(); + var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim(); + var minimumPriceFrom = (document.getElementById('skipPriceAsinFilterPriceFrom').value || '').trim(); + var minimumPriceTo = (document.getElementById('skipPriceAsinFilterPriceTo').value || '').trim(); + if (groupId) query += (query ? '&' : '') + 'group_id=' + encodeURIComponent(groupId); + if (shopName) query += (query ? '&' : '') + 'shop_name=' + encodeURIComponent(shopName); + if (country) query += (query ? '&' : '') + 'country=' + encodeURIComponent(country); + if (asin) query += (query ? '&' : '') + 'asin=' + encodeURIComponent(asin); + if (minimumPriceFrom) query += (query ? '&' : '') + 'minimum_price_from=' + encodeURIComponent(minimumPriceFrom); + if (minimumPriceTo) query += (query ? '&' : '') + 'minimum_price_to=' + encodeURIComponent(minimumPriceTo); + return query; + } + function buildSkipPriceAsinOperatorQuery() { + var query = ''; + if (currentUserId) query += 'operator_id=' + encodeURIComponent(currentUserId); + if (currentUserRole === 'super_admin') query += (query ? '&' : '') + 'super_admin=true'; + return query; + } + function buildSkipPriceAsinQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize; + var filterQuery = buildSkipPriceAsinFilterQuery(); + if (filterQuery) query += '&' + filterQuery; + var operatorQuery = buildSkipPriceAsinOperatorQuery(); + if (operatorQuery) query += '&' + operatorQuery; + return query; + } + function extractDownloadFilename(disposition, fallbackName) { + var fallback = fallbackName || 'download.xlsx'; + if (!disposition) return fallback; + var utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i); + if (utf8Match && utf8Match[1]) { + try { + return decodeURIComponent(utf8Match[1]); + } catch (e) { } + } + var plainMatch = disposition.match(/filename=\"?([^\";]+)\"?/i); + if (plainMatch && plainMatch[1]) return plainMatch[1]; + return fallback; + } + function triggerBrowserDownload(blob, filename) { + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename || 'download.xlsx'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(function () { URL.revokeObjectURL(url); }, 1000); + } + function exportSkipPriceAsin() { + var query = buildSkipPriceAsinFilterQuery(); + var operatorQuery = buildSkipPriceAsinOperatorQuery(); + if (operatorQuery) query += (query ? '&' : '') + operatorQuery; + var url = '/api/admin/skip-price-asins/export' + (query ? ('?' + query) : ''); + fetch(url) + .then(function (response) { + var contentType = response.headers.get('content-type') || ''; + if (!response.ok || contentType.indexOf('application/json') >= 0) { + return response.json().then(function (res) { + throw new Error((res && (res.error || res.msg)) || '导出失败'); + }).catch(function (err) { + throw err instanceof Error ? err : new Error('导出失败'); + }); + } + return response.blob().then(function (blob) { + return { + blob: blob, + filename: extractDownloadFilename(response.headers.get('content-disposition'), 'skip-price-asin.xlsx') + }; + }); + }) + .then(function (payload) { + triggerBrowserDownload(payload.blob, payload.filename); + }) + .catch(function (err) { + alert((err && err.message) || '导出失败'); + }); + } + function renderAsinCopyButton(asinValue) { + if (!asinValue) return '-'; + return ''; + } + function buildSkipPriceAsinEntries(item) { + var entries = skipPriceCountryColumns.map(function (country) { + return { + country: country, + asin: String(item[country.field] || '').trim(), + minimumPrice: formatSkipPriceMinimumPrice(item[country.minimumPriceField]) + }; + }).filter(function (entry) { + return entry.asin || entry.minimumPrice; + }); + if (!entries.length) { + entries.push({ country: null, asin: '', minimumPrice: '' }); + } + return entries; + } + function renderSkipPriceAsinRows(item, rowNo) { + var entries = buildSkipPriceAsinEntries(item); + var rowspan = entries.length; + var shopName = escapeHtml(item.shop_name || ''); + return entries.map(function (entry, index) { + var isFirst = index === 0; + return '' + + (isFirst ? '' + rowNo + '' : '') + + (isFirst ? '' + renderShopTableText(item.group_name) + '' : '') + + (isFirst ? '' + renderShopTableText(item.shop_name) + '' : '') + + '' + renderAsinCopyButton(entry.asin) + '' + + '' + (entry.country ? escapeHtml(entry.country.label) : '-') + '' + + '' + (entry.country ? escapeHtml(entry.minimumPrice || '-') : '-') + '' + + (isFirst ? '' : '') + + ''; + }).join(''); + } + function openSkipPriceAsinDrawer(item) { + document.getElementById('skipPriceAsinDrawerId').value = item.id; + document.getElementById('skipPriceAsinDrawerSubtitle').textContent = + (item.group_name ? item.group_name + ' / ' : '') + (item.shop_name || ''); + document.getElementById('skipPriceAsinDrawerFields').innerHTML = skipPriceCountryColumns.map(function (country) { + var asinValue = String(item[country.field] || '').trim(); + var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]); + return '
    ' + + '' + + '
    ' + + '' + + '' + + '
    '; + }).join(''); + var msgEl = document.getElementById('msgSkipPriceAsinDrawer'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + document.getElementById('skipPriceAsinDrawer').classList.add('show'); + } + function closeSkipPriceAsinDrawer() { + document.getElementById('skipPriceAsinDrawer').classList.remove('show'); + } + function saveSkipPriceAsinDrawer() { + var itemId = (document.getElementById('skipPriceAsinDrawerId').value || '').trim(); + var item = skipPriceAsinItemsById[itemId]; + var msgEl = document.getElementById('msgSkipPriceAsinDrawer'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!itemId || !item) { + msgEl.textContent = '缺少编辑记录'; + msgEl.className = 'msg err'; + return; + } + var requests = []; + for (var i = 0; i < skipPriceCountryColumns.length; i++) { + var country = skipPriceCountryColumns[i]; + var asinInput = document.querySelector('[data-skip-price-drawer-input="' + country.code + '"]'); + var priceInput = document.querySelector('[data-skip-price-drawer-minimum-price="' + country.code + '"]'); + var nextAsin = asinInput ? (asinInput.value || '').trim().toUpperCase() : ''; + var nextPrice = priceInput ? (priceInput.value || '').trim() : ''; + var currentAsin = String(item[country.field] || '').trim().toUpperCase(); + var currentPrice = formatSkipPriceMinimumPrice(item[country.minimumPriceField]); + if (nextPrice) { + var priceNumber = Number(nextPrice); + if (!isFinite(priceNumber) || priceNumber < 0) { + msgEl.textContent = country.label + ' 最低价格式不正确'; + msgEl.className = 'msg err'; + return; + } + } + if (!nextAsin && nextPrice) { + msgEl.textContent = country.label + ' 填写最低价时必须填写 ASIN'; + msgEl.className = 'msg err'; + return; + } + if (nextAsin === currentAsin && (nextPrice || '') === (currentPrice || '')) continue; + if (!nextAsin) { + requests.push({ country: country, method: 'DELETE', body: null }); + } else { + requests.push({ country: country, method: 'PUT', body: { asin: nextAsin, minimum_price: nextPrice || null } }); + } + } + if (!requests.length) { + closeSkipPriceAsinDrawer(); + return; + } + var operatorQuery = buildSkipPriceAsinOperatorQuery(); + Promise.all(requests.map(function (request) { + var options = { method: request.method }; + if (request.body) { + options.headers = { 'Content-Type': 'application/json' }; + options.body = JSON.stringify(request.body); + } + var url = '/api/admin/skip-price-asin/' + itemId + '/country/' + request.country.code + + (operatorQuery ? ('?' + operatorQuery) : ''); + return fetch(url, options) + .then(function (r) { return r.json(); }) + .then(function (res) { + return res && res.success ? null : (request.country.label + ':' + ((res && res.error) || '保存失败')); + }) + .catch(function () { return request.country.label + ':请求失败'; }); + })).then(function (errors) { + var failed = errors.filter(Boolean); + if (failed.length) { + msgEl.textContent = failed.join(';'); + msgEl.className = 'msg err'; + loadSkipPriceAsin(skipPriceAsinPage); + return; + } + closeSkipPriceAsinDrawer(); + loadSkipPriceAsin(skipPriceAsinPage); + }); + } + function bindSkipPriceAsinActions() { + document.querySelectorAll('#skipPriceAsinListBody [data-copy-asin]').forEach(function (btn) { + btn.onclick = function () { copyAsinText(btn); }; + }); + document.querySelectorAll('[data-skip-price-asin-configure]').forEach(function (btn) { + btn.onclick = function () { + var item = skipPriceAsinItemsById[btn.dataset.skipPriceAsinConfigure]; + if (item) openSkipPriceAsinDrawer(item); + }; + }); + } + function loadSkipPriceAsin(page) { + skipPriceAsinPage = page || 1; + fetch('/api/admin/skip-price-asins?' + buildSkipPriceAsinQuery(skipPriceAsinPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('skipPriceAsinListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + skipPriceAsinItemsById = {}; + items.forEach(function (item) { skipPriceAsinItemsById[String(item.id)] = item; }); + if (items.length === 0) { + tbody.innerHTML = '暂无数据'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1; + return renderSkipPriceAsinRows(item, rowNo); + }).join(''); + } + renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin); + bindSkipPriceAsinActions(); + }) + .catch(function () { + document.getElementById('skipPriceAsinListBody').innerHTML = '请求失败'; + }); + } + var skipPriceAsinImportPollTimer = null; + var skipPriceAsinDeleteImportPollTimer = null; + var skipPriceAsinImportPollSeq = 0; + var skipPriceAsinDeleteImportPollSeq = 0; + function stopSkipPriceAsinImportProgress() { + skipPriceAsinImportPollSeq += 1; + if (skipPriceAsinImportPollTimer) { + clearTimeout(skipPriceAsinImportPollTimer); + skipPriceAsinImportPollTimer = null; + } + } + function stopSkipPriceAsinDeleteImportProgress() { + skipPriceAsinDeleteImportPollSeq += 1; + if (skipPriceAsinDeleteImportPollTimer) { + clearTimeout(skipPriceAsinDeleteImportPollTimer); + skipPriceAsinDeleteImportPollTimer = null; + } + } + function setSkipPriceAsinImportProgress(percent, text) { + var wrap = document.getElementById('skipPriceAsinImportProgressWrap'); + var fill = document.getElementById('skipPriceAsinImportProgressFill'); + var textEl = document.getElementById('skipPriceAsinImportProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function setSkipPriceAsinDeleteImportProgress(percent, text) { + var wrap = document.getElementById('skipPriceAsinDeleteImportProgressWrap'); + var fill = document.getElementById('skipPriceAsinDeleteImportProgressFill'); + var textEl = document.getElementById('skipPriceAsinDeleteImportProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function pollSkipPriceAsinImport(importId) { + stopSkipPriceAsinImportProgress(); + var pollSeq = ++skipPriceAsinImportPollSeq; + var finished = false; + var inFlight = false; + var attempts = 0; + var maxAttempts = 300; + function isActive() { + return !finished && pollSeq === skipPriceAsinImportPollSeq; + } + function finish() { + finished = true; + if (skipPriceAsinImportPollTimer) { + clearTimeout(skipPriceAsinImportPollTimer); + skipPriceAsinImportPollTimer = null; + } + } + function schedule() { + if (isActive()) { + skipPriceAsinImportPollTimer = setTimeout(tick, 1500); + } + } + function tick() { + if (!isActive() || inFlight) { + return; + } + attempts += 1; + if (attempts > maxAttempts) { + finish(); + document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度超时,请稍后刷新列表确认结果'; + document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; + return; + } + inFlight = true; + fetch('/api/admin/skip-price-asins/import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!isActive()) { + return; + } + if (!res.success) { + finish(); + document.getElementById('msgSkipPriceAsinImport').textContent = res.error || '查询导入进度失败'; + document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setSkipPriceAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + finish(); + document.getElementById('msgSkipPriceAsinImport').textContent = '导入成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgSkipPriceAsinImport').className = 'msg ok'; + loadSkipPriceAsin(1); + } else if (progress.status === 'failed') { + finish(); + document.getElementById('msgSkipPriceAsinImport').textContent = progress.error_message || '导入失败'; + document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; + } else { + schedule(); + } + }) + .catch(function () { + if (!isActive()) { + return; + } + finish(); + document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度失败'; + document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; + }) + .finally(function () { + inFlight = false; + }); + } + tick(); + } + function pollSkipPriceAsinDeleteImport(importId) { + stopSkipPriceAsinDeleteImportProgress(); + var pollSeq = ++skipPriceAsinDeleteImportPollSeq; + var finished = false; + var inFlight = false; + var attempts = 0; + var maxAttempts = 300; + function isActive() { + return !finished && pollSeq === skipPriceAsinDeleteImportPollSeq; + } + function finish() { + finished = true; + if (skipPriceAsinDeleteImportPollTimer) { + clearTimeout(skipPriceAsinDeleteImportPollTimer); + skipPriceAsinDeleteImportPollTimer = null; + } + } + function schedule() { + if (isActive()) { + skipPriceAsinDeleteImportPollTimer = setTimeout(tick, 1500); + } + } + function tick() { + if (!isActive() || inFlight) { + return; + } + attempts += 1; + if (attempts > maxAttempts) { + finish(); + document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度超时,请稍后刷新列表确认结果'; + document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; + return; + } + inFlight = true; + fetch('/api/admin/skip-price-asins/delete-import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!isActive()) { + return; + } + if (!res.success) { + finish(); + document.getElementById('msgSkipPriceAsinDeleteImport').textContent = res.error || '查询删除进度失败'; + document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setSkipPriceAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + finish(); + document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg ok'; + loadSkipPriceAsin(1); + } else if (progress.status === 'failed') { + finish(); + document.getElementById('msgSkipPriceAsinDeleteImport').textContent = progress.error_message || '删除失败'; + document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; + } else { + schedule(); + } + }) + .catch(function () { + if (!isActive()) { + return; + } + finish(); + document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度失败'; + document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; + }) + .finally(function () { + inFlight = false; + }); + } + tick(); + } + function uploadSkipPriceAsinImport(deleteMode) { + var fileInput = document.getElementById(deleteMode ? 'skipPriceAsinDeleteImportFile' : 'skipPriceAsinImportFile'); + var msgEl = document.getElementById(deleteMode ? 'msgSkipPriceAsinDeleteImport' : 'msgSkipPriceAsinImport'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (deleteMode) { + stopSkipPriceAsinDeleteImportProgress(); + document.getElementById('skipPriceAsinDeleteImportProgressWrap').style.display = 'none'; + } else { + stopSkipPriceAsinImportProgress(); + document.getElementById('skipPriceAsinImportProgressWrap').style.display = 'none'; + } + var groupId = (document.getElementById(deleteMode ? 'skipPriceAsinDeleteImportGroupId' : 'skipPriceAsinImportGroupId').value || '').trim(); + if (!groupId) { + 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; + } + if (deleteMode && !confirm('确定按 Excel 中的删除ASIN批量删除该店铺跳过跟价 ASIN 吗?')) { + return; + } + var formData = new FormData(); + formData.append('file', file); + formData.append('group_id', groupId); + if (currentUserId) formData.append('operator_id', currentUserId); + if (currentUserRole === 'super_admin') formData.append('super_admin', 'true'); + var xhr = new XMLHttpRequest(); + xhr.open('POST', deleteMode ? '/api/admin/skip-price-asins/delete-import' : '/api/admin/skip-price-asins/import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + if (deleteMode) setSkipPriceAsinDeleteImportProgress(percent, '上传中:' + percent + '%'); + else setSkipPriceAsinImportProgress(percent, '上传中:' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; } + if (!res.success) { + msgEl.textContent = res.error || (deleteMode ? '删除失败' : '导入失败'); + msgEl.className = 'msg err'; + return; + } + if (deleteMode) { + setSkipPriceAsinDeleteImportProgress(100, '上传完成,后端处理中...'); + pollSkipPriceAsinDeleteImport(res.import_id); + } else { + setSkipPriceAsinImportProgress(100, '上传完成,后端处理中...'); + pollSkipPriceAsinImport(res.import_id); + } + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + } + document.getElementById('skipPriceAsinGroupSelect').onchange = function () { + loadAsinShopNameOptions('skipPriceAsinShopName', (this.value || '').trim()); + }; + document.getElementById('btnCloseSkipPriceAsinDrawer').onclick = closeSkipPriceAsinDrawer; + document.getElementById('btnCancelSkipPriceAsinDrawer').onclick = closeSkipPriceAsinDrawer; + document.getElementById('btnSaveSkipPriceAsinDrawer').onclick = saveSkipPriceAsinDrawer; + document.getElementById('skipPriceAsinDrawer').addEventListener('click', function (event) { + if (event.target === this) closeSkipPriceAsinDrawer(); + }); + document.getElementById('btnSearchSkipPriceAsin').onclick = function () { + loadSkipPriceAsin(1); + }; + document.getElementById('btnExportSkipPriceAsin').onclick = function () { + exportSkipPriceAsin(); + }; + document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadSkipPriceAsin(1); + } + }); + document.getElementById('skipPriceAsinFilterAsin').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadSkipPriceAsin(1); + } + }); + document.getElementById('btnOpenCreateSkipPriceAsin').onclick = function () { + document.getElementById('skipPriceAsinGroupSelect').value = ''; + loadAsinShopNameOptions('skipPriceAsinShopName', ''); + document.getElementById('skipPriceAsinCountry').value = ''; + document.getElementById('skipPriceAsinValue').value = ''; + document.getElementById('skipPriceAsinMinimumPrice').value = ''; + document.getElementById('msgSkipPriceAsin').textContent = ''; + document.getElementById('msgSkipPriceAsin').className = 'msg'; + document.getElementById('createSkipPriceAsinModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateSkipPriceAsinModal').onclick = function () { + document.getElementById('createSkipPriceAsinModal').classList.remove('show'); + }; + document.getElementById('btnOpenImportSkipPriceAsin').onclick = function () { + document.getElementById('skipPriceAsinImportGroupId').value = ''; + document.getElementById('skipPriceAsinImportFile').value = ''; + document.getElementById('msgSkipPriceAsinImport').textContent = ''; + document.getElementById('msgSkipPriceAsinImport').className = 'msg'; + document.getElementById('skipPriceAsinImportProgressWrap').style.display = 'none'; + document.getElementById('importSkipPriceAsinModal').classList.add('show'); + }; + document.getElementById('btnCloseImportSkipPriceAsinModal').onclick = function () { + document.getElementById('importSkipPriceAsinModal').classList.remove('show'); + }; + document.getElementById('btnOpenDeleteImportSkipPriceAsin').onclick = function () { + document.getElementById('skipPriceAsinDeleteImportGroupId').value = ''; + document.getElementById('skipPriceAsinDeleteImportFile').value = ''; + document.getElementById('msgSkipPriceAsinDeleteImport').textContent = ''; + document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg'; + document.getElementById('skipPriceAsinDeleteImportProgressWrap').style.display = 'none'; + document.getElementById('deleteImportSkipPriceAsinModal').classList.add('show'); + }; + document.getElementById('btnCloseDeleteImportSkipPriceAsinModal').onclick = function () { + document.getElementById('deleteImportSkipPriceAsinModal').classList.remove('show'); + }; + document.getElementById('btnCreateSkipPriceAsin').onclick = function () { + var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); + var shopName = (document.getElementById('skipPriceAsinShopName').value || '').trim(); + var country = (document.getElementById('skipPriceAsinCountry').value || '').trim(); + var asinInput = document.getElementById('skipPriceAsinValue'); + var minimumPriceInput = document.getElementById('skipPriceAsinMinimumPrice'); + var asin = (asinInput.value || '').trim().toUpperCase(); + var minimumPrice = (minimumPriceInput.value || '').trim(); + var msgEl = document.getElementById('msgSkipPriceAsin'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !country || !asin) { + msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN'; + msgEl.className = 'msg err'; + return; + } + if (minimumPrice) { + var minimumPriceNumber = Number(minimumPrice); + if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) { + msgEl.textContent = '最低价格式不正确'; + msgEl.className = 'msg err'; + return; + } + } + var asinMappings = {}; + asinMappings[country] = asin; + var minimumPriceMappings = {}; + if (minimumPrice) minimumPriceMappings[country] = minimumPrice; + var operatorQuery = buildSkipPriceAsinOperatorQuery(); + fetch('/api/admin/skip-price-asin' + (operatorQuery ? ('?' + operatorQuery) : ''), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + group_id: Number(groupId), + shop_name: shopName, + countries: [country], + asin: asin, + asin_mappings: asinMappings, + minimum_price_mappings: minimumPriceMappings + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '保存失败'; + msgEl.className = 'msg err'; + return; + } + // 保留分组/店铺/国家,方便连续录入同一店铺的多个 ASIN + asinInput.value = ''; + minimumPriceInput.value = ''; + asinInput.focus(); + msgEl.textContent = res.msg || '保存成功'; + msgEl.className = 'msg ok'; + loadSkipPriceAsin(1); + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + document.getElementById('btnImportSkipPriceAsin').onclick = function () { + uploadSkipPriceAsinImport(false); + }; + document.getElementById('btnDeleteImportSkipPriceAsin').onclick = function () { + uploadSkipPriceAsinImport(true); + }; + + // ========== 查询 ASIN ========== + var queryAsinPage = 1, queryAsinPageSize = 15; + var queryAsinItemsById = {}; + var queryAsinCountryColumns = [ + { code: 'DE', field: 'asin_de', label: '德国' }, + { code: 'UK', field: 'asin_uk', label: '英国' }, + { code: 'FR', field: 'asin_fr', label: '法国' }, + { code: 'IT', field: 'asin_it', label: '意大利' }, + { code: 'ES', field: 'asin_es', label: '西班牙' } + ]; + function buildQueryAsinQuery(page) { + var query = 'page=' + (page || 1) + '&page_size=' + queryAsinPageSize; + var filterQuery = buildQueryAsinFilterQuery(); + if (filterQuery) query += '&' + filterQuery; + return query; + } + function buildQueryAsinFilterQuery() { + var query = ''; + var groupId = (document.getElementById('queryAsinFilterGroupId').value || '').trim(); + var shopName = (document.getElementById('queryAsinFilterShopName').value || '').trim(); + var country = (document.getElementById('queryAsinFilterCountry').value || '').trim(); + var asin = (document.getElementById('queryAsinFilterAsin').value || '').trim(); + if (groupId) query += (query ? '&' : '') + 'group_id=' + encodeURIComponent(groupId); + if (shopName) query += (query ? '&' : '') + 'shop_name=' + encodeURIComponent(shopName); + if (country) query += (query ? '&' : '') + 'country=' + encodeURIComponent(country); + if (asin) query += (query ? '&' : '') + 'asin=' + encodeURIComponent(asin); + return query; + } + function exportQueryAsin() { + var query = buildQueryAsinFilterQuery(); + var url = '/api/admin/query-asins/export' + (query ? ('?' + query) : ''); + fetch(url) + .then(function (response) { + var contentType = response.headers.get('content-type') || ''; + if (!response.ok || contentType.indexOf('application/json') >= 0) { + return response.json().then(function (res) { + throw new Error((res && (res.error || res.msg)) || '导出失败'); + }).catch(function (err) { + throw err instanceof Error ? err : new Error('导出失败'); + }); + } + return response.blob().then(function (blob) { + return { + blob: blob, + filename: extractDownloadFilename(response.headers.get('content-disposition'), 'query-asin.xlsx') + }; + }); + }) + .then(function (payload) { + triggerBrowserDownload(payload.blob, payload.filename); + }) + .catch(function (err) { + alert((err && err.message) || '导出失败'); + }); + } + function openQueryAsinDrawer(item) { + document.getElementById('queryAsinDrawerId').value = item.id; + document.getElementById('queryAsinDrawerSubtitle').textContent = + (item.group_name ? item.group_name + ' / ' : '') + (item.shop_name || ''); + document.getElementById('queryAsinDrawerFields').innerHTML = queryAsinCountryColumns.map(function (country) { + var value = String(item[country.field] || '').trim(); + return '
    ' + + '' + + '' + + '
    '; + }).join(''); + var msgEl = document.getElementById('msgQueryAsinDrawer'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + document.getElementById('queryAsinDrawer').classList.add('show'); + } + function closeQueryAsinDrawer() { + document.getElementById('queryAsinDrawer').classList.remove('show'); + } + function saveQueryAsinDrawer() { + var itemId = (document.getElementById('queryAsinDrawerId').value || '').trim(); + var item = queryAsinItemsById[itemId]; + var msgEl = document.getElementById('msgQueryAsinDrawer'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!itemId || !item) { + msgEl.textContent = '缺少编辑记录'; + msgEl.className = 'msg err'; + return; + } + var requests = []; + for (var i = 0; i < queryAsinCountryColumns.length; i++) { + var country = queryAsinCountryColumns[i]; + var input = document.querySelector('[data-query-asin-drawer-input="' + country.code + '"]'); + var nextValue = input ? (input.value || '').trim().toUpperCase() : ''; + var currentValue = String(item[country.field] || '').trim().toUpperCase(); + if (nextValue === currentValue) continue; + if (!nextValue) { + requests.push({ country: country, method: 'DELETE', body: null }); + } else { + requests.push({ country: country, method: 'PUT', body: { asin: nextValue } }); + } + } + if (!requests.length) { + closeQueryAsinDrawer(); + return; + } + Promise.all(requests.map(function (request) { + var options = { method: request.method }; + if (request.body) { + options.headers = { 'Content-Type': 'application/json' }; + options.body = JSON.stringify(request.body); + } + return fetch('/api/admin/query-asin/' + itemId + '/country/' + request.country.code, options) + .then(function (r) { return r.json(); }) + .then(function (res) { + return res && res.success ? null : (request.country.label + ':' + ((res && res.error) || '保存失败')); + }) + .catch(function () { return request.country.label + ':请求失败'; }); + })).then(function (errors) { + var failed = errors.filter(Boolean); + if (failed.length) { + msgEl.textContent = failed.join(';'); + msgEl.className = 'msg err'; + loadQueryAsin(queryAsinPage); + return; + } + closeQueryAsinDrawer(); + loadQueryAsin(queryAsinPage); + }); + } + function renderQueryAsinEntries(item) { + var entries = queryAsinCountryColumns.map(function (country) { + return { + country: country, + asin: String(item[country.field] || '').trim() + }; + }).filter(function (entry) { + return entry.asin; + }); + if (!entries.length) { + entries.push({ country: null, asin: '' }); + } + return entries; + } + function renderQueryAsinRows(item, rowNo) { + var entries = renderQueryAsinEntries(item); + var rowspan = entries.length; + var shopName = escapeHtml(item.shop_name || ''); + return entries.map(function (entry, index) { + var isFirst = index === 0; + return '' + + (isFirst ? '' + rowNo + '' : '') + + (isFirst ? '' + renderShopTableText(item.group_name) + '' : '') + + (isFirst ? '' + renderShopTableText(item.shop_name) + '' : '') + + '' + renderAsinCopyButton(entry.asin) + '' + + '' + (entry.country ? escapeHtml(entry.country.label) : '-') + '' + + (isFirst ? '' : '') + + ''; + }).join(''); + } + function bindQueryAsinActions() { + document.querySelectorAll('#queryAsinListBody [data-copy-asin]').forEach(function (btn) { + btn.onclick = function () { copyAsinText(btn); }; + }); + document.querySelectorAll('[data-query-asin-configure]').forEach(function (btn) { + btn.onclick = function () { + var item = queryAsinItemsById[btn.dataset.queryAsinConfigure]; + if (item) openQueryAsinDrawer(item); + }; + }); + } + function loadQueryAsin(page) { + queryAsinPage = page || 1; + fetch('/api/admin/query-asins?' + buildQueryAsinQuery(queryAsinPage)) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('queryAsinListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + var items = res.items || []; + queryAsinItemsById = {}; + items.forEach(function (item) { queryAsinItemsById[String(item.id)] = item; }); + if (items.length === 0) { + tbody.innerHTML = '暂无数据'; + } else { + tbody.innerHTML = items.map(function (item, index) { + var rowNo = (queryAsinPage - 1) * queryAsinPageSize + index + 1; + return renderQueryAsinRows(item, rowNo); + }).join(''); + } + renderPagination('queryAsinPagination', res.total, res.page, res.page_size, loadQueryAsin); + bindQueryAsinActions(); + }) + .catch(function () { + document.getElementById('queryAsinListBody').innerHTML = '请求失败'; + }); + } + var queryAsinImportPollTimer = null; + var queryAsinDeleteImportPollTimer = null; + function stopQueryAsinImportProgress() { + if (queryAsinImportPollTimer) { + clearInterval(queryAsinImportPollTimer); + queryAsinImportPollTimer = null; + } + } + function stopQueryAsinDeleteImportProgress() { + if (queryAsinDeleteImportPollTimer) { + clearInterval(queryAsinDeleteImportPollTimer); + queryAsinDeleteImportPollTimer = null; + } + } + function setQueryAsinImportProgress(percent, text) { + var wrap = document.getElementById('queryAsinImportProgressWrap'); + var fill = document.getElementById('queryAsinImportProgressFill'); + var textEl = document.getElementById('queryAsinImportProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function setQueryAsinDeleteImportProgress(percent, text) { + var wrap = document.getElementById('queryAsinDeleteImportProgressWrap'); + var fill = document.getElementById('queryAsinDeleteImportProgressFill'); + var textEl = document.getElementById('queryAsinDeleteImportProgressText'); + wrap.style.display = 'block'; + fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; + textEl.textContent = text || ''; + } + function pollQueryAsinImport(importId) { + stopQueryAsinImportProgress(); + function tick() { + fetch('/api/admin/query-asins/import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopQueryAsinImportProgress(); + document.getElementById('msgQueryAsinImport').textContent = res.error || '查询导入进度失败'; + document.getElementById('msgQueryAsinImport').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setQueryAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopQueryAsinImportProgress(); + document.getElementById('msgQueryAsinImport').textContent = '导入成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgQueryAsinImport').className = 'msg ok'; + loadQueryAsin(1); + } else if (progress.status === 'failed') { + stopQueryAsinImportProgress(); + document.getElementById('msgQueryAsinImport').textContent = progress.error_message || '导入失败'; + document.getElementById('msgQueryAsinImport').className = 'msg err'; + } + }) + .catch(function () { + stopQueryAsinImportProgress(); + document.getElementById('msgQueryAsinImport').textContent = '查询导入进度失败'; + document.getElementById('msgQueryAsinImport').className = 'msg err'; + }); + } + tick(); + queryAsinImportPollTimer = setInterval(tick, 1000); + } + function pollQueryAsinDeleteImport(importId) { + stopQueryAsinDeleteImportProgress(); + function tick() { + fetch('/api/admin/query-asins/delete-import/' + encodeURIComponent(importId)) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + stopQueryAsinDeleteImportProgress(); + document.getElementById('msgQueryAsinDeleteImport').textContent = res.error || '查询删除进度失败'; + document.getElementById('msgQueryAsinDeleteImport').className = 'msg err'; + return; + } + var progress = res.progress || {}; + var totalRows = progress.total_rows || 0; + var processedRows = progress.processed_rows || 0; + var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; + setQueryAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); + if (progress.status === 'success') { + stopQueryAsinDeleteImportProgress(); + document.getElementById('msgQueryAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); + document.getElementById('msgQueryAsinDeleteImport').className = 'msg ok'; + loadQueryAsin(1); + } else if (progress.status === 'failed') { + stopQueryAsinDeleteImportProgress(); + document.getElementById('msgQueryAsinDeleteImport').textContent = progress.error_message || '删除失败'; + document.getElementById('msgQueryAsinDeleteImport').className = 'msg err'; + } + }) + .catch(function () { + stopQueryAsinDeleteImportProgress(); + document.getElementById('msgQueryAsinDeleteImport').textContent = '查询删除进度失败'; + document.getElementById('msgQueryAsinDeleteImport').className = 'msg err'; + }); + } + tick(); + queryAsinDeleteImportPollTimer = setInterval(tick, 1000); + } + function uploadQueryAsinImport(deleteMode) { + var fileInput = document.getElementById(deleteMode ? 'queryAsinDeleteImportFile' : 'queryAsinImportFile'); + var msgEl = document.getElementById(deleteMode ? 'msgQueryAsinDeleteImport' : 'msgQueryAsinImport'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (deleteMode) { + stopQueryAsinDeleteImportProgress(); + document.getElementById('queryAsinDeleteImportProgressWrap').style.display = 'none'; + } else { + stopQueryAsinImportProgress(); + document.getElementById('queryAsinImportProgressWrap').style.display = 'none'; + } + 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; + } + if (deleteMode && !confirm('确定按 Excel 中的分组、店铺名和国家 ASIN 批量删除吗?')) { + return; + } + var formData = new FormData(); + formData.append('file', file); + var groupId = (document.getElementById(deleteMode ? 'queryAsinDeleteImportGroupId' : 'queryAsinImportGroupId').value || '').trim(); + if (groupId) formData.append('group_id', groupId); + var xhr = new XMLHttpRequest(); + xhr.open('POST', deleteMode ? '/api/admin/query-asins/delete-import' : '/api/admin/query-asins/import', true); + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.round(event.loaded * 100 / event.total); + if (deleteMode) setQueryAsinDeleteImportProgress(percent, '上传中:' + percent + '%'); + else setQueryAsinImportProgress(percent, '上传中:' + percent + '%'); + } + }; + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) return; + if (xhr.status < 200 || xhr.status >= 300) { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + return; + } + var res; + try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; } + if (!res.success) { + msgEl.textContent = res.error || (deleteMode ? '删除失败' : '导入失败'); + msgEl.className = 'msg err'; + return; + } + if (deleteMode) { + setQueryAsinDeleteImportProgress(100, '上传完成,后端处理中...'); + pollQueryAsinDeleteImport(res.import_id); + } else { + setQueryAsinImportProgress(100, '上传完成,后端处理中...'); + pollQueryAsinImport(res.import_id); + } + fileInput.value = ''; + }; + xhr.onerror = function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }; + xhr.send(formData); + } + document.getElementById('queryAsinGroupSelect').onchange = function () { + loadAsinShopNameOptions('queryAsinShopName', (this.value || '').trim()); + }; + document.getElementById('btnCloseQueryAsinDrawer').onclick = closeQueryAsinDrawer; + document.getElementById('btnCancelQueryAsinDrawer').onclick = closeQueryAsinDrawer; + document.getElementById('btnSaveQueryAsinDrawer').onclick = saveQueryAsinDrawer; + document.getElementById('queryAsinDrawer').addEventListener('click', function (event) { + if (event.target === this) closeQueryAsinDrawer(); + }); + document.getElementById('btnSearchQueryAsin').onclick = function () { + loadQueryAsin(1); + }; + document.getElementById('btnExportQueryAsin').onclick = function () { + exportQueryAsin(); + }; + document.getElementById('queryAsinFilterShopName').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadQueryAsin(1); + } + }); + document.getElementById('queryAsinFilterAsin').addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + loadQueryAsin(1); + } + }); + document.getElementById('btnOpenCreateQueryAsin').onclick = function () { + document.getElementById('queryAsinGroupSelect').value = ''; + loadAsinShopNameOptions('queryAsinShopName', ''); + document.getElementById('queryAsinCountry').value = ''; + document.getElementById('queryAsinValue').value = ''; + document.getElementById('msgQueryAsin').textContent = ''; + document.getElementById('msgQueryAsin').className = 'msg'; + document.getElementById('createQueryAsinModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateQueryAsinModal').onclick = function () { + document.getElementById('createQueryAsinModal').classList.remove('show'); + }; + document.getElementById('btnOpenImportQueryAsin').onclick = function () { + document.getElementById('queryAsinImportGroupId').value = ''; + document.getElementById('queryAsinImportFile').value = ''; + document.getElementById('msgQueryAsinImport').textContent = ''; + document.getElementById('msgQueryAsinImport').className = 'msg'; + document.getElementById('queryAsinImportProgressWrap').style.display = 'none'; + document.getElementById('importQueryAsinModal').classList.add('show'); + }; + document.getElementById('btnCloseImportQueryAsinModal').onclick = function () { + document.getElementById('importQueryAsinModal').classList.remove('show'); + }; + document.getElementById('btnOpenDeleteImportQueryAsin').onclick = function () { + document.getElementById('queryAsinDeleteImportGroupId').value = ''; + document.getElementById('queryAsinDeleteImportFile').value = ''; + document.getElementById('msgQueryAsinDeleteImport').textContent = ''; + document.getElementById('msgQueryAsinDeleteImport').className = 'msg'; + document.getElementById('queryAsinDeleteImportProgressWrap').style.display = 'none'; + document.getElementById('deleteImportQueryAsinModal').classList.add('show'); + }; + document.getElementById('btnCloseDeleteImportQueryAsinModal').onclick = function () { + document.getElementById('deleteImportQueryAsinModal').classList.remove('show'); + }; + document.getElementById('btnCreateQueryAsin').onclick = function () { + var groupId = (document.getElementById('queryAsinGroupSelect').value || '').trim(); + var shopName = (document.getElementById('queryAsinShopName').value || '').trim(); + var country = (document.getElementById('queryAsinCountry').value || '').trim(); + var asinInput = document.getElementById('queryAsinValue'); + var asin = (asinInput.value || '').trim().toUpperCase(); + var msgEl = document.getElementById('msgQueryAsin'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!groupId || !shopName || !country || !asin) { + msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN'; + msgEl.className = 'msg err'; + return; + } + var asinMappings = {}; + asinMappings[country] = asin; + fetch('/api/admin/query-asin', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + group_id: Number(groupId), + shop_name: shopName, + countries: [country], + asin: asin, + asin_mappings: asinMappings + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '保存失败'; + msgEl.className = 'msg err'; + return; + } + // 保留分组/店铺/国家,方便连续录入同一店铺的多个 ASIN + asinInput.value = ''; + asinInput.focus(); + msgEl.textContent = res.msg || '保存成功'; + msgEl.className = 'msg ok'; + loadQueryAsin(1); + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + document.getElementById('btnImportQueryAsin').onclick = function () { + uploadQueryAsinImport(false); + }; + document.getElementById('btnDeleteImportQueryAsin').onclick = function () { + uploadQueryAsinImport(true); + }; + + var productCategoryItems = []; + var productCategoryNodeMap = {}; + var productCategoryPageSize = 20; + var productCategoryRootState = { children: [], page: 0, total: 0, hasMore: true, loaded: false, loading: false }; + var productCategorySearchState = { items: [], page: 0, total: 0, hasMore: false, loading: false }; + var productCategoryKeyword = ''; + var editingProductCategoryId = null; + + function resetProductCategoryForm() { + editingProductCategoryId = null; + document.getElementById('productCategoryParentId').value = ''; + document.getElementById('productCategoryName').value = ''; + document.getElementById('productCategorySortOrder').value = ''; + document.getElementById('productCategoryDescription').value = ''; + document.getElementById('btnCreateProductCategory').textContent = '新增类目'; + document.getElementById('btnCancelProductCategoryEdit').style.display = 'none'; + renderProductCategoryParentOptions(); + } + + function renderProductCategoryParentOptions(selectedValue, excludedId) { + var select = document.getElementById('productCategoryParentId'); + if (!select) return; + var selected = selectedValue == null ? select.value : String(selectedValue || ''); + var excluded = excludedId == null ? editingProductCategoryId : excludedId; + var options = ['']; + productCategoryItems.slice().sort(function (a, b) { + return String(a.path || a.name || '').localeCompare(String(b.path || b.name || ''), 'zh-CN'); + }).forEach(function (item) { + if (excluded && String(item.id) === String(excluded)) return; + var prefix = new Array((item.level || 0) + 1).join('  '); + options.push(''); + }); + if (selected && !productCategoryItems.some(function (item) { return String(item.id) === selected; })) { + options.push(''); + } + select.innerHTML = options.join(''); + select.value = selected; + } + + function normalizeProductCategoryItem(item, parent) { + var id = String(item.id || ''); + var existing = productCategoryNodeMap[id] || {}; + var level = item.level != null ? item.level : (parent ? (parent.level || 0) + 1 : 0); + var path = item.path || (parent && parent.path ? parent.path + ' / ' + (item.name || '') : (item.name || '')); + var normalized = Object.assign(existing, item, { + id: item.id, + parent_id: item.parent_id == null ? null : item.parent_id, + child_count: item.child_count || 0, + level: level, + path: path, + children: existing.children || [], + page: existing.page || 0, + total: existing.total || 0, + hasMore: existing.hasMore !== false, + loaded: existing.loaded || false, + loading: false, + expanded: existing.expanded || false + }); + productCategoryNodeMap[id] = normalized; + productCategoryItems = Object.keys(productCategoryNodeMap).map(function (key) { return productCategoryNodeMap[key]; }); + return normalized; + } + + function flattenVisibleProductCategories(items, output) { + (items || []).forEach(function (item) { + output.push({ type: 'item', item: item }); + if ((item.child_count || 0) > 0 && item.expanded) { + flattenVisibleProductCategories(item.children || [], output); + if (item.hasMore) { + output.push({ type: 'more', parent: item }); + } + } + }); + return output; + } + + function renderProductCategoryRows() { + var tbody = document.getElementById('productCategoryListBody'); + if (!tbody) return; + var rows = productCategoryKeyword + ? productCategorySearchState.items.map(function (item) { return { type: 'item', item: item, search: true }; }) + : flattenVisibleProductCategories(productCategoryRootState.children, []); + if (!productCategoryKeyword && productCategoryRootState.hasMore) { + rows.push({ type: 'more', parent: null }); + } + if (productCategoryKeyword && productCategorySearchState.hasMore) { + rows.push({ type: 'searchMore' }); + } + if (!rows.length) { + tbody.innerHTML = '暂无商品类目'; + return; + } + tbody.innerHTML = rows.map(function (row) { + if (row.type === 'more') { + var parent = row.parent; + var level = parent ? (parent.level || 0) + 1 : 0; + var indentMore = Math.max(0, level) * 24; + var loading = parent ? parent.loading : productCategoryRootState.loading; + var loadedCount = parent ? (parent.children || []).length : (productCategoryRootState.children || []).length; + var totalCount = parent ? (parent.total || 0) : (productCategoryRootState.total || 0); + return '
    ' + + '' + + '已加载 ' + escapeHtml(loadedCount) + ' / ' + escapeHtml(totalCount) + '
    '; + } + if (row.type === 'searchMore') { + var searchLoadedCount = (productCategorySearchState.items || []).length; + var searchTotalCount = productCategorySearchState.total || 0; + return ' ' + + '已加载 ' + escapeHtml(searchLoadedCount) + ' / ' + escapeHtml(searchTotalCount) + ''; + } + var item = row.item; + var indent = Math.max(0, item.level || 0) * 24; + var hasChildren = (item.child_count || 0) > 0; + var expanded = !!item.expanded; + var mark = hasChildren ? (expanded ? '-' : '+') : ''; + return '' + + '
    ' + + '' + + '' + escapeHtml(item.name || '') + '
    ' + + '' + escapeHtml(item.path || item.name || '') + '' + + '' + escapeHtml(item.sort_order || 0) + '' + + '' + (item.is_builtin ? '内置' : '自定义') + '' + + '' + escapeHtml(item.description || '') + '' + + ' ' + + '' + + ''; + }).join(''); + document.querySelectorAll('[data-product-category-toggle]').forEach(function (btn) { + btn.onclick = function () { + var id = String(btn.dataset.productCategoryToggle || ''); + if (!id || btn.disabled) return; + toggleProductCategoryNode(id); + }; + }); + document.querySelectorAll('[data-product-category-load-more]').forEach(function (btn) { + btn.onclick = function () { + var parentId = (btn.dataset.productCategoryLoadMore || '').trim(); + loadProductCategoryChildren(parentId || null, true); + }; + }); + document.querySelectorAll('[data-product-category-search-more]').forEach(function (btn) { + btn.onclick = function () { + loadProductCategorySearch(true); + }; + }); + document.querySelectorAll('[data-product-category-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = productCategoryItems.find(function (row) { return String(row.id) === String(btn.dataset.productCategoryEdit); }); + if (!item) return; + editingProductCategoryId = item.id; + renderProductCategoryParentOptions(item.parent_id || '', item.id); + document.getElementById('productCategoryName').value = item.name || ''; + document.getElementById('productCategorySortOrder').value = item.sort_order || ''; + document.getElementById('productCategoryDescription').value = item.description || ''; + document.getElementById('btnCreateProductCategory').textContent = '保存类目'; + document.getElementById('btnCancelProductCategoryEdit').style.display = ''; + document.getElementById('msgProductCategory').textContent = ''; + document.getElementById('msgProductCategory').className = 'msg'; + document.getElementById('createProductCategoryModalTitle').textContent = '编辑商品类目'; + document.getElementById('createProductCategoryModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-product-category-delete]').forEach(function (btn) { + btn.onclick = function () { + var name = btn.dataset.productCategoryName || '该类目'; + if (!confirm('确定删除“' + name + '”?')) return; + fetch('/api/admin/product-category/' + encodeURIComponent(btn.dataset.productCategoryDelete), { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + alert(res.error || '删除失败'); + return; + } + resetProductCategoryForm(); + loadProductCategories(); + }) + .catch(function () { alert('请求失败'); }); + }; + }); + } + + function loadProductCategories() { + if (productCategoryKeyword) { + productCategorySearchState = { items: [], page: 0, total: 0, hasMore: false, loading: false }; + loadProductCategorySearch(false); + return; + } + productCategoryRootState = { children: [], page: 0, total: 0, hasMore: true, loaded: false, loading: false }; + productCategoryNodeMap = {}; + productCategoryItems = []; + renderProductCategoryParentOptions(); + loadProductCategoryChildren(null, false); + } + + function loadProductCategoryChildren(parentId, append) { + var parent = parentId ? productCategoryNodeMap[String(parentId)] : null; + var state = parent || productCategoryRootState; + if (state.loading) return; + state.loading = true; + var failed = false; + renderProductCategoryRows(); + var nextPage = append ? (state.page || 0) + 1 : 1; + var query = '?page=' + nextPage + '&page_size=' + productCategoryPageSize; + if (parentId) query += '&parent_id=' + encodeURIComponent(parentId); + fetch('/api/admin/product-categories/children' + query) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('productCategoryListBody'); + if (!res.success) { + failed = true; + if (tbody) tbody.innerHTML = '加载失败: ' + escapeHtml(res.error || '') + ''; + return; + } + var children = (res.items || []).map(function (item) { + return normalizeProductCategoryItem(item, parent); + }); + if (!append) { + state.children = []; + } + state.children = state.children.concat(children); + state.page = res.page || nextPage; + state.total = res.total || 0; + state.hasMore = !!res.has_more; + state.loaded = true; + renderProductCategoryParentOptions(); + renderProductCategoryRows(); + }) + .catch(function () { + failed = true; + var tbody = document.getElementById('productCategoryListBody'); + if (tbody) tbody.innerHTML = '请求失败'; + }) + .finally(function () { + state.loading = false; + if (!failed) renderProductCategoryRows(); + }); + } + + function loadProductCategorySearch(append) { + if (productCategorySearchState.loading) return; + productCategorySearchState.loading = true; + var failed = false; + renderProductCategoryRows(); + var nextPage = append ? (productCategorySearchState.page || 0) + 1 : 1; + fetch('/api/admin/product-categories/search?keyword=' + encodeURIComponent(productCategoryKeyword) + + '&page=' + nextPage + '&page_size=' + productCategoryPageSize) + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('productCategoryListBody'); + if (!res.success) { + failed = true; + if (tbody) tbody.innerHTML = '加载失败: ' + escapeHtml(res.error || '') + ''; + return; + } + var items = (res.items || []).map(function (item) { + var normalized = normalizeProductCategoryItem(item, null); + normalized.expanded = false; + return normalized; + }); + productCategorySearchState.items = append + ? productCategorySearchState.items.concat(items) + : items; + productCategorySearchState.page = res.page || nextPage; + productCategorySearchState.total = res.total || 0; + productCategorySearchState.hasMore = !!res.has_more; + renderProductCategoryParentOptions(); + renderProductCategoryRows(); + }) + .catch(function () { + failed = true; + var tbody = document.getElementById('productCategoryListBody'); + if (tbody) tbody.innerHTML = '请求失败'; + }) + .finally(function () { + productCategorySearchState.loading = false; + if (!failed) renderProductCategoryRows(); + }); + } + + function toggleProductCategoryNode(id) { + var item = productCategoryNodeMap[String(id)]; + if (!item) return; + item.expanded = !item.expanded; + if (item.expanded && !item.loaded) { + loadProductCategoryChildren(id, false); + return; + } + renderProductCategoryRows(); + } + + function exportProductCategories() { + var keyword = (document.getElementById('productCategoryKeyword').value || '').trim(); + var url = '/api/admin/product-categories/export' + (keyword ? ('?keyword=' + encodeURIComponent(keyword)) : ''); + fetch(url) + .then(function (response) { + var contentType = response.headers.get('content-type') || ''; + if (!response.ok || contentType.indexOf('application/json') >= 0) { + return response.json().then(function (res) { + throw new Error((res && (res.error || res.msg)) || '导出失败'); + }).catch(function (err) { + throw err instanceof Error ? err : new Error('导出失败'); + }); + } + return response.blob().then(function (blob) { + return { + blob: blob, + filename: extractDownloadFilename(response.headers.get('content-disposition'), 'product-categories.xlsx') + }; + }); + }) + .then(function (payload) { + triggerBrowserDownload(payload.blob, payload.filename); + }) + .catch(function (err) { + alert(err.message || '导出失败'); + }); + } + + document.getElementById('btnOpenCreateProductCategory').onclick = function () { + resetProductCategoryForm(); + document.getElementById('createProductCategoryModalTitle').textContent = '新增商品类目'; + document.getElementById('createProductCategoryModal').classList.add('show'); + }; + + document.getElementById('btnCloseCreateProductCategoryModal').onclick = function () { + document.getElementById('createProductCategoryModal').classList.remove('show'); + }; + + document.getElementById('btnCancelProductCategoryEdit').onclick = function () { + resetProductCategoryForm(); + }; + + document.getElementById('btnSearchProductCategory').onclick = function () { + productCategoryKeyword = (document.getElementById('productCategoryKeyword').value || '').trim(); + loadProductCategories(); + }; + + document.getElementById('btnClearProductCategorySearch').onclick = function () { + productCategoryKeyword = ''; + document.getElementById('productCategoryKeyword').value = ''; + loadProductCategories(); + }; + + document.getElementById('btnExportProductCategory').onclick = function () { + exportProductCategories(); + }; + + document.getElementById('productCategoryKeyword').onkeydown = function (event) { + if (event.key === 'Enter') { + event.preventDefault(); + document.getElementById('btnSearchProductCategory').click(); + } + }; + + document.getElementById('btnCreateProductCategory').onclick = function () { + var msgEl = document.getElementById('msgProductCategory'); + var name = (document.getElementById('productCategoryName').value || '').trim(); + var parentId = (document.getElementById('productCategoryParentId').value || '').trim(); + var sortOrder = (document.getElementById('productCategorySortOrder').value || '').trim(); + var description = (document.getElementById('productCategoryDescription').value || '').trim(); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { + msgEl.textContent = '请填写类目名称'; + msgEl.className = 'msg err'; + return; + } + var payload = { + parent_id: parentId ? Number(parentId) : null, + name: name, + sort_order: sortOrder ? Number(sortOrder) : null, + description: description + }; + var url = editingProductCategoryId + ? '/api/admin/product-category/' + encodeURIComponent(editingProductCategoryId) + : '/api/admin/product-category'; + fetch(url, { + method: editingProductCategoryId ? 'PUT' : 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + msgEl.textContent = res.error || '保存失败'; + msgEl.className = 'msg err'; + return; + } + msgEl.textContent = res.msg || '保存成功'; + msgEl.className = 'msg ok'; + resetProductCategoryForm(); + document.getElementById('createProductCategoryModal').classList.remove('show'); + loadProductCategories(); + }) + .catch(function () { + msgEl.textContent = '请求失败'; + msgEl.className = 'msg err'; + }); + }; + + function loadSoftwareVersions() { + fetch('/api/admin/versions') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('versionListBody'); + var pagination = document.getElementById('versionPagination'); + if (pagination) pagination.innerHTML = ''; + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || res.message || '') + ''; + return; + } + var data = res.data || {}; + var items = Array.isArray(data) ? data : (data.items || []); + if (!items.length) { + tbody.innerHTML = '暂无版本记录'; + return; + } + tbody.innerHTML = items.map(function (v) { + var url = v.file_url || ''; + return '' + (v.version || '') + '' + url + '' + (v.created_at || '') + '下载'; + }).join(''); + }) + .catch(function () { + document.getElementById('versionListBody').innerHTML = '请求失败'; + }); + } + + function loadDigitalHumanVersions(page) { + page = page || 1; + fetch('/api/admin/digital-human-versions?page=' + page + '&pageSize=20') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('digitalHumanVersionListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.message || res.error || '') + ''; + return; + } + // 兼容两种返回格式:直接数组 或 分页对象 + var data = res.data; + var items = []; + var pages = 1; + var current = 1; + if (Array.isArray(data)) { + items = data; + } else if (data && Array.isArray(data.records)) { + items = data.records; + pages = data.pages || 1; + current = data.current || 1; + } else if (data && Array.isArray(data.items)) { + items = data.items; + } + if (items.length === 0) { + tbody.innerHTML = '暂无版本记录'; + } else { + tbody.innerHTML = items.map(function (v) { + var statusText = v.status === 'DRAFT' ? '草稿' : (v.status === 'RELEASED' ? '已发布' : '已废弃'); + var statusColor = v.status === 'DRAFT' ? '#999' : (v.status === 'RELEASED' ? '#52c41a' : '#ff4d4f'); + var isLatestBadge = v.isLatest ? '' : ''; + var fileSize = v.fileSize ? (v.fileSize / 1024 / 1024).toFixed(2) + ' MB' : '-'; + var md5Short = (v.md5 || 'unknown').substring(0, 12) + '...'; + var changelog = (v.changelog || '-').substring(0, 50); + if ((v.changelog || '').length > 50) changelog += '...'; + var releasedAt = v.releasedAt || '-'; + var actions = ''; + if (v.status === 'DRAFT') { + actions += ' '; + } + if (v.status === 'RELEASED' && !v.isLatest) { + actions += ' '; + } + if (v.status === 'RELEASED') { + actions += ' '; + } + if (!v.isLatest) { + actions += ''; + } + + return '' + + '' + (v.version || '') + '' + + '' + statusText + '' + + '' + isLatestBadge + '' + + '' + fileSize + '' + + '' + md5Short + '' + + '' + changelog + '' + + '' + releasedAt + '' + + '' + actions + '' + + ''; + }).join(''); + } + + // 分页 + var pagination = document.getElementById('digitalHumanVersionPagination'); + if (pages > 1) { + var html = ''; + for (var i = 1; i <= pages; i++) { + var active = i === current ? ' active' : ''; + html += '' + i + ''; + } + pagination.innerHTML = html; + appendPaginationQuickJump(pagination, pages, current, loadDigitalHumanVersions); + } else { + pagination.innerHTML = ''; + } + }) + .catch(function (err) { + document.getElementById('digitalHumanVersionListBody').innerHTML = '请求失败: ' + (err.message || '') + ''; + }); + } + function downloadDigitalHumanVersion(encodedVersion) { + var version = decodeURIComponent(encodedVersion || ''); + if (!version) { + alert('版本号为空'); + return; + } + fetch('/api/admin/digital-human-versions/' + encodeURIComponent(version) + '/download-url') + .then(function (r) { return r.json(); }) + .then(function (res) { + if (!res.success) { + throw new Error(res.message || res.error || '获取下载链接失败'); + } + var data = res.data || {}; + var downloadUrl = typeof data === 'string' ? data : (data.downloadUrl || data.url || ''); + if (!downloadUrl) { + throw new Error('Java 后端未返回下载链接'); + } + var a = document.createElement('a'); + a.href = downloadUrl; + a.download = 'ShuFuDigitalHuman-' + version + '.zip'; + a.target = '_blank'; + a.rel = 'noopener'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }) + .catch(function (err) { + alert(err.message || '下载失败'); + }); + } + + window.loadDigitalHumanVersions = loadDigitalHumanVersions; + window.downloadDigitalHumanVersion = downloadDigitalHumanVersion; + + document.getElementById('btnOpenCreateVersion').onclick = function () { + document.getElementById('versionNumber').value = ''; + document.getElementById('versionZip').value = ''; + document.getElementById('msgVersion').textContent = ''; + document.getElementById('msgVersion').className = 'msg'; + document.getElementById('createVersionModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateVersionModal').onclick = function () { + document.getElementById('createVersionModal').classList.remove('show'); + }; + document.getElementById('btnOpenCreateDigitalHumanVersion').onclick = function () { + document.getElementById('digitalHumanVersionNumber').value = ''; + document.getElementById('digitalHumanMinClientVersion').value = ''; + document.getElementById('digitalHumanVersionChangelog').value = ''; + document.getElementById('digitalHumanVersionZip').value = ''; + document.getElementById('msgDigitalHumanVersion').textContent = ''; + document.getElementById('msgDigitalHumanVersion').className = 'msg'; + document.getElementById('createDigitalHumanVersionModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateDigitalHumanVersionModal').onclick = function () { + document.getElementById('createDigitalHumanVersionModal').classList.remove('show'); + }; + document.getElementById('btnUploadVersion').onclick = function () { + var version = (document.getElementById('versionNumber').value || '').trim(); + var fileInput = document.getElementById('versionZip'); + var msgEl = document.getElementById('msgVersion'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + + if (!version) { + msgEl.textContent = '请填写版本号'; + msgEl.classList.add('err'); + return; + } + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '请选择 zip 压缩包'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + if (!(file.name || '').toLowerCase().endsWith('.zip')) { + msgEl.textContent = '仅支持 .zip 格式'; + msgEl.classList.add('err'); + return; + } + + var formData = new FormData(); + formData.append('file', file); + formData.append('version', version); + + msgEl.textContent = '上传中,请稍候...'; + msgEl.classList.remove('err', 'ok'); + + fetch('/api/admin/version', { + method: 'POST', + body: formData + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + var data = res.data || {}; + msgEl.textContent = '发布成功。版本:' + (data.version || '') + ',链接:' + (data.file_url || ''); + msgEl.classList.add('ok'); + document.getElementById('versionNumber').value = ''; + fileInput.value = ''; + loadSoftwareVersions(); + } else { + msgEl.textContent = res.error || res.message || '上传失败'; + msgEl.classList.add('err'); + } + }) + .catch(function (err) { + msgEl.textContent = '请求失败: ' + (err.message || ''); + msgEl.classList.add('err'); + }); + }; + + document.getElementById('btnUploadDigitalHumanVersion').onclick = function () { + var version = (document.getElementById('digitalHumanVersionNumber').value || '').trim(); + var minClientVersion = (document.getElementById('digitalHumanMinClientVersion').value || '').trim(); + var changelog = (document.getElementById('digitalHumanVersionChangelog').value || '').trim(); + var fileInput = document.getElementById('digitalHumanVersionZip'); + var msgEl = document.getElementById('msgDigitalHumanVersion'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + + if (!version) { + msgEl.textContent = '请填写版本号'; + msgEl.classList.add('err'); + return; + } + if (!fileInput.files || fileInput.files.length === 0) { + msgEl.textContent = '请选择 zip 压缩包'; + msgEl.classList.add('err'); + return; + } + var file = fileInput.files[0]; + if (!(file.name || '').toLowerCase().endsWith('.zip')) { + msgEl.textContent = '仅支持 .zip 格式'; + msgEl.classList.add('err'); + return; + } + + var formData = new FormData(); + formData.append('file', file); + formData.append('version', version); + if (minClientVersion) formData.append('minClientVersion', minClientVersion); + if (changelog) formData.append('changelog', changelog); + + msgEl.textContent = '上传中,请稍候...'; + msgEl.classList.remove('err', 'ok'); + + var uploadUrl = window.DIGITAL_HUMAN_JAVA_UPLOAD_URL; + if (!uploadUrl) { + uploadUrl = '/api/admin/digital-human-versions/upload'; + } + + var xhr = new XMLHttpRequest(); + xhr.open('POST', uploadUrl, true); + xhr.timeout = 1800000; + xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + var percent = Math.floor((event.loaded / event.total) * 100); + msgEl.textContent = '正在上传:' + percent + '%'; + } else { + msgEl.textContent = '正在上传,请稍候...'; + } + }; + xhr.onload = function () { + var res = null; + try { + res = JSON.parse(xhr.responseText || '{}'); + } catch (e) { + msgEl.textContent = '上传失败:Java 返回不是 JSON'; + msgEl.classList.add('err'); + return; + } + + if (xhr.status >= 200 && xhr.status < 300 && res.success && res.data) { + msgEl.textContent = '上传成功!版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)'; + msgEl.classList.add('ok'); + document.getElementById('digitalHumanVersionNumber').value = ''; + document.getElementById('digitalHumanMinClientVersion').value = ''; + document.getElementById('digitalHumanVersionChangelog').value = ''; + fileInput.value = ''; + loadDigitalHumanVersions(); + } else { + msgEl.textContent = (res && (res.message || res.error)) || ('上传失败:HTTP ' + xhr.status); + msgEl.classList.add('err'); + } + }; + xhr.onerror = function () { + msgEl.textContent = '请求失败:无法连接上传服务 ' + uploadUrl; + msgEl.classList.add('err'); + }; + xhr.ontimeout = function () { + msgEl.textContent = '上传超时,请检查 Java 服务或网关超时配置'; + msgEl.classList.add('err'); + }; + xhr.send(formData); + }; + + window.releaseVersion = function(version) { + if (!confirm('确认发布版本 ' + version + ' 吗?')) return; + fetch('/api/admin/digital-human-versions/' + version + '/release', { + method: 'POST' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + alert('发布成功!'); + loadDigitalHumanVersions(); + } else { + alert('发布失败:' + (res.message || res.error || '')); + } + }) + .catch(function (err) { + alert('请求失败:' + (err.message || '')); + }); + }; + + window.setLatestVersion = function(version) { + if (!confirm('确认将版本 ' + version + ' 设为最新吗?客户端将自动检测并更新到此版本。')) return; + fetch('/api/admin/digital-human-versions/' + version + '/set-latest', { + method: 'POST' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + alert('设置成功!版本 ' + version + ' 已标记为最新。'); + loadDigitalHumanVersions(); + } else { + alert('设置失败:' + (res.message || res.error || '')); + } + }) + .catch(function (err) { + alert('请求失败:' + (err.message || '')); + }); + }; + + window.deleteVersion = function(version) { + if (!confirm('确认删除版本 ' + version + ' 吗?此操作将删除 OSS 文件,不可恢复!')) return; + fetch('/api/admin/digital-human-versions/' + version, { + method: 'DELETE' + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + alert('删除成功!'); + loadDigitalHumanVersions(); + } else { + alert('删除失败:' + (res.message || res.error || '')); + } + }) + .catch(function (err) { + alert('请求失败:' + (err.message || '')); + }); + }; + + + // ========== 栏目权限配置 ========== + document.getElementById('btnCloseEditColumn').onclick = function () { + document.getElementById('editColumnModal').classList.remove('show'); + }; + document.getElementById('btnOpenCreateColumn').onclick = function () { + document.getElementById('columnName').value = ''; + document.getElementById('columnPageSelect').value = ''; + document.getElementById('columnMenuType').value = 'admin'; + document.getElementById('columnParentId').value = ''; + document.getElementById('msgColumn').textContent = ''; + document.getElementById('msgColumn').className = 'msg'; + if (allColumnsList.length) { + populateColumnPageSelects(); + populateColumnParentSelects(); + } else { + loadColumns(); + } + document.getElementById('createColumnModal').classList.add('show'); + }; + document.getElementById('btnCloseCreateColumnModal').onclick = function () { + document.getElementById('createColumnModal').classList.remove('show'); + }; + + // 菜单标识与页面路径都是后端固定匹配的值,这里用已知页面清单代替手工输入。 + var COLUMN_PAGE_CATALOG = [ + { name: '用户管理', column_key: 'admin_users', route_path: 'users', menu_type: 'admin' }, + { name: '菜单权限配置', column_key: 'admin_columns', route_path: 'columns', menu_type: 'admin' }, + { name: '分组管理', column_key: 'admin_group_manage', route_path: 'group-manage', menu_type: 'admin' }, + { name: '去重数据汇总', column_key: 'admin_dedupe_total_data', route_path: 'dedupe-total-data', menu_type: 'admin' }, + { name: '品牌数据库', column_key: 'admin_invalid_asin_data', route_path: 'invalid-asin-data', menu_type: 'admin' }, + { name: '查询ASIN', column_key: 'admin_query_asin', route_path: 'query-asin', menu_type: 'admin' }, + { name: '商品类目', column_key: 'admin_product_categories', route_path: 'product-categories', menu_type: 'admin' }, + { name: '店铺密钥管理', column_key: 'admin_shop_keys', route_path: 'shop-keys', menu_type: 'admin' }, + { name: '店铺管理', column_key: 'admin_shop_manage', route_path: 'shop-manage', menu_type: 'admin' }, + { name: '最低价ASIN设置', column_key: 'admin_skip_price_asin', route_path: 'skip-price-asin', menu_type: 'admin' }, + { name: '店铺数据记录', column_key: 'admin_shop_data_crawl_tasks', route_path: 'shop-data-crawl-tasks', menu_type: 'admin' }, + { name: '视频任务记录', column_key: 'admin_image_video_tasks', route_path: 'image-video-tasks', menu_type: 'admin' }, + { name: '生成记录', column_key: 'admin_history', route_path: 'history', menu_type: 'admin' }, + { name: '软件版本管理', column_key: 'admin_version', route_path: 'version', menu_type: 'admin' }, + { name: '数字人版本管理', column_key: 'digital_human_version', route_path: 'digital-human-version', menu_type: 'admin' }, + { name: '视频', column_key: 'wb', route_path: 'image-video', menu_type: 'app' }, + { name: '图片', column_key: 'image', route_path: 'image', menu_type: 'app' }, + { name: '数字人', column_key: 'digital-human', route_path: 'digital-human', menu_type: 'app' }, + { name: '带货视频', column_key: 'delivery-video', route_path: 'delivery-video', menu_type: 'app' }, + { name: '混剪', column_key: 'mix-video', route_path: 'mix-video', menu_type: 'app' }, + { name: '前端工具', column_key: 'brand_front_tools', route_path: 'brand-front-tools', menu_type: 'app' }, + { name: '运营工具', column_key: 'brand_operation_tools', route_path: 'brand-operation-tools', menu_type: 'app' }, + { name: '后勤工具', column_key: 'brand_logistics_tools', route_path: 'brand-logistics-tools', menu_type: 'app' }, + { name: '品牌检测', column_key: 'brand', route_path: 'brand', menu_type: 'app' }, + { name: '上架', column_key: 'publish', route_path: 'publish', menu_type: 'app' }, + { name: '数据去重', column_key: 'dedupe', route_path: 'dedupe', menu_type: 'app' }, + { name: '格式转换', column_key: 'convert', route_path: 'convert', menu_type: 'app' }, + { name: '数据拆分', column_key: 'split', route_path: 'split', menu_type: 'app' }, + { name: '删除ASIN', column_key: 'delete-brand', route_path: 'delete-brand', menu_type: 'app' }, + { name: '外观专利检测', column_key: 'appearance-patent', route_path: 'appearance-patent', menu_type: 'app' }, + { name: '货源查询', column_key: 'similar-asin', route_path: 'similar-asin', menu_type: 'app' }, + { name: '商品风险解决', column_key: 'product-risk', route_path: 'product-risk', menu_type: 'app' }, + { name: '定时匹配', column_key: 'shop-match', route_path: 'shop-match', menu_type: 'app' }, + { name: '跟价', column_key: 'pricing', route_path: 'price-track', menu_type: 'app' }, + { name: '巡店删除', column_key: 'patrol-delete', route_path: 'patrol-delete', menu_type: 'app' }, + { name: '查询ASIN', column_key: 'query-asin', route_path: 'query-asin', menu_type: 'app' }, + { name: '店铺数据抓取', column_key: 'shop_data_crawl', route_path: 'shop-data-crawl', menu_type: 'app' }, + { name: '采集数据', column_key: 'collect-data', route_path: 'collect-data', menu_type: 'app' }, + { name: 'ASIN变体采集', column_key: 'variant-collection', route_path: 'variant-collection', menu_type: 'app' }, + { name: '取款', column_key: 'withdraw', route_path: 'withdraw', menu_type: 'app' }, + { name: '采购', column_key: 'purchase', route_path: 'purchase', menu_type: 'app' }, + { name: 'ERP', column_key: 'erp', route_path: 'erp', menu_type: 'app' } + ]; + var COLUMN_PAGE_VALUE_SEPARATOR = '::'; + function columnPageValue(columnKey, routePath) { + return String(columnKey || '') + COLUMN_PAGE_VALUE_SEPARATOR + String(routePath || ''); + } + function parseColumnPageValue(value) { + var parts = String(value || '').split(COLUMN_PAGE_VALUE_SEPARATOR); + return { column_key: parts[0] || '', route_path: parts[1] || '' }; + } + // 目录里没有的历史菜单也要能编辑,所以把列表中已在用的页面并进来。 + function columnPageOptions(menuType) { + var seen = {}; + var options = []; + var push = function (item) { + var key = columnPageValue(item.column_key, item.route_path); + // 分组行(route_path 为空)不映射真实页面,不出现在页面选择器里。 + if (!item.column_key || !item.route_path || seen[key]) return; + seen[key] = true; + options.push({ value: key, label: item.name || item.route_path }); + }; + COLUMN_PAGE_CATALOG.forEach(function (item) { + if (String(item.menu_type) === String(menuType)) push(item); + }); + allColumnsList.forEach(function (item) { + // 一级分组行不是真实页面,不能作为新增菜单的页面来源。 + if (isAdminMenuGroup(item)) return; + if (String(item.menu_type || 'app') === String(menuType)) push(item); + }); + return options; + } + function populateColumnPageSelects() { + [ + { selectId: 'columnPageSelect', typeId: 'columnMenuType' }, + { selectId: 'editColumnPageSelect', typeId: 'editColumnMenuType' } + ].forEach(function (config) { + var select = document.getElementById(config.selectId); + if (!select) return; + var typeSelect = document.getElementById(config.typeId); + var menuType = typeSelect ? (typeSelect.value || 'admin') : 'admin'; + var current = select.value; + select.innerHTML = ''; + columnPageOptions(menuType).forEach(function (option) { + var el = document.createElement('option'); + el.value = option.value; + el.textContent = option.label; + select.appendChild(el); + }); + // 编辑分组行时没有合法页面可选,追加分组虚拟项兜底 + // (用户改不回页面时才需要,保存时按分组处理)。 + if (select.value === '' && current) { + var fallback = document.createElement('option'); + fallback.value = current; + fallback.textContent = '(分组菜单)'; + select.appendChild(fallback); + select.value = current; + } + select.value = current; + }); + } + // 列表按「同类型 → 父级 → 排序」展开成树形顺序,拖动排序才有直观的同级关系。 + function columnTreeOrderedItems() { + var byParent = {}; + allColumnsList.forEach(function (item) { + var groupKey = columnSiblingGroupKey(item); + (byParent[groupKey] = byParent[groupKey] || []).push(item); + }); + var ordered = []; + var walk = function (menuType, parentId, depth) { + (byParent[menuType + '|' + parentId] || []).forEach(function (item) { + ordered.push({ item: item, depth: depth }); + walk(menuType, columnId(item), depth + 1); + }); + }; + ['admin', 'app'].forEach(function (menuType) { walk(menuType, null, 0); }); + // 父级缺失(被删除或不可见)的菜单仍然要出现在列表里。 + if (ordered.length < allColumnsList.length) { + var shown = {}; + ordered.forEach(function (entry) { shown[columnId(entry.item)] = true; }); + allColumnsList.forEach(function (item) { + if (!shown[columnId(item)]) ordered.push({ item: item, depth: 0 }); + }); + } + return ordered; + } + function columnSiblingGroupKey(item) { + return String(item && item.menu_type || 'app') + '|' + columnParentId(item); + } + + function loadColumns() { + fetch('/api/admin/columns') + .then(function (r) { return r.json(); }) + .then(function (res) { + var tbody = document.getElementById('columnListBody'); + if (!res.success) { + tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; + return; + } + allColumnsList = (res.items || []).slice().sort(function (a, b) { + var left = a.sort_order != null ? Number(a.sort_order) : 0; + var right = b.sort_order != null ? Number(b.sort_order) : 0; + if (left !== right) return left - right; + return columnId(a) - columnId(b); + }); + if (allColumnsList.length === 0) { + tbody.innerHTML = '暂无菜单,请先在上方新增'; + } else { + tbody.innerHTML = columnTreeOrderedItems().map(function (entry) { + var c = entry.item; + var parent = allColumnsList.find(function (item) { return columnId(item) === columnParentId(c); }); + var indent = entry.depth > 0 ? '' : ''; + return '' + + '' + + '' + escapeHtml(c.id) + '' + + '' + indent + escapeHtml(c.name || '') + '' + + '' + ((c.menu_type || '') === 'admin' ? '后台' : '软件') + '' + + '' + (parent ? escapeHtml(parent.name || '') : '-') + '' + + '' + escapeHtml(c.created_at || '') + '' + + ' ' + + ''; + }).join(''); + } + populateColumnPageSelects(); + bindColumnActions(); + }) + .catch(function () { + document.getElementById('columnListBody').innerHTML = '请求失败'; + }); + } + function columnSiblingItems(item) { + var menuType = String(item && item.menu_type || 'app'); + var parentId = columnParentId(item); + return allColumnsList.filter(function (candidate) { + return String(candidate.menu_type || 'app') === menuType && columnParentId(candidate) === parentId; + }); + } + function bindColumnActions() { + document.querySelectorAll('[data-column-edit]').forEach(function (btn) { + btn.onclick = function () { + var item = allColumnsList.find(function (candidate) { return String(candidate.id) === String(btn.dataset.columnEdit); }); + if (!item) return; + document.getElementById('editColumnId').value = item.id; + document.getElementById('editColumnName').value = item.name || ''; + document.getElementById('editColumnMenuType').value = item.menu_type || 'app'; + document.getElementById('editColumnSortOrder').value = item.sort_order != null ? String(item.sort_order) : '0'; + populateColumnPageSelects(); + document.getElementById('editColumnPageSelect').value = columnPageValue(item.column_key, item.route_path); + populateColumnParentSelects(); + document.getElementById('editColumnParentId').value = columnParentId(item) || ''; + document.getElementById('msgEditColumn').textContent = ''; + document.getElementById('editColumnModal').classList.add('show'); + }; + }); + document.querySelectorAll('[data-column-delete]').forEach(function (btn) { + btn.onclick = function () { + if (!confirm('确定删除菜单“' + (btn.dataset.name || '').replace(/"/g, '"') + '”吗?')) return; + fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); } + else { alert(res.error || '删除失败'); } + }); + }; + }); + bindColumnDragSort(); + } + // 只允许同级之间拖动:跨父级或跨菜单类型的落点直接忽略。 + var columnDragState = null; + function clearColumnDropMarks() { + document.querySelectorAll('#columnListBody tr').forEach(function (row) { + row.classList.remove('is-column-drop-before', 'is-column-drop-after'); + }); + } + function bindColumnDragSort() { + var tbody = document.getElementById('columnListBody'); + if (!tbody) return; + tbody.querySelectorAll('[data-column-drag-handle]').forEach(function (handle) { + var row = handle.closest('tr'); + if (!row) return; + handle.addEventListener('dragstart', function (event) { + columnDragState = { id: row.dataset.columnId, group: row.dataset.columnGroup }; + row.classList.add('is-column-dragging'); + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + try { event.dataTransfer.setData('text/plain', row.dataset.columnId); } catch (error) {} + if (event.dataTransfer.setDragImage) event.dataTransfer.setDragImage(row, 24, row.offsetHeight / 2); + } + }); + handle.addEventListener('dragend', function () { + row.classList.remove('is-column-dragging'); + clearColumnDropMarks(); + columnDragState = null; + }); + }); + tbody.querySelectorAll('tr[data-column-id]').forEach(function (row) { + row.addEventListener('dragover', function (event) { + if (!columnDragState || columnDragState.group !== row.dataset.columnGroup) return; + if (columnDragState.id === row.dataset.columnId) return; + event.preventDefault(); + if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; + var rect = row.getBoundingClientRect(); + var placeAfter = (event.clientY - rect.top) > rect.height / 2; + clearColumnDropMarks(); + row.classList.add(placeAfter ? 'is-column-drop-after' : 'is-column-drop-before'); + }); + row.addEventListener('drop', function (event) { + if (!columnDragState || columnDragState.group !== row.dataset.columnGroup) return; + event.preventDefault(); + var rect = row.getBoundingClientRect(); + var placeAfter = (event.clientY - rect.top) > rect.height / 2; + var draggedId = columnDragState.id; + clearColumnDropMarks(); + columnDragState = null; + applyColumnDrop(draggedId, row.dataset.columnId, placeAfter); + }); + }); + tbody.addEventListener('dragleave', function (event) { + if (event.target === tbody) clearColumnDropMarks(); + }); + } + function applyColumnDrop(draggedId, targetId, placeAfter) { + if (String(draggedId) === String(targetId)) return; + var dragged = allColumnsList.find(function (item) { return String(item.id) === String(draggedId); }); + if (!dragged) return; + var siblings = columnSiblingItems(dragged); + var fromIndex = siblings.findIndex(function (item) { return String(item.id) === String(draggedId); }); + if (fromIndex < 0) return; + siblings.splice(fromIndex, 1); + var targetIndex = siblings.findIndex(function (item) { return String(item.id) === String(targetId); }); + if (targetIndex < 0) return; + siblings.splice(placeAfter ? targetIndex + 1 : targetIndex, 0, dragged); + persistColumnOrder(dragged.menu_type || 'app', siblings); + } + function persistColumnOrder(menuType, orderedSiblings) { + fetch('/api/admin/column/reorder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + menu_type: menuType, + ordered_ids: orderedSiblings.map(function (item) { return item.id; }) + }) + }).then(function (r) { return r.json(); }) + .then(function (res) { + if (!res || !res.success) { + alert((res && res.error) || '排序保存失败'); + loadColumns(); + return; + } + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + }).catch(function () { + alert('排序保存失败'); + loadColumns(); + }); + } + // 新增的菜单排到同级末尾,之后由列表拖动调整顺序。 + function nextColumnSortOrder(menuType, parentId) { + var maxOrder = 0; + allColumnsList.forEach(function (item) { + if (String(item.menu_type || 'app') !== String(menuType)) return; + if (columnParentId(item) !== parentId) return; + var order = item.sort_order != null ? Number(item.sort_order) : 0; + if (isFinite(order) && order > maxOrder) maxOrder = order; + }); + return maxOrder + 10; + } + document.getElementById('btnAddColumn').onclick = function () { + var name = (document.getElementById('columnName').value || '').trim(); + var page = parseColumnPageValue(document.getElementById('columnPageSelect').value); + var menuType = (document.getElementById('columnMenuType').value || 'admin').trim() || 'admin'; + var parentIdValue = (document.getElementById('columnParentId').value || '').trim(); + var msgEl = document.getElementById('msgColumn'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } + if (!page.column_key || !page.route_path) { + msgEl.textContent = '请选择页面'; msgEl.classList.add('err'); return; + } + fetch('/api/admin/column', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name, + column_key: page.column_key, + route_path: page.route_path, + menu_type: menuType, + sort_order: nextColumnSortOrder(menuType, parentIdValue ? Number(parentIdValue) : null), + parent_id: parentIdValue ? Number(parentIdValue) : null + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + msgEl.textContent = res.msg || '新增成功'; + msgEl.classList.add('ok'); + document.getElementById('columnName').value = ''; + document.getElementById('columnPageSelect').value = ''; + document.getElementById('columnMenuType').value = 'admin'; + document.getElementById('columnParentId').value = ''; + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + } else { + msgEl.textContent = res.error || '新增失败'; + msgEl.classList.add('err'); + } + }) + .catch(function () { msgEl.textContent = '请求失败'; msgEl.classList.add('err'); }); + }; + document.getElementById('btnSaveColumn').onclick = function () { + var cid = document.getElementById('editColumnId').value; + var name = (document.getElementById('editColumnName').value || '').trim(); + var page = parseColumnPageValue(document.getElementById('editColumnPageSelect').value); + var sortOrderValue = (document.getElementById('editColumnSortOrder').value || '').trim(); + var menuType = (document.getElementById('editColumnMenuType').value || 'admin').trim() || 'admin'; + var parentIdValue = (document.getElementById('editColumnParentId').value || '').trim(); + var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10); + var msgEl = document.getElementById('msgEditColumn'); + msgEl.textContent = ''; + msgEl.className = 'msg'; + if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } + if (!page.column_key || !page.route_path) { + msgEl.textContent = '请选择页面'; msgEl.classList.add('err'); return; + } + fetch('/api/admin/column/' + cid, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: name, + column_key: page.column_key, + route_path: page.route_path, + menu_type: menuType, + sort_order: isNaN(sortOrder) ? null : sortOrder, + parent_id: parentIdValue ? Number(parentIdValue) : null + }) + }) + .then(function (r) { return r.json(); }) + .then(function (res) { + if (res.success) { + document.getElementById('editColumnModal').classList.remove('show'); + loadColumns(); + loadColumnsForPermission(); + loadAdminMenus(getActiveAdminTabName()); + } else { + msgEl.textContent = res.error || '保存失败'; + msgEl.classList.add('err'); + } + }); + }; + ['columnMenuType', 'editColumnMenuType'].forEach(function (id) { + var select = document.getElementById(id); + if (!select) return; + select.onchange = function () { + populateColumnParentSelects(); + populateColumnPageSelects(); + }; + }); + + // ========== 分页 ========== + function appendPaginationQuickJump(el, totalPages, page, onPage) { + el.insertAdjacentHTML('beforeend', + '' + + ''); + var jumpInput = el.querySelector('[data-page-jump-input]'); + var jumpToPage = function () { + var targetPage = parseInt(jumpInput.value, 10); + if (isNaN(targetPage)) { + jumpInput.focus(); + return; + } + targetPage = Math.min(Math.max(targetPage, 1), totalPages); + jumpInput.value = targetPage; + if (targetPage !== page) onPage(targetPage); + }; + el.querySelector('[data-page-jump]').onclick = jumpToPage; + jumpInput.onkeydown = function (event) { + if (event.key === 'Enter') jumpToPage(); + }; + } + + function renderPagination(elId, total, page, pageSize, onPage) { + var el = document.getElementById(elId); + if (!el) return; + var totalPages = Math.max(1, Math.ceil(total / pageSize)); + el.innerHTML = '共' + total + ' 条' + + '' + + '第' + page + ' / ' + totalPages + ' 页' + + ''; + el.querySelectorAll('[data-p]').forEach(function (b) { + if (!b.disabled) b.onclick = function () { onPage(parseInt(b.dataset.p, 10)); }; + }); + appendPaginationQuickJump(el, totalPages, page, onPage); + } + + // 初始化 + var adminCurrentUserPromise = null; + function loadAdminCurrentUser() { + if (adminCurrentUserPromise) return adminCurrentUserPromise; + adminCurrentUserPromise = fetch('/api/admin/current-user') + .then(function (r) { + if (r.status === 404) { + return fetch('/api/auth/check') + .then(function (fallbackResp) { return fallbackResp.json(); }) + .then(function (fallbackRes) { + if (fallbackRes && fallbackRes.logged_in) { + return { + success: true, + item: { + username: '当前用户', + role: '' + } + }; + } + return { success: false }; + }); + } + return r.json(); + }) + .then(function (res) { + var nameEl = document.getElementById('adminCurrentUsername'); + var roleEl = document.getElementById('adminCurrentUserRole'); + if (!res.success) { + nameEl.textContent = '未登录'; + roleEl.style.display = 'none'; + return; + } + var item = res.item || {}; + currentUserId = item.id || currentUserId; + currentUserRole = item.role || currentUserRole; + currentUserUsername = item.username || currentUserUsername; + nameEl.textContent = item.username || '管理员'; + if (item.role) { + roleEl.textContent = item.role === 'super_admin' + ? '超级管理员' + : (item.role === 'admin' ? '管理员' : '普通账号'); + roleEl.style.display = 'inline-block'; + } else { + roleEl.style.display = 'none'; + } + updateShopManageGroupButtonsAccess(); + updateImageVideoPermissionAccess(); + }) + .catch(function () { + return fetch('/api/auth/check') + .then(function (r) { return r.json(); }) + .then(function (res) { + document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录'; + document.getElementById('adminCurrentUserRole').style.display = 'none'; + updateShopManageGroupButtonsAccess(); + updateImageVideoPermissionAccess(); + }) + .catch(function () { + document.getElementById('adminCurrentUsername').textContent = '当前用户'; + document.getElementById('adminCurrentUserRole').style.display = 'none'; + updateShopManageGroupButtonsAccess(); + updateImageVideoPermissionAccess(); + }); + }) + .finally(function () { + if (!currentUserId && currentUserRole !== 'super_admin') { + adminCurrentUserPromise = null; + } + }); + return adminCurrentUserPromise; + } + + document.getElementById('btnAdminLogout').onclick = function () { + // Java auth 模块 POST /logout 清 cookie;无论成败均回登录页(GET /login 由页面控制器转发到 login.html)。 + fetch('/logout', { + method: 'POST', + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .catch(function () { /* 忽略网络错误,仍跳登录页 */ }) + .finally(function () { + window.location.replace('/login?logout=1'); + }); + }; + + loadAdminCurrentUser().finally(function () { + loadUserOptions(); + loadShopManageGroups(); + loadAdminMenus(); + }); + }) (); diff --git a/backend-java/src/main/resources/static/static/bg.jpg b/backend-java/src/main/resources/static/static/bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de6e38d8f381a8c2e06f259aab20665bda03e0ed GIT binary patch literal 164259 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

    |2!~hYkh>j5F9L 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®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 Q0eSltA}n%)Wgq7M2gHA@#Q*>R literal 0 HcmV?d00001 diff --git a/backend-java/src/main/resources/static/static/logo_thumb.png b/backend-java/src/main/resources/static/static/logo_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..f8a6f41bfa65eda0d042dffc99dd20ba4f64dfd5 GIT binary patch literal 6360 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