(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; } }; } })(); // Tab 切换 var adminTabsEl = document.getElementById('adminTabs'); if (adminTabsEl) adminTabsEl.innerHTML = ''; var activeAdminTabName = ''; var ADMIN_PANEL_MAP = { 'users': 'panel-users', 'columns': 'panel-columns', 'dedupe-total-data': 'panel-dedupe-total-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', 'history': 'panel-history', 'version': 'panel-version' }; function runTabLoader(tabName) { if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); } else if (tabName === 'columns') loadColumns(); else if (tabName === 'dedupe-total-data') loadDedupeTotalData(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 === 'history') loadHistory(1); else if (tabName === 'version') loadVersions(); } 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('#adminTabs .tab').forEach(function (tab) { tab.classList.toggle('active', tab.dataset.tab === tabName); }); hideAllAdminPanels(); panel.style.display = ''; panel.classList.add('active'); runTabLoader(tabName); } function renderAdminTabs(items) { var knownItems = (items || []).filter(function (item) { return !!ADMIN_PANEL_MAP[item.route_path]; }); if (!knownItems.length) { activeAdminTabName = ''; if (adminTabsEl) adminTabsEl.innerHTML = '
暂无可用菜单
'; hideAllAdminPanels(); return; } if (adminTabsEl) { adminTabsEl.innerHTML = knownItems.map(function (item) { return '
' + (item.name || item.route_path) + '
'; }).join(''); } document.querySelectorAll('#adminTabs .tab').forEach(function (tab) { tab.onclick = function () { activateAdminTab(tab.dataset.tab); }; }); var fallbackTab = activeAdminTabName && knownItems.some(function (item) { return item.route_path === activeAdminTabName; }) ? activeAdminTabName : knownItems[0].route_path; activateAdminTab(fallbackTab); } function loadAdminMenus(preferredTab) { if (preferredTab) activeAdminTabName = preferredTab; fetch('/api/admin/current-user/menus') .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { if (adminTabsEl) adminTabsEl.innerHTML = '
菜单加载失败
'; hideAllAdminPanels(); return; } renderAdminTabs(res.items || []); }) .catch(function () { if (adminTabsEl) adminTabsEl.innerHTML = '
菜单加载失败
'; hideAllAdminPanels(); }); } hideAllAdminPanels(); // ========== 用户管理 ========== var userPage = 1, userPageSize = 15; var currentUserId = null; var currentUserRole = 'admin'; var currentUserUsername = ''; var adminsList = []; function roleLabel(role) { if (role === 'super_admin') return '超级管理员'; if (role === 'admin') return '管理员'; return '普通账号'; } function buildUserListQuery(page) { var q = 'page=' + (page || 1) + '&page_size=' + userPageSize; var kw = (document.getElementById('searchUsername').value || '').trim(); if (kw) q += '&username=' + encodeURIComponent(kw); var cby = document.getElementById('filterCreatedBy').value; if (cby) q += '&created_by_id=' + encodeURIComponent(cby); return q; } function loadUsers(page) { userPage = page || 1; fetch('/api/admin/users?' + buildUserListQuery(userPage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('userListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } currentUserId = res.current_user_id || null; currentUserRole = res.current_user_role || 'admin'; currentUserUsername = (res.current_user_username || '').trim(); adminsList = res.admins || []; var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无用户'; } else { tbody.innerHTML = items.map(function (u) { return '' + u.id + '' + (u.username || '') + '' + roleLabel(u.role || 'normal') + '' + (u.creator_username || '-') + '' + (u.created_at || '') + '' + ' ' + '' + ''; }).join(''); } renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers); bindUserActions(); updateCreateFormByRole(); updateUserFilterByRole(); }) .catch(function () { document.getElementById('userListBody').innerHTML = '请求失败'; }); } function updateUserFilterByRole() { var grp = document.getElementById('filterCreatedByGroup'); var sel = document.getElementById('filterCreatedBy'); if (currentUserRole === 'super_admin') { grp.style.display = 'block'; var cur = sel.value; sel.innerHTML = ''; adminsList.forEach(function (a) { var opt = document.createElement('option'); opt.value = a.id; opt.textContent = a.username; sel.appendChild(opt); }); sel.value = cur || ''; } else { grp.style.display = 'none'; } } function updateDedupeTotalDataAccess() { var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]'); var panel = document.getElementById('panel-dedupe-total-data'); var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === ''); if (tab) tab.style.display = canUse ? '' : 'none'; if (panel) panel.style.display = canUse ? '' : 'none'; if (!canUse && tab && tab.classList.contains('active')) { tab.classList.remove('active'); 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'); if (usersPanel) usersPanel.classList.add('active'); } } 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'); panel.classList.remove('active'); var usersTab = document.querySelector('.tab[data-tab="users"]'); var usersPanel = document.getElementById('panel-users'); if (usersTab) usersTab.classList.add('active'); if (usersPanel) usersPanel.classList.add('active'); } } 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'); 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'); if (usersPanel) usersPanel.classList.add('active'); } } 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 = []; function loadColumnsForPermission() { fetch('/api/admin/columns') .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) return; allColumnsList = res.items || []; renderColumnPermissionWrap('createColumnPermissionWrap'); renderColumnPermissionWrap('editColumnPermissionWrap'); var selCreate = document.getElementById('createColumnPermissionWrap'); var selEdit = document.getElementById('editColumnPermissionWrap'); if (selCreate && !selCreate._colCardsBound) { selCreate._colCardsBound = true; selCreate.onchange = function () { renderColumnCards('createColumnPermissionWrap'); }; } if (selEdit && !selEdit._colCardsBound) { selEdit._colCardsBound = true; selEdit.onchange = function () { renderColumnCards('editColumnPermissionWrap'); }; } }) .catch(function () { }); } var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' }; function renderColumnCards(selectId) { var sel = document.getElementById(selectId); var cardsId = columnCardsContainerMap[selectId]; var cardsEl = cardsId ? document.getElementById(cardsId) : null; if (!sel || sel.tagName !== 'SELECT' || !cardsEl) return; cardsEl.innerHTML = ''; for (var i = 0; i < sel.options.length; i++) { var opt = sel.options[i]; if (opt.disabled || !opt.selected) continue; var card = document.createElement('span'); card.className = 'column-permission-card'; card.textContent = opt.textContent; var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'col-card-remove'; btn.setAttribute('aria-label', '移除'); btn.textContent = '×'; (function (option, seldId) { btn.onclick = function () { option.selected = false; renderColumnCards(seldId); }; })(opt, selectId); card.appendChild(btn); cardsEl.appendChild(card); } } function renderColumnPermissionWrap(wrapId) { var sel = document.getElementById(wrapId); if (!sel || sel.tagName !== 'SELECT') return; sel.innerHTML = ''; allColumnsList.forEach(function (c) { var opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.name + ' (' + c.column_key + ')'; sel.appendChild(opt); }); if (allColumnsList.length === 0) { var opt = document.createElement('option'); opt.disabled = true; opt.textContent = '暂无菜单,请先在“栏目权限配置”中新增菜单'; sel.appendChild(opt); } renderColumnCards(wrapId); } function getSelectedColumnIds(wrapId) { var sel = document.getElementById(wrapId); if (!sel || sel.tagName !== 'SELECT') return []; var ids = []; for (var i = 0; i < sel.options.length; i++) { if (sel.options[i].selected) { var v = parseInt(sel.options[i].value, 10); if (!isNaN(v)) ids.push(v); } } return ids; } function setColumnPermissionCheckboxes(wrapId, columnIds) { var sel = document.getElementById(wrapId); if (!sel || sel.tagName !== 'SELECT') return; var set = {}; (columnIds || []).forEach(function (id) { set[id] = true; }); for (var i = 0; i < sel.options.length; i++) { var opt = sel.options[i]; if (opt.disabled) continue; opt.selected = set[parseInt(opt.value, 10)] || false; } renderColumnCards(wrapId); } function updateCreateFormByRole() { var roleSel = document.getElementById('createRole'); var optAdmin = document.getElementById('optAdmin'); var formCreatedBy = document.getElementById('formGroupCreatedBy'); var selCreatedBy = document.getElementById('createCreatedBy'); if (currentUserRole === 'super_admin') { if (optAdmin) optAdmin.style.display = ''; formCreatedBy.style.display = (roleSel.value === 'normal') ? 'block' : 'none'; selCreatedBy.innerHTML = ''; adminsList.forEach(function (a) { var opt = document.createElement('option'); opt.value = a.id; opt.textContent = a.username; selCreatedBy.appendChild(opt); }); } else { if (optAdmin) optAdmin.style.display = 'none'; roleSel.value = 'normal'; formCreatedBy.style.display = 'none'; } } function bindUserActions() { document.querySelectorAll('[data-edit]').forEach(function (btn) { btn.onclick = function () { var raw = (btn.getAttribute('data-user') || '{}').replace(/"/g, '"'); var u; try { u = JSON.parse(raw); } catch (e) { u = {}; } document.getElementById('editUserId').value = u.id || ''; document.getElementById('editUsername').value = u.username || ''; document.getElementById('editPassword').value = ''; var editRole = document.getElementById('editRole'); var editFormGroupRole = document.getElementById('editFormGroupRole'); var editFormGroupCreator = document.getElementById('editFormGroupCreator'); var editCreatorName = document.getElementById('editCreatorName'); editFormGroupRole.style.display = (currentUserRole === 'super_admin' && u.role !== 'super_admin') ? 'block' : 'none'; editFormGroupCreator.style.display = (u.role === 'normal' && u.creator_username) ? 'block' : 'none'; editCreatorName.value = u.creator_username || ''; if (u.role !== 'super_admin') { editRole.value = u.role || 'normal'; } document.getElementById('msgEdit').textContent = ''; if (allColumnsList.length) { renderColumnPermissionWrap('editColumnPermissionWrap'); fetch('/api/admin/user/' + (u.id) + '/columns') .then(function (r) { return r.json(); }) .then(function (res) { if (res.success && res.column_ids) setColumnPermissionCheckboxes('editColumnPermissionWrap', res.column_ids); }); } 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('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; } 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 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 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 loadHistory(page) { historyPage = page || 1; fetch('/api/admin/history?' + buildHistoryQuery(historyPage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('historyListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无记录'; } else { tbody.innerHTML = items.map(function (h) { var urls = (h.result_urls || []); var thumbUrls = (h.long_image_url ? [h.long_image_url] : []).concat(urls); var thumbs = thumbUrls.slice(0, 3).map(function (url) { return ''; }).join(''); return '' + h.id + '' + (h.username || '-') + '' + (h.panel_type || '-') + '' + (h.created_at || '') + '' + '
' + (thumbs || '-') + '
'; }).join(''); } renderPagination('historyPagination', res.total, res.page, res.page_size, loadHistory); }) .catch(function () { document.getElementById('historyListBody').innerHTML = '请求失败'; }); } var shopManageAllUsers = []; function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } var dropdownMultiSelects = {}; function initDropdownMultiSelect(selectId, placeholder) { var select = document.getElementById(selectId); if (!select || dropdownMultiSelects[selectId]) return; select.classList.add('multi-select-native'); var wrapper = document.createElement('div'); wrapper.className = 'multi-select-dropdown'; wrapper.setAttribute('data-multi-select-id', selectId); var trigger = document.createElement('button'); trigger.type = 'button'; trigger.className = 'multi-select-trigger'; trigger.textContent = placeholder || '请选择'; var panel = document.createElement('div'); panel.className = 'multi-select-panel'; select.parentNode.insertBefore(wrapper, select); wrapper.appendChild(trigger); wrapper.appendChild(panel); wrapper.appendChild(select); function selectedOptions() { return Array.from(select.options).filter(function (option) { return option.selected; }); } function updateSummary() { var selected = selectedOptions(); if (!selected.length) { trigger.textContent = placeholder || '请选择'; trigger.title = ''; return; } var labels = selected.map(function (option) { return option.textContent || option.value; }); trigger.textContent = labels.join('、'); trigger.title = labels.join('、'); } function renderOptions() { panel.innerHTML = Array.from(select.options).map(function (option, index) { var optionId = selectId + '_multi_' + index; return ''; }).join(''); panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) { checkbox.onchange = function () { var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))]; if (!option) return; option.selected = checkbox.checked; updateSummary(); select.dispatchEvent(new Event('change', { bubbles: true })); }; }); } trigger.onclick = function (event) { event.stopPropagation(); document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) { if (item !== wrapper) item.classList.remove('open'); }); wrapper.classList.toggle('open'); }; panel.onclick = function (event) { event.stopPropagation(); }; select.addEventListener('change', function () { panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) { var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))]; checkbox.checked = !!(option && option.selected); }); updateSummary(); }); renderOptions(); updateSummary(); dropdownMultiSelects[selectId] = { refresh: function () { renderOptions(); updateSummary(); } }; } function refreshDropdownMultiSelect(selectId) { if (dropdownMultiSelects[selectId]) { dropdownMultiSelects[selectId].refresh(); } } document.addEventListener('click', function () { document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) { item.classList.remove('open'); }); }); function getEligibleShopManageGroupUsers(leaderUserId) { var canViewAllMembers = currentUserRole === 'super_admin'; return shopManageAllUsers.filter(function (u) { if (String(u.id || '') === String(leaderUserId || '')) return false; if (canViewAllMembers) return true; return String(u.role || '') === 'normal' && String(u.created_by_id || '') === String(leaderUserId || ''); }); } function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) { var sel = document.getElementById('shopManageGroupMemberSelect'); var helpEl = document.getElementById('shopManageGroupMemberHelp'); if (!sel) return; var selectedMap = {}; (selectedUserIds || []).forEach(function (id) { selectedMap[String(id)] = true; }); var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId); var options = eligibleUsers.map(function (u) { return ''; }); if (options.length) { sel.innerHTML = options.join(''); if (helpEl) { helpEl.textContent = '可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。'; } return; } sel.innerHTML = ''; if (helpEl) { helpEl.textContent = currentUserRole === 'normal' ? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。' : '当前组长名下暂无可添加的普通员工账号。'; } } function setShopManageGroupLeader(leaderUserId, leaderUsername, selectedUserIds) { var leaderIdEl = document.getElementById('shopManageGroupLeaderUserId'); var leaderNameEl = document.getElementById('shopManageGroupLeaderName'); if (leaderIdEl) leaderIdEl.value = leaderUserId ? String(leaderUserId) : ''; if (leaderNameEl) leaderNameEl.value = leaderUsername || ''; refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds || []); } function getSelectedShopManageGroupMemberIds() { var sel = document.getElementById('shopManageGroupMemberSelect'); if (!sel) return []; return Array.prototype.slice.call(sel.options || []) .filter(function (opt) { return !!opt.selected; }) .map(function (opt) { return Number(opt.value); }) .filter(function (id) { return !!id; }); } function loadUserOptions() { return fetch('/api/admin/users?page=1&page_size=999') .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { throw new Error(res.error || '加载用户失败'); } var items = res.items || []; shopManageAllUsers = items.map(function (u) { return { id: u.id, username: u.username || '', role: u.role || 'normal', created_by_id: u.created_by_id || null }; }); setShopManageGroupLeader( document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '', document.getElementById('shopManageGroupLeaderName') ? document.getElementById('shopManageGroupLeaderName').value : '', getSelectedShopManageGroupMemberIds() ); var sel = document.getElementById('filterUser'); var cur = sel.value; sel.innerHTML = ''; items.forEach(function (u) { var opt = document.createElement('option'); opt.value = u.id; opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; sel.appendChild(opt); }); sel.value = cur || ''; }) .catch(function () { shopManageAllUsers = []; var sel = document.getElementById('filterUser'); if (sel) { sel.innerHTML = ''; } refreshShopManageGroupMemberSelect( document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '', [] ); }); } document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); }; // ========== 数据去重总数据 ========== var dedupeTotalDataPage = 1, dedupeTotalDataPageSize = 15; function buildDedupeTotalDataQuery(page) { var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize; var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim(); if (keyword) q += '&keyword=' + encodeURIComponent(keyword); return q; } function loadDedupeTotalData(page) { 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 = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无总数据'; } else { tbody.innerHTML = items.map(function (item) { return '' + item.id + '' + (item.data_value || '') + '' + (item.created_at || '') + '' + ' ' + '' + ''; }).join(''); } renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData); bindDedupeTotalDataActions(); }) .catch(function () { document.getElementById('dedupeTotalDataListBody').innerHTML = '请求失败'; }); } function bindDedupeTotalDataActions() { document.querySelectorAll('[data-dedupe-total-edit]').forEach(function (btn) { btn.onclick = function () { document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || ''; document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/"/g, '"'); document.getElementById('msgEditDedupeTotalData').textContent = ''; document.getElementById('msgEditDedupeTotalData').className = 'msg'; document.getElementById('editDedupeTotalDataModal').classList.add('show'); }; }); document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) { btn.onclick = function () { var value = (btn.dataset.value || '').replace(/"/g, '"'); if (!confirm('确定删除总数据“' + value + '”吗?')) return; fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); } else { alert(res.error || '删除失败'); } }); }; }); } document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); }; var 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('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 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); 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 formData = new FormData(); formData.append('file', file); 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 msgEl = document.getElementById('msgEditDedupeTotalData'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!value) { 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 }) }) .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 shopKeyPage = 1, shopKeyPageSize = 15; function buildShopKeyQuery(page) { return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize; } function loadShopKeys(page) { shopKeyPage = page || 1; fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('shopKeyListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无店铺密钥'; } else { tbody.innerHTML = items.map(function (item, index) { var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1; return '' + rowNo + '' + (item.ziniao_account_name || '') + '' + (item.ziniao_token || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + ' ' + '' + ''; }).join(''); } renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys); bindShopKeyActions(); }) .catch(function () { document.getElementById('shopKeyListBody').innerHTML = '请求失败'; }); } function bindShopKeyActions() { document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) { btn.onclick = function () { var item = {}; try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; } document.getElementById('editShopKeyId').value = item.id || ''; document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || ''; document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || ''; document.getElementById('msgEditShopKey').textContent = ''; document.getElementById('msgEditShopKey').className = 'msg'; document.getElementById('editShopKeyModal').classList.add('show'); }; }); document.querySelectorAll('[data-shop-key-delete]').forEach(function (btn) { btn.onclick = function () { var name = (btn.dataset.ziniaoAccountName || '').replace(/"/g, '"'); if (!confirm('确定删除店铺密钥“' + name + '”吗?')) return; fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) { loadShopKeys(shopKeyPage); } else { alert(res.error || '删除失败'); } }); }; }); } document.getElementById('btnCreateShopKey').onclick = function () { 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({ ziniao_account_name: ziniaoAccountName, ziniao_token: ziniaoToken }) }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) { 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 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({ 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 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 loadShopManage(page) { shopManagePage = page || 1; fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('shopManageListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无店铺'; } else { tbody.innerHTML = items.map(function (item, index) { var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1; return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + (item.password || '') + '' + (item.created_at || '') + '' + (item.updated_at || '') + '' + ' ' + '' + ''; }).join(''); } renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage); bindShopManageActions(); }) .catch(function () { document.getElementById('shopManageListBody').innerHTML = '请求失败'; }); } function bindShopManageActions() { document.querySelectorAll('[data-shop-manage-edit]').forEach(function (btn) { btn.onclick = function () { var item = {}; try { item = JSON.parse((btn.dataset.shopManage || '').replace(/"/g, '"')); } catch (e) { item = {}; } document.getElementById('editShopManageId').value = item.id || ''; document.getElementById('editShopManageShopName').value = item.shop_name || ''; document.getElementById('editShopManageMallName').value = item.mall_name || ''; document.getElementById('editShopManageAccount').value = item.account || ''; document.getElementById('editShopManagePassword').value = item.password || ''; document.getElementById('msgEditShopManage').textContent = ''; document.getElementById('msgEditShopManage').className = 'msg'; loadShopManageGroups(null, item.group_id).then(function () { document.getElementById('editShopManageModal').classList.add('show'); }); }; }); document.querySelectorAll('[data-shop-manage-delete]').forEach(function (btn) { btn.onclick = function () { var name = (btn.dataset.shopManageName || '').replace(/"/g, '"'); if (!confirm('确定删除店铺“' + name + '”吗?')) return; fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) { loadShopManage(shopManagePage); } else { alert(res.error || '删除失败'); } }); }; }); } function refreshShopGroupSelects(selectedCreateId, selectedEditId) { var createSel = document.getElementById('shopManageGroupSelect'); var editSel = document.getElementById('editShopManageGroupSelect'); var filterSel = document.getElementById('shopManageFilterGroupId'); var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect'); var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId'); var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId'); var queryCreateSel = document.getElementById('queryAsinGroupSelect'); var queryFilterSel = document.getElementById('queryAsinFilterGroupId'); var chooseQueryShopSel = document.getElementById('chooseQueryAsinShopGroupId'); var selectedFilterId = filterSel ? filterSel.value : ''; var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : ''; var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : ''; var selectedChooseSkipShopId = chooseSkipShopSel ? chooseSkipShopSel.value : ''; var selectedQueryCreateId = queryCreateSel ? queryCreateSel.value : ''; var selectedQueryFilterId = queryFilterSel ? queryFilterSel.value : ''; var selectedChooseQueryShopId = chooseQueryShopSel ? chooseQueryShopSel.value : ''; var createOpts = ['']; var filterOpts = ['']; shopManageGroups.forEach(function (g) { var option = ''; createOpts.push(option); filterOpts.push(option); }); createSel.innerHTML = createOpts.join(''); editSel.innerHTML = createOpts.join(''); if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join(''); if (queryCreateSel) queryCreateSel.innerHTML = createOpts.join(''); if (filterSel) filterSel.innerHTML = filterOpts.join(''); if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join(''); if (chooseSkipShopSel) chooseSkipShopSel.innerHTML = filterOpts.join(''); if (queryFilterSel) queryFilterSel.innerHTML = filterOpts.join(''); if (chooseQueryShopSel) chooseQueryShopSel.innerHTML = filterOpts.join(''); if (selectedCreateId != null) createSel.value = String(selectedCreateId); if (selectedEditId != null) editSel.value = String(selectedEditId); if (filterSel && selectedFilterId) filterSel.value = selectedFilterId; if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId; if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId; if (chooseSkipShopSel && selectedChooseSkipShopId) chooseSkipShopSel.value = selectedChooseSkipShopId; if (queryCreateSel && selectedQueryCreateId) queryCreateSel.value = selectedQueryCreateId; if (queryFilterSel && selectedQueryFilterId) queryFilterSel.value = selectedQueryFilterId; if (chooseQueryShopSel && selectedChooseQueryShopId) chooseQueryShopSel.value = selectedChooseQueryShopId; } 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 || []; shopManageGroupsLoadedAt = Date.now(); refreshShopGroupSelects(selectedCreateId, selectedEditId); return shopManageGroups; }) .catch(function () { shopManageGroups = []; shopManageGroupsLoadedAt = 0; refreshShopGroupSelects(); return []; }); } 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() : []; } function updateShopManageGroupButtonsAccess() { var canManageGroups = !!currentUserId; ['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups', 'btnManageQueryAsinGroups'].forEach(function (id) { var btn = document.getElementById(id); if (btn) btn.style.display = canManageGroups ? '' : 'none'; }); } function openShopManageGroupModal() { resetShopManageGroupForm(); loadUserOptions() .then(function () { return loadShopManageGroups(); }) .then(function () { renderShopManageGroupRows(); document.getElementById('shopManageGroupModal').classList.add('show'); }); } function renderShopManageGroupRows() { var tbody = document.getElementById('shopManageGroupListBody'); if (!shopManageGroups.length) { tbody.innerHTML = '暂无分组'; return; } tbody.innerHTML = shopManageGroups.map(function (item, index) { var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames : []; var memberHtml = memberNames.length ? '
' + memberNames.map(function (name) { return '' + escapeHtml(name || '') + ''; }).join('') + '
' : '-'; var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || ''); var actionHtml = canEdit ? (' ' + '') : '-'; return '' + '' + (index + 1) + '' + '' + escapeHtml(item.group_name || '') + '' + '' + escapeHtml(item.leader_username || '') + '' + '' + escapeHtml(item.member_count || 0) + '' + '' + memberHtml + '' + '' + escapeHtml(item.created_at || '') + '' + '' + escapeHtml(item.updated_at || '') + '' + '' + actionHtml + '' + ''; }).join(''); document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) { btn.onclick = function () { var item = null; shopManageGroups.some(function (group) { if (String(group.id || '') === String(btn.dataset.shopGroupEdit || '')) { item = group; return true; } return false; }); if (!item) return; document.getElementById('shopManageGroupEditId').value = item.id || ''; document.getElementById('shopManageGroupInput').value = item.group_name || ''; setShopManageGroupLeader(item.leader_user_id || '', item.leader_username || '', item.member_user_ids || []); document.getElementById('msgShopManageGroup').textContent = ''; document.getElementById('msgShopManageGroup').className = 'msg'; }; }); document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) { btn.onclick = function () { var gid = btn.dataset.shopGroupDelete; var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"'); if (!confirm('确定删除分组“' + gname + '”吗?')) return; fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { alert(res.error || '删除失败'); return; } loadShopManageGroups(null, null, true).then(function () { renderShopManageGroupRows(); loadShopManage(shopManagePage); loadSkipPriceAsin(skipPriceAsinPage); }); }); }; }); } document.getElementById('btnManageShopGroups').onclick = function () { setShopManageGroupGrantRoutes(['shop-manage']); openShopManageGroupModal(); }; document.getElementById('btnManageShopGroupsFromEdit').onclick = function () { setShopManageGroupGrantRoutes(['shop-manage']); openShopManageGroupModal(); }; document.getElementById('btnManageSkipPriceAsinGroups').onclick = function () { setShopManageGroupGrantRoutes(['skip-price-asin']); openShopManageGroupModal(); }; document.getElementById('btnManageQueryAsinGroups').onclick = function () { setShopManageGroupGrantRoutes(['query-asin']); openShopManageGroupModal(); }; document.getElementById('btnSearchShopManage').onclick = function () { loadShopManage(1); }; document.getElementById('shopManageFilterShopName').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); loadShopManage(1); } }); document.getElementById('btnCloseShopManageGroupModal').onclick = function () { document.getElementById('shopManageGroupModal').classList.remove('show'); }; document.getElementById('btnCancelShopManageGroupEdit').onclick = resetShopManageGroupForm; 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(); msgEl.textContent = res.msg || '保存成功'; msgEl.className = 'msg ok'; loadShopManageGroups(null, null, true).then(function () { renderShopManageGroupRows(); loadShopManage(shopManagePage); loadSkipPriceAsin(skipPriceAsinPage); }); }) .catch(function () { msgEl.textContent = '请求失败'; msgEl.className = 'msg err'; }); }; 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 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, 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('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 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, 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 ========== var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15; var chooseSkipPriceAsinShopPage = 1, chooseSkipPriceAsinShopPageSize = 10; 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 buildChooseSkipPriceAsinShopQuery(page) { var query = 'page=' + (page || 1) + '&page_size=' + chooseSkipPriceAsinShopPageSize; var groupId = (document.getElementById('chooseSkipPriceAsinShopGroupId').value || '').trim(); var shopName = (document.getElementById('chooseSkipPriceAsinShopKeyword').value || '').trim(); if (groupId) query += '&group_id=' + encodeURIComponent(groupId); if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); return query; } function loadChooseSkipPriceAsinShops(page) { chooseSkipPriceAsinShopPage = page || 1; fetch('/api/admin/shop-manages?' + buildChooseSkipPriceAsinShopQuery(chooseSkipPriceAsinShopPage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('chooseSkipPriceAsinShopListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (!items.length) { tbody.innerHTML = '暂无店铺'; } else { tbody.innerHTML = items.map(function (item, index) { var rowNo = (chooseSkipPriceAsinShopPage - 1) * chooseSkipPriceAsinShopPageSize + index + 1; return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.account || '') + '' + '' + ''; }).join(''); } renderPagination('chooseSkipPriceAsinShopPagination', res.total, res.page, res.page_size, loadChooseSkipPriceAsinShops); bindChooseSkipPriceAsinShopActions(); }) .catch(function () { document.getElementById('chooseSkipPriceAsinShopListBody').innerHTML = '请求失败'; }); } function bindChooseSkipPriceAsinShopActions() { document.querySelectorAll('[data-choose-skip-price-asin-shop]').forEach(function (btn) { btn.onclick = function () { document.getElementById('skipPriceAsinShopName').value = (btn.dataset.shopName || '').replace(/"/g, '"'); if (!document.getElementById('skipPriceAsinGroupSelect').value && btn.dataset.groupId) { document.getElementById('skipPriceAsinGroupSelect').value = btn.dataset.groupId; } document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); }; }); } function openChooseSkipPriceAsinShopModal() { var currentGroupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); if (currentGroupId) { document.getElementById('chooseSkipPriceAsinShopGroupId').value = currentGroupId; } document.getElementById('chooseSkipPriceAsinShopKeyword').value = (document.getElementById('skipPriceAsinShopName').value || '').trim(); document.getElementById('chooseSkipPriceAsinShopModal').classList.add('show'); loadChooseSkipPriceAsinShops(1); } function setupSkipPriceAsinShopPicker() { var input = document.getElementById('skipPriceAsinShopName'); var button = document.getElementById('btnChooseSkipPriceAsinShop'); if (!input || !button) return; var inputParent = input.parentNode; if (inputParent && inputParent.style && inputParent.style.display === 'flex') return; var legacyWrap = button.parentNode; var wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.gap = '8px'; wrapper.style.alignItems = 'center'; input.parentNode.insertBefore(wrapper, input); wrapper.appendChild(input); wrapper.appendChild(button); button.style.whiteSpace = 'nowrap'; button.style.marginTop = '0'; if (legacyWrap && legacyWrap !== wrapper) { legacyWrap.style.display = 'none'; } } function getSelectedSkipPriceCountries() { return Array.from(document.getElementById('skipPriceAsinCountries').selectedOptions).map(function (option) { return option.value; }); } 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 getSkipPriceCountryLabel(countryCode) { var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; }); return country ? country.label : countryCode; } function renderSkipPriceAsinInputs() { var container = document.getElementById('skipPriceAsinInputs'); var selectedCountries = getSelectedSkipPriceCountries(); var existingValues = {}; var existingMinimumPrices = {}; container.querySelectorAll('[data-skip-price-country-input]').forEach(function (input) { existingValues[input.getAttribute('data-skip-price-country-input')] = input.value; }); container.querySelectorAll('[data-skip-price-country-minimum-price-input]').forEach(function (input) { existingMinimumPrices[input.getAttribute('data-skip-price-country-minimum-price-input')] = input.value; }); if (!selectedCountries.length) { container.innerHTML = '
请选择国家后输入 ASIN 和最低价
'; return; } container.innerHTML = selectedCountries.map(function (countryCode) { var label = getSkipPriceCountryLabel(countryCode); var value = existingValues[countryCode] || ''; var minimumPrice = existingMinimumPrices[countryCode] || ''; return '
' + '' + label + '' + '' + '' + '
'; }).join(''); } function collectSkipPriceAsinMappings(countries) { var asinMappings = {}; var minimumPriceMappings = {}; for (var i = 0; i < countries.length; i++) { var countryCode = countries[i]; var input = document.querySelector('[data-skip-price-country-input="' + countryCode + '"]'); var minimumPriceInput = document.querySelector('[data-skip-price-country-minimum-price-input="' + countryCode + '"]'); var asin = input ? (input.value || '').trim().toUpperCase() : ''; if (!asin) { var label = getSkipPriceCountryLabel(countryCode); throw new Error(label + ' ASIN 不能为空'); } var minimumPrice = minimumPriceInput ? (minimumPriceInput.value || '').trim() : ''; if (minimumPrice) { var minimumPriceNumber = Number(minimumPrice); if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) { throw new Error(getSkipPriceCountryLabel(countryCode) + ' 最低价格式不正确'); } minimumPriceMappings[countryCode] = minimumPrice; } asinMappings[countryCode] = asin; } return { asinMappings: asinMappings, minimumPriceMappings: minimumPriceMappings }; } function buildSkipPriceAsinQuery(page) { var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize; var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim(); var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim(); var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim(); if (groupId) query += '&group_id=' + encodeURIComponent(groupId); if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); if (asin) query += '&asin=' + encodeURIComponent(asin); return query; } function renderSkipPriceAsinCell(item, country) { var asinValue = item[country.field] || ''; var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]); var hasValue = !!asinValue || !!minimumPriceValue; var infoHtml = hasValue ? ('
' + '' + (asinValue || '-') + '' + '最低价:' + (minimumPriceValue || '-') + '' + '
') : '-'; var deleteHtml = hasValue ? ('') : ''; return '
' + infoHtml + '' + deleteHtml + '
'; } function openEditSkipPriceAsinModal(itemId, countryCode, shopName, asinValue, minimumPriceValue) { document.getElementById('editSkipPriceAsinId').value = itemId || ''; document.getElementById('editSkipPriceAsinCountry').value = countryCode || ''; document.getElementById('editSkipPriceAsinShopName').value = shopName || ''; document.getElementById('editSkipPriceAsinCountryLabel').value = getSkipPriceCountryLabel(countryCode || ''); document.getElementById('editSkipPriceAsinValue').value = asinValue || ''; document.getElementById('editSkipPriceMinimumPrice').value = minimumPriceValue || ''; document.getElementById('msgEditSkipPriceAsin').textContent = ''; document.getElementById('msgEditSkipPriceAsin').className = 'msg'; document.getElementById('editSkipPriceAsinModal').classList.add('show'); } function bindSkipPriceAsinActions() { document.querySelectorAll('[data-skip-price-asin-edit]').forEach(function (btn) { btn.onclick = function () { var shopName = (btn.dataset.shopName || '').replace(/"/g, '"'); var countryCode = btn.dataset.country || ''; var currentAsin = (btn.dataset.asin || '').replace(/"/g, '"'); var currentMinimumPrice = (btn.dataset.minimumPrice || '').replace(/"/g, '"'); openEditSkipPriceAsinModal( btn.dataset.skipPriceAsinEdit, countryCode, shopName, currentAsin, currentMinimumPrice ); }; }); document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function (btn) { btn.onclick = function () { var shopName = (btn.dataset.shopName || '').replace(/"/g, '"'); var countryCode = btn.dataset.country || ''; if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return; fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) loadSkipPriceAsin(skipPriceAsinPage); else alert(res.error || '删除失败'); }); }; }); } function loadSkipPriceAsin(page) { skipPriceAsinPage = page || 1; fetch('/api/admin/skip-price-asins?' + buildSkipPriceAsinQuery(skipPriceAsinPage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('skipPriceAsinListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无数据'; } else { tbody.innerHTML = items.map(function (item, index) { var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1; return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + skipPriceCountryColumns.map(function (country) { return '' + renderSkipPriceAsinCell(item, country) + ''; }).join('') + ''; }).join(''); } renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin); bindSkipPriceAsinActions(); }) .catch(function () { document.getElementById('skipPriceAsinListBody').innerHTML = '请求失败'; }); } var skipPriceAsinImportPollTimer = null; var skipPriceAsinDeleteImportPollTimer = null; var skipPriceAsinImportPollSeq = 0; var skipPriceAsinDeleteImportPollSeq = 0; function stopSkipPriceAsinImportProgress() { skipPriceAsinImportPollSeq += 1; if (skipPriceAsinImportPollTimer) { clearTimeout(skipPriceAsinImportPollTimer); skipPriceAsinImportPollTimer = null; } } function stopSkipPriceAsinDeleteImportProgress() { skipPriceAsinDeleteImportPollSeq += 1; if (skipPriceAsinDeleteImportPollTimer) { clearTimeout(skipPriceAsinDeleteImportPollTimer); skipPriceAsinDeleteImportPollTimer = null; } } function setSkipPriceAsinImportProgress(percent, text) { var wrap = document.getElementById('skipPriceAsinImportProgressWrap'); var fill = document.getElementById('skipPriceAsinImportProgressFill'); var textEl = document.getElementById('skipPriceAsinImportProgressText'); wrap.style.display = 'block'; fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; textEl.textContent = text || ''; } function setSkipPriceAsinDeleteImportProgress(percent, text) { var wrap = document.getElementById('skipPriceAsinDeleteImportProgressWrap'); var fill = document.getElementById('skipPriceAsinDeleteImportProgressFill'); var textEl = document.getElementById('skipPriceAsinDeleteImportProgressText'); wrap.style.display = 'block'; fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; textEl.textContent = text || ''; } function pollSkipPriceAsinImport(importId) { stopSkipPriceAsinImportProgress(); var pollSeq = ++skipPriceAsinImportPollSeq; var finished = false; var inFlight = false; var attempts = 0; var maxAttempts = 300; function isActive() { return !finished && pollSeq === skipPriceAsinImportPollSeq; } function finish() { finished = true; if (skipPriceAsinImportPollTimer) { clearTimeout(skipPriceAsinImportPollTimer); skipPriceAsinImportPollTimer = null; } } function schedule() { if (isActive()) { skipPriceAsinImportPollTimer = setTimeout(tick, 1500); } } function tick() { if (!isActive() || inFlight) { return; } attempts += 1; if (attempts > maxAttempts) { finish(); document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度超时,请稍后刷新列表确认结果'; document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; return; } inFlight = true; fetch('/api/admin/skip-price-asins/import/' + encodeURIComponent(importId)) .then(function (r) { return r.json(); }) .then(function (res) { if (!isActive()) { return; } if (!res.success) { finish(); document.getElementById('msgSkipPriceAsinImport').textContent = res.error || '查询导入进度失败'; document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; return; } var progress = res.progress || {}; var totalRows = progress.total_rows || 0; var processedRows = progress.processed_rows || 0; var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; setSkipPriceAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); if (progress.status === 'success') { finish(); document.getElementById('msgSkipPriceAsinImport').textContent = '导入成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); document.getElementById('msgSkipPriceAsinImport').className = 'msg ok'; loadSkipPriceAsin(1); } else if (progress.status === 'failed') { finish(); document.getElementById('msgSkipPriceAsinImport').textContent = progress.error_message || '导入失败'; document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; } else { schedule(); } }) .catch(function () { if (!isActive()) { return; } finish(); document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度失败'; document.getElementById('msgSkipPriceAsinImport').className = 'msg err'; }) .finally(function () { inFlight = false; }); } tick(); } function pollSkipPriceAsinDeleteImport(importId) { stopSkipPriceAsinDeleteImportProgress(); var pollSeq = ++skipPriceAsinDeleteImportPollSeq; var finished = false; var inFlight = false; var attempts = 0; var maxAttempts = 300; function isActive() { return !finished && pollSeq === skipPriceAsinDeleteImportPollSeq; } function finish() { finished = true; if (skipPriceAsinDeleteImportPollTimer) { clearTimeout(skipPriceAsinDeleteImportPollTimer); skipPriceAsinDeleteImportPollTimer = null; } } function schedule() { if (isActive()) { skipPriceAsinDeleteImportPollTimer = setTimeout(tick, 1500); } } function tick() { if (!isActive() || inFlight) { return; } attempts += 1; if (attempts > maxAttempts) { finish(); document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度超时,请稍后刷新列表确认结果'; document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; return; } inFlight = true; fetch('/api/admin/skip-price-asins/delete-import/' + encodeURIComponent(importId)) .then(function (r) { return r.json(); }) .then(function (res) { if (!isActive()) { return; } if (!res.success) { finish(); document.getElementById('msgSkipPriceAsinDeleteImport').textContent = res.error || '查询删除进度失败'; document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; return; } var progress = res.progress || {}; var totalRows = progress.total_rows || 0; var processedRows = progress.processed_rows || 0; var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; setSkipPriceAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); if (progress.status === 'success') { finish(); document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg ok'; loadSkipPriceAsin(1); } else if (progress.status === 'failed') { finish(); document.getElementById('msgSkipPriceAsinDeleteImport').textContent = progress.error_message || '删除失败'; document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; } else { schedule(); } }) .catch(function () { if (!isActive()) { return; } finish(); document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度失败'; document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err'; }) .finally(function () { inFlight = false; }); } tick(); } function uploadSkipPriceAsinImport(deleteMode) { var fileInput = document.getElementById(deleteMode ? 'skipPriceAsinDeleteImportFile' : 'skipPriceAsinImportFile'); var msgEl = document.getElementById(deleteMode ? 'msgSkipPriceAsinDeleteImport' : 'msgSkipPriceAsinImport'); msgEl.textContent = ''; msgEl.className = 'msg'; if (deleteMode) { stopSkipPriceAsinDeleteImportProgress(); document.getElementById('skipPriceAsinDeleteImportProgressWrap').style.display = 'none'; } else { stopSkipPriceAsinImportProgress(); document.getElementById('skipPriceAsinImportProgressWrap').style.display = 'none'; } var groupId = (document.getElementById('skipPriceAsinGroupSelect').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); 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('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal; document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () { loadChooseSkipPriceAsinShops(1); }; document.getElementById('chooseSkipPriceAsinShopGroupId').onchange = function () { loadChooseSkipPriceAsinShops(1); }; document.getElementById('chooseSkipPriceAsinShopKeyword').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); loadChooseSkipPriceAsinShops(1); } }); document.getElementById('btnCloseChooseSkipPriceAsinShopModal').onclick = function () { document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show'); }; document.getElementById('btnCloseEditSkipPriceAsin').onclick = function () { document.getElementById('editSkipPriceAsinModal').classList.remove('show'); }; document.getElementById('btnSaveSkipPriceAsin').onclick = function () { var itemId = (document.getElementById('editSkipPriceAsinId').value || '').trim(); var countryCode = (document.getElementById('editSkipPriceAsinCountry').value || '').trim(); var asin = (document.getElementById('editSkipPriceAsinValue').value || '').trim().toUpperCase(); var minimumPrice = (document.getElementById('editSkipPriceMinimumPrice').value || '').trim(); var msgEl = document.getElementById('msgEditSkipPriceAsin'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!itemId || !countryCode) { msgEl.textContent = '缺少编辑记录'; msgEl.className = 'msg err'; return; } if (!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; } } fetch('/api/admin/skip-price-asin/' + itemId + '/country/' + countryCode, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ asin: asin, minimum_price: minimumPrice || null }) }) .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { msgEl.textContent = res.error || '保存失败'; msgEl.className = 'msg err'; return; } document.getElementById('editSkipPriceAsinModal').classList.remove('show'); loadSkipPriceAsin(skipPriceAsinPage); }) .catch(function () { msgEl.textContent = '请求失败'; msgEl.className = 'msg err'; }); }; document.getElementById('skipPriceAsinCountries').addEventListener('change', renderSkipPriceAsinInputs); document.getElementById('btnSearchSkipPriceAsin').onclick = function () { loadSkipPriceAsin(1); }; 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('btnCreateSkipPriceAsin').onclick = function () { var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim(); var shopName = (document.getElementById('skipPriceAsinShopName').value || '').trim(); var countries = getSelectedSkipPriceCountries(); var msgEl = document.getElementById('msgSkipPriceAsin'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!groupId || !shopName || !countries.length) { msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN'; msgEl.className = 'msg err'; return; } var asinMappings = {}; var minimumPriceMappings = {}; try { var mappings = collectSkipPriceAsinMappings(countries); asinMappings = mappings.asinMappings || {}; minimumPriceMappings = mappings.minimumPriceMappings || {}; } catch (err) { msgEl.textContent = err.message || '请输入 ASIN'; msgEl.className = 'msg err'; return; } var fallbackAsin = ''; Object.keys(asinMappings).some(function (countryCode) { fallbackAsin = asinMappings[countryCode] || ''; return !!fallbackAsin; }); fetch('/api/admin/skip-price-asin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, countries: countries, asin: fallbackAsin, 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; } document.getElementById('skipPriceAsinGroupSelect').value = ''; document.getElementById('skipPriceAsinShopName').value = ''; Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function (option) { option.selected = false; }); refreshDropdownMultiSelect('skipPriceAsinCountries'); renderSkipPriceAsinInputs(); msgEl.textContent = res.msg || '保存成功'; msgEl.className = 'msg ok'; loadSkipPriceAsin(1); }) .catch(function () { msgEl.textContent = '请求失败'; msgEl.className = 'msg err'; }); }; setupSkipPriceAsinShopPicker(); initDropdownMultiSelect('skipPriceAsinCountries', '请选择国家'); document.getElementById('btnImportSkipPriceAsin').onclick = function () { uploadSkipPriceAsinImport(false); }; document.getElementById('btnDeleteImportSkipPriceAsin').onclick = function () { uploadSkipPriceAsinImport(true); }; renderSkipPriceAsinInputs(); // ========== 查询 ASIN ========== var queryAsinPage = 1, queryAsinPageSize = 15; var chooseQueryAsinShopPage = 1, chooseQueryAsinShopPageSize = 10; 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 getQueryAsinCountryLabel(countryCode) { var country = queryAsinCountryColumns.find(function (item) { return item.code === countryCode; }); return country ? country.label : countryCode; } function getSelectedQueryAsinCountries() { return Array.from(document.getElementById('queryAsinCountries').selectedOptions).map(function (option) { return option.value; }); } function renderQueryAsinInputs() { var container = document.getElementById('queryAsinInputs'); var selectedCountries = getSelectedQueryAsinCountries(); var existingValues = {}; container.querySelectorAll('[data-query-asin-country-input]').forEach(function (input) { existingValues[input.getAttribute('data-query-asin-country-input')] = input.value; }); if (!selectedCountries.length) { container.innerHTML = '
请选择国家后输入 ASIN
'; return; } container.innerHTML = selectedCountries.map(function (countryCode) { var label = getQueryAsinCountryLabel(countryCode); var value = existingValues[countryCode] || ''; return '
' + '' + label + '' + '' + '
'; }).join(''); } function collectQueryAsinMappings(countries) { var asinMappings = {}; for (var i = 0; i < countries.length; i++) { var countryCode = countries[i]; var input = document.querySelector('[data-query-asin-country-input="' + countryCode + '"]'); var asin = input ? (input.value || '').trim().toUpperCase() : ''; if (!asin) { throw new Error(getQueryAsinCountryLabel(countryCode) + ' ASIN 不能为空'); } asinMappings[countryCode] = asin; } return asinMappings; } function buildChooseQueryAsinShopQuery(page) { var query = 'page=' + (page || 1) + '&page_size=' + chooseQueryAsinShopPageSize; var groupId = (document.getElementById('chooseQueryAsinShopGroupId').value || '').trim(); var shopName = (document.getElementById('chooseQueryAsinShopKeyword').value || '').trim(); if (groupId) query += '&group_id=' + encodeURIComponent(groupId); if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); return query; } function loadChooseQueryAsinShops(page) { chooseQueryAsinShopPage = page || 1; fetch('/api/admin/shop-manages?' + buildChooseQueryAsinShopQuery(chooseQueryAsinShopPage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('chooseQueryAsinShopListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无店铺'; } else { tbody.innerHTML = items.map(function (item, index) { var rowNo = (chooseQueryAsinShopPage - 1) * chooseQueryAsinShopPageSize + index + 1; return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + (item.mall_name || '') + '' + (item.username || '') + '' + '' + ''; }).join(''); } renderPagination('chooseQueryAsinShopPagination', res.total, res.page, res.page_size, loadChooseQueryAsinShops); bindChooseQueryAsinShopActions(); }) .catch(function () { document.getElementById('chooseQueryAsinShopListBody').innerHTML = '请求失败'; }); } function bindChooseQueryAsinShopActions() { document.querySelectorAll('[data-choose-query-asin-shop]').forEach(function (btn) { btn.onclick = function () { document.getElementById('queryAsinShopName').value = (btn.dataset.shopName || '').replace(/"/g, '"'); if (btn.dataset.groupId) { document.getElementById('queryAsinGroupSelect').value = btn.dataset.groupId; } document.getElementById('chooseQueryAsinShopModal').classList.remove('show'); }; }); } function openChooseQueryAsinShopModal() { var currentGroupId = (document.getElementById('queryAsinGroupSelect').value || '').trim(); if (currentGroupId) { document.getElementById('chooseQueryAsinShopGroupId').value = currentGroupId; } document.getElementById('chooseQueryAsinShopKeyword').value = (document.getElementById('queryAsinShopName').value || '').trim(); document.getElementById('chooseQueryAsinShopModal').classList.add('show'); loadChooseQueryAsinShops(1); } function buildQueryAsinQuery(page) { var query = 'page=' + (page || 1) + '&page_size=' + queryAsinPageSize; var groupId = (document.getElementById('queryAsinFilterGroupId').value || '').trim(); var shopName = (document.getElementById('queryAsinFilterShopName').value || '').trim(); var asin = (document.getElementById('queryAsinFilterAsin').value || '').trim(); if (groupId) query += '&group_id=' + encodeURIComponent(groupId); if (shopName) query += '&shop_name=' + encodeURIComponent(shopName); if (asin) query += '&asin=' + encodeURIComponent(asin); return query; } function renderQueryAsinCell(item, country) { var asinValue = item[country.field] || ''; var infoHtml = asinValue ? '' + asinValue + '' : '-'; var deleteHtml = asinValue ? ('') : ''; return '
' + infoHtml + '' + deleteHtml + '
'; } function openEditQueryAsinModal(itemId, countryCode, shopName, asinValue) { document.getElementById('editQueryAsinId').value = itemId || ''; document.getElementById('editQueryAsinCountry').value = countryCode || ''; document.getElementById('editQueryAsinShopName').value = shopName || ''; document.getElementById('editQueryAsinCountryLabel').value = getQueryAsinCountryLabel(countryCode || ''); document.getElementById('editQueryAsinValue').value = asinValue || ''; document.getElementById('msgEditQueryAsin').textContent = ''; document.getElementById('msgEditQueryAsin').className = 'msg'; document.getElementById('editQueryAsinModal').classList.add('show'); } function bindQueryAsinActions() { document.querySelectorAll('[data-query-asin-edit]').forEach(function (btn) { btn.onclick = function () { openEditQueryAsinModal( btn.dataset.queryAsinEdit, btn.dataset.country || '', (btn.dataset.shopName || '').replace(/"/g, '"'), (btn.dataset.asin || '').replace(/"/g, '"') ); }; }); document.querySelectorAll('[data-query-asin-delete]').forEach(function (btn) { btn.onclick = function () { var shopName = (btn.dataset.shopName || '').replace(/"/g, '"'); var countryCode = btn.dataset.country || ''; if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return; fetch('/api/admin/query-asin/' + btn.dataset.queryAsinDelete + '/country/' + countryCode, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) loadQueryAsin(queryAsinPage); else alert(res.error || '删除失败'); }); }; }); } function loadQueryAsin(page) { queryAsinPage = page || 1; fetch('/api/admin/query-asins?' + buildQueryAsinQuery(queryAsinPage)) .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('queryAsinListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无数据'; } else { tbody.innerHTML = items.map(function (item, index) { var rowNo = (queryAsinPage - 1) * queryAsinPageSize + index + 1; return '' + rowNo + '' + (item.group_name || '') + '' + (item.shop_name || '') + '' + queryAsinCountryColumns.map(function (country) { return '' + renderQueryAsinCell(item, country) + ''; }).join('') + ''; }).join(''); } renderPagination('queryAsinPagination', res.total, res.page, res.page_size, loadQueryAsin); bindQueryAsinActions(); }) .catch(function () { document.getElementById('queryAsinListBody').innerHTML = '请求失败'; }); } var queryAsinImportPollTimer = null; var queryAsinDeleteImportPollTimer = null; function stopQueryAsinImportProgress() { if (queryAsinImportPollTimer) { clearInterval(queryAsinImportPollTimer); queryAsinImportPollTimer = null; } } function stopQueryAsinDeleteImportProgress() { if (queryAsinDeleteImportPollTimer) { clearInterval(queryAsinDeleteImportPollTimer); queryAsinDeleteImportPollTimer = null; } } function setQueryAsinImportProgress(percent, text) { var wrap = document.getElementById('queryAsinImportProgressWrap'); var fill = document.getElementById('queryAsinImportProgressFill'); var textEl = document.getElementById('queryAsinImportProgressText'); wrap.style.display = 'block'; fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; textEl.textContent = text || ''; } function setQueryAsinDeleteImportProgress(percent, text) { var wrap = document.getElementById('queryAsinDeleteImportProgressWrap'); var fill = document.getElementById('queryAsinDeleteImportProgressFill'); var textEl = document.getElementById('queryAsinDeleteImportProgressText'); wrap.style.display = 'block'; fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%'; textEl.textContent = text || ''; } function pollQueryAsinImport(importId) { stopQueryAsinImportProgress(); function tick() { fetch('/api/admin/query-asins/import/' + encodeURIComponent(importId)) .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { stopQueryAsinImportProgress(); document.getElementById('msgQueryAsinImport').textContent = res.error || '查询导入进度失败'; document.getElementById('msgQueryAsinImport').className = 'msg err'; return; } var progress = res.progress || {}; var totalRows = progress.total_rows || 0; var processedRows = progress.processed_rows || 0; var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; setQueryAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); if (progress.status === 'success') { stopQueryAsinImportProgress(); document.getElementById('msgQueryAsinImport').textContent = '导入成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); document.getElementById('msgQueryAsinImport').className = 'msg ok'; loadQueryAsin(1); } else if (progress.status === 'failed') { stopQueryAsinImportProgress(); document.getElementById('msgQueryAsinImport').textContent = progress.error_message || '导入失败'; document.getElementById('msgQueryAsinImport').className = 'msg err'; } }) .catch(function () { stopQueryAsinImportProgress(); document.getElementById('msgQueryAsinImport').textContent = '查询导入进度失败'; document.getElementById('msgQueryAsinImport').className = 'msg err'; }); } tick(); queryAsinImportPollTimer = setInterval(tick, 1000); } function pollQueryAsinDeleteImport(importId) { stopQueryAsinDeleteImportProgress(); function tick() { fetch('/api/admin/query-asins/delete-import/' + encodeURIComponent(importId)) .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { stopQueryAsinDeleteImportProgress(); document.getElementById('msgQueryAsinDeleteImport').textContent = res.error || '查询删除进度失败'; document.getElementById('msgQueryAsinDeleteImport').className = 'msg err'; return; } var progress = res.progress || {}; var totalRows = progress.total_rows || 0; var processedRows = progress.processed_rows || 0; var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0; setQueryAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0)); if (progress.status === 'success') { stopQueryAsinDeleteImportProgress(); document.getElementById('msgQueryAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0); document.getElementById('msgQueryAsinDeleteImport').className = 'msg ok'; loadQueryAsin(1); } else if (progress.status === 'failed') { stopQueryAsinDeleteImportProgress(); document.getElementById('msgQueryAsinDeleteImport').textContent = progress.error_message || '删除失败'; document.getElementById('msgQueryAsinDeleteImport').className = 'msg err'; } }) .catch(function () { stopQueryAsinDeleteImportProgress(); document.getElementById('msgQueryAsinDeleteImport').textContent = '查询删除进度失败'; document.getElementById('msgQueryAsinDeleteImport').className = 'msg err'; }); } tick(); queryAsinDeleteImportPollTimer = setInterval(tick, 1000); } function uploadQueryAsinImport(deleteMode) { var fileInput = document.getElementById(deleteMode ? 'queryAsinDeleteImportFile' : 'queryAsinImportFile'); var msgEl = document.getElementById(deleteMode ? 'msgQueryAsinDeleteImport' : 'msgQueryAsinImport'); msgEl.textContent = ''; msgEl.className = 'msg'; if (deleteMode) { stopQueryAsinDeleteImportProgress(); document.getElementById('queryAsinDeleteImportProgressWrap').style.display = 'none'; } else { stopQueryAsinImportProgress(); document.getElementById('queryAsinImportProgressWrap').style.display = 'none'; } if (!fileInput.files || fileInput.files.length === 0) { msgEl.textContent = '请选择 Excel 文件'; msgEl.className = 'msg err'; return; } var file = fileInput.files[0]; var lowerName = (file.name || '').toLowerCase(); if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) { msgEl.textContent = '仅支持 .xlsx 或 .xls 文件'; msgEl.className = 'msg err'; return; } if (deleteMode && !confirm('确定按 Excel 中的分组、店铺名和国家 ASIN 批量删除吗?')) { return; } var formData = new FormData(); formData.append('file', file); var groupId = (document.getElementById('queryAsinGroupSelect').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('btnChooseQueryAsinShop').onclick = openChooseQueryAsinShopModal; document.getElementById('btnSearchChooseQueryAsinShop').onclick = function () { loadChooseQueryAsinShops(1); }; document.getElementById('chooseQueryAsinShopGroupId').onchange = function () { loadChooseQueryAsinShops(1); }; document.getElementById('chooseQueryAsinShopKeyword').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); loadChooseQueryAsinShops(1); } }); document.getElementById('btnCloseChooseQueryAsinShopModal').onclick = function () { document.getElementById('chooseQueryAsinShopModal').classList.remove('show'); }; document.getElementById('btnCloseEditQueryAsin').onclick = function () { document.getElementById('editQueryAsinModal').classList.remove('show'); }; document.getElementById('btnSaveQueryAsin').onclick = function () { var itemId = (document.getElementById('editQueryAsinId').value || '').trim(); var countryCode = (document.getElementById('editQueryAsinCountry').value || '').trim(); var asin = (document.getElementById('editQueryAsinValue').value || '').trim().toUpperCase(); var msgEl = document.getElementById('msgEditQueryAsin'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!itemId || !countryCode) { msgEl.textContent = '缺少编辑记录'; msgEl.className = 'msg err'; return; } if (!asin) { msgEl.textContent = 'ASIN 不能为空'; msgEl.className = 'msg err'; return; } fetch('/api/admin/query-asin/' + itemId + '/country/' + countryCode, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ asin: asin }) }) .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { msgEl.textContent = res.error || '保存失败'; msgEl.className = 'msg err'; return; } document.getElementById('editQueryAsinModal').classList.remove('show'); loadQueryAsin(queryAsinPage); }) .catch(function () { msgEl.textContent = '请求失败'; msgEl.className = 'msg err'; }); }; document.getElementById('queryAsinCountries').addEventListener('change', renderQueryAsinInputs); document.getElementById('btnSearchQueryAsin').onclick = function () { loadQueryAsin(1); }; 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('btnCreateQueryAsin').onclick = function () { var groupId = (document.getElementById('queryAsinGroupSelect').value || '').trim(); var shopName = (document.getElementById('queryAsinShopName').value || '').trim(); var countries = getSelectedQueryAsinCountries(); var msgEl = document.getElementById('msgQueryAsin'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!groupId || !shopName || !countries.length) { msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN'; msgEl.className = 'msg err'; return; } var asinMappings = {}; try { asinMappings = collectQueryAsinMappings(countries); } catch (err) { msgEl.textContent = err.message || '请输入 ASIN'; msgEl.className = 'msg err'; return; } var fallbackAsin = ''; Object.keys(asinMappings).some(function (countryCode) { fallbackAsin = asinMappings[countryCode] || ''; return !!fallbackAsin; }); fetch('/api/admin/query-asin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, countries: countries, asin: fallbackAsin, asin_mappings: asinMappings }) }) .then(function (r) { return r.json(); }) .then(function (res) { if (!res.success) { msgEl.textContent = res.error || '保存失败'; msgEl.className = 'msg err'; return; } document.getElementById('queryAsinGroupSelect').value = ''; document.getElementById('queryAsinShopName').value = ''; Array.from(document.getElementById('queryAsinCountries').options).forEach(function (option) { option.selected = false; }); refreshDropdownMultiSelect('queryAsinCountries'); renderQueryAsinInputs(); msgEl.textContent = res.msg || '保存成功'; msgEl.className = 'msg ok'; loadQueryAsin(1); }) .catch(function () { msgEl.textContent = '请求失败'; msgEl.className = 'msg err'; }); }; initDropdownMultiSelect('queryAsinCountries', '请选择国家'); document.getElementById('btnImportQueryAsin').onclick = function () { uploadQueryAsinImport(false); }; document.getElementById('btnDeleteImportQueryAsin').onclick = function () { uploadQueryAsinImport(true); }; renderQueryAsinInputs(); var productCategoryItems = []; var editingProductCategoryId = null; function resetProductCategoryForm() { editingProductCategoryId = null; document.getElementById('productCategoryParentId').value = ''; document.getElementById('productCategoryName').value = ''; document.getElementById('productCategorySortOrder').value = ''; document.getElementById('productCategoryDescription').value = ''; document.getElementById('btnCreateProductCategory').textContent = '新增类目'; document.getElementById('btnCancelProductCategoryEdit').style.display = 'none'; renderProductCategoryParentOptions(); } function renderProductCategoryParentOptions(selectedValue, excludedId) { var select = document.getElementById('productCategoryParentId'); if (!select) return; var selected = selectedValue == null ? select.value : String(selectedValue || ''); var excluded = excludedId == null ? editingProductCategoryId : excludedId; var options = ['']; productCategoryItems.forEach(function (item) { if (excluded && String(item.id) === String(excluded)) return; var prefix = new Array((item.level || 0) + 1).join('  '); options.push(''); }); select.innerHTML = options.join(''); select.value = selected; } function renderProductCategoryRows(items) { var tbody = document.getElementById('productCategoryListBody'); if (!tbody) return; if (!items.length) { tbody.innerHTML = '暂无商品类目'; return; } tbody.innerHTML = items.map(function (item) { var indent = Math.max(0, item.level || 0) * 24; var mark = item.child_count > 0 ? '+' : '-'; return '' + '
' + mark + '' + escapeHtml(item.name || '') + '
' + '
' + escapeHtml(item.path || item.name || '') + '
' + escapeHtml(item.category_key || '') + '
' + '' + escapeHtml(item.sort_order || 0) + '' + '' + (item.is_builtin ? '内置' : '自定义') + '' + '' + escapeHtml(item.description || '') + '' + ' ' + '' + ''; }).join(''); 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.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() { fetch('/api/admin/product-categories') .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('productCategoryListBody'); if (!res.success) { if (tbody) tbody.innerHTML = '加载失败: ' + escapeHtml(res.error || '') + ''; return; } productCategoryItems = res.items || []; renderProductCategoryParentOptions(); renderProductCategoryRows(productCategoryItems); }) .catch(function () { var tbody = document.getElementById('productCategoryListBody'); if (tbody) tbody.innerHTML = '请求失败'; }); } document.getElementById('btnCancelProductCategoryEdit').onclick = function () { resetProductCategoryForm(); }; 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(); loadProductCategories(); }) .catch(function () { msgEl.textContent = '请求失败'; msgEl.className = 'msg err'; }); }; function loadVersions() { fetch('/api/admin/versions') .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('versionListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } var items = res.items || []; if (items.length === 0) { tbody.innerHTML = '暂无版本记录'; } else { tbody.innerHTML = items.map(function (v) { var url = (v.file_url || '').replace(/"/g, '"'); return '' + (v.version || '') + '' + url + '' + (v.created_at || '') + '下载'; }).join(''); } }) .catch(function () { document.getElementById('versionListBody').innerHTML = '请求失败'; }); } 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('version', version); formData.append('file', file); 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 = ''; loadVersions(); } else { msgEl.textContent = res.error || '上传失败'; msgEl.classList.add('err'); } }) .catch(function () { msgEl.textContent = '请求失败'; msgEl.classList.add('err'); }); }; // ========== 栏目权限配置 ========== function loadColumns() { fetch('/api/admin/columns') .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('columnListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } allColumnsList = res.items || []; if (allColumnsList.length === 0) { tbody.innerHTML = '暂无菜单,请在上方新增'; } else { tbody.innerHTML = allColumnsList.map(function (c) { return '' + c.id + '' + (c.name || '') + '' + (c.column_key || '') + '' + (c.created_at || '') + '' + ' ' + ''; }).join(''); } bindColumnActions(); }) .catch(function () { document.getElementById('columnListBody').innerHTML = '请求失败'; }); } function bindColumnActions() { document.querySelectorAll('[data-column-edit]').forEach(function (btn) { btn.onclick = function () { document.getElementById('editColumnId').value = btn.dataset.columnEdit || ''; document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/"/g, '"'); document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/"/g, '"'); document.getElementById('msgEditColumn').textContent = ''; document.getElementById('editColumnModal').classList.add('show'); }; }); document.querySelectorAll('[data-column-delete]').forEach(function (btn) { btn.onclick = function () { if (!confirm('确定删除菜单“' + (btn.dataset.name || '').replace(/"/g, '"') + '”吗?')) return; fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); } else { alert(res.error || '删除失败'); } }); }; }); } document.getElementById('btnAddColumn').onclick = function () { var name = (document.getElementById('columnName').value || '').trim(); var key = (document.getElementById('columnKey').value || '').trim(); var msgEl = document.getElementById('msgColumn'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } if (!key) { 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: key }) }) .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('columnKey').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 key = (document.getElementById('editColumnKey').value || '').trim(); var msgEl = document.getElementById('msgEditColumn'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } if (!key) { 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: key }) }) .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'); } }); }; document.getElementById('btnCloseEditColumn').onclick = function () { document.getElementById('editColumnModal').classList.remove('show'); }; function loadColumns() { fetch('/api/admin/columns') .then(function (r) { return r.json(); }) .then(function (res) { var tbody = document.getElementById('columnListBody'); if (!res.success) { tbody.innerHTML = '加载失败: ' + (res.error || '') + ''; return; } allColumnsList = res.items || []; if (allColumnsList.length === 0) { tbody.innerHTML = '暂无菜单,请先在上方新增'; } else { tbody.innerHTML = allColumnsList.map(function (c, index) { var moveButtons = ' ' + ' '; return '' + c.id + '' + (c.name || '') + '' + (c.column_key || '') + '' + ((c.menu_type || '') === 'admin' ? '后台(admin)' : '软件(app)') + '' + (c.sort_order != null ? c.sort_order : 0) + '' + (c.route_path || '') + '' + (c.created_at || '') + '' + moveButtons + ' ' + ''; }).join(''); } bindColumnActions(); }) .catch(function () { document.getElementById('columnListBody').innerHTML = '请求失败'; }); } function bindColumnActions() { document.querySelectorAll('[data-column-move-up]').forEach(function (btn) { btn.onclick = function () { if (btn.disabled) return; moveColumnItem(btn.dataset.columnMoveUp, -1, btn); }; }); document.querySelectorAll('[data-column-move-down]').forEach(function (btn) { btn.onclick = function () { if (btn.disabled) return; moveColumnItem(btn.dataset.columnMoveDown, 1, btn); }; }); document.querySelectorAll('[data-column-edit]').forEach(function (btn) { btn.onclick = function () { document.getElementById('editColumnId').value = btn.dataset.columnEdit || ''; document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/"/g, '"'); document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/"/g, '"'); document.getElementById('editColumnRoutePath').value = (btn.dataset.route || '').replace(/"/g, '"'); document.getElementById('editColumnMenuType').value = ((btn.dataset.menuType || 'app').replace(/"/g, '"') || 'app'); document.getElementById('editColumnSortOrder').value = ((btn.dataset.sortOrder || '0').replace(/"/g, '"') || '0'); document.getElementById('msgEditColumn').textContent = ''; document.getElementById('editColumnModal').classList.add('show'); }; }); document.querySelectorAll('[data-column-delete]').forEach(function (btn) { btn.onclick = function () { if (!confirm('确定删除菜单“' + (btn.dataset.name || '').replace(/"/g, '"') + '”吗?')) return; fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' }) .then(function (r) { return r.json(); }) .then(function (res) { if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); } else { alert(res.error || '删除失败'); } }); }; }); } function moveColumnItem(columnId, delta, triggerBtn) { var currentIndex = -1; for (var i = 0; i < allColumnsList.length; i++) { if (String(allColumnsList[i].id) === String(columnId)) { currentIndex = i; break; } } if (currentIndex < 0) return; var targetIndex = currentIndex + delta; if (targetIndex < 0 || targetIndex >= allColumnsList.length) return; var currentItem = allColumnsList[currentIndex]; var targetItem = allColumnsList[targetIndex]; if (!currentItem || !targetItem) return; if (triggerBtn) triggerBtn.disabled = true; fetch('/api/admin/column/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ column_id: currentItem.id, target_id: targetItem.id }) }).then(function (r) { return r.json(); }) .then(function (res) { if (!res || !res.success) { alert((res && res.error) || '排序保存失败'); return; } loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); }).catch(function () { alert('排序保存失败'); }).finally(function () { if (triggerBtn) triggerBtn.disabled = false; }); } document.getElementById('btnAddColumn').onclick = function () { var name = (document.getElementById('columnName').value || '').trim(); var key = (document.getElementById('columnKey').value || '').trim(); var routePath = (document.getElementById('columnRoutePath').value || '').trim(); var sortOrderValue = (document.getElementById('columnSortOrder').value || '').trim(); var menuType = (document.getElementById('columnMenuType').value || 'admin').trim() || 'admin'; var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10); var msgEl = document.getElementById('msgColumn'); msgEl.textContent = ''; msgEl.className = 'msg'; if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; } if (!key) { msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; } if (!routePath) { msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return; } if (sortOrderValue !== '' && isNaN(sortOrder)) { 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: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder }) }) .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('columnKey').value = ''; document.getElementById('columnRoutePath').value = ''; document.getElementById('columnSortOrder').value = ''; document.getElementById('columnMenuType').value = 'admin'; 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 key = (document.getElementById('editColumnKey').value || '').trim(); var routePath = (document.getElementById('editColumnRoutePath').value || '').trim(); var sortOrderValue = (document.getElementById('editColumnSortOrder').value || '').trim(); var menuType = (document.getElementById('editColumnMenuType').value || 'admin').trim() || 'admin'; 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 (!key) { msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return; } if (!routePath) { msgEl.textContent = '请填写菜单路由'; msgEl.classList.add('err'); return; } if (sortOrderValue !== '' && isNaN(sortOrder)) { 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: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder }) }) .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'); } }); }; // ========== 分页 ========== function renderPagination(elId, total, page, pageSize, onPage) { var el = document.getElementById(elId); if (!el) return; var totalPages = Math.max(1, Math.ceil(total / pageSize)); el.innerHTML = '共' + total + ' 条' + '' + '第' + page + ' / ' + totalPages + ' 页' + ''; el.querySelectorAll('[data-p]').forEach(function (b) { if (!b.disabled) b.onclick = function () { onPage(parseInt(b.dataset.p, 10)); }; }); } // 初始化 function loadAdminCurrentUser() { 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(); }) .catch(function () { 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(); }) .catch(function () { document.getElementById('adminCurrentUsername').textContent = '当前用户'; document.getElementById('adminCurrentUserRole').style.display = 'none'; updateShopManageGroupButtonsAccess(); }); }); } 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(); loadUserOptions(); loadColumnsForPermission(); loadShopManageGroups(); loadAdminMenus(); }) ();