Files
crawler-plugin/backend/static/admin.js
T
huangzd1997 417a2bf831
Build Backend JAR / build (push) Has been cancelled
后台店铺数据记录页新增 Tab 切换与重复 ASIN 分析;task-169/170 HTTP 客户端配置接入;dedupe/brand 相关改造
- 管理后台:店铺数据记录改为列表分页展示(去任务号/文件列/累计标题,'最新'改'更新时间'),新增'重复 ASIN' Tab 按跨店聚合展示 ASIN/店铺/分组/国家/日期/价格;修复失败/进行中任务被过滤不显示的问题
- task-169: BrandCheckHttpConfigResolver 从 aiimage.http-client.* 读取品牌检查超时/重试(默认同现状、钳制),附 8 条测试
- task-170: surefire 内存调整为 1536m
- dedupe: DedupeRunService 重构(事务边界/进度)、新增 DedupeRunProgressVo、Controller 与结果 VO 调整,PermissionMenuService 相应适配
- brand/dedupe 前端:BrandDedupeTab/BrandPatrolDeleteTab 改造、新增 CountrySelector 组件与 country-options、类型与接口端对齐、测试更新
- 移除无引用文件:backend/static/logo.jpg、prompts/
2026-09-03 01:57:41 +08:00

6479 lines
381 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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();
}
}
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 {
return nativeFetch(input, options).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 () {
this.__adminSkipLoading = false;
return nativeOpen.apply(this, arguments);
};
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'] },
{ key: 'record', title: '记录与版本', items: ['history', 'version', 'digital-human-version', 'image-video-tasks'] }
];
var ADMIN_MENU_ICONS = {
'users': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>',
'group-manage': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>',
'columns': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="7" height="7" x="3" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="14" rx="1"></rect><rect width="7" height="7" x="3" y="14" rx="1"></rect></svg>',
'dedupe-total-data': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"></path><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"></path><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"></path></svg>',
'invalid-asin-data': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>',
'shop-keys': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"></path><path d="m21 2-9.6 9.6"></path><circle cx="7.5" cy="15.5" r="5.5"></circle></svg>',
'shop-manage': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"></path><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"></path><path d="M2 7h20"></path><path d="M22 7v3a2 2 0 0 1-2 2 2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"></path></svg>',
'skip-price-asin': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="m4.9 4.9 14.2 14.2"></path></svg>',
'query-asin': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>',
'product-categories': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"></path></svg>',
'image-video-tasks': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m22 8-6 4 6 4V8Z"></path><rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect></svg>',
'shop-data-crawl-tasks': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M3 5v14a9 3 0 0 0 18 0V5"></path><path d="M3 12a9 3 0 0 0 18 0"></path></svg>',
'history': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M12 7v5l4 2"></path></svg>',
'version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7.5 4.27 9 5.15"></path><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"></path><path d="M3.3 7 12 12l8.7-5"></path><path d="M12 22V12"></path></svg>',
'digital-human-version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>'
};
var ADMIN_MENU_FALLBACK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 8v8"></path><path d="M8 12h8"></path></svg>';
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',
'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 === '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 = '<div class="menu-empty">暂无可用菜单</div>';
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, '&quot;');
return '<button class="tab" type="button" data-tab="' + route + '" aria-controls="panel-' + route + '" title="' + label + '">' +
'<span class="adm-icon">' + adminMenuIcon(route) + '</span>' +
'<span class="adm-label">' + label + '</span></button>';
}).join('');
groupsHtml += '<div class="menu-group" data-menu-group="' + group.key + '">' +
'<button class="menu-group-title" type="button" aria-expanded="true">' + group.title +
'<span class="menu-group-chevron" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span>' +
'</button>' +
'<div class="menu-group-body">' + itemsHtml + '</div></div>';
});
if (!groupsHtml) {
activeAdminTabName = '';
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">暂无可用菜单</div>';
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 loadAdminMenus(preferredTab) {
if (preferredTab) activeAdminTabName = preferredTab;
fetch('/api/admin/current-user/menus')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">菜单加载失败</div>';
hideAllAdminPanels();
return;
}
renderAdminTabs(res.items || []);
})
.catch(function () {
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">菜单加载失败</div>';
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 = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
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 = '<tr><td colspan="6" class="empty-tip">暂无用户</td></tr>';
} else {
tbody.innerHTML = items.map(function (u) {
return '<tr><td>' + u.id + '</td><td>' + (u.username || '') + '</td><td>' +
roleLabel(u.role || 'normal') + '</td><td>' + (u.creator_username || '-') + '</td><td>' + (u.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-edit="' + u.id + '" data-user="' + (JSON.stringify(u).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-delete="' + u.id + '" data-name="' + (u.username || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers);
bindUserActions();
updateCreateFormByRole();
updateUserFilterByRole();
})
.catch(function () {
document.getElementById('userListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
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 = '<option value="">全部</option>';
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 = '<option value="">无(一级菜单)</option>';
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 = '<option value="">请选择管理员</option>';
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(/&quot;/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 '<div class="image-video-permission-row">' +
'<input type="checkbox" aria-label="' + escapeHtml(user.username || '-') + (persistedGranted ? ' 已保存分配' : ' 未分配') + '" data-image-video-permission-user="' + userId + '"' +
(pendingGranted ? ' checked' : '') + '>' +
'<span class="image-video-permission-user">' +
'<span class="image-video-permission-name" title="' + escapeHtml(user.username || '') + '">' + escapeHtml(user.username || '-') + '</span>' +
'<span class="image-video-permission-role">' + escapeHtml(roleLabel(user.role)) + '</span>' +
'</span>' +
'<button class="image-video-permission-state' + (pendingChanged ? ' pending' : (persistedGranted ? ' granted' : '')) + '" type="button" data-image-video-permission-toggle="' + userId + '">' +
actionText +
'</button>' +
'</div>';
}).join('') : '<div class="image-video-permission-empty">暂无匹配用户</div>';
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 = '<div class="image-video-permission-empty">加载中...</div>';
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 = '<div class="image-video-permission-empty">' + escapeHtml(error.message || '权限加载失败') + '</div>';
})
.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 '<span class="image-video-status ' + escapeHtml(status) + '" title="' + escapeHtml(status) + '">' + escapeHtml(imageVideoStatusLabel(status)) + '</span>';
}
function safeAdminUrl(value) {
var url = String(value || '').trim();
return /^https?:\/\//i.test(url) ? url : '';
}
function imageVideoDownloadIcon() {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"></path></svg>';
}
function imageVideoUnavailableHtml(message) {
return '<div class="image-video-unavailable">' +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 10 4.5-2.5v9L15 14"></path><rect x="3" y="6" width="12" height="12" rx="2"></rect><path d="m4 4 16 16"></path></svg>' +
'<strong>' + escapeHtml(message || '暂无可用视频') + '</strong>' +
'</div>';
}
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
? '<input class="image-video-card-check" type="checkbox" data-image-video-select="' + escapeHtml(card.key) + '" aria-label="选择任务 ' + escapeHtml(task.task_id) + ' 的视频 ' + (card.videoIndex + 1) + '">' +
'<video src="' + escapeHtml(video.displayUrl) + '" controls playsinline preload="metadata"></video>'
: imageVideoUnavailableHtml(task.status === 'FAILED' ? '任务失败,未生成视频' : '视频生成中或暂无结果');
return '<article class="image-video-card" data-image-video-card="' + escapeHtml(card.key) + '">' +
'<div class="image-video-media">' + mediaHtml + '</div>' +
'<div class="image-video-card-body">' +
'<div class="image-video-card-head">' +
'<div class="image-video-card-title" title="任务 ' + escapeHtml(task.task_id) + '">任务 ' + escapeHtml(task.task_id) + (video ? ' · 视频 ' + (card.videoIndex + 1) : '') + '</div>' +
renderImageVideoStatus(task.status) +
'</div>' +
'<div class="image-video-card-info">' +
'<div class="image-video-info-row"><label>用户名</label><span>' + escapeHtml(task.username || '-') + '</span></div>' +
'<div class="image-video-info-row"><label>所属分组</label><span>' + escapeHtml(task.group_name || '-') + '</span></div>' +
'<div class="image-video-info-row"><label>任务模式</label><span>' + escapeHtml(task.mode || '-') + '</span></div>' +
'<div class="image-video-info-row"><label>生成时间</label><span>' + escapeHtml(generatedAt) + '</span></div>' +
'<div class="image-video-info-row"><label>调试链接</label>' + (debugUrl
? '<button class="image-video-copy-link" type="button" data-image-video-copy-debug="' + escapeHtml(card.key) + '">复制链接</button>'
: '<span>-</span>') + '</div>' +
'</div>' +
'<div class="image-video-card-actions">' +
'<button class="image-video-card-action" type="button" data-image-video-download="' + escapeHtml(card.key) + '"' + (hasVideo ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载</button>' +
'</div>' +
'</div>' +
'</article>';
}
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('')
: '<div class="image-video-empty">暂无符合条件的视频任务</div>';
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 = '<div class="image-video-empty">加载中...</div>';
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 = '<div class="image-video-empty">加载失败:' + escapeHtml(res.error || '') + '</div>';
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 = '<div class="image-video-empty">请求失败,请稍后重试</div>';
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 '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18"></path><path d="M8 6V4h8v2"></path><path d="M19 6l-1 15H6L5 6"></path><path d="M10 11v6m4-6v6"></path></svg>';
}
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 '<span class="shop-data-status ' + cls + '"' + errorTitle + '>' + escapeHtml(imageVideoStatusLabel(normalized)) + '</span>';
}
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 = '<input type="checkbox" data-shop-data-select="' + (resultId || '') + '"' +
(selected ? ' checked' : '') + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>';
return '<tr data-shop-data-card="' + (resultId || '') + '">' +
'<td style="width:36px;">' + checkbox + '</td>' +
'<td class="dup-shop" title="' + escapeHtml(item.shop_name || '-') + '">' + escapeHtml(item.shop_name || '-') + '</td>' +
'<td class="muted">' + escapeHtml(item.group_name || '-') + '</td>' +
'<td class="dup-country">' + escapeHtml(countryListLabel(countryCodes)) + '</td>' +
'<td>' + renderShopDataStatus(item, status || item.file_status) + '</td>' +
'<td class="dup-date">' + escapeHtml(updatedAt) + '</td>' +
'<td style="width:170px;">' +
'<button class="shop-data-record-action" type="button" data-shop-data-download="' + (resultId || '') + '"' + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载文件</button>' +
'<button class="shop-data-record-action danger" type="button" data-shop-data-delete="' + (resultId || '') + '"' + (terminal && resultId > 0 ? '' : ' disabled') + ' style="margin-left:8px;">' + shopDataDeleteIcon() + '删除</button>' +
'</td>' +
'</tr>';
}
function renderShopDataRecordTable() {
var rows = shopDataTasks.map(renderShopDataRecordRow).join('');
if (!shopDataTasks.length) {
return '<div class="shop-data-empty-hint">暂无符合条件的店铺数据任务</div>';
}
return '<table>' +
'<thead><tr>' +
'<th style="width:36px;"></th>' +
'<th style="width:18%;">店铺</th>' +
'<th style="width:14%;">分组</th>' +
'<th style="width:16%;">国家</th>' +
'<th style="width:10%;">状态</th>' +
'<th style="width:18%;">更新时间</th>' +
'<th style="width:170px;">操作</th>' +
'</tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>';
}
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 = '<div class="image-video-empty">加载中...</div>';
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 = '<div class="image-video-empty">加载失败:' + escapeHtml(error.message || '') + '</div>';
document.getElementById('shopDataTaskTotal').textContent = '';
syncShopDataSelectionUi();
});
}
// ========== 重复 ASIN 分析 ==========
var shopDataDuplicatePage = 1, shopDataDuplicatePageSize = 10;
var shopDataDuplicateItems = [];
var shopDataDuplicateTotal = 0;
var shopDataDuplicateAnalyzed = { shopCount: 0, resultCount: 0 };
var shopDataDuplicateLoading = false;
function buildShopDataDuplicateQuery(page) {
var params = new URLSearchParams();
params.set('page', String(page || 1));
params.set('page_size', String(shopDataDuplicatePageSize));
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 shopDataDuplicateHeader(header) {
return '<thead><tr>' + header.map(function (col) {
return '<th style="width:' + (col.width || '') + ';">' + col.label + '</th>';
}).join('') + '</tr></thead>';
}
function renderShopDataDuplicateCard(item) {
var occurrences = Array.isArray(item.occurrences) ? item.occurrences : [];
var brand = '';
occurrences.forEach(function (occ) { if (occ.brand && !brand) brand = occ.brand; });
var rows = occurrences.map(function (occ) {
var countries = countryListLabel(occ.country_codes);
return '<tr>' +
'<td class="dup-shop">' + escapeHtml(occ.shop_name || '-') + '</td>' +
'<td class="dup-country">' + escapeHtml(occ.group_name || '-') + '</td>' +
'<td class="dup-country">' + escapeHtml(countries) + '</td>' +
'<td class="dup-date">' + escapeHtml(occ.date || '-') + '</td>' +
'<td class="dup-date">' + escapeHtml(occ.price || '-') + '</td>' +
'</tr>';
}).join('');
return '<article class="duplicate-asin-card" data-duplicate-asin="' + escapeHtml(item.asin) + '">' +
'<div class="duplicate-asin-card-head">' +
'<span class="dup-asin">' + escapeHtml(item.asin) + '</span>' +
'<span class="dup-count">' + item.shop_count + ' 家店铺</span>' +
'<span class="dup-brand" title="' + escapeHtml(brand) + '">' + escapeHtml(brand || '') + '</span>' +
'</div>' +
'<table class="duplicate-asin-table">' +
shopDataDuplicateHeader([
{ label: '店铺', width: '18%' },
{ label: '分组', width: '14%' },
{ label: '国家', width: '16%' },
{ label: '日期', width: '14%' },
{ label: '价格', width: '12%' }
]) +
'<tbody>' + rows + '</tbody>' +
'</table>' +
'</article>';
}
function renderShopDataDuplicateList() {
var list = document.getElementById('shopDataDuplicateList');
if (!shopDataDuplicateItems.length) {
list.innerHTML = '<div class="shop-data-empty-hint">暂无重复 ASIN。请在左侧筛选条件后点击"查询"生效范围,再点击"重新分析"。</div>';
return;
}
list.innerHTML = shopDataDuplicateItems.map(renderShopDataDuplicateCard).join('');
}
function loadShopDataDuplicateAsins(page) {
if (shopDataDuplicateLoading) return;
shopDataDuplicatePage = page || 1;
shopDataDuplicateLoading = true;
var list = document.getElementById('shopDataDuplicateList');
var progress = document.getElementById('shopDataDuplicateProgress');
var button = document.getElementById('btnRefreshShopDataDuplicates');
progress.textContent = '正在读取各店铺结果文件并分析,请稍候...';
button.disabled = true;
list.innerHTML = '<div class="shop-data-empty-hint">分析中...</div>';
fetch('/api/admin/shop-data-crawl/duplicate-asins?' + buildShopDataDuplicateQuery(shopDataDuplicatePage))
.then(function (response) { return response.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '分析失败');
shopDataDuplicateItems = res.items || [];
shopDataDuplicateTotal = Number(res.total) || 0;
shopDataDuplicateAnalyzed.shopCount = Number(res.analyzed_shop_count) || 0;
shopDataDuplicateAnalyzed.resultCount = Number(res.analyzed_result_count) || 0;
var totalEl = document.getElementById('shopDataDuplicateTotal');
totalEl.textContent = '共 ' + shopDataDuplicateTotal + ' 个重复 ASIN · 已分析 ' + shopDataDuplicateAnalyzed.shopCount + ' 家店铺';
renderShopDataDuplicateList();
renderPagination('shopDataDuplicatePagination', shopDataDuplicateTotal, shopDataDuplicatePage, shopDataDuplicatePageSize, loadShopDataDuplicateAsins);
})
.catch(function (error) {
shopDataDuplicateItems = [];
shopDataDuplicateTotal = 0;
list.innerHTML = '<div class="shop-data-empty-hint">分析失败:' + escapeHtml(error.message || '') + '</div>';
document.getElementById('shopDataDuplicateTotal').textContent = '';
})
.finally(function () {
shopDataDuplicateLoading = false;
progress.textContent = '';
button.disabled = false;
});
}
function switchShopDataSubTab(view) {
var recordsView = document.getElementById('shopDataRecordsView');
var duplicatesView = document.getElementById('shopDataDuplicatesView');
var recordsTab = document.getElementById('shopDataSubTabRecords');
var duplicatesTab = document.getElementById('shopDataSubTabDuplicates');
var recordsActive = view === 'records';
recordsView.style.display = recordsActive ? '' : 'none';
duplicatesView.style.display = recordsActive ? 'none' : '';
recordsTab.classList.toggle('active', recordsActive);
recordsTab.setAttribute('aria-selected', recordsActive ? 'true' : 'false');
duplicatesTab.classList.toggle('active', !recordsActive);
duplicatesTab.setAttribute('aria-selected', !recordsActive ? 'true' : 'false');
if (!recordsActive) {
loadShopDataDuplicateAsins(1);
}
}
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 '<div class="image-video-permission-row">' +
'<input type="checkbox" data-shop-data-permission-user="' + userId + '"' + (selected ? ' checked' : '') + '>' +
'<span class="image-video-permission-user"><span class="image-video-permission-name">' + escapeHtml(user.username || '-') + '</span><span class="image-video-permission-role">' + escapeHtml(roleLabel(user.role)) + '</span></span>' +
'<button class="image-video-permission-state' + (changed ? ' pending' : (saved ? ' granted' : '')) + '" type="button" data-shop-data-permission-toggle="' + userId + '">' +
(changed ? (selected ? '待保存分配' : '待保存取消') : (saved ? '取消分配' : '分配')) +
'</button></div>';
}).join('')
: '<div class="image-video-permission-empty">暂无匹配用户</div>';
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 = '<div class="image-video-permission-empty">加载中...</div>';
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 = '<div class="image-video-permission-empty">' + escapeHtml(error.message || '权限加载失败') + '</div>';
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);
if (document.getElementById('shopDataDuplicatesView').style.display !== 'none') {
loadShopDataDuplicateAsins(1);
}
};
document.getElementById('btnResetShopDataTasks').onclick = function () {
['shopDataTaskFilterShop', 'shopDataTaskFilterGroup', 'shopDataTaskFilterCountry', 'shopDataTaskFilterFrom', 'shopDataTaskFilterTo']
.forEach(function (id) { document.getElementById(id).value = ''; });
loadShopDataCrawlTasks(1);
if (document.getElementById('shopDataDuplicatesView').style.display !== 'none') {
loadShopDataDuplicateAsins(1);
}
};
document.getElementById('shopDataSubTabRecords').onclick = function () { switchShopDataSubTab('records'); };
document.getElementById('shopDataSubTabDuplicates').onclick = function () { switchShopDataSubTab('duplicates'); };
document.getElementById('btnRefreshShopDataDuplicates').onclick = function () { loadShopDataDuplicateAsins(1); };
document.getElementById('shopDataTaskSelectAll').onchange = function (event) {
shopDataTasks.forEach(function (item) {
var resultId = shopDataResultId(item);
if (!item.file_ready || !resultId) return;
if (event.target.checked) selectedShopDataResultIds.add(resultId);
else selectedShopDataResultIds.delete(resultId);
});
syncShopDataSelectionUi();
};
document.getElementById('btnBatchDownloadShopDataTasks').onclick = downloadShopDataTasksZip;
document.getElementById('shopDataTaskGrid').onchange = function (event) {
var checkbox = event.target.closest('[data-shop-data-select]');
if (!checkbox) return;
var resultId = Number(checkbox.dataset.shopDataSelect);
if (!resultId) return;
if (checkbox.checked) selectedShopDataResultIds.add(resultId);
else selectedShopDataResultIds.delete(resultId);
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('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 = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无记录</td></tr>';
} 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 '<img src="' + (url || '').replace(/"/g, '&quot;') + '" class="thumb" alt="">';
}).join('');
return '<tr><td>' + h.id + '</td><td>' + (h.username || '-') + '</td><td>' +
panelTypeLabel(h.panel_type) + '</td><td>' + (h.created_at || '') + '</td><td>' +
'<div class="thumb-wrap">' + (thumbs || '-') + '</div></td></tr>';
}).join('');
}
renderPagination('historyPagination', res.total, res.page, res.page_size, loadHistory);
})
.catch(function () {
document.getElementById('historyListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
var shopManageAllUsers = [];
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// 站点列改成纯展示后,点击 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 '<label class="multi-select-option" for="' + escapeHtml(optionId) + '">' +
'<input type="checkbox" id="' + escapeHtml(optionId) + '" data-multi-option-index="' + index + '"' + (option.selected ? ' checked' : '') + '>' +
'<span>' + escapeHtml(option.textContent || option.value) + '</span>' +
'</label>';
}).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 || '');
});
}
function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) {
var sel = document.getElementById('shopManageGroupMemberSelect');
var helpEl = document.getElementById('shopManageGroupMemberHelp');
if (!sel) return;
var selectedMap = {};
(selectedUserIds || []).forEach(function (id) {
selectedMap[String(id)] = true;
});
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
var options = eligibleUsers.map(function (u) {
return '<option value="' + escapeHtml(u.id) + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + escapeHtml(u.username || '') + '</option>';
});
if (options.length) {
sel.innerHTML = options.join('');
if (helpEl) {
helpEl.textContent = '可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。';
}
return;
}
sel.innerHTML = '<option value="" disabled>当前组长暂无可添加组员</option>';
if (helpEl) {
helpEl.textContent = 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
};
});
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 = '<option value="">全部用户</option>';
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 = '<option value="">全部用户</option>';
}
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 = '<tr><td colspan="7" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">暂无总数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item) {
return '<tr><td>' + escapeHtml(item.id) + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(getDedupeTotalDataCountryLabel(item.country)) + '</td><td>' + escapeHtml(item.username || '') + '</td><td>' + escapeHtml(item.group_name || '未分组') + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-dedupe-total-edit="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '" data-group-id="' + escapeHtml(item.group_id || '') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-dedupe-total-delete="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData);
bindDedupeTotalDataActions();
})
.catch(function () {
document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
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(/&quot;/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(/&quot;/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 = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">暂无数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item) {
var dataValueAttr = (item.data_value || '').replace(/"/g, '&quot;');
var brandAttr = (item.brand || '').replace(/"/g, '&quot;');
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 '<tr><td>' + escapeHtml(item.id || '') + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(item.brand || '') + '</td><td>' + escapeHtml(item.group_name || '') + '</td><td>' + sourceLabel + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-invalid-asin-edit="' + item.id + '" data-value="' + dataValueAttr + '" data-brand="' + brandAttr + '" data-group-id="' + groupId + '" data-source="' + source + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-invalid-asin-delete="' + item.id + '" data-value="' + dataValueAttr + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('invalidAsinDataPagination', res.total, res.page, res.page_size, loadInvalidAsinData);
bindInvalidAsinDataActions();
})
.catch(function () {
document.getElementById('invalidAsinDataListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
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(/&quot;/g, '"');
document.getElementById('editInvalidAsinDataBrand').value = (btn.dataset.brand || '').replace(/&quot;/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(/&quot;/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 '<span class="shop-key-whitelist-status ' + meta.className + '" role="status" aria-label="白名单状态:' + escapeHtml(meta.label) + '" title="' +
escapeHtml(detailText) + '">' + escapeHtml(meta.label) + '</span>';
}
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 = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无店铺密钥</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + renderShopTableText(item.remark_name) + '</td><td>' + renderShopTableText(item.ziniao_account_name) + '</td><td>' + renderShopTableText(item.ziniao_token) + '</td><td>' + renderShopKeyWhitelistStatus(item) + '</td><td>' + renderShopTableText(item.created_at) + '</td><td>' + renderShopTableText(item.updated_at) + '</td><td>' +
'<button type="button" class="btn btn-sm" data-shop-key-edit="' + escapeHtml(item.id) + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button type="button" class="btn btn-sm btn-danger" data-shop-key-delete="' + escapeHtml(item.id) + '" data-ziniao-account-name="' + escapeHtml(item.ziniao_account_name || '') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys);
bindShopKeyActions();
})
.catch(function () {
document.getElementById('shopKeyListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
function bindShopKeyActions() {
document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) {
btn.onclick = function () {
var item = {};
try { item = JSON.parse((btn.dataset.shopKey || '').replace(/&quot;/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(/&quot;/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 '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m2 2 20 20"></path><path d="M6.71 6.71C4.7 8.1 3.34 10.08 2 12c2.12 3.04 5.5 6 10 6 1.67 0 3.17-.41 4.47-1.05"></path><path d="M10.73 5.08A9.36 9.36 0 0 1 12 5c4.5 0 7.88 2.96 10 7a15.82 15.82 0 0 1-2.12 2.91"></path><path d="M14.12 14.12A3 3 0 0 1 9.88 9.88"></path></svg>';
}
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.06 12.35a1 1 0 0 1 0-.7C3.54 8.04 7.06 5.5 12 5.5s8.46 2.54 9.94 6.15a1 1 0 0 1 0 .7C20.46 15.96 16.94 18.5 12 18.5S3.54 15.96 2.06 12.35"></path><circle cx="12" cy="12" r="3"></circle></svg>';
}
function renderShopPasswordCell(item) {
var maskedPassword = item.password || '******';
return '<span class="shop-password-cell">' +
'<span class="shop-password-value" data-shop-password-value title="' + escapeHtml(maskedPassword) + '">' + escapeHtml(maskedPassword) + '</span>' +
'<button type="button" class="shop-password-toggle" data-shop-password-toggle="' + escapeHtml(item.id) + '" data-shop-name="' + escapeHtml(item.shop_name || '') + '" data-masked-password="' + escapeHtml(maskedPassword) + '" aria-label="显示密码" aria-pressed="false" title="显示密码">' +
shopPasswordIcon(false) + '</button></span>';
}
function renderShopTableText(value, fallback) {
var text = String(value == null ? '' : value).trim();
var shown = text || fallback || '-';
return '<span class="table-ellipsis" title="' + escapeHtml(shown) + '">' + escapeHtml(shown) + '</span>';
}
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 = '<tr><td colspan="10" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">暂无店铺</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1;
return '<tr>' +
'<td class="shop-col-index">' + rowNo + '</td>' +
'<td class="shop-col-group">' + renderShopTableText(item.group_name) + '</td>' +
'<td class="shop-col-name">' + renderShopTableText(item.shop_name) + '</td>' +
'<td class="shop-col-mall">' + renderShopTableText(item.mall_name) + '</td>' +
'<td class="shop-col-automation">' + renderShopTableText(item.zn_username) + '</td>' +
'<td class="shop-col-account">' + renderShopTableText(item.account) + '</td>' +
'<td class="shop-col-password">' + renderShopPasswordCell(item) + '</td>' +
'<td class="shop-col-created">' + renderShopTableText(item.created_at) + '</td>' +
'<td class="shop-col-updated">' + renderShopTableText(item.updated_at) + '</td>' +
'<td class="shop-col-actions">' +
'<button type="button" class="btn btn-sm" data-shop-manage-edit="' + escapeHtml(item.id) + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button type="button" class="btn btn-sm btn-danger" data-shop-manage-delete="' + escapeHtml(item.id) + '" data-shop-manage-name="' + escapeHtml(item.shop_name || '') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage);
bindShopManageActions();
})
.catch(function () {
document.getElementById('shopManageListBody').innerHTML = '<tr><td colspan="10" class="empty-tip">请求失败</td></tr>';
});
}
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(/&quot;/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(/&quot;/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 = ['<option value="">请选择分组</option>'];
var filterOpts = ['<option value="">全部分组</option>'];
shopManageGroups.forEach(function (g) {
var option = '<option value="' + g.id + '">' + (g.group_name || '') + '</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
? ['<option value="' + lockedGroup.id + '">' + (lockedGroup.group_name || '') + '</option>']
: [createOpts[0]];
}
var invalidFilterOpts = currentUserRole === 'super_admin'
? filterOpts
: (lockedGroup
? ['<option value="' + lockedGroup.id + '">' + (lockedGroup.group_name || '') + '</option>']
: [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 = '<span class="dedupe-group-summary-chip">全部分组 · ' +
escapeHtml(shopManageGroups.length) + ' 个</span>';
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 '<span class="dedupe-group-summary-chip" title="' + escapeHtml(group.group_name || '') + '">' +
escapeHtml(group.group_name || '') + ' · ' + relation + '</span>';
}).join('') + (shopManageGroups.length > visibleGroups.length
? '<span>另 ' + escapeHtml(shopManageGroups.length - visibleGroups.length) + ' 个</span>'
: '');
}
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 = '<span class="dedupe-group-summary-chip">全部分组 · ' +
escapeHtml(shopManageGroups.length) + ' 个</span>';
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 '<span class="dedupe-group-summary-chip" title="' + escapeHtml(group.group_name || '') + '">' +
escapeHtml(group.group_name || '') + ' · ' + relation + '</span>';
}).join('') + (shopManageGroups.length > visibleGroups.length
? '<span>另 ' + escapeHtml(shopManageGroups.length - visibleGroups.length) + ' 个</span>'
: '');
}
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 = '<tr><td colspan="8" class="empty-tip">暂无分组</td></tr>';
return;
}
tbody.innerHTML = shopManageGroups.map(function (item, index) {
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames : [];
var memberHtml = memberNames.length
? '<div class="shop-group-members">' +
memberNames.map(function (name) {
return '<span class="shop-group-member-chip" title="' + escapeHtml(name || '') + '">' + escapeHtml(name || '') + '</span>';
}).join('') + '</div>'
: '<span style="color:#999;">-</span>';
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
var actionHtml = canEdit
? ('<button class="btn btn-sm" data-shop-group-edit="' + escapeHtml(item.id) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + escapeHtml(item.id) + '" data-shop-group-name="' + escapeHtml(item.group_name || '') + '">删除</button>')
: '<span style="color:#999;">-</span>';
return '<tr>' +
'<td>' + (index + 1) + '</td>' +
'<td class="shop-group-name-cell">' + escapeHtml(item.group_name || '') + '</td>' +
'<td class="shop-group-leader-cell">' + escapeHtml(item.leader_username || '') + '</td>' +
'<td>' + escapeHtml(item.member_count || 0) + '</td>' +
'<td>' + memberHtml + '</td>' +
'<td class="shop-group-time-cell">' + escapeHtml(item.created_at || '') + '</td>' +
'<td class="shop-group-time-cell">' + escapeHtml(item.updated_at || '') + '</td>' +
'<td class="shop-group-action-cell">' + actionHtml + '</td>' +
'</tr>';
}).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(/&quot;/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');
};
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 = '<option value="">请先选择分组</option>';
select.disabled = true;
return;
}
select.innerHTML = '<option value="">加载中…</option>';
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 = '<option value="">该分组暂无店铺</option>';
return;
}
select.innerHTML = ['<option value="">请选择店铺</option>'].concat(names.map(function (name) {
return '<option value="' + escapeHtml(name) + '">' + escapeHtml(name) + '</option>';
})).join('');
select.disabled = false;
})
.catch(function () {
if (select.getAttribute('data-loading-group-id') !== requestGroupId) return;
select.innerHTML = '<option value="">店铺加载失败</option>';
});
}
// ========== 跳过跟价 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 '<span class="asin-empty-value">-</span>';
return '<button type="button" class="asin-copy-text" title="点击复制" data-copy-asin="' + escapeHtml(asinValue) + '">' + escapeHtml(asinValue) + '</button>';
}
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 '<tr>' +
(isFirst ? '<td class="asin-col-index" rowspan="' + rowspan + '">' + rowNo + '</td>' : '') +
(isFirst ? '<td class="asin-col-group" rowspan="' + rowspan + '">' + renderShopTableText(item.group_name) + '</td>' : '') +
(isFirst ? '<td class="asin-col-shop" rowspan="' + rowspan + '">' + renderShopTableText(item.shop_name) + '</td>' : '') +
'<td class="asin-col-country">' + renderAsinCopyButton(entry.asin) + '</td>' +
'<td class="asin-col-country">' + (entry.country ? escapeHtml(entry.country.label) : '<span class="asin-empty-value">-</span>') + '</td>' +
'<td class="asin-col-country asin-col-price">' + (entry.country ? escapeHtml(entry.minimumPrice || '-') : '<span class="asin-empty-value">-</span>') + '</td>' +
(isFirst ? '<td class="asin-col-actions" rowspan="' + rowspan + '"><button type="button" class="btn btn-sm" data-skip-price-asin-configure="' + escapeHtml(item.id) + '" aria-label="配置' + shopName + '各站点 ASIN">配置</button></td>' : '') +
'</tr>';
}).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 '<div class="form-group">' +
'<label for="skipPriceAsinDrawer_' + escapeHtml(country.code) + '">' + escapeHtml(country.label) + '</label>' +
'<div class="drawer-price-grid">' +
'<input type="text" id="skipPriceAsinDrawer_' + escapeHtml(country.code) + '" data-skip-price-drawer-input="' + escapeHtml(country.code) + '" value="' + escapeHtml(asinValue) + '" placeholder="请输入' + escapeHtml(country.label) + ' ASIN">' +
'<input type="number" min="0" step="0.01" data-skip-price-drawer-minimum-price="' + escapeHtml(country.code) + '" value="' + escapeHtml(minimumPriceValue) + '" placeholder="最低价" aria-label="' + escapeHtml(country.label) + '最低价">' +
'</div></div>';
}).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 = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
skipPriceAsinItemsById = {};
items.forEach(function (item) { skipPriceAsinItemsById[String(item.id)] = item; });
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">暂无数据</td></tr>';
} 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 = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
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 '<div class="form-group">' +
'<label for="queryAsinDrawer_' + escapeHtml(country.code) + '">' + escapeHtml(country.label) + '</label>' +
'<input type="text" id="queryAsinDrawer_' + escapeHtml(country.code) + '" data-query-asin-drawer-input="' + escapeHtml(country.code) + '" value="' + escapeHtml(value) + '" placeholder="请输入' + escapeHtml(country.label) + ' ASIN">' +
'</div>';
}).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 '<tr>' +
(isFirst ? '<td class="asin-col-index" rowspan="' + rowspan + '">' + rowNo + '</td>' : '') +
(isFirst ? '<td class="asin-col-group" rowspan="' + rowspan + '">' + renderShopTableText(item.group_name) + '</td>' : '') +
(isFirst ? '<td class="asin-col-shop" rowspan="' + rowspan + '">' + renderShopTableText(item.shop_name) + '</td>' : '') +
'<td class="asin-col-country">' + renderAsinCopyButton(entry.asin) + '</td>' +
'<td class="asin-col-country">' + (entry.country ? escapeHtml(entry.country.label) : '<span class="asin-empty-value">-</span>') + '</td>' +
(isFirst ? '<td class="asin-col-actions" rowspan="' + rowspan + '"><button type="button" class="btn btn-sm" data-query-asin-configure="' + escapeHtml(item.id) + '" aria-label="配置' + shopName + '各站点 ASIN">配置</button></td>' : '') +
'</tr>';
}).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 = '<tr><td colspan="6" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
queryAsinItemsById = {};
items.forEach(function (item) { queryAsinItemsById[String(item.id)] = item; });
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无数据</td></tr>';
} 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 = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
});
}
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 = ['<option value="">顶级类目</option>'];
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('<option value="' + escapeHtml(item.id) + '">' + prefix + escapeHtml(item.name || '') + '</option>');
});
if (selected && !productCategoryItems.some(function (item) { return String(item.id) === selected; })) {
options.push('<option value="' + escapeHtml(selected) + '">当前父级 #' + escapeHtml(selected) + '</option>');
}
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 = '<tr><td colspan="6" class="empty-tip">暂无商品类目</td></tr>';
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 '<tr><td colspan="6"><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indentMore + 'px;"></span>' +
'<button class="btn btn-sm btn-secondary" type="button" data-product-category-load-more="' + escapeHtml(parent ? parent.id : '') + '"' + (loading ? ' disabled' : '') + '>' +
(loading ? '加载中...' : '加载更多') + '</button>' +
'<span class="empty-tip" style="padding:0;">已加载 ' + escapeHtml(loadedCount) + ' / ' + escapeHtml(totalCount) + '</span></div></td></tr>';
}
if (row.type === 'searchMore') {
var searchLoadedCount = (productCategorySearchState.items || []).length;
var searchTotalCount = productCategorySearchState.total || 0;
return '<tr><td colspan="6"><button class="btn btn-sm btn-secondary" type="button" data-product-category-search-more' + (productCategorySearchState.loading ? ' disabled' : '') + '>' +
(productCategorySearchState.loading ? '加载中...' : '加载更多搜索结果') + '</button> ' +
'<span class="empty-tip" style="padding:0;">已加载 ' + escapeHtml(searchLoadedCount) + ' / ' + escapeHtml(searchTotalCount) + '</span></td></tr>';
}
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 '<tr>' +
'<td><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indent + 'px;"></span>' +
'<button type="button" class="tree-node-mark' + (hasChildren ? '' : ' is-leaf') + '" data-product-category-toggle="' + escapeHtml(item.id) + '"' + (hasChildren ? '' : ' disabled') + '>' + mark + '</button>' +
'<strong>' + escapeHtml(item.name || '') + '</strong></div></td>' +
'<td><span class="category-path-value table-ellipsis" title="' + escapeHtml(item.path || item.name || '') + '">' + escapeHtml(item.path || item.name || '') + '</span></td>' +
'<td>' + escapeHtml(item.sort_order || 0) + '</td>' +
'<td><span class="category-tag">' + (item.is_builtin ? '内置' : '自定义') + '</span></td>' +
'<td>' + escapeHtml(item.description || '') + '</td>' +
'<td><button class="btn btn-sm" data-product-category-edit="' + escapeHtml(item.id) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-product-category-delete="' + escapeHtml(item.id) + '" data-product-category-name="' + escapeHtml(item.name || '') + '"' + (item.child_count > 0 ? ' disabled' : '') + '>删除</button></td>' +
'</tr>';
}).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 = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
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 = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
})
.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 = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
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 = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
})
.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 = '<tr><td colspan="4" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">暂无版本记录</td></tr>';
return;
}
tbody.innerHTML = items.map(function (v) {
var url = v.file_url || '';
return '<tr><td>' + (v.version || '') + '</td><td><a href="' + url + '" target="_blank" rel="noopener">' + url + '</a></td><td>' + (v.created_at || '') + '</td><td><a href="' + url + '" download class="btn btn-sm">下载</a></td></tr>';
}).join('');
})
.catch(function () {
document.getElementById('versionListBody').innerHTML = '<tr><td colspan="4" class="empty-tip">请求失败</td></tr>';
});
}
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 = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.message || res.error || '') + '</td></tr>';
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 = '<tr><td colspan="8" class="empty-tip">暂无版本记录</td></tr>';
} 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 ? '<span style="color:#ff4d4f;font-weight:600;">★</span>' : '';
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 += '<button type="button" class="btn btn-sm" data-admin-confirm data-confirm-message="确认发布版本 ' + v.version + ' 吗?" onclick="releaseVersion(\'' + v.version + '\')">发布</button> ';
}
if (v.status === 'RELEASED' && !v.isLatest) {
actions += '<button type="button" class="btn btn-sm" data-admin-confirm data-confirm-message="确认将版本 ' + v.version + ' 设为最新吗?客户端会自动检测更新。" onclick="setLatestVersion(\'' + v.version + '\')">设为最新</button> ';
}
if (v.status === 'RELEASED') {
actions += '<button class="btn btn-sm" onclick="downloadDigitalHumanVersion(\'' + encodeURIComponent(v.version || '') + '\')">下载</button> ';
}
if (!v.isLatest) {
actions += '<button type="button" class="btn btn-sm btn-danger" data-admin-confirm data-confirm-message="确认删除版本 ' + v.version + ' 吗?该操作会删除对应文件,无法恢复。" onclick="deleteVersion(\'' + v.version + '\')">删除</button>';
}
return '<tr>' +
'<td>' + (v.version || '') + '</td>' +
'<td style="color:' + statusColor + ';">' + statusText + '</td>' +
'<td>' + isLatestBadge + '</td>' +
'<td>' + fileSize + '</td>' +
'<td title="' + (v.md5 || '') + '">' + md5Short + '</td>' +
'<td>' + changelog + '</td>' +
'<td>' + releasedAt + '</td>' +
'<td>' + actions + '</td>' +
'</tr>';
}).join('');
}
// 分页
var pagination = document.getElementById('digitalHumanVersionPagination');
if (pages > 1) {
var html = '';
for (var i = 1; i <= pages; i++) {
var active = i === current ? ' active' : '';
html += '<span class="page-item' + active + '" onclick="loadDigitalHumanVersions(' + i + ')">' + i + '</span>';
}
pagination.innerHTML = html;
appendPaginationQuickJump(pagination, pages, current, loadDigitalHumanVersions);
} else {
pagination.innerHTML = '';
}
})
.catch(function (err) {
document.getElementById('digitalHumanVersionListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败: ' + (err.message || '') + '</td></tr>';
});
}
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) {
msgEl.textContent = '发布成功。版本:' + res.version + ',链接:' + (res.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 = '<option value="">请选择页面</option>';
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 = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
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 = '<tr><td colspan="7" class="empty-tip">暂无菜单,请先在上方新增</td></tr>';
} 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 ? '<span class="column-child-mark" style="margin-left:' + ((entry.depth - 1) * 16) + 'px;">└</span>' : '';
return '<tr data-column-id="' + escapeHtml(c.id) + '" data-column-group="' + escapeHtml(columnSiblingGroupKey(c)) + '">' +
'<td class="column-drag-cell"><span class="column-drag-handle" draggable="true" role="button" tabindex="0" title="拖动调整同级顺序" aria-label="拖动调整“' + escapeHtml(c.name || '') + '”的顺序" data-column-drag-handle="' + escapeHtml(c.id) + '">⠿</span></td>' +
'<td>' + escapeHtml(c.id) + '</td>' +
'<td>' + indent + escapeHtml(c.name || '') + '</td>' +
'<td>' + ((c.menu_type || '') === 'admin' ? '后台' : '软件') + '</td>' +
'<td>' + (parent ? escapeHtml(parent.name || '') : '-') + '</td>' +
'<td>' + escapeHtml(c.created_at || '') + '</td>' +
'<td><button class="btn btn-sm" data-column-edit="' + escapeHtml(c.id) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-column-delete="' + escapeHtml(c.id) + '" data-name="' + escapeHtml(c.name || '') + '">删除</button></td></tr>';
}).join('');
}
populateColumnPageSelects();
bindColumnActions();
})
.catch(function () {
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
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(/&quot;/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',
'<label class="pagination-jump">跳至<input type="number" min="1" max="' + totalPages + '" step="1" inputmode="numeric" aria-label="跳转页码" data-page-jump-input>页</label>' +
'<button type="button" data-page-jump>跳转</button>');
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 = '<span>共' + total + ' 条</span>' +
'<button ' + (page <= 1 ? 'disabled' : '') + ' data-p="' + (page - 1) + '">上一页</button>' +
'<span>第' + page + ' / ' + totalPages + ' 页</span>' +
'<button ' + (page >= totalPages ? 'disabled' : '') + ' data-p="' + (page + 1) + '">下一页</button>';
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 || '管理员';
var avatarEl = document.getElementById('adminUserAvatar');
if (avatarEl) avatarEl.textContent = (item.username || '管').charAt(0).toUpperCase();
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 () {
fetch('/api/admin/logout', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) {
if (r.status === 404) {
window.location.href = '/logout';
return null;
}
return r.json();
})
.then(function (res) {
if (!res) return;
if (!res.success) {
alert(res.error || '退出失败');
return;
}
window.location.replace(res.redirect || '/login?logout=1');
})
.catch(function () {
window.location.replace('/logout');
});
};
loadAdminCurrentUser().finally(function () {
loadUserOptions();
loadShopManageGroups();
loadAdminMenus();
});
}) ();