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 00000000..de6e38d8 Binary files /dev/null and b/backend-java/src/main/resources/static/static/bg.jpg differ diff --git a/backend-java/src/main/resources/static/static/logo.jpg b/backend-java/src/main/resources/static/static/logo.jpg new file mode 100644 index 00000000..ab825d78 Binary files /dev/null and b/backend-java/src/main/resources/static/static/logo.jpg differ 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 00000000..f8a6f41b Binary files /dev/null and b/backend-java/src/main/resources/static/static/logo_thumb.png differ