Files
crawler-plugin/backend/static/admin.js
T
huangzd1997 d3b499b596 feat(admin): 店铺重复检查导出「站点」列改「国家」中文显示;撞款抽屉按点击处定位
- 导出 CSV 列头 站点→国家,值映射中文(DE德国/UK英国/FR法国/IT意大利/ES西班牙,未知回退原值)
- 撞款详情抽屉以触发元素为垂直锚点并钳制在视口内,不再贴区块/页面顶部导致看不见需上滑
- 同步 Java 静态托管副本;更新 Java/Python 相关断言
2026-09-05 01:53:12 +08:00

7207 lines
426 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', 'shop-data-duplicate-check'] },
{ 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>',
'shop-data-duplicate-check': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.35 11.1h-9.17a2 2 0 0 1-1.75-2.98l1.67-2.79a2 2 0 0 0-1.75-2.98l-5.98-.01a2 2 0 0 0-2 2v1.5a2 2 0 0 0 2 2h3.36l-2.38 3.97a2 2 0 0 0 1.75 2.98h11.5a2 2 0 0 0 2-2v-1.5a2 2 0 0 0-2-2Z"></path><path d="M3 21h18"></path></svg>',
'history': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M12 7v5l4 2"></path></svg>',
'version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7.5 4.27 9 5.15"></path><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"></path><path d="M3.3 7 12 12l8.7-5"></path><path d="M12 22V12"></path></svg>',
'digital-human-version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>'
};
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',
'shop-data-duplicate-check': 'panel-shop-data-duplicate-check',
'history': 'panel-history',
'version': 'panel-version',
'digital-human-version': 'panel-digital-human-version'
};
function runTabLoader(tabName) {
if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); }
else if (tabName === 'columns') loadColumns();
else if (tabName === 'group-manage') initShopManageGroupPanel();
else if (tabName === 'dedupe-total-data') {
loadDedupeTotalData(1);
loadDedupeGroupSummary();
}
else if (tabName === 'invalid-asin-data') {
loadShopManageGroups();
loadInvalidAsinData(1);
}
else if (tabName === 'shop-keys') loadShopKeys(1);
else if (tabName === 'shop-manage') loadShopManage(1);
else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1);
else if (tabName === 'query-asin') loadQueryAsin(1);
else if (tabName === 'product-categories') loadProductCategories();
else if (tabName === 'image-video-tasks') loadImageVideoTasks(1);
else if (tabName === 'shop-data-crawl-tasks') loadShopDataCrawlTasks(1);
else if (tabName === 'shop-data-duplicate-check') loadShopDataDuplicateCheckOverview();
else if (tabName === 'history') loadHistory(1);
else if (tabName === 'version') loadSoftwareVersions();
else if (tabName === 'digital-human-version') loadDigitalHumanVersions();
}
function hideAllAdminPanels() {
document.querySelectorAll('.tab-panel').forEach(function (panel) {
panel.classList.remove('active');
panel.style.display = 'none';
});
}
function getActiveAdminTabName() {
return activeAdminTabName;
}
function activateAdminTab(tabName) {
var panelId = ADMIN_PANEL_MAP[tabName];
var panel = panelId ? document.getElementById(panelId) : null;
if (!panel) return;
activeAdminTabName = tabName;
document.querySelectorAll('#adminMenu .tab').forEach(function (tab) {
var isActive = tab.dataset.tab === tabName;
tab.classList.toggle('active', isActive);
if (isActive) tab.setAttribute('aria-current', 'page');
else tab.removeAttribute('aria-current');
});
// 激活页面时自动展开所属分组(折叠由用户显式控制)
if (adminMenuEl) {
adminMenuEl.querySelectorAll('.menu-group').forEach(function (group) {
if (group.querySelector('.tab[data-tab="' + tabName + '"]')) {
group.classList.remove('is-collapsed');
var groupTitle = group.querySelector('.menu-group-title');
if (groupTitle) groupTitle.setAttribute('aria-expanded', 'true');
}
});
}
hideAllAdminPanels();
panel.style.display = '';
panel.classList.add('active');
var nameItem = adminMenuByRoute[tabName];
var titleEl = document.getElementById('adminPageTitle');
if (titleEl) titleEl.textContent = (nameItem && nameItem.name) || tabName;
runTabLoader(tabName);
}
function renderAdminTabs(items) {
var knownItems = (items || []).filter(function (item) {
return !!ADMIN_PANEL_MAP[item.route_path];
});
if (!knownItems.length) {
activeAdminTabName = '';
if (adminMenuEl) adminMenuEl.innerHTML = '<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 bindAdminMenuTabs(serverRendered) {
adminMenuByRoute = {};
var routes = [];
adminMenuEl.querySelectorAll('#adminMenu .tab').forEach(function (tab) {
var route = tab.dataset.tab;
if (!route) return;
routes.push(route);
if (!adminMenuByRoute[route]) {
adminMenuByRoute[route] = { route_path: route, name: tab.getAttribute('title') || route };
}
tab.onclick = function () {
activateAdminTab(tab.dataset.tab);
};
});
return routes;
}
function loadAdminMenus(preferredTab) {
if (preferredTab) activeAdminTabName = preferredTab;
// 服务端已按权限渲染菜单(data-server-rendered=true):直接绑定事件并激活,
// 不再请求接口重渲染,避免"全量菜单→折叠"的闪烁。
if (!adminMenuEl || adminMenuEl.getAttribute('data-server-rendered') !== 'true') {
renderAdminTabsFromApi();
return;
}
var serverRoutes = bindAdminMenuTabs(true);
if (!serverRoutes.length) {
activeAdminTabName = '';
hideAllAdminPanels();
return;
}
if (window.__initAdminMenuCollapse) window.__initAdminMenuCollapse();
var fallbackTab = activeAdminTabName && serverRoutes.indexOf(activeAdminTabName) >= 0
? activeAdminTabName
: serverRoutes[0];
activateAdminTab(fallbackTab);
}
function renderAdminTabsFromApi() {
fetch('/api/admin/current-user/menus')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
if (adminMenuEl) adminMenuEl.innerHTML = '<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() {
if (!shopDataTasks.length) {
return '<div class="shop-data-empty-hint">暂无符合条件的店铺数据任务</div>';
}
// 卡片网格:每店铺一张卡片,按截图样式(店铺名、当日累计文件、分组、最新时间、任务号+状态、国家、文件名、下载/删除)
var cards = shopDataTasks.map(function (item) {
var resultId = shopDataResultId(item);
var selected = resultId > 0 && selectedShopDataResultIds.has(resultId);
var status = String(item.status || item.file_status || '').toUpperCase();
var terminal = ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(status) >= 0;
var countryCodes = item.country_codes != null ? item.country_codes : item.countryCodes;
var updatedAt = item.updated_at || item.latest_created_at || item.created_at || item.finished_at || '-';
var taskNo = item.task_id || item.taskId || '-';
var errorTitle = item.error ? ' title="' + escapeHtml(item.error) + '"' : '';
return '<article class="shop-data-card" data-shop-data-card="' + (resultId || '') + '">' +
'<div class="shop-data-card-head">' +
'<span class="shop-data-card-shop">' + escapeHtml(item.shop_name || '-') + '</span>' +
'</div>' +
'<div class="shop-data-card-select-row">' +
'<input type="checkbox" data-shop-data-select="' + (resultId || '') + '"' +
(selected ? ' checked' : '') + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>' +
'<span class="shop-data-card-task-no">任务 ' + escapeHtml(String(taskNo)) + '</span>' +
'<span class="shop-data-status ' + (terminal ? (status === 'SUCCESS' ? 'success' : 'failed') : 'running') + '"' + errorTitle + '>' +
escapeHtml(imageVideoStatusLabel(status)) + '</span>' +
'</div>' +
'<div class="shop-data-card-row"><span class="shop-data-card-label">分组</span>' +
'<span class="shop-data-card-value">' + escapeHtml(item.group_name || '-') + '</span></div>' +
'<div class="shop-data-card-row"><span class="shop-data-card-label">最新</span>' +
'<span class="shop-data-card-value">' + escapeHtml(updatedAt) + '</span></div>' +
'<div class="shop-data-card-row"><span class="shop-data-card-label">国家</span>' +
'<span class="shop-data-card-value">' + escapeHtml(countryListLabel(countryCodes)) + '</span></div>' +
'<div class="shop-data-card-row"><span class="shop-data-card-label">文件</span>' +
'<span class="shop-data-card-value shop-data-card-file">' + escapeHtml(item.output_filename || '-') + '</span></div>' +
'<div class="shop-data-card-actions">' +
'<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') + '>' + shopDataDeleteIcon() + '删除</button>' +
'</div>' +
'</article>';
}).join('');
return '<div class="shop-data-card-grid">' + cards + '</div>';
}
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();
});
}
// ========== 店铺数据重复检查(指标卡 + 分布 + 矩阵表格 + 抽屉)==========
var shopDataDuplicatePage = 1, shopDataDuplicatePageSize = 20;
var shopDataDuplicateItems = [];
var shopDataDuplicateShops = [];
var shopDataDuplicateTotal = 0;
var shopDataDuplicateView = 'monitor';
var shopDataDuplicateLoading = false;
var shopDataDuplicateScannedAt = '';
var shopDataDuplicateOverviewPending = false;
var shopDataDuplicateExporting = false;
// 撞款详情卡片区(跨店重复 ASIN)独立分页
var shopDataDuplicateDetailPage = 1;
var shopDataDuplicateDetailPageSize = 6;
var shopDataDuplicateDetailItems = [];
var shopDataDuplicateDetailTotal = 0;
var shopDataDuplicateDetailLoading = false;
// 站点代码 → 中文国家名(撞款详情/抽屉展示用),未识别回退原值
var DUP_SITE_LABELS = { DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙' };
function dupSiteLabel(site) {
var key = String(site || '').trim().toUpperCase();
return DUP_SITE_LABELS[key] || String(site || '').trim();
}
function buildShopDataDuplicateQuery(page) {
var params = new URLSearchParams();
params.set('page', String(page || 1));
params.set('page_size', String(shopDataDuplicatePageSize));
params.set('view', shopDataDuplicateView);
var values = {
asin: document.getElementById('shopDataDupFilterAsin').value.trim(),
shop_name: document.getElementById('shopDataDupFilterShop').value.trim(),
country: document.getElementById('shopDataDupFilterCountry').value.trim(),
site: document.getElementById('shopDataDupFilterSite').value.trim(),
date_from: document.getElementById('shopDataDupFilterDateFrom').value,
date_to: document.getElementById('shopDataDupFilterDateTo').value
};
Object.keys(values).forEach(function (key) {
if (values[key]) params.set(key, values[key]);
});
return params.toString();
}
function pad2(value) {
var n = parseInt(value, 10);
if (isNaN(n)) return String(value);
return n < 10 ? '0' + n : String(n);
}
// 中文日期(如 2026年8月18日 上午4:34)归一化为 YYYY-MM-DD HH:MM[:SS],便于展示与排序
function normalizeDuplicateTime(raw) {
if (!raw) return '';
var s = String(raw).trim();
var m = s.match(/^(\d{4})年(\d{1,2})月(\d{1,2})日\s*(.*)$/);
if (m) {
var md = m[1] + '-' + pad2(m[2]) + '-' + pad2(m[3]);
var tm = m[4].match(/(上午|下午|晚上|凌晨)?\s*(\d{1,2})[:](\d{2})(?:[:](\d{1,2}))?/);
if (!tm) return md;
var hour = parseInt(tm[2], 10);
var period = tm[1] || '';
if (period === '下午' || period === '晚上') hour = hour % 12 + 12;
if ((period === '上午' || period === '凌晨') && hour === 12) hour = 0;
return md + ' ' + pad2(hour) + ':' + tm[3] + (tm[4] ? ':' + pad2(tm[4]) : '');
}
var dm = s.replace(/\//g, '-').match(/^(\d{4})-(\d{1,2})-(\d{1,2})(.*)$/);
if (dm) return dm[1] + '-' + pad2(dm[2]) + '-' + pad2(dm[3]) + (dm[4] || '');
return s;
}
function renderShopDataDuplicateMetrics(summary) {
var metricsEl = document.getElementById('shopDataDuplicateMetrics');
if (!summary || summary.asin_total == null) {
metricsEl.innerHTML = '<div class="dup-check-metric" style="grid-column:1/-1;"><span class="dup-check-metric-label">暂无扫描结果</span>' +
'<span class="dup-check-metric-value">-</span></div>';
return;
}
var defs = [
{ label: '唯一ASIN', value: summary.asin_total, accent: true },
{ label: '上架记录', value: summary.record_total, accent: true },
{ label: '在线店铺', value: summary.shop_count, accent: true },
{ label: '重复ASIN', value: summary.duplicate_asin_total, warn: true },
{ label: '重复店铺', value: summary.duplicate_shop_count, warn: true },
{ label: '店铺平均ASIN', value: summary.asin_per_shop }
];
metricsEl.innerHTML = defs.map(function (def) {
return '<div class="dup-check-metric' + (def.accent ? ' accent' : '') + (def.warn ? ' warn' : '') + '">' +
'<span class="dup-check-metric-label">' + def.label + '</span>' +
'<span class="dup-check-metric-value">' + def.value + '</span></div>';
}).join('');
}
function renderShopDataDuplicateDistribution(shops) {
var barsEl = document.getElementById('shopDataDuplicateDistribution');
var noteEl = document.getElementById('shopDataDuplicateDistributionNote');
if (!shops || !shops.length) {
barsEl.innerHTML = '<div class="shop-data-empty-hint">暂无店铺数据</div>';
noteEl.textContent = '';
return;
}
var max = 0;
shops.forEach(function (shop) { if (shop.asin_count > max) max = shop.asin_count; });
// 新样式:双列网格,每格 = 店名 + 记录数 / 进度条 / N 个 ASIN(按 ASIN 数降序)
barsEl.innerHTML = shops.map(function (shop) {
var width = max ? Math.max(2, Math.round(shop.asin_count / max * 100)) : 0;
var recordCount = Number(shop.record_count || 0);
var asinCount = Number(shop.asin_count || 0);
return '<div class="dup-dist-item">' +
'<div class="dup-dist-item-head">' +
'<span class="dup-dist-name" title="' + escapeHtml(shop.shop_name) + '">' + escapeHtml(shop.shop_name) + '</span>' +
'<span class="dup-dist-record">' + recordCount.toLocaleString() + '</span>' +
'</div>' +
'<div class="dup-dist-track"><span class="dup-dist-fill" style="width:' + width + '%"></span></div>' +
'<div class="dup-dist-asin">' + asinCount.toLocaleString() + ' 个 ASIN</div>' +
'</div>';
}).join('');
noteEl.textContent = '各店铺在本批站点中的上架记录量 · 共 ' + shops.length + ' 家店铺';
}
function shopDataDuplicateCellCount(item, shopName) {
var count = 0;
(item.occurrences || []).forEach(function (occ) {
if ((occ.shop_name || '') === shopName) count++;
});
return count;
}
function renderShopDataDuplicateMatrix() {
var list = document.getElementById('shopDataDuplicateList');
if (!shopDataDuplicateItems.length) {
list.innerHTML = '<div class="shop-data-empty-hint">暂无数据'
+ (shopDataDuplicateView === 'monitor' && !shopDataDuplicateTotal ? ':当前没有跨店铺重复的 ASIN' : '')
+ '。可点击右上角「重新分析」重新扫描。</div>';
return;
}
var shops = shopDataDuplicateShops;
var thead = '<thead><tr><th>ASIN</th><th class="dup-cell">店铺数</th>' +
shops.map(function (shop) {
return '<th class="dup-cell" title="' + escapeHtml(shop.shop_name) + '">' + escapeHtml(shop.shop_name) + '</th>';
}).join('') + '</tr></thead>';
var tbody = shopDataDuplicateItems.map(function (item) {
var cells = shops.map(function (shop) {
var count = shopDataDuplicateCellCount(item, shop.shop_name);
var cls = count === 0 ? 'zero' : (item.shop_count >= 2 ? 'danger' : 'has');
return '<td class="dup-cell"><span class="dup-cell-num ' + cls + '"'
+ (count ? ' data-open-drawer="' + escapeHtml(item.asin) + '" title="' + escapeHtml(item.asin) + ' 在 ' + escapeHtml(shop.shop_name) + '' + count + ' 条"' : '')
+ '>' + (count === 0 ? '0' : count) + '</span></td>';
}).join('');
return '<tr>' +
'<td class="dup-asin-col" data-open-drawer="' + escapeHtml(item.asin) + '" title="查看详情">' + escapeHtml(item.asin) + '</td>' +
'<td class="dup-cell"><span class="dup-cell-num ' + (item.shop_count >= 2 ? 'danger' : 'has') + '">' + item.shop_count + '</span></td>' +
cells + '</tr>';
}).join('');
list.innerHTML = '<table class="dup-check-table"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table>';
}
function renderShopDataDuplicateList() {
renderShopDataDuplicateMatrix();
}
// 主时间戳(转数值失败时按字符串比较)
function dupTimeRank(raw) {
var t = Date.parse(normalizeDuplicateTime(raw));
return isNaN(t) ? -1 : t;
}
// 「查看明细」按钮点击开抽屉(动态元素,事件委托),并把被点按钮作为抽屉的垂直锚点
function handleDuplicateDetailClick(event) {
var button = event.target.closest('[data-open-asin-detail]');
if (button) openShopDataDuplicateDrawer(button.dataset.openAsinDetail, button);
}
// 站点徽章:中文国家(未识别回退原值)
function dupSiteBadge(site) {
return '<span class="dup-site-badge dup-site-' + escapeHtml(String(site || '-').toUpperCase()) + '">'
+ escapeHtml(dupSiteLabel(site)) + '</span>';
}
// 撞款详情卡片区:同一 ASIN 在多条店铺的上架明细(独立分页,仅跨店铺重复)
function renderShopDataDuplicateDetailCards() {
var block = document.getElementById('dupCheckDetailBlock');
var cardsEl = document.getElementById('dupCheckDetailCards');
if (!block || !cardsEl) return;
if (!shopDataDuplicateDetailItems.length) {
block.style.display = 'none';
cardsEl.innerHTML = '';
return;
}
block.style.display = '';
cardsEl.innerHTML = shopDataDuplicateDetailItems.map(function (item) {
// 按店铺分组:名称 + 次数 + 国家 + 上架时间(倒序)
var shopsMap = {};
(item.occurrences || []).forEach(function (occ) {
var key = occ.shop_name || '-';
if (!shopsMap[key]) shopsMap[key] = { shop: key, sites: {}, times: [], count: 0 };
var info = shopsMap[key];
info.count++;
if (occ.country) info.sites[occ.country] = true;
var d = normalizeDuplicateTime(occ.date);
if (d) info.times.push(d);
});
var rows = Object.keys(shopsMap).sort().map(function (key) {
var info = shopsMap[key];
info.times.sort(function (a, b) { return (a < b ? 1 : (a > b ? -1 : 0)); });
info.times = Array.from(new Set(info.times));
var rowSites = Object.keys(info.sites).sort().map(function (site) {
return dupSiteBadge(site);
}).join('');
return '<tr>' +
'<td class="dup-shop">' + escapeHtml(info.shop) + '<span class="dup-detail-count">' + info.count + ' 次</span></td>' +
'<td>' + (rowSites || '-') + '</td>' +
'<td class="dup-date">' + info.times.map(escapeHtml).join('、') + '</td>' +
'</tr>';
}).join('');
return '<div class="duplicate-asin-card">' +
'<div class="duplicate-asin-card-head">' +
'<span class="dup-asin" title="' + escapeHtml(item.asin) + '">' + escapeHtml(item.asin) + '</span>' +
'<span class="dup-count">' + item.shop_count + ' 家店铺</span>' +
'<span class="dup-brand">' + escapeHtml(item.brand || '') + '</span>' +
'<button class="btn btn-sm btn-secondary dup-detail-view-btn" type="button" data-open-asin-detail="' + escapeHtml(item.asin) + '">查看明细</button>' +
'</div>' +
'<table class="duplicate-asin-table dup-card-table"><thead>' +
'<tr><th>店铺</th><th>国家</th><th>上架时间</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table>' +
'</div>';
}).join('');
}
// 撞款详情分页加载:筛选/重置时回到第 1 页,矩阵翻页不影响详情页
function loadShopDataDuplicateDetail(page) {
if (shopDataDuplicateDetailLoading) return;
shopDataDuplicateDetailLoading = true;
var block = document.getElementById('dupCheckDetailBlock');
var cardsEl = document.getElementById('dupCheckDetailCards');
var params = new URLSearchParams();
params.set('page', String(page || 1));
params.set('page_size', String(shopDataDuplicateDetailPageSize));
var values = {
asin: document.getElementById('shopDataDupFilterAsin').value.trim(),
shop_name: document.getElementById('shopDataDupFilterShop').value.trim(),
country: document.getElementById('shopDataDupFilterCountry').value.trim(),
site: document.getElementById('shopDataDupFilterSite').value.trim(),
date_from: document.getElementById('shopDataDupFilterDateFrom').value,
date_to: document.getElementById('shopDataDupFilterDateTo').value
};
Object.keys(values).forEach(function (key) {
if (values[key]) params.set(key, values[key]);
});
cardsEl.innerHTML = '<div class="shop-data-empty-hint">加载中...</div>';
fetch('/api/admin/shop-data-crawl/duplicate-check-detail?' + params.toString())
.then(function (response) { return response.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '加载失败');
shopDataDuplicateDetailItems = res.items || [];
shopDataDuplicateDetailTotal = Number(res.total) || 0;
shopDataDuplicateDetailPage = res.page || 1;
renderShopDataDuplicateDetailCards();
if (block.style.display !== 'none') {
renderPagination('dupCheckDetailPagination', shopDataDuplicateDetailTotal,
shopDataDuplicateDetailPage, shopDataDuplicateDetailPageSize,
loadShopDataDuplicateDetail);
}
})
.catch(function (error) {
shopDataDuplicateDetailItems = [];
shopDataDuplicateDetailTotal = 0;
renderShopDataDuplicateDetailCards();
})
.finally(function () {
shopDataDuplicateDetailLoading = false;
});
}
// 抽屉徽章:基础滴状标签(按上下文附加不同配色类)
function dupDrawerBadge(text, cls) {
return '<span class="dup-drawer-badge' + (cls ? ' ' + cls : '') + '">' + escapeHtml(text) + '</span>';
}
// 图3 样式抽屉:ASIN + 店在售徽章 / 品牌行 / 彩色徽章组 / 按时间降序明细 + 次数列
// 块内定位:抽屉横向贴到「撞款详情」区块,纵向以触发元素(被点的 ASIN/「查看明细」按钮)为锚点、
// 钳制在视口内,保证弹窗出现在点击处附近而非页面/滚动区顶部(顶部弹出会被当前滚动位置盖住看不见)。
var dupDrawerTriggerEl = null;
function positionDupDrawer() {
var mask = document.getElementById('dupCheckDrawerMask');
var aside = mask ? mask.querySelector('.drawer') : null;
var block = document.getElementById('dupCheckDetailBlock');
if (!aside || !block) return;
var bodyHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
var padTop = 24, padRight = 24, padBottom = 24;
var maxHeight = Math.max(260, bodyHeight - padTop - padBottom);
var blockRect = block.getBoundingClientRect();
var left = Math.max(12, Math.min(blockRect.left + 12, Math.max(12, bodyWidth() - 860)));
// 垂直锚点优先取触发元素;未传入或已脱离文档时回退到「撞款详情」区块顶部
var anchor = (dupDrawerTriggerEl && dupDrawerTriggerEl.isConnected) ? dupDrawerTriggerEl : block;
var rect = anchor.getBoundingClientRect();
var top = Math.max(padTop, rect.top - padTop);
// 点击处整体已滚出视口下方时,改放到视口底部上方留出可读高度
if (rect.top >= bodyHeight - padBottom) top = Math.max(padTop, bodyHeight - 300);
// 顶部再钳制一次,保证抽屉头部可见
top = Math.min(top, Math.max(padTop, bodyHeight - 260));
aside.style.left = left + 'px';
aside.style.top = top + 'px';
aside.style.bottom = 'auto';
aside.style.maxHeight = (maxHeight - (top - padTop)) + 'px';
var body = document.getElementById('dupCheckDrawerBody');
if (body) body.style.maxHeight = (maxHeight - (top - padTop) - 96) + 'px';
}
function bodyWidth() {
var w = document.documentElement.clientWidth || window.innerWidth;
return w;
}
// 上架时间降序排列(晚到早;同时间再按店铺名稳定排序)
function dupTimeDesc(a, b) {
var ta = a._dupTime || '', tb = b._dupTime || '';
if (ta !== tb) {
if (ta === '') return 1;
if (tb === '') return -1;
return ta < tb ? 1 : -1;
}
var sa = (a.shop_name || '').toLowerCase(), sb = (b.shop_name || '').toLowerCase();
return sa < sb ? -1 : (sa > sb ? 1 : 0);
}
// 打开前把 mask 挂到 body 下。.tab-panel.active 的入场动画带 transform
// 会成为 fixed 定位的包含块,导致 mask/drawer 定位错误(未移出时 top 会算到面板外面)
function mountDupDrawerMaskToBody() {
var mask = document.getElementById('dupCheckDrawerMask');
if (mask && mask.parentElement !== document.body) {
document.body.appendChild(mask);
}
return mask;
}
function openShopDataDuplicateDrawer(asin, triggerEl) {
mountDupDrawerMaskToBody();
dupDrawerTriggerEl = triggerEl || null;
var item = null;
(shopDataDuplicateDetailItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
if (!item) {
(shopDataDuplicateItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
}
if (!item) return;
var occurrences = (item.occurrences || []).slice();
var brand = item.brand || '';
// 上架时间归一化并按时间降序排列(晚到早)
occurrences.forEach(function (occ) { occ._dupTime = normalizeDuplicateTime(occ.date); });
occurrences.sort(dupTimeDesc);
var sites = {}, lastDate = '';
occurrences.forEach(function (occ) {
if (occ.country) sites[occ.country] = true;
if (!brand && occ.brand) brand = occ.brand;
var t = occ._dupTime;
if (t && t > lastDate) lastDate = t;
});
// 店铺 + 国家两级聚合:次数 = 该店铺在该国家的上架记录数
var countsMap = {};
occurrences.forEach(function (occ) {
var shopKey = occ.shop_name || '-';
var siteKey = (occ.country || '').trim().toUpperCase() || '-';
var key = shopKey + '|||' + siteKey;
countsMap[key] = (countsMap[key] || 0) + 1;
});
var rows = occurrences.map(function (occ) {
var shopKey = occ.shop_name || '-';
var siteKey = (occ.country || '').trim().toUpperCase() || '-';
var times = [];
if (occ._dupTime && occ._dupTime.length >= 16) times.push(occ._dupTime.slice(0, 16));
else if (normalizeDuplicateTime(occ.date)) times.push(normalizeDuplicateTime(occ.date));
else occ.date || '';
var count = countsMap[shopKey + '|||' + siteKey] || 0;
var siteBadge = siteKey === '-' ? '<span class="dup-site-badge dup-site-NA">-</span>' : dupSiteBadge(siteKey);
return '<tr>' +
'<td><span class="dup-asin-cell">' + escapeHtml(shopKey) + '</span></td>' +
'<td>' + siteBadge + '</td>' +
'<td class="dup-date">' + escapeHtml(times.join('')) + '</td>' +
'<td>' + count + '</td></tr>';
}).join('');
// 顶部「N 店在售」红色徽章
var shopCount = Number(item.shop_count || 0);
var recordCount = Number(item.record_count || 0);
var headerBadges =
dupDrawerBadge(shopCount + ' 店在售', 'dup-drawer-badge-danger') +
dupDrawerBadge(shopCount + ' 家店铺', 'dup-drawer-badge-blue') +
Object.keys(sites).sort().map(function (site) {
return dupDrawerBadge(dupSiteLabel(site), 'dup-site-' + site.toUpperCase());
}).join('') +
dupDrawerBadge(recordCount + ' 次上架', 'dup-drawer-badge-green') +
(lastDate ? dupDrawerBadge('最晚上架 ' + lastDate.slice(0, 16), 'dup-drawer-badge-cyan') : '');
document.getElementById('dupCheckDrawerAsin').textContent = item.asin;
document.getElementById('dupCheckDrawerAsin').className = 'dup-drawer-asin';
var subtitleEl = document.getElementById('dupCheckDrawerSubtitle');
subtitleEl.className = 'dup-drawer-subtitle dup-drawer-inline-badges';
subtitleEl.innerHTML = headerBadges;
document.getElementById('dupCheckDrawerBody').innerHTML =
'<div class="dup-drawer-brand-line">品牌:' + escapeHtml(brand || '-') + '</div>' +
'<div class="table-scroll"><table class="dup-check-drawer-table">' +
'<thead><tr><th>店铺</th><th>国家</th><th>上架时间</th><th>次数</th></tr></thead>' +
'<tbody>' + (rows || '<tr><td colspan="4" class="shop-data-empty-hint">暂无明细</td></tr>') + '</tbody></table></div>';
document.getElementById('dupCheckDrawerMask').classList.add('show');
positionDupDrawer();
}
function closeShopDataDuplicateDrawer() {
document.getElementById('dupCheckDrawerMask').classList.remove('show');
}
function loadShopDataDuplicateCheckOverview(force) {
var progress = document.getElementById('shopDataDuplicateProgress');
var button = document.getElementById('btnRefreshShopDataDuplicates');
if (button) button.disabled = true;
if (force) progress.textContent = '正在全量扫描各店铺结果文件,请稍候...';
fetch('/api/admin/shop-data-crawl/duplicate-check-overview' + (force ? '?force=1' : ''))
.then(function (response) { return response.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '加载失败');
shopDataDuplicateOverviewPending = !!res.pending;
shopDataDuplicateScannedAt = res.scanned_at || '';
var summary = res.summary || {};
renderShopDataDuplicateMetrics(summary);
renderShopDataDuplicateDistribution(res.shops || []);
var dupTabCount = document.getElementById('dupCheckMonitorCount');
var allTabCount = document.getElementById('dupCheckAllCount');
if (dupTabCount) dupTabCount.textContent = summary.duplicate_asin_total != null ? summary.duplicate_asin_total : 0;
if (allTabCount) allTabCount.textContent = summary.asin_total != null ? summary.asin_total : 0;
loadShopDataDuplicateCheckItems(1);
})
.catch(function (error) {
var metricsEl = document.getElementById('shopDataDuplicateMetrics');
if (metricsEl) {
metricsEl.innerHTML = '<div class="shop-data-empty-hint">加载失败:'
+ escapeHtml(error.message || '') + '</div>';
}
})
.finally(function () {
if (button) button.disabled = false;
if (progress) progress.textContent = '';
});
}
function loadShopDataDuplicateCheckItems(page) {
if (shopDataDuplicateLoading) return;
shopDataDuplicatePage = page || 1;
shopDataDuplicateLoading = true;
var list = document.getElementById('shopDataDuplicateList');
var progress = document.getElementById('shopDataDuplicateProgress');
var exportButton = document.getElementById('btnExportShopDataDuplicates');
list.innerHTML = '<div class="shop-data-empty-hint">加载中...</div>';
if (exportButton) exportButton.disabled = true;
fetch('/api/admin/shop-data-crawl/duplicate-check-items?' + buildShopDataDuplicateQuery(shopDataDuplicatePage))
.then(function (response) { return response.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '加载失败');
shopDataDuplicateItems = res.items || [];
shopDataDuplicateShops = res.shops || [];
shopDataDuplicateTotal = Number(res.total) || 0;
if (res.scanned_at) shopDataDuplicateScannedAt = res.scanned_at;
var totalEl = document.getElementById('shopDataDuplicateTotal');
totalEl.textContent = (shopDataDuplicateView === 'monitor' ? '重复监控' : '全部ASIN台账')
+ ' · 共 ' + shopDataDuplicateTotal + ' 个 ASIN'
+ (shopDataDuplicateScannedAt ? ' · 扫描时间 ' + shopDataDuplicateScannedAt
: (shopDataDuplicateOverviewPending ? ' · 尚无扫描结果(点击「重新分析」立即扫描)' : ''));
renderShopDataDuplicateMatrix();
loadShopDataDuplicateDetail(shopDataDuplicateDetailPage || 1);
renderPagination('shopDataDuplicatePagination', shopDataDuplicateTotal,
shopDataDuplicatePage, shopDataDuplicatePageSize, loadShopDataDuplicateCheckItems);
})
.catch(function (error) {
shopDataDuplicateItems = [];
shopDataDuplicateTotal = 0;
list.innerHTML = '<div class="shop-data-empty-hint">加载失败:' + escapeHtml(error.message || '') + '</div>';
document.getElementById('shopDataDuplicateTotal').textContent = '';
loadShopDataDuplicateDetail(1);
})
.finally(function () {
shopDataDuplicateLoading = false;
if (progress) progress.textContent = '';
if (exportButton) exportButton.disabled = false;
});
}
function switchDupCheckView(view) {
if (view === shopDataDuplicateView) return;
shopDataDuplicateView = view;
var monitorTab = document.getElementById('dupCheckTabMonitor');
var allTab = document.getElementById('dupCheckTabAll');
var monitorActive = view === 'monitor';
monitorTab.classList.toggle('active', monitorActive);
monitorTab.setAttribute('aria-selected', monitorActive ? 'true' : 'false');
allTab.classList.toggle('active', !monitorActive);
allTab.setAttribute('aria-selected', !monitorActive ? 'true' : 'false');
shopDataDuplicateDetailPage = 1;
loadShopDataDuplicateCheckItems(1);
}
function exportShopDataDuplicates() {
if (shopDataDuplicateExporting) return;
shopDataDuplicateExporting = true;
var progress = document.getElementById('shopDataDuplicateProgress');
var button = document.getElementById('btnExportShopDataDuplicates');
if (button) button.disabled = true;
if (progress) progress.textContent = '正在导出当前筛选结果...';
var query = new URLSearchParams(buildShopDataDuplicateQuery(1));
query.delete('page');
query.delete('page_size');
fetch('/api/admin/shop-data-crawl/duplicate-check-export?' + query.toString())
.then(function (response) {
if (!response.ok) {
return response.json().then(function (data) { throw new Error(data.error || '导出失败'); });
}
return response.blob();
})
.then(function (blob) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
var now = new Date();
function pad(n) { return n < 10 ? '0' + n : String(n); }
a.download = '店铺数据重复检查_' + now.getFullYear() + pad(now.getMonth() + 1) + pad(now.getDate())
+ '_' + pad(now.getHours()) + pad(now.getMinutes()) + '.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
})
.catch(function (error) {
if (progress) progress.textContent = '导出失败:' + (error.message || '');
})
.finally(function () {
shopDataDuplicateExporting = false;
if (button) button.disabled = false;
if (progress) progress.textContent = '';
});
}
function downloadShopDataTask(item) {
var resultId = shopDataResultId(item);
if (!item || !item.file_ready || !resultId) return;
var filename = item.output_filename || ('shop-data-task-' + resultId + '.xlsx');
triggerImageVideoLink('/api/admin/shop-data-crawl-tasks/' + resultId + '/download', filename, false);
}
function deleteShopDataTask(item) {
var resultId = shopDataResultId(item);
if (!item || !resultId || ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(String(item.status || item.file_status || '').toUpperCase()) < 0) return;
if (!window.confirm('确认删除店铺“' + (item.shop_name || '-') + '”的任务 ' + item.task_id + ' 及结果文件?')) return;
var progress = document.getElementById('shopDataTaskDownloadProgress');
progress.textContent = '正在删除任务 ' + item.task_id + '...';
fetch('/api/admin/shop-data-crawl-tasks/' + resultId, { method: 'DELETE' })
.then(function (response) { return response.json().then(function (data) { return { ok: response.ok, data: data }; }); })
.then(function (result) {
if (!result.ok || !result.data.success) throw new Error(result.data.error || '删除失败');
progress.textContent = result.data.msg || '删除成功';
loadShopDataCrawlTasks(shopDataTaskPage);
})
.catch(function (error) {
progress.textContent = error.message || '删除失败';
});
}
function downloadShopDataTasksZip() {
var resultIds = Array.from(selectedShopDataResultIds);
if (shopDataDownloadInProgress || !resultIds.length) return;
shopDataDownloadInProgress = true;
syncShopDataSelectionUi();
var progress = document.getElementById('shopDataTaskDownloadProgress');
progress.textContent = '正在打包 ' + resultIds.length + ' 个文件...';
fetch('/api/admin/shop-data-crawl-tasks/download-zip', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ result_ids: resultIds }),
__skipLoading: true
}).then(function (response) {
if (!response.ok) {
return response.json().catch(function () { return {}; }).then(function (data) {
throw new Error(data.error || '压缩包生成失败');
});
}
var filename = imageVideoZipFilename(response);
var errorCount = Number(response.headers.get('X-Archive-Error-Count') || 0);
return response.blob().then(function (blob) {
return { blob: blob, filename: filename, errorCount: errorCount };
});
}).then(function (result) {
var objectUrl = URL.createObjectURL(result.blob);
triggerImageVideoLink(objectUrl, result.filename, false);
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
progress.textContent = result.errorCount
? '压缩包已下载,' + result.errorCount + ' 个文件失败,详见包内清单'
: '压缩包下载已开始';
}).catch(function (error) {
progress.textContent = error.message || '批量下载失败';
}).finally(function () {
shopDataDownloadInProgress = false;
syncShopDataSelectionUi();
});
}
function shopDataPermissionUsersForView() {
return shopDataPermissionView === 'granted'
? shopDataPermissionUsers.filter(function (user) {
return shopDataPermissionInitialUserIds.has(Number(user.id));
})
: shopDataPermissionUsers;
}
function filteredShopDataPermissionUsers() {
var users = shopDataPermissionUsersForView();
var keyword = (document.getElementById('shopDataTaskPermissionSearch').value || '').trim().toLowerCase();
return keyword ? users.filter(function (user) {
return String(user.username || '').toLowerCase().indexOf(keyword) >= 0;
}) : users;
}
function renderShopDataPermissionUsers() {
var visibleUsers = filteredShopDataPermissionUsers();
document.getElementById('shopDataTaskPermissionGrantedCount').textContent = '(' + shopDataPermissionInitialUserIds.size + ')';
document.getElementById('shopDataTaskPermissionAllCount').textContent = '(' + shopDataPermissionUsers.length + ')';
document.querySelectorAll('[data-shop-data-permission-view]').forEach(function (tab) {
var active = tab.dataset.shopDataPermissionView === shopDataPermissionView;
tab.classList.toggle('active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
var pendingCount = shopDataPermissionUsers.filter(function (user) {
var userId = Number(user.id);
return shopDataPermissionInitialUserIds.has(userId) !== selectedShopDataPermissionUserIds.has(userId);
}).length;
document.getElementById('shopDataTaskPermissionSummary').textContent =
(shopDataPermissionView === 'granted' ? '当前显示已分配用户,共 ' + visibleUsers.length + ' 人' : '当前显示全部用户,已分配 ' + shopDataPermissionInitialUserIds.size + ' 人') +
(pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '');
document.getElementById('shopDataTaskPermissionList').innerHTML = visibleUsers.length
? visibleUsers.map(function (user) {
var userId = Number(user.id);
var saved = shopDataPermissionInitialUserIds.has(userId);
var selected = selectedShopDataPermissionUserIds.has(userId);
var changed = saved !== selected;
return '<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);
};
document.getElementById('btnResetShopDataTasks').onclick = function () {
['shopDataTaskFilterShop', 'shopDataTaskFilterGroup', 'shopDataTaskFilterCountry', 'shopDataTaskFilterFrom', 'shopDataTaskFilterTo']
.forEach(function (id) { document.getElementById(id).value = ''; });
loadShopDataCrawlTasks(1);
};
document.getElementById('btnRefreshShopDataDuplicates').onclick = function () { loadShopDataDuplicateCheckOverview(true); };
document.getElementById('btnFilterShopDataDuplicates').onclick = function () {
shopDataDuplicateDetailPage = 1;
loadShopDataDuplicateCheckItems(1);
};
document.getElementById('btnResetShopDataDuplicates').onclick = function () {
['shopDataDupFilterAsin', 'shopDataDupFilterShop', 'shopDataDupFilterCountry', 'shopDataDupFilterSite', 'shopDataDupFilterDateFrom', 'shopDataDupFilterDateTo']
.forEach(function (id) { document.getElementById(id).value = ''; });
shopDataDuplicateDetailPage = 1;
loadShopDataDuplicateCheckItems(1);
};
document.getElementById('dupCheckTabMonitor').onclick = function () { switchDupCheckView('monitor'); };
document.getElementById('dupCheckTabAll').onclick = function () { switchDupCheckView('all'); };
document.getElementById('btnExportShopDataDuplicates').onclick = exportShopDataDuplicates;
document.getElementById('btnCloseDupCheckDrawer').onclick = closeShopDataDuplicateDrawer;
document.getElementById('dupCheckDrawerMask').onclick = function (event) {
if (event.target === this) closeShopDataDuplicateDrawer();
};
// ========== 撞款导入(新增-测试 / 删除-测试)==========
var dupCheckImportDeleteMode = false;
function openDupCheckImport(deleteMode) {
dupCheckImportDeleteMode = !!deleteMode;
document.getElementById('dupCheckImportTitle').textContent = deleteMode ? '导入(删除-测试)' : '导入(新增-测试)';
document.getElementById('dupCheckImportShopName').value = '';
document.getElementById('dupCheckImportCountryCodes').value = '';
document.getElementById('dupCheckImportFile').value = '';
var msg = document.getElementById('msgDupCheckImport');
msg.textContent = '';
msg.className = 'msg';
document.getElementById('dupCheckImportModal').classList.add('show');
}
function closeDupCheckImportModal() {
document.getElementById('dupCheckImportModal').classList.remove('show');
}
function submitDupCheckImport() {
var shopName = document.getElementById('dupCheckImportShopName').value.trim();
var fileInput = document.getElementById('dupCheckImportFile');
var msgEl = document.getElementById('msgDupCheckImport');
var submitBtn = document.getElementById('btnDupCheckImportSubmit');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!shopName) {
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;
}
var formData = new FormData();
formData.append('file', file);
formData.append('mode', dupCheckImportDeleteMode ? 'delete' : 'add');
formData.append('shop_name', shopName);
var codes = document.getElementById('dupCheckImportCountryCodes').value.trim();
if (codes) formData.append('country_codes', codes);
submitBtn.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/admin/shop-data-crawl/duplicate-check-import', true);
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
submitBtn.disabled = false;
if (xhr.status < 200 || xhr.status >= 300) {
msgEl.textContent = '请求失败:HTTP ' + xhr.status;
msgEl.className = 'msg err';
return;
}
var res;
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, message: '返回格式错误' }; }
if (!res.success) {
msgEl.textContent = res.message || res.error || '导入失败';
msgEl.className = 'msg err';
return;
}
var data = res.data || {};
msgEl.textContent = (data.message || (dupCheckImportDeleteMode ? '删除' : '新增') + '导入成功')
+ ':解析 ' + (data.total_rows != null ? data.total_rows : '?') + ' 行';
msgEl.className = 'msg ok';
fileInput.value = '';
// 导入落库新扫描行,重新加载视图让页面立即反映
loadShopDataDuplicateCheckOverview(false);
};
xhr.onerror = function () {
submitBtn.disabled = false;
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
};
xhr.send(formData);
}
document.getElementById('btnOpenDupCheckImportAdd').onclick = function () { openDupCheckImport(false); };
document.getElementById('btnOpenDupCheckImportDelete').onclick = function () { openDupCheckImport(true); };
document.getElementById('btnCloseDupCheckImportModal').onclick = closeDupCheckImportModal;
document.getElementById('btnDupCheckImportSubmit').onclick = submitDupCheckImport;
document.getElementById('dupCheckImportModal').onclick = function (event) {
if (event.target === this) closeDupCheckImportModal();
};
// 矩阵表格内 ASIN / 数字徽标点击打开抽屉(动态元素,事件委托),并传入被点元素作为锚点
document.getElementById('shopDataDuplicateList').onclick = function (event) {
var target = event.target.closest('[data-open-drawer]');
if (target) openShopDataDuplicateDrawer(target.dataset.openDrawer, target);
};
// 撞款详情卡片区「查看明细」按钮(动态元素,事件委托)
document.getElementById('dupCheckDetailCards').onclick = handleDuplicateDetailClick;
// 全选移到表格表头后为动态元素,用事件委托
document.getElementById('shopDataTaskGrid').onchange = function (event) {
var checkbox = event.target.closest('[data-shop-data-select]');
if (checkbox) {
var resultId = Number(checkbox.dataset.shopDataSelect);
if (!resultId) return;
if (checkbox.checked) selectedShopDataResultIds.add(resultId);
else selectedShopDataResultIds.delete(resultId);
syncShopDataSelectionUi();
}
};
// 全选移到工具栏后不在 grid 容器内,单独绑定 change 事件
document.getElementById('shopDataTaskSelectAll').onchange = function (event) {
shopDataTasks.forEach(function (item) {
var sid = shopDataResultId(item);
if (!item.file_ready || !sid) return;
if (event.target.checked) selectedShopDataResultIds.add(sid);
else selectedShopDataResultIds.delete(sid);
});
syncShopDataSelectionUi();
};
document.getElementById('shopDataTaskGrid').onclick = function (event) {
var downloadButton = event.target.closest('[data-shop-data-download]');
if (downloadButton) {
var downloadItem = shopDataTasks.find(function (task) { return shopDataResultId(task) === Number(downloadButton.dataset.shopDataDownload); });
downloadShopDataTask(downloadItem);
return;
}
var deleteButton = event.target.closest('[data-shop-data-delete]');
if (deleteButton) {
var deleteItem = shopDataTasks.find(function (task) { return shopDataResultId(task) === Number(deleteButton.dataset.shopDataDelete); });
deleteShopDataTask(deleteItem);
}
};
document.getElementById('btnBatchDownloadShopDataTasks').onclick = downloadShopDataTasksZip;
document.getElementById('btnOpenShopDataTaskPermissions').onclick = openShopDataTaskPermissions;
document.getElementById('btnCloseShopDataTaskPermissions').onclick = closeShopDataTaskPermissions;
document.getElementById('btnCancelShopDataTaskPermissions').onclick = closeShopDataTaskPermissions;
document.getElementById('btnSaveShopDataTaskPermissions').onclick = saveShopDataTaskPermissions;
document.querySelectorAll('[data-shop-data-permission-view]').forEach(function (tab) {
tab.onclick = function () {
shopDataPermissionView = tab.dataset.shopDataPermissionView || 'granted';
document.getElementById('shopDataTaskPermissionSearch').value = '';
renderShopDataPermissionUsers();
};
});
document.getElementById('shopDataTaskPermissionSearch').oninput = renderShopDataPermissionUsers;
document.getElementById('shopDataTaskPermissionSelectAll').onchange = function (event) {
filteredShopDataPermissionUsers().forEach(function (user) {
if (event.target.checked) selectedShopDataPermissionUserIds.add(Number(user.id));
else selectedShopDataPermissionUserIds.delete(Number(user.id));
});
renderShopDataPermissionUsers();
};
document.getElementById('shopDataTaskPermissionList').onclick = function (event) {
var button = event.target.closest('[data-shop-data-permission-toggle]');
if (!button) return;
var userId = Number(button.dataset.shopDataPermissionToggle);
if (selectedShopDataPermissionUserIds.has(userId)) selectedShopDataPermissionUserIds.delete(userId);
else selectedShopDataPermissionUserIds.add(userId);
renderShopDataPermissionUsers();
};
document.getElementById('shopDataTaskPermissionList').onchange = function (event) {
var checkbox = event.target.closest('[data-shop-data-permission-user]');
if (!checkbox) return;
var userId = Number(checkbox.dataset.shopDataPermissionUser);
if (checkbox.checked) selectedShopDataPermissionUserIds.add(userId);
else selectedShopDataPermissionUserIds.delete(userId);
renderShopDataPermissionUsers();
};
document.getElementById('shopDataTaskPermissionModal').onclick = function (event) {
if (event.target === event.currentTarget) closeShopDataTaskPermissions();
};
var historyPage = 1, historyPageSize = 15;
function toSqlDatetime(val) {
if (!val) return '';
return val.replace('T', ' ');
}
function buildHistoryQuery(page) {
var q = 'page=' + (page || 1) + '&page_size=' + historyPageSize;
var uid = document.getElementById('filterUser').value;
var start = toSqlDatetime(document.getElementById('filterTimeStart').value);
var end = toSqlDatetime(document.getElementById('filterTimeEnd').value);
if (uid) q += '&user_id=' + uid;
if (start) q += '&time_start=' + encodeURIComponent(start);
if (end) q += '&time_end=' + encodeURIComponent(end);
return q;
}
function panelTypeLabel(value) {
var panelType = String(value || '');
var labels = {
textToImage: '反推词', productMainImage: '产品主图', buyerShow: '买家秀',
productPoster: '产品海报', clonePoster: '克隆海报', randomPoster: '随机海报',
clothingDetail: '服装详情', productDetail: '产品详情', extremeDetail: '极致详情',
cloneDetail: '克隆详情', imageEdit: '图片编辑'
};
if (labels[panelType]) return labels[panelType];
return panelType ? panelType : '-';
}
function loadHistory(page) {
historyPage = page || 1;
fetch('/api/admin/history?' + buildHistoryQuery(historyPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('historyListBody');
if (!res.success) {
tbody.innerHTML = '<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 || '');
});
}
// ========== 分组管理通讯录:按拼音分组 + 模糊搜索 + 字母索引 ==========
// 隐藏 selectshopManageGroupMemberSelect)仅作为数据交换层,保存逻辑不变。
var shopManageGroupEligibleUsers = [];
var shopManageGroupSelectedIds = {};
var shopManageGroupSearchKeyword = '';
var shopManageGroupContactGroupsCache = [];
var SHOP_GROUP_AVATAR_COLORS = ['#f97316', '#0ea5e9', '#8b5cf6', '#10b981', '#ef4444', '#eab308', '#14b8a6', '#6366f1'];
function shopGroupAvatarColor(name) {
var code = String(name || '#').charCodeAt(0) || 0;
return SHOP_GROUP_AVATAR_COLORS[code % SHOP_GROUP_AVATAR_COLORS.length];
}
function shopGroupUserInitial(user) {
var abbr = String(user.pinyin_abbr || '').trim();
if (abbr) {
var first = abbr.charAt(0).toUpperCase();
return /^[A-Z0-9]$/.test(first) ? first : '#';
}
var name = String(user.username || '').trim();
if (!name) return '#';
var first = name.charAt(0);
if (/[A-Za-z0-9]/.test(first)) return first.toUpperCase();
return '#';
}
function shopGroupMatchSearch(user, keyword) {
if (!keyword) return true;
var kw = keyword.toLowerCase();
if (String(user.username || '').toLowerCase().indexOf(kw) !== -1) return true;
if (String(user.pinyin_abbr || '').toLowerCase().indexOf(kw) !== -1) return true;
return false;
}
function buildShopManageGroupContactGroups(users) {
var groups = {};
users.forEach(function (u) {
var initial = shopGroupUserInitial(u);
var key = /^[A-Z]$/.test(initial) ? initial : '#';
(groups[key] = groups[key] || []).push(u);
});
var keys = Object.keys(groups).sort(function (a, b) {
if (a === '#') return 1;
if (b === '#') return -1;
return a < b ? -1 : 1;
});
return keys.map(function (key) {
var list = groups[key].slice().sort(function (a, b) {
return String(a.username || '').localeCompare(String(b.username || ''), 'zh');
});
return { key: key, items: list };
});
}
function renderShopManageGroupContact() {
var contactEl = document.getElementById('shopManageGroupContact');
var indexEl = document.getElementById('shopManageGroupMemberIndex');
if (!contactEl) return;
var keyword = (shopManageGroupSearchKeyword || '').trim();
var filtered = shopManageGroupEligibleUsers.filter(function (u) {
return shopGroupMatchSearch(u, keyword);
});
if (!shopManageGroupEligibleUsers.length) {
contactEl.innerHTML = '<div class="shop-group-contact-empty">当前组长名下暂无可添加的普通员工账号。</div>';
if (indexEl) indexEl.innerHTML = '';
return;
}
if (!filtered.length) {
contactEl.innerHTML = '<div class="shop-group-contact-empty">未找到与「' + escapeHtml(keyword) + '」匹配的用户,可尝试输入拼音首字母,如 zwh。</div>';
if (indexEl) indexEl.innerHTML = '';
return;
}
shopManageGroupContactGroupsCache = buildShopManageGroupContactGroups(filtered);
contactEl.innerHTML = shopManageGroupContactGroupsCache.map(function (group) {
var itemsHtml = group.items.map(function (u) {
var selected = !!shopManageGroupSelectedIds[String(u.id)];
var name = u.username || '';
return '<div class="shop-group-contact-item' + (selected ? ' is-selected' : '') + '" data-user-id="' + escapeHtml(u.id) + '" title="' + escapeHtml(name) + '">' +
'<span class="shop-group-item-avatar" style="background:' + shopGroupAvatarColor(name) + ';">' + escapeHtml(shopGroupUserInitial(u)) + '</span>' +
'<span class="shop-group-item-name">' + escapeHtml(name) + '</span>' +
'<span class="shop-group-item-check" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg></span>' +
'</div>';
}).join('');
return '<div class="shop-group-contact-group" data-letter="' + escapeHtml(group.key) + '">' + escapeHtml(group.key) + '</div>' +
'<div class="shop-group-contact-items">' + itemsHtml + '</div>';
}).join('');
if (indexEl) {
indexEl.innerHTML = shopManageGroupContactGroupsCache.map(function (group) {
return '<button type="button" data-letter="' + escapeHtml(group.key) + '">' + escapeHtml(group.key) + '</button>';
}).join('');
}
}
function syncShopManageGroupMemberSelect() {
var sel = document.getElementById('shopManageGroupMemberSelect');
if (!sel) return;
sel.innerHTML = shopManageGroupEligibleUsers.map(function (u) {
var selected = !!shopManageGroupSelectedIds[String(u.id)];
return '<option value="' + escapeHtml(u.id) + '"' + (selected ? ' selected' : '') + '>' + escapeHtml(u.username || '') + '</option>';
}).join('');
}
function updateShopManageGroupMemberCount() {
var countEl = document.getElementById('shopManageGroupMemberCount');
if (countEl) {
countEl.textContent = '已选 ' + Object.keys(shopManageGroupSelectedIds).length + ' 人';
}
}
function toggleShopManageGroupMember(userId) {
var key = String(userId || '');
if (!key) return;
var contactEl = document.getElementById('shopManageGroupContact');
var itemEl = contactEl ? Array.prototype.find.call(
contactEl.querySelectorAll('.shop-group-contact-item'),
function (el) { return el.dataset.userId === key; }
) : null;
if (shopManageGroupSelectedIds[key]) {
delete shopManageGroupSelectedIds[key];
} else {
shopManageGroupSelectedIds[key] = true;
}
if (itemEl) itemEl.classList.toggle('is-selected', !!shopManageGroupSelectedIds[key]);
syncShopManageGroupMemberSelect();
updateShopManageGroupMemberCount();
}
function shopGroupScrollToLetter(letter) {
var contactEl = document.getElementById('shopManageGroupContact');
if (!contactEl) return;
var groupEl = Array.prototype.find.call(
contactEl.querySelectorAll('.shop-group-contact-group'),
function (el) { return el.dataset.letter === letter; }
);
if (groupEl) groupEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function resetShopManageGroupSearch() {
shopManageGroupSearchKeyword = '';
var searchInput = document.getElementById('shopManageGroupMemberSearch');
if (searchInput) searchInput.value = '';
}
function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) {
var sel = document.getElementById('shopManageGroupMemberSelect');
var helpEl = document.getElementById('shopManageGroupMemberHelp');
if (!sel) return;
resetShopManageGroupSearch();
shopManageGroupEligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
shopManageGroupSelectedIds = {};
(selectedUserIds || []).forEach(function (id) {
shopManageGroupSelectedIds[String(id)] = true;
});
syncShopManageGroupMemberSelect();
renderShopManageGroupContact();
updateShopManageGroupMemberCount();
if (helpEl) {
helpEl.textContent = shopManageGroupEligibleUsers.length
? '可添加当前组长创建的普通员工账号,点击用户即可选中 / 取消;顶部支持按用户名或拼音首字母搜索。'
: (currentUserRole === 'normal'
? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。'
: '当前组长名下暂无可添加的普通员工账号。');
}
}
function setShopManageGroupLeader(leaderUserId, leaderUsername, selectedUserIds) {
var leaderIdEl = document.getElementById('shopManageGroupLeaderUserId');
var leaderNameEl = document.getElementById('shopManageGroupLeaderName');
if (leaderIdEl) leaderIdEl.value = leaderUserId ? String(leaderUserId) : '';
if (leaderNameEl) leaderNameEl.value = leaderUsername || '';
refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds || []);
}
function getSelectedShopManageGroupMemberIds() {
var sel = document.getElementById('shopManageGroupMemberSelect');
if (!sel) return [];
return Array.prototype.slice.call(sel.options || [])
.filter(function (opt) { return !!opt.selected; })
.map(function (opt) { return Number(opt.value); })
.filter(function (id) { return !!id; });
}
function loadUserOptions() {
return fetch('/api/admin/users?page=1&page_size=999')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
throw new Error(res.error || '加载用户失败');
}
var items = res.items || [];
shopManageAllUsers = items.map(function (u) {
return {
id: u.id,
username: u.username || '',
role: u.role || 'normal',
created_by_id: u.created_by_id || null,
pinyin_abbr: u.pinyin_abbr || ''
};
});
setShopManageGroupLeader(
document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '',
document.getElementById('shopManageGroupLeaderName') ? document.getElementById('shopManageGroupLeaderName').value : '',
getSelectedShopManageGroupMemberIds()
);
var sel = document.getElementById('filterUser');
var cur = sel.value;
sel.innerHTML = '<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');
};
// 组员通讯录:搜索 / 字母索引 / 点击选中(打开弹窗时 refresh 会重置搜索词)
var groupMemberSearchInput = document.getElementById('shopManageGroupMemberSearch');
if (groupMemberSearchInput) {
groupMemberSearchInput.addEventListener('input', function () {
shopManageGroupSearchKeyword = groupMemberSearchInput.value || '';
renderShopManageGroupContact();
});
}
var groupMemberIndexEl = document.getElementById('shopManageGroupMemberIndex');
if (groupMemberIndexEl) {
groupMemberIndexEl.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-letter]');
if (btn) shopGroupScrollToLetter(btn.dataset.letter);
});
}
var groupMemberContactEl = document.getElementById('shopManageGroupContact');
if (groupMemberContactEl) {
groupMemberContactEl.addEventListener('click', function (e) {
var item = e.target.closest('.shop-group-contact-item');
if (item) toggleShopManageGroupMember(item.dataset.userId);
});
}
document.getElementById('btnSearchShopManage').onclick = function () {
loadShopManage(1);
};
document.getElementById('shopManageFilterShopName').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadShopManage(1);
}
});
document.getElementById('btnSaveShopManageGroup').onclick = function () {
var editId = (document.getElementById('shopManageGroupEditId').value || '').trim();
var groupName = (document.getElementById('shopManageGroupInput').value || '').trim();
var memberUserIds = getSelectedShopManageGroupMemberIds();
var msgEl = document.getElementById('msgShopManageGroup');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupName) {
msgEl.textContent = '请输入分组名称';
msgEl.className = 'msg err';
return;
}
var method = editId ? 'PUT' : 'POST';
var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group';
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
group_name: groupName,
member_user_ids: memberUserIds,
grant_menu_routes: currentShopManageGroupGrantRoutes
})
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
resetShopManageGroupForm();
document.getElementById('shopManageGroupModal').classList.remove('show');
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
loadShopManageGroups(null, null, true).then(function () {
renderShopManageGroupRows();
renderShopManageGroupSummary();
renderDedupeGroupSummary();
loadShopManage(shopManagePage);
loadSkipPriceAsin(skipPriceAsinPage);
if (getActiveAdminTabName() === 'dedupe-total-data') loadDedupeTotalData(dedupeTotalDataPage);
if (getActiveAdminTabName() === 'invalid-asin-data') loadInvalidAsinData(invalidAsinDataPage);
});
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('btnOpenCreateShopManage').onclick = function () {
document.getElementById('shopManageGroupSelect').value = '';
document.getElementById('shopManageShopName').value = '';
document.getElementById('shopManageMallName').value = '';
document.getElementById('shopManageZnUsername').value = '';
document.getElementById('shopManageAccount').value = '';
document.getElementById('shopManagePassword').value = '';
document.getElementById('msgShopManage').textContent = '';
document.getElementById('msgShopManage').className = 'msg';
document.getElementById('createShopManageModal').classList.add('show');
};
document.getElementById('btnCloseCreateShopManage').onclick = function () {
document.getElementById('createShopManageModal').classList.remove('show');
};
document.getElementById('btnCreateShopManage').onclick = function () {
var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim();
var shopName = (document.getElementById('shopManageShopName').value || '').trim();
var mallName = (document.getElementById('shopManageMallName').value || '').trim();
var znUsername = (document.getElementById('shopManageZnUsername').value || '').trim();
var account = (document.getElementById('shopManageAccount').value || '').trim();
var password = (document.getElementById('shopManagePassword').value || '').trim();
var msgEl = document.getElementById('msgShopManage');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupId || !shopName || !mallName || !account || !password) {
msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/shop-manage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, zn_username: znUsername, account: account, password: password })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
document.getElementById('shopManageGroupSelect').value = '';
document.getElementById('shopManageShopName').value = '';
document.getElementById('shopManageMallName').value = '';
document.getElementById('shopManageZnUsername').value = '';
document.getElementById('shopManageAccount').value = '';
document.getElementById('shopManagePassword').value = '';
msgEl.textContent = res.msg || '创建成功';
msgEl.className = 'msg ok';
loadShopManage(1);
} else {
msgEl.textContent = res.error || '创建失败';
msgEl.className = 'msg err';
}
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('btnSaveShopManage').onclick = function () {
var itemId = document.getElementById('editShopManageId').value;
var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim();
var shopName = (document.getElementById('editShopManageShopName').value || '').trim();
var mallName = (document.getElementById('editShopManageMallName').value || '').trim();
var znUsername = (document.getElementById('editShopManageZnUsername').value || '').trim();
var account = (document.getElementById('editShopManageAccount').value || '').trim();
var password = (document.getElementById('editShopManagePassword').value || '').trim();
var msgEl = document.getElementById('msgEditShopManage');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!groupId || !shopName || !mallName || !account || !password) {
msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/shop-manage/' + itemId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, zn_username: znUsername, account: account, password: password })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
document.getElementById('editShopManageModal').classList.remove('show');
loadShopManage(shopManagePage);
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.classList.add('err');
});
};
document.getElementById('btnCloseEditShopManage').onclick = function () {
document.getElementById('editShopManageModal').classList.remove('show');
};
// ========== 版本管理 ==========
// ========== 新增 ASIN 弹窗共用:店铺下拉 ==========
// 按分组拉取该组全部店铺名(接口单页上限 100,逐页取完)
function loadAsinShopNameOptions(selectId, groupId) {
var select = document.getElementById(selectId);
if (!select) return;
if (!groupId) {
select.innerHTML = '<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 || '管理员';
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();
});
}) ();