Files
crawler-plugin/backend/static/admin.js
2026-07-20 20:12:38 +08:00

4860 lines
272 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
// 全局请求动画:拦截本页所有 fetch 和 XMLHttpRequest 请求。
(function installRequestLoadingInterceptor() {
if (window.__adminRequestLoadingInstalled) return;
window.__adminRequestLoadingInstalled = true;
var activeRequests = 0;
var showTimer = null;
var hideTimer = null;
var showDelayMs = 180;
var loadingEl = document.getElementById('requestLoading');
var loadingBarEl = document.getElementById('requestLoadingBar');
function showRequestLoading() {
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
if (!loadingEl || !loadingBarEl) return;
loadingBarEl.classList.remove('finishing');
loadingBarEl.classList.add('show');
loadingEl.classList.add('show');
}
function hideRequestLoading() {
if (showTimer) {
clearTimeout(showTimer);
showTimer = null;
}
if (!loadingEl || !loadingBarEl) return;
loadingBarEl.classList.remove('show');
loadingBarEl.classList.add('finishing');
loadingEl.classList.remove('show');
hideTimer = setTimeout(function () {
loadingBarEl.classList.remove('finishing');
}, 220);
}
function beginRequest() {
activeRequests += 1;
if (activeRequests === 1) {
showTimer = setTimeout(showRequestLoading, showDelayMs);
}
}
function endRequest() {
activeRequests = Math.max(0, activeRequests - 1);
if (activeRequests === 0) {
hideRequestLoading();
}
}
if (window.fetch) {
var nativeFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
var options = init || {};
var skipLoading = !!options.__skipLoading;
if (skipLoading) {
options = Object.assign({}, options);
delete options.__skipLoading;
} else {
beginRequest();
}
try {
return nativeFetch(input, options).finally(function () {
if (!skipLoading) endRequest();
});
} catch (err) {
if (!skipLoading) endRequest();
throw err;
}
};
}
if (window.XMLHttpRequest) {
var nativeOpen = XMLHttpRequest.prototype.open;
var nativeSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function () {
this.__adminSkipLoading = false;
return nativeOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
if (!this.__adminSkipLoading) {
beginRequest();
this.addEventListener('loadend', endRequest, { once: true });
}
try {
return nativeSend.apply(this, arguments);
} catch (err) {
if (!this.__adminSkipLoading) endRequest();
throw err;
}
};
}
})();
// 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',
'invalid-asin-data': 'panel-invalid-asin-data',
'shop-keys': 'panel-shop-keys',
'shop-manage': 'panel-shop-manage',
'skip-price-asin': 'panel-skip-price-asin',
'query-asin': 'panel-query-asin',
'product-categories': 'panel-product-categories',
'image-video-tasks': 'panel-image-video-tasks',
'history': 'panel-history',
'version': 'panel-version',
'digital-human-version': 'panel-digital-human-version'
};
function runTabLoader(tabName) {
if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); }
else if (tabName === 'columns') loadColumns();
else if (tabName === 'dedupe-total-data') {
loadDedupeTotalData(1);
loadDedupeGroupSummary();
}
else if (tabName === 'invalid-asin-data') loadInvalidAsinData(1);
else if (tabName === 'shop-keys') loadShopKeys(1);
else if (tabName === 'shop-manage') loadShopManage(1);
else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1);
else if (tabName === 'query-asin') loadQueryAsin(1);
else if (tabName === 'product-categories') loadProductCategories();
else if (tabName === 'image-video-tasks') loadImageVideoTasks(1);
else if (tabName === 'history') loadHistory(1);
else if (tabName === 'version') loadSoftwareVersions();
else if (tabName === 'digital-human-version') loadDigitalHumanVersions();
}
function hideAllAdminPanels() {
document.querySelectorAll('.tab-panel').forEach(function (panel) {
panel.classList.remove('active');
panel.style.display = 'none';
});
}
function getActiveAdminTabName() {
return activeAdminTabName;
}
function activateAdminTab(tabName) {
var panelId = ADMIN_PANEL_MAP[tabName];
var panel = panelId ? document.getElementById(panelId) : null;
if (!panel) return;
activeAdminTabName = tabName;
document.querySelectorAll('#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 = '<div class="tab active" style="cursor:default;">暂无可用菜单</div>';
hideAllAdminPanels();
return;
}
if (adminTabsEl) {
adminTabsEl.innerHTML = knownItems.map(function (item) {
return '<div class="tab" data-tab="' + item.route_path + '">' + (item.name || item.route_path) + '</div>';
}).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 = '<div class="tab active" style="cursor:default;">菜单加载失败</div>';
hideAllAdminPanels();
return;
}
renderAdminTabs(res.items || []);
})
.catch(function () {
if (adminTabsEl) adminTabsEl.innerHTML = '<div class="tab active" style="cursor:default;">菜单加载失败</div>';
hideAllAdminPanels();
});
}
hideAllAdminPanels();
// ========== 用户管理 ==========
var userPage = 1, userPageSize = 15;
var currentUserId = null;
var currentUserRole = 'admin';
var currentUserUsername = '';
var adminsList = [];
function roleLabel(role) {
if (role === 'super_admin') return '超级管理员';
if (role === 'admin') return '管理员';
return '普通账号';
}
function buildUserListQuery(page) {
var q = 'page=' + (page || 1) + '&page_size=' + userPageSize;
var kw = (document.getElementById('searchUsername').value || '').trim();
if (kw) q += '&username=' + encodeURIComponent(kw);
var cby = document.getElementById('filterCreatedBy').value;
if (cby) q += '&created_by_id=' + encodeURIComponent(cby);
return q;
}
function loadUsers(page) {
userPage = page || 1;
fetch('/api/admin/users?' + buildUserListQuery(userPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('userListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
currentUserId = res.current_user_id || null;
currentUserRole = res.current_user_role || 'admin';
currentUserUsername = (res.current_user_username || '').trim();
adminsList = res.admins || [];
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无用户</td></tr>';
} else {
tbody.innerHTML = items.map(function (u) {
return '<tr><td>' + u.id + '</td><td>' + (u.username || '') + '</td><td>' +
roleLabel(u.role || 'normal') + '</td><td>' + (u.creator_username || '-') + '</td><td>' + (u.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-edit="' + u.id + '" data-user="' + (JSON.stringify(u).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-delete="' + u.id + '" data-name="' + (u.username || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers);
bindUserActions();
updateCreateFormByRole();
updateUserFilterByRole();
})
.catch(function () {
document.getElementById('userListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
function updateUserFilterByRole() {
var grp = document.getElementById('filterCreatedByGroup');
var sel = document.getElementById('filterCreatedBy');
if (currentUserRole === 'super_admin') {
grp.style.display = 'block';
var cur = sel.value;
sel.innerHTML = '<option value="">全部</option>';
adminsList.forEach(function (a) {
var opt = document.createElement('option');
opt.value = a.id;
opt.textContent = a.username;
sel.appendChild(opt);
});
sel.value = cur || '';
} else {
grp.style.display = 'none';
}
}
function updateDedupeTotalDataAccess() {
var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]');
var panel = document.getElementById('panel-dedupe-total-data');
var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === '');
if (tab) tab.style.display = canUse ? '' : 'none';
if (panel) panel.style.display = canUse ? '' : 'none';
if (!canUse && tab && tab.classList.contains('active')) {
tab.classList.remove('active');
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() {
var ownedKeys = {};
var ownedRoutes = {};
var ownPermissionRequest = (currentUserRole === 'admin' && currentUserId)
? fetch('/api/admin/user/' + currentUserId + '/column-permissions')
.then(function (r) { return r.json(); })
.then(function (res) {
(res.items || []).forEach(function (item) {
if (item.column_key) ownedKeys[String(item.column_key).trim()] = true;
if (item.route_path) ownedRoutes[String(item.route_path).trim()] = true;
});
})
.catch(function () { })
: Promise.resolve();
ownPermissionRequest.then(function () {
return fetch('/api/admin/columns');
}).then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) return;
allColumnsList = (res.items || []).filter(function (item) {
if (item.column_key === 'admin_image_video_task_data') return false;
if (currentUserRole !== 'admin') return true;
return !!ownedKeys[String(item.column_key || '').trim()] ||
!!ownedRoutes[String(item.route_path || '').trim()];
});
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 = '<option value="">请选择管理员</option>';
adminsList.forEach(function (a) {
var opt = document.createElement('option');
opt.value = a.id;
opt.textContent = a.username;
selCreatedBy.appendChild(opt);
});
} else {
if (optAdmin) optAdmin.style.display = 'none';
roleSel.value = 'normal';
formCreatedBy.style.display = 'none';
}
}
function bindUserActions() {
document.querySelectorAll('[data-edit]').forEach(function (btn) {
btn.onclick = function () {
var raw = (btn.getAttribute('data-user') || '{}').replace(/&quot;/g, '"');
var u;
try { u = JSON.parse(raw); } catch (e) { u = {}; }
document.getElementById('editUserId').value = u.id || '';
document.getElementById('editUsername').value = u.username || '';
document.getElementById('editPassword').value = '';
var editRole = document.getElementById('editRole');
var editFormGroupRole = document.getElementById('editFormGroupRole');
var editFormGroupCreator = document.getElementById('editFormGroupCreator');
var editCreatorName = document.getElementById('editCreatorName');
editFormGroupRole.style.display = (currentUserRole === 'super_admin' && u.role !== 'super_admin') ? 'block' : 'none';
editFormGroupCreator.style.display = (u.role === 'normal' && u.creator_username) ? 'block' : 'none';
editCreatorName.value = u.creator_username || '';
if (u.role !== 'super_admin') { editRole.value = u.role || 'normal'; }
document.getElementById('msgEdit').textContent = '';
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 imageVideoTaskPage = 1, imageVideoTaskPageSize = 20;
var imageVideoCards = [];
var selectedImageVideoKeys = new Set();
var imageVideoDownloadInProgress = false;
var imageVideoPermissionUsers = [];
var imageVideoPermissionInitialUserIds = new Set();
var selectedImageVideoPermissionUserIds = new Set();
var imageVideoPermissionView = 'granted';
function updateImageVideoPermissionAccess() {
var button = document.getElementById('btnOpenImageVideoPermissions');
if (button) button.style.display = currentUserRole === 'super_admin' ? 'inline-flex' : 'none';
}
function imageVideoPermissionUsersForView() {
if (imageVideoPermissionView === 'granted') {
return imageVideoPermissionUsers.filter(function (user) {
return imageVideoPermissionInitialUserIds.has(Number(user.id));
});
}
return imageVideoPermissionUsers;
}
function syncImageVideoPermissionTabs() {
document.getElementById('imageVideoPermissionGrantedCount').textContent = '(' + imageVideoPermissionInitialUserIds.size + ')';
document.getElementById('imageVideoPermissionAllCount').textContent = '(' + imageVideoPermissionUsers.length + ')';
document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) {
var active = tab.dataset.imageVideoPermissionView === imageVideoPermissionView;
tab.classList.toggle('active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
}
function filteredImageVideoPermissionUsers() {
var users = imageVideoPermissionUsersForView();
var keyword = (document.getElementById('imageVideoPermissionSearch').value || '').trim().toLowerCase();
if (!keyword) return users;
return users.filter(function (user) {
return String(user.username || '').toLowerCase().indexOf(keyword) >= 0;
});
}
function syncImageVideoPermissionSelectAll() {
var visibleUsers = filteredImageVideoPermissionUsers();
var selectedCount = visibleUsers.filter(function (user) {
return selectedImageVideoPermissionUserIds.has(Number(user.id));
}).length;
var selectAll = document.getElementById('imageVideoPermissionSelectAll');
selectAll.checked = visibleUsers.length > 0 && selectedCount === visibleUsers.length;
selectAll.indeterminate = selectedCount > 0 && selectedCount < visibleUsers.length;
selectAll.disabled = visibleUsers.length === 0;
}
function renderImageVideoPermissionUsers() {
var list = document.getElementById('imageVideoPermissionList');
var summary = document.getElementById('imageVideoPermissionSummary');
var visibleUsers = filteredImageVideoPermissionUsers();
syncImageVideoPermissionTabs();
var pendingCount = imageVideoPermissionUsers.filter(function (user) {
return imageVideoPermissionInitialUserIds.has(Number(user.id)) !== selectedImageVideoPermissionUserIds.has(Number(user.id));
}).length;
summary.textContent = imageVideoPermissionView === 'granted'
? '当前显示已保存分配用户,共 ' + visibleUsers.length + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '')
: '当前显示全部用户,已保存分配 ' + imageVideoPermissionInitialUserIds.size + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '');
list.innerHTML = visibleUsers.length ? visibleUsers.map(function (user) {
var userId = Number(user.id);
var persistedGranted = imageVideoPermissionInitialUserIds.has(userId);
var pendingGranted = selectedImageVideoPermissionUserIds.has(userId);
var pendingChanged = persistedGranted !== pendingGranted;
var actionText = pendingChanged
? (pendingGranted ? '待保存分配' : '待保存取消')
: (persistedGranted ? '取消分配' : '分配');
return '<div class="image-video-permission-row">' +
'<input type="checkbox" aria-label="' + escapeHtml(user.username || '-') + (persistedGranted ? ' 已保存分配' : ' 未分配') + '" data-image-video-permission-user="' + userId + '"' +
(pendingGranted ? ' checked' : '') + '>' +
'<span class="image-video-permission-user">' +
'<span class="image-video-permission-name" title="' + escapeHtml(user.username || '') + '">' + escapeHtml(user.username || '-') + '</span>' +
'<span class="image-video-permission-role">' + escapeHtml(roleLabel(user.role)) + '</span>' +
'</span>' +
'<button class="image-video-permission-state' + (pendingChanged ? ' pending' : (persistedGranted ? ' granted' : '')) + '" type="button" data-image-video-permission-toggle="' + userId + '">' +
actionText +
'</button>' +
'</div>';
}).join('') : '<div class="image-video-permission-empty">暂无匹配用户</div>';
syncImageVideoPermissionSelectAll();
}
function setImageVideoPermissionLoading(loading) {
document.getElementById('btnSaveImageVideoPermissions').disabled = loading;
document.getElementById('imageVideoPermissionSearch').disabled = loading;
if (loading) document.getElementById('imageVideoPermissionSelectAll').disabled = true;
}
function openImageVideoPermissions() {
if (currentUserRole !== 'super_admin') return;
var modal = document.getElementById('imageVideoPermissionModal');
var message = document.getElementById('imageVideoPermissionMessage');
modal.classList.add('show');
message.textContent = '';
message.className = 'msg';
imageVideoPermissionView = 'granted';
syncImageVideoPermissionTabs();
document.getElementById('imageVideoPermissionSearch').value = '';
document.getElementById('imageVideoPermissionList').innerHTML = '<div class="image-video-permission-empty">加载中...</div>';
setImageVideoPermissionLoading(true);
fetch('/api/admin/image-video-task-permissions')
.then(function (response) { return response.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '权限加载失败');
imageVideoPermissionUsers = res.items || [];
imageVideoPermissionInitialUserIds = new Set(imageVideoPermissionUsers.filter(function (user) {
return !!user.granted;
}).map(function (user) { return Number(user.id); }));
selectedImageVideoPermissionUserIds = new Set(imageVideoPermissionInitialUserIds);
renderImageVideoPermissionUsers();
})
.catch(function (error) {
imageVideoPermissionUsers = [];
selectedImageVideoPermissionUserIds.clear();
document.getElementById('imageVideoPermissionList').innerHTML = '<div class="image-video-permission-empty">' + escapeHtml(error.message || '权限加载失败') + '</div>';
})
.finally(function () { setImageVideoPermissionLoading(false); });
}
function closeImageVideoPermissions() {
document.getElementById('imageVideoPermissionModal').classList.remove('show');
}
function saveImageVideoPermissions() {
var message = document.getElementById('imageVideoPermissionMessage');
message.textContent = '保存中...';
message.className = 'msg';
setImageVideoPermissionLoading(true);
fetch('/api/admin/image-video-task-permissions', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_ids: Array.from(selectedImageVideoPermissionUserIds).sort(function (a, b) { return a - b; }) })
})
.then(function (response) { return response.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '权限保存失败');
imageVideoPermissionUsers.forEach(function (user) {
user.granted = selectedImageVideoPermissionUserIds.has(Number(user.id));
});
imageVideoPermissionInitialUserIds = new Set(selectedImageVideoPermissionUserIds);
renderImageVideoPermissionUsers();
message.textContent = res.msg || '保存成功';
message.className = 'msg ok';
})
.catch(function (error) {
message.textContent = error.message || '权限保存失败';
message.className = 'msg err';
})
.finally(function () {
setImageVideoPermissionLoading(false);
syncImageVideoPermissionSelectAll();
});
}
function buildImageVideoTaskQuery(page) {
var params = new URLSearchParams();
params.set('page', String(page || 1));
params.set('page_size', String(imageVideoTaskPageSize));
var values = {
username: document.getElementById('imageVideoFilterUsername').value.trim(),
submitted_from: document.getElementById('imageVideoFilterFrom').value,
submitted_to: document.getElementById('imageVideoFilterTo').value
};
Object.keys(values).forEach(function (key) { if (values[key]) params.set(key, values[key]); });
return params.toString();
}
function renderImageVideoStatus(value) {
var status = String(value || '-').toUpperCase();
return '<span class="image-video-status ' + escapeHtml(status) + '">' + escapeHtml(status) + '</span>';
}
function safeAdminUrl(value) {
var url = String(value || '').trim();
return /^https?:\/\//i.test(url) ? url : '';
}
function imageVideoDownloadIcon() {
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"></path></svg>';
}
function imageVideoUnavailableHtml(message) {
return '<div class="image-video-unavailable">' +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 10 4.5-2.5v9L15 14"></path><rect x="3" y="6" width="12" height="12" rx="2"></rect><path d="m4 4 16 16"></path></svg>' +
'<strong>' + escapeHtml(message || '暂无可用视频') + '</strong>' +
'</div>';
}
function imageVideoCardKey(taskId, videoIndex) {
return String(taskId) + ':' + String(videoIndex);
}
function flattenImageVideoTasks(items) {
var cards = [];
(items || []).forEach(function (task) {
var videos = Array.isArray(task.videos) ? task.videos : [];
if (!videos.length) {
cards.push({ key: imageVideoCardKey(task.task_id, 'empty'), task: task, video: null, videoIndex: 0 });
return;
}
videos.forEach(function (video, index) {
cards.push({
key: imageVideoCardKey(task.task_id, index),
task: task,
video: {
sourceUrl: safeAdminUrl(video.source_url),
archivedUrl: safeAdminUrl(video.archived_url),
displayUrl: safeAdminUrl(video.display_url),
objectKey: String(video.object_key || ''),
archiveStatus: String(video.archive_status || ''),
archiveError: String(video.archive_error || '')
},
videoIndex: index
});
});
});
return cards;
}
function renderImageVideoCard(card) {
var task = card.task;
var video = card.video;
var hasVideo = !!(video && video.displayUrl);
var debugUrl = safeAdminUrl(task.debug_url);
var generatedAt = task.completed_at || task.submitted_at || '-';
var mediaHtml = hasVideo
? '<input class="image-video-card-check" type="checkbox" data-image-video-select="' + escapeHtml(card.key) + '" aria-label="选择任务 ' + escapeHtml(task.task_id) + ' 的视频 ' + (card.videoIndex + 1) + '">' +
'<video src="' + escapeHtml(video.displayUrl) + '" controls playsinline preload="metadata"></video>'
: imageVideoUnavailableHtml(task.status === 'FAILED' ? '任务失败,未生成视频' : '视频生成中或暂无结果');
return '<article class="image-video-card" data-image-video-card="' + escapeHtml(card.key) + '">' +
'<div class="image-video-media">' + mediaHtml + '</div>' +
'<div class="image-video-card-body">' +
'<div class="image-video-card-head">' +
'<div class="image-video-card-title" title="任务 ' + escapeHtml(task.task_id) + '">任务 #' + escapeHtml(task.task_id) + (video ? ' · 视频 ' + (card.videoIndex + 1) : '') + '</div>' +
renderImageVideoStatus(task.status) +
'</div>' +
'<div class="image-video-card-info">' +
'<div class="image-video-info-row"><label>用户名</label><span>' + escapeHtml(task.username || '-') + '</span></div>' +
'<div class="image-video-info-row"><label>所属分组</label><span>' + escapeHtml(task.group_name || '-') + '</span></div>' +
'<div class="image-video-info-row"><label>任务模式</label><span>' + escapeHtml(task.mode || '-') + '</span></div>' +
'<div class="image-video-info-row"><label>生成时间</label><span>' + escapeHtml(generatedAt) + '</span></div>' +
'<div class="image-video-info-row"><label>调试链接</label>' + (debugUrl
? '<button class="image-video-copy-link" type="button" data-image-video-copy-debug="' + escapeHtml(card.key) + '">复制链接</button>'
: '<span>-</span>') + '</div>' +
'</div>' +
'<div class="image-video-card-actions">' +
'<button class="image-video-card-action" type="button" data-image-video-download="' + escapeHtml(card.key) + '"' + (hasVideo ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载</button>' +
'</div>' +
'</div>' +
'</article>';
}
function syncImageVideoSelectionUi() {
var selectable = imageVideoCards.filter(function (card) { return !!(card.video && card.video.displayUrl); });
document.querySelectorAll('[data-image-video-card]').forEach(function (cardEl) {
var selected = selectedImageVideoKeys.has(cardEl.dataset.imageVideoCard);
cardEl.classList.toggle('selected', selected);
var checkbox = cardEl.querySelector('[data-image-video-select]');
if (checkbox) checkbox.checked = selected;
});
var selectedCount = selectable.filter(function (card) { return selectedImageVideoKeys.has(card.key); }).length;
var selectAll = document.getElementById('imageVideoSelectAll');
selectAll.checked = selectable.length > 0 && selectedCount === selectable.length;
selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length;
selectAll.disabled = imageVideoDownloadInProgress || selectable.length === 0;
var batchButton = document.getElementById('btnBatchDownloadImageVideos');
batchButton.disabled = imageVideoDownloadInProgress || selectedCount === 0;
batchButton.innerHTML = imageVideoDownloadIcon() + (imageVideoDownloadInProgress ? '处理中' : '批量下载' + (selectedCount ? ' (' + selectedCount + ')' : ''));
document.querySelectorAll('[data-image-video-download]').forEach(function (button) {
var card = imageVideoCards.find(function (item) { return item.key === button.dataset.imageVideoDownload; });
button.disabled = imageVideoDownloadInProgress || !card || !card.video || !card.video.displayUrl;
});
}
function renderImageVideoCards() {
var grid = document.getElementById('imageVideoTaskGrid');
grid.innerHTML = imageVideoCards.length
? imageVideoCards.map(renderImageVideoCard).join('')
: '<div class="image-video-empty">暂无符合条件的视频任务</div>';
grid.querySelectorAll('video').forEach(function (video) {
video.addEventListener('error', function () {
var media = video.closest('.image-video-media');
if (!media || media.querySelector('.image-video-unavailable')) return;
video.remove();
media.insertAdjacentHTML('beforeend', imageVideoUnavailableHtml('视频加载失败,可尝试下载'));
}, { once: true });
});
syncImageVideoSelectionUi();
}
function resetImageVideoSelection() {
selectedImageVideoKeys.clear();
document.getElementById('imageVideoDownloadProgress').textContent = '';
syncImageVideoSelectionUi();
}
function loadImageVideoTasks(page) {
imageVideoTaskPage = page || 1;
selectedImageVideoKeys.clear();
var grid = document.getElementById('imageVideoTaskGrid');
grid.innerHTML = '<div class="image-video-empty">加载中...</div>';
document.getElementById('imageVideoDownloadProgress').textContent = '';
fetch('/api/admin/image-video-tasks?' + buildImageVideoTaskQuery(imageVideoTaskPage))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
imageVideoCards = [];
grid.innerHTML = '<div class="image-video-empty">加载失败:' + escapeHtml(res.error || '') + '</div>';
document.getElementById('imageVideoTaskTotal').textContent = '';
syncImageVideoSelectionUi();
return;
}
var items = res.items || [];
imageVideoCards = flattenImageVideoTasks(items);
var videoCount = imageVideoCards.filter(function (card) { return !!card.video; }).length;
document.getElementById('imageVideoTaskTotal').textContent = '共 ' + (res.total || 0) + ' 个任务 · 本页 ' + videoCount + ' 个视频';
renderImageVideoCards();
renderPagination('imageVideoTaskPagination', res.total, res.page, res.page_size, loadImageVideoTasks);
})
.catch(function () {
imageVideoCards = [];
grid.innerHTML = '<div class="image-video-empty">请求失败,请稍后重试</div>';
document.getElementById('imageVideoTaskTotal').textContent = '';
syncImageVideoSelectionUi();
});
}
function imageVideoFilename(card) {
var url = card.video.displayUrl || '';
var path = url.split(/[?#]/)[0];
var match = path.match(/\.([a-z0-9]{2,5})$/i);
return 'task-' + card.task.task_id + '-video-' + (card.videoIndex + 1) + '.' + (match ? match[1].toLowerCase() : 'mp4');
}
function triggerImageVideoLink(url, filename, newWindow) {
var link = document.createElement('a');
link.href = url;
link.download = filename;
if (newWindow) {
link.target = '_blank';
link.rel = 'noreferrer';
}
document.body.appendChild(link);
link.click();
link.remove();
}
function downloadImageVideo(card) {
var url = card && card.video ? card.video.displayUrl : '';
if (!url) return Promise.resolve({ fallback: false, skipped: true });
var filename = imageVideoFilename(card);
return fetch(url, { __skipLoading: true })
.then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.blob();
})
.then(function (blob) {
var objectUrl = URL.createObjectURL(blob);
triggerImageVideoLink(objectUrl, filename, false);
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
return { fallback: false, skipped: false };
})
.catch(function () {
triggerImageVideoLink(url, filename, true);
return { fallback: true, skipped: false };
});
}
function runImageVideoDownloads(cards) {
if (imageVideoDownloadInProgress || !cards.length) return;
imageVideoDownloadInProgress = true;
syncImageVideoSelectionUi();
var progress = document.getElementById('imageVideoDownloadProgress');
progress.textContent = '正在下载';
downloadImageVideo(cards[0]).then(function (result) {
progress.textContent = result.fallback ? '已在新窗口打开视频' : '下载已开始';
}).finally(function () {
imageVideoDownloadInProgress = false;
syncImageVideoSelectionUi();
});
}
function imageVideoZipFilename(response) {
var disposition = response.headers.get('Content-Disposition') || '';
var encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (encoded) {
try { return decodeURIComponent(encoded[1]); } catch (error) { }
}
var plain = disposition.match(/filename="?([^";]+)"?/i);
return plain ? plain[1] : 'video-tasks.zip';
}
function downloadImageVideoZip(cards) {
if (imageVideoDownloadInProgress || !cards.length) return;
imageVideoDownloadInProgress = true;
syncImageVideoSelectionUi();
var progress = document.getElementById('imageVideoDownloadProgress');
progress.textContent = '正在打包 ' + cards.length + ' 个视频...';
fetch('/api/admin/image-video-tasks/download-zip', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: cards.map(function (card) {
return { task_id: Number(card.task.task_id), video_index: card.videoIndex };
})
}),
__skipLoading: true
}).then(function (response) {
if (!response.ok) {
return response.json().catch(function () { return {}; }).then(function (data) {
throw new Error(data.error || '压缩包生成失败');
});
}
var filename = imageVideoZipFilename(response);
var errorCount = Number(response.headers.get('X-Archive-Error-Count') || 0);
return response.blob().then(function (blob) {
return { blob: blob, filename: filename, errorCount: errorCount };
});
}).then(function (result) {
var objectUrl = URL.createObjectURL(result.blob);
triggerImageVideoLink(objectUrl, result.filename, false);
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
progress.textContent = result.errorCount
? '压缩包已下载,' + result.errorCount + ' 个视频失败,详见包内错误清单'
: '压缩包下载已开始';
}).catch(function (error) {
progress.textContent = error.message || '压缩包下载失败';
}).finally(function () {
imageVideoDownloadInProgress = false;
syncImageVideoSelectionUi();
});
}
function copyImageVideoDebugUrl(card, button) {
var url = card ? safeAdminUrl(card.task.debug_url) : '';
if (!url) return;
var copyPromise;
if (navigator.clipboard && navigator.clipboard.writeText) {
copyPromise = navigator.clipboard.writeText(url);
} else {
copyPromise = new Promise(function (resolve, reject) {
var textarea = document.createElement('textarea');
textarea.value = url;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
var copied = document.execCommand('copy');
textarea.remove();
if (copied) resolve();
else reject(new Error('copy failed'));
});
}
copyPromise.then(function () {
button.textContent = '已复制';
button.classList.add('copied');
setTimeout(function () {
button.textContent = '复制链接';
button.classList.remove('copied');
}, 1600);
}).catch(function () {
button.textContent = '复制失败';
setTimeout(function () { button.textContent = '复制链接'; }, 1600);
});
}
document.getElementById('btnFilterImageVideoTasks').onclick = function () { loadImageVideoTasks(1); };
document.getElementById('btnOpenImageVideoPermissions').onclick = openImageVideoPermissions;
document.getElementById('btnCloseImageVideoPermissions').onclick = closeImageVideoPermissions;
document.getElementById('btnCancelImageVideoPermissions').onclick = closeImageVideoPermissions;
document.getElementById('btnSaveImageVideoPermissions').onclick = saveImageVideoPermissions;
document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) {
tab.onclick = function () {
imageVideoPermissionView = tab.dataset.imageVideoPermissionView || 'granted';
document.getElementById('imageVideoPermissionSearch').value = '';
renderImageVideoPermissionUsers();
};
});
document.getElementById('imageVideoPermissionSearch').oninput = renderImageVideoPermissionUsers;
document.getElementById('imageVideoPermissionSelectAll').onchange = function (event) {
filteredImageVideoPermissionUsers().forEach(function (user) {
var userId = Number(user.id);
if (event.target.checked) selectedImageVideoPermissionUserIds.add(userId);
else selectedImageVideoPermissionUserIds.delete(userId);
});
renderImageVideoPermissionUsers();
};
document.getElementById('imageVideoPermissionList').onclick = function (event) {
var toggle = event.target.closest('[data-image-video-permission-toggle]');
if (!toggle) return;
var userId = Number(toggle.dataset.imageVideoPermissionToggle);
if (selectedImageVideoPermissionUserIds.has(userId)) selectedImageVideoPermissionUserIds.delete(userId);
else selectedImageVideoPermissionUserIds.add(userId);
renderImageVideoPermissionUsers();
};
document.getElementById('imageVideoPermissionList').onchange = function (event) {
var checkbox = event.target.closest('[data-image-video-permission-user]');
if (!checkbox) return;
var userId = Number(checkbox.dataset.imageVideoPermissionUser);
if (checkbox.checked) selectedImageVideoPermissionUserIds.add(userId);
else selectedImageVideoPermissionUserIds.delete(userId);
renderImageVideoPermissionUsers();
};
document.getElementById('imageVideoPermissionModal').onclick = function (event) {
if (event.target === event.currentTarget) closeImageVideoPermissions();
};
document.getElementById('btnResetImageVideoTasks').onclick = function () {
['imageVideoFilterUsername', 'imageVideoFilterFrom', 'imageVideoFilterTo']
.forEach(function (id) { document.getElementById(id).value = ''; });
loadImageVideoTasks(1);
};
document.getElementById('imageVideoSelectAll').onchange = function (event) {
imageVideoCards.forEach(function (card) {
if (!card.video || !card.video.displayUrl) return;
if (event.target.checked) selectedImageVideoKeys.add(card.key);
else selectedImageVideoKeys.delete(card.key);
});
syncImageVideoSelectionUi();
};
document.getElementById('btnBatchDownloadImageVideos').onclick = function () {
downloadImageVideoZip(imageVideoCards.filter(function (card) { return selectedImageVideoKeys.has(card.key); }));
};
document.getElementById('imageVideoTaskGrid').onchange = function (event) {
var checkbox = event.target.closest('[data-image-video-select]');
if (!checkbox) return;
if (checkbox.checked) selectedImageVideoKeys.add(checkbox.dataset.imageVideoSelect);
else selectedImageVideoKeys.delete(checkbox.dataset.imageVideoSelect);
syncImageVideoSelectionUi();
};
document.getElementById('imageVideoTaskGrid').onclick = function (event) {
var copyButton = event.target.closest('[data-image-video-copy-debug]');
if (copyButton) {
var copyCard = imageVideoCards.find(function (item) { return item.key === copyButton.dataset.imageVideoCopyDebug; });
if (copyCard) copyImageVideoDebugUrl(copyCard, copyButton);
}
var downloadButton = event.target.closest('[data-image-video-download]');
if (downloadButton) {
var card = imageVideoCards.find(function (item) { return item.key === downloadButton.dataset.imageVideoDownload; });
if (card) runImageVideoDownloads([card]);
}
};
var 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 = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无记录</td></tr>';
} else {
tbody.innerHTML = items.map(function (h) {
var urls = (h.result_urls || []);
var thumbUrls = (h.long_image_url ? [h.long_image_url] : []).concat(urls);
var thumbs = thumbUrls.slice(0, 3).map(function (url) {
return '<img src="' + (url || '').replace(/"/g, '&quot;') + '" class="thumb" alt="">';
}).join('');
return '<tr><td>' + h.id + '</td><td>' + (h.username || '-') + '</td><td>' +
(h.panel_type || '-') + '</td><td>' + (h.created_at || '') + '</td><td>' +
'<div class="thumb-wrap">' + (thumbs || '-') + '</div></td></tr>';
}).join('');
}
renderPagination('historyPagination', res.total, res.page, res.page_size, loadHistory);
})
.catch(function () {
document.getElementById('historyListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
var shopManageAllUsers = [];
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
var dropdownMultiSelects = {};
function initDropdownMultiSelect(selectId, placeholder) {
var select = document.getElementById(selectId);
if (!select || dropdownMultiSelects[selectId]) return;
select.classList.add('multi-select-native');
var wrapper = document.createElement('div');
wrapper.className = 'multi-select-dropdown';
wrapper.setAttribute('data-multi-select-id', selectId);
var trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'multi-select-trigger';
trigger.textContent = placeholder || '请选择';
var panel = document.createElement('div');
panel.className = 'multi-select-panel';
select.parentNode.insertBefore(wrapper, select);
wrapper.appendChild(trigger);
wrapper.appendChild(panel);
wrapper.appendChild(select);
function selectedOptions() {
return Array.from(select.options).filter(function (option) { return option.selected; });
}
function updateSummary() {
var selected = selectedOptions();
if (!selected.length) {
trigger.textContent = placeholder || '请选择';
trigger.title = '';
return;
}
var labels = selected.map(function (option) { return option.textContent || option.value; });
trigger.textContent = labels.join('、');
trigger.title = labels.join('、');
}
function renderOptions() {
panel.innerHTML = Array.from(select.options).map(function (option, index) {
var optionId = selectId + '_multi_' + index;
return '<label class="multi-select-option" for="' + escapeHtml(optionId) + '">' +
'<input type="checkbox" id="' + escapeHtml(optionId) + '" data-multi-option-index="' + index + '"' + (option.selected ? ' checked' : '') + '>' +
'<span>' + escapeHtml(option.textContent || option.value) + '</span>' +
'</label>';
}).join('');
panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) {
checkbox.onchange = function () {
var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))];
if (!option) return;
option.selected = checkbox.checked;
updateSummary();
select.dispatchEvent(new Event('change', { bubbles: true }));
};
});
}
trigger.onclick = function (event) {
event.stopPropagation();
document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) {
if (item !== wrapper) item.classList.remove('open');
});
wrapper.classList.toggle('open');
};
panel.onclick = function (event) {
event.stopPropagation();
};
select.addEventListener('change', function () {
panel.querySelectorAll('[data-multi-option-index]').forEach(function (checkbox) {
var option = select.options[Number(checkbox.getAttribute('data-multi-option-index'))];
checkbox.checked = !!(option && option.selected);
});
updateSummary();
});
renderOptions();
updateSummary();
dropdownMultiSelects[selectId] = {
refresh: function () {
renderOptions();
updateSummary();
}
};
}
function refreshDropdownMultiSelect(selectId) {
if (dropdownMultiSelects[selectId]) {
dropdownMultiSelects[selectId].refresh();
}
}
document.addEventListener('click', function () {
document.querySelectorAll('.multi-select-dropdown.open').forEach(function (item) {
item.classList.remove('open');
});
});
function getEligibleShopManageGroupUsers(leaderUserId) {
var canViewAllMembers = currentUserRole === 'super_admin';
return shopManageAllUsers.filter(function (u) {
if (String(u.id || '') === String(leaderUserId || '')) return false;
if (canViewAllMembers) return true;
return String(u.role || '') === 'normal' &&
String(u.created_by_id || '') === String(leaderUserId || '');
});
}
function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) {
var sel = document.getElementById('shopManageGroupMemberSelect');
var helpEl = document.getElementById('shopManageGroupMemberHelp');
if (!sel) return;
var selectedMap = {};
(selectedUserIds || []).forEach(function (id) {
selectedMap[String(id)] = true;
});
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
var options = eligibleUsers.map(function (u) {
return '<option value="' + escapeHtml(u.id) + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + escapeHtml(u.username || '') + '</option>';
});
if (options.length) {
sel.innerHTML = options.join('');
if (helpEl) {
helpEl.textContent = '可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。';
}
return;
}
sel.innerHTML = '<option value="" disabled>当前组长暂无可添加组员</option>';
if (helpEl) {
helpEl.textContent = currentUserRole === 'normal'
? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。'
: '当前组长名下暂无可添加的普通员工账号。';
}
}
function setShopManageGroupLeader(leaderUserId, leaderUsername, selectedUserIds) {
var leaderIdEl = document.getElementById('shopManageGroupLeaderUserId');
var leaderNameEl = document.getElementById('shopManageGroupLeaderName');
if (leaderIdEl) leaderIdEl.value = leaderUserId ? String(leaderUserId) : '';
if (leaderNameEl) leaderNameEl.value = leaderUsername || '';
refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds || []);
}
function getSelectedShopManageGroupMemberIds() {
var sel = document.getElementById('shopManageGroupMemberSelect');
if (!sel) return [];
return Array.prototype.slice.call(sel.options || [])
.filter(function (opt) { return !!opt.selected; })
.map(function (opt) { return Number(opt.value); })
.filter(function (id) { return !!id; });
}
function loadUserOptions() {
return fetch('/api/admin/users?page=1&page_size=999')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
throw new Error(res.error || '加载用户失败');
}
var items = res.items || [];
shopManageAllUsers = items.map(function (u) {
return {
id: u.id,
username: u.username || '',
role: u.role || 'normal',
created_by_id: u.created_by_id || null
};
});
setShopManageGroupLeader(
document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '',
document.getElementById('shopManageGroupLeaderName') ? document.getElementById('shopManageGroupLeaderName').value : '',
getSelectedShopManageGroupMemberIds()
);
var sel = document.getElementById('filterUser');
var cur = sel.value;
sel.innerHTML = '<option value="">全部用户</option>';
items.forEach(function (u) {
var opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')';
sel.appendChild(opt);
});
sel.value = cur || '';
})
.catch(function () {
shopManageAllUsers = [];
var sel = document.getElementById('filterUser');
if (sel) {
sel.innerHTML = '<option value="">全部用户</option>';
}
refreshShopManageGroupMemberSelect(
document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '',
[]
);
});
}
document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); };
// ========== 数据去重总数据 ==========
var dedupeTotalDataPage = 1, dedupeTotalDataPageSize = 15;
function buildDedupeTotalDataQuery(page) {
var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize;
var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim();
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
if (username) q += '&username=' + encodeURIComponent(username);
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 = '<tr><td colspan="5" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无总数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item) {
return '<tr><td>' + escapeHtml(item.id) + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(item.username || '') + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-dedupe-total-edit="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-dedupe-total-delete="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData);
bindDedupeTotalDataActions();
})
.catch(function () {
document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
function bindDedupeTotalDataActions() {
document.querySelectorAll('[data-dedupe-total-edit]').forEach(function (btn) {
btn.onclick = function () {
document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || '';
document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/&quot;/g, '"');
document.getElementById('msgEditDedupeTotalData').textContent = '';
document.getElementById('msgEditDedupeTotalData').className = 'msg';
document.getElementById('editDedupeTotalDataModal').classList.add('show');
};
});
document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) {
btn.onclick = function () {
var value = (btn.dataset.value || '').replace(/&quot;/g, '"');
if (!confirm('确定删除总数据“' + value + '”吗?')) return;
fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); };
var 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');
};
// ========== 不符合ASIN数据 ==========
var invalidAsinDataPage = 1, invalidAsinDataPageSize = 15;
function buildInvalidAsinDataQuery(page) {
var q = 'page=' + (page || 1) + '&page_size=' + invalidAsinDataPageSize;
var keyword = (document.getElementById('searchInvalidAsinData').value || '').trim();
if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
return q;
}
function loadInvalidAsinData(page) {
invalidAsinDataPage = page || 1;
fetch('/api/admin/invalid-asin-data?' + buildInvalidAsinDataQuery(invalidAsinDataPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('invalidAsinDataListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item) {
var dataValueAttr = (item.data_value || '').replace(/"/g, '&quot;');
var brandAttr = (item.brand || '').replace(/"/g, '&quot;');
return '<tr><td>' + item.id + '</td><td>' + (item.data_value || '') + '</td><td>' + (item.brand || '') + '</td><td>' + (item.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-invalid-asin-edit="' + item.id + '" data-value="' + dataValueAttr + '" data-brand="' + brandAttr + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-invalid-asin-delete="' + item.id + '" data-value="' + dataValueAttr + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('invalidAsinDataPagination', res.total, res.page, res.page_size, loadInvalidAsinData);
bindInvalidAsinDataActions();
})
.catch(function () {
document.getElementById('invalidAsinDataListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
function bindInvalidAsinDataActions() {
document.querySelectorAll('[data-invalid-asin-edit]').forEach(function (btn) {
btn.onclick = function () {
document.getElementById('editInvalidAsinDataId').value = btn.dataset.invalidAsinEdit || '';
document.getElementById('editInvalidAsinDataValue').value = (btn.dataset.value || '').replace(/&quot;/g, '"');
document.getElementById('editInvalidAsinDataBrand').value = (btn.dataset.brand || '').replace(/&quot;/g, '"');
document.getElementById('msgEditInvalidAsinData').textContent = '';
document.getElementById('msgEditInvalidAsinData').className = 'msg';
document.getElementById('editInvalidAsinDataModal').classList.add('show');
};
});
document.querySelectorAll('[data-invalid-asin-delete]').forEach(function (btn) {
btn.onclick = function () {
var value = (btn.dataset.value || '').replace(/&quot;/g, '"');
if (!confirm('确定删除“' + value + '”吗?')) return;
fetch('/api/admin/invalid-asin-data/' + btn.dataset.invalidAsinDelete, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) { loadInvalidAsinData(invalidAsinDataPage); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('btnSearchInvalidAsinData').onclick = function () { loadInvalidAsinData(1); };
document.getElementById('btnAddInvalidAsinData').onclick = function () {
var dataValue = (document.getElementById('invalidAsinDataValue').value || '').trim();
var brand = (document.getElementById('invalidAsinDataBrand').value || '').trim();
var msgEl = document.getElementById('msgInvalidAsinData');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!dataValue) {
msgEl.textContent = '请填写 ASIN';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/invalid-asin-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data_value: dataValue, brand: brand })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
msgEl.textContent = res.msg || '创建成功';
msgEl.classList.add('ok');
document.getElementById('invalidAsinDataValue').value = '';
document.getElementById('invalidAsinDataBrand').value = '';
loadInvalidAsinData(1);
} else {
msgEl.textContent = res.error || '创建失败';
msgEl.classList.add('err');
}
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.classList.add('err');
});
};
document.getElementById('btnSaveInvalidAsinData').onclick = function () {
var itemId = document.getElementById('editInvalidAsinDataId').value;
var dataValue = (document.getElementById('editInvalidAsinDataValue').value || '').trim();
var brand = (document.getElementById('editInvalidAsinDataBrand').value || '').trim();
var msgEl = document.getElementById('msgEditInvalidAsinData');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!dataValue) {
msgEl.textContent = '请填写 ASIN';
msgEl.classList.add('err');
return;
}
fetch('/api/admin/invalid-asin-data/' + itemId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data_value: dataValue, brand: brand })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
document.getElementById('editInvalidAsinDataModal').classList.remove('show');
loadInvalidAsinData(invalidAsinDataPage);
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
});
};
document.getElementById('btnCloseEditInvalidAsinData').onclick = function () {
document.getElementById('editInvalidAsinDataModal').classList.remove('show');
};
// ========== 店铺密钥管理 ==========
var shopKeyPage = 1, shopKeyPageSize = 15;
function buildShopKeyQuery(page) {
return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize;
}
function loadShopKeys(page) {
shopKeyPage = page || 1;
fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage))
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('shopKeyListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无店铺密钥</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.ziniao_account_name || '') + '</td><td>' + (item.ziniao_token || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-key-edit="' + item.id + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-key-delete="' + item.id + '" data-ziniao-account-name="' + (item.ziniao_account_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys);
bindShopKeyActions();
})
.catch(function () {
document.getElementById('shopKeyListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
function bindShopKeyActions() {
document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) {
btn.onclick = function () {
var item = {};
try { item = JSON.parse((btn.dataset.shopKey || '').replace(/&quot;/g, '"')); } catch (e) { item = {}; }
document.getElementById('editShopKeyId').value = item.id || '';
document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || '';
document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || '';
document.getElementById('msgEditShopKey').textContent = '';
document.getElementById('msgEditShopKey').className = 'msg';
document.getElementById('editShopKeyModal').classList.add('show');
};
});
document.querySelectorAll('[data-shop-key-delete]').forEach(function (btn) {
btn.onclick = function () {
var name = (btn.dataset.ziniaoAccountName || '').replace(/&quot;/g, '"');
if (!confirm('确定删除店铺密钥“' + name + '”吗?')) return;
fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) { loadShopKeys(shopKeyPage); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('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 = '<tr><td colspan="9" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="9" class="empty-tip">暂无店铺</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.mall_name || '') + '</td><td>' + (item.account || '') + '</td><td>' + (item.password || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-manage-edit="' + item.id + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-manage-delete="' + item.id + '" data-shop-manage-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
}).join('');
}
renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage);
bindShopManageActions();
})
.catch(function () {
document.getElementById('shopManageListBody').innerHTML = '<tr><td colspan="9" class="empty-tip">请求失败</td></tr>';
});
}
function bindShopManageActions() {
document.querySelectorAll('[data-shop-manage-edit]').forEach(function (btn) {
btn.onclick = function () {
var item = {};
try { item = JSON.parse((btn.dataset.shopManage || '').replace(/&quot;/g, '"')); } catch (e) { item = {}; }
document.getElementById('editShopManageId').value = item.id || '';
document.getElementById('editShopManageShopName').value = item.shop_name || '';
document.getElementById('editShopManageMallName').value = item.mall_name || '';
document.getElementById('editShopManageAccount').value = item.account || '';
document.getElementById('editShopManagePassword').value = item.password || '';
document.getElementById('msgEditShopManage').textContent = '';
document.getElementById('msgEditShopManage').className = 'msg';
loadShopManageGroups(null, item.group_id).then(function () {
document.getElementById('editShopManageModal').classList.add('show');
});
};
});
document.querySelectorAll('[data-shop-manage-delete]').forEach(function (btn) {
btn.onclick = function () {
var name = (btn.dataset.shopManageName || '').replace(/&quot;/g, '"');
if (!confirm('确定删除店铺“' + name + '”吗?')) return;
fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) { loadShopManage(shopManagePage); }
else { alert(res.error || '删除失败'); }
});
};
});
}
function 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 = ['<option value="">请选择分组</option>'];
var filterOpts = ['<option value="">全部分组</option>'];
shopManageGroups.forEach(function (g) {
var option = '<option value="' + g.id + '">' + (g.group_name || '') + '</option>';
createOpts.push(option);
filterOpts.push(option);
});
createSel.innerHTML = createOpts.join('');
editSel.innerHTML = createOpts.join('');
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 renderDedupeGroupSummary() {
var summaryEl = document.getElementById('dedupeGroupSummary');
if (!summaryEl) return;
if (currentUserRole === 'super_admin') {
summaryEl.innerHTML = '<span class="dedupe-group-summary-chip">全部分组 · ' +
escapeHtml(shopManageGroups.length) + ' 个</span>';
return;
}
if (!shopManageGroups.length) {
summaryEl.textContent = '未加入分组';
return;
}
var visibleGroups = shopManageGroups.slice(0, 4);
summaryEl.innerHTML = visibleGroups.map(function (group) {
var relation = String(group.leader_user_id || '') === String(currentUserId || '') ? '组长' : '组员';
return '<span class="dedupe-group-summary-chip" title="' + escapeHtml(group.group_name || '') + '">' +
escapeHtml(group.group_name || '') + ' · ' + relation + '</span>';
}).join('') + (shopManageGroups.length > visibleGroups.length
? '<span>另 ' + escapeHtml(shopManageGroups.length - visibleGroups.length) + ' 个</span>'
: '');
}
function loadDedupeGroupSummary(force) {
var summaryEl = document.getElementById('dedupeGroupSummary');
if (!summaryEl) return Promise.resolve([]);
summaryEl.textContent = '正在加载...';
return loadShopManageGroups(null, null, force).then(function (groups) {
renderDedupeGroupSummary();
return groups;
});
}
function resetShopManageGroupForm() {
document.getElementById('shopManageGroupEditId').value = '';
document.getElementById('shopManageGroupLeaderUserId').value = currentUserId || '';
document.getElementById('shopManageGroupLeaderName').value = currentUserUsername || '';
document.getElementById('shopManageGroupInput').value = '';
document.getElementById('msgShopManageGroup').textContent = '';
document.getElementById('msgShopManageGroup').className = 'msg';
refreshShopManageGroupMemberSelect(currentUserId, []);
}
function setShopManageGroupGrantRoutes(routes) {
currentShopManageGroupGrantRoutes = Array.isArray(routes) ? routes.slice() : [];
}
function updateShopManageGroupButtonsAccess() {
var canManageGroups = !!currentUserId;
['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups', 'btnManageQueryAsinGroups', 'btnManageDedupeGroups'].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 = '<tr><td colspan="8" class="empty-tip">暂无分组</td></tr>';
return;
}
tbody.innerHTML = shopManageGroups.map(function (item, index) {
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames : [];
var memberHtml = memberNames.length
? '<div class="shop-group-members" title="' + escapeHtml(memberNames.join('、')) + '">' +
memberNames.map(function (name) {
return '<span class="shop-group-member-chip">' + escapeHtml(name || '') + '</span>';
}).join('') + '</div>'
: '<span style="color:#999;">-</span>';
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
var actionHtml = canEdit
? ('<button class="btn btn-sm" data-shop-group-edit="' + escapeHtml(item.id) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + escapeHtml(item.id) + '" data-shop-group-name="' + escapeHtml(item.group_name || '') + '">删除</button>')
: '<span style="color:#999;">-</span>';
return '<tr>' +
'<td>' + (index + 1) + '</td>' +
'<td class="shop-group-name-cell">' + escapeHtml(item.group_name || '') + '</td>' +
'<td class="shop-group-leader-cell">' + escapeHtml(item.leader_username || '') + '</td>' +
'<td>' + escapeHtml(item.member_count || 0) + '</td>' +
'<td>' + memberHtml + '</td>' +
'<td class="shop-group-time-cell">' + escapeHtml(item.created_at || '') + '</td>' +
'<td class="shop-group-time-cell">' + escapeHtml(item.updated_at || '') + '</td>' +
'<td class="shop-group-action-cell">' + actionHtml + '</td>' +
'</tr>';
}).join('');
document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) {
btn.onclick = function () {
var item = null;
shopManageGroups.some(function (group) {
if (String(group.id || '') === String(btn.dataset.shopGroupEdit || '')) {
item = group;
return true;
}
return false;
});
if (!item) return;
document.getElementById('shopManageGroupEditId').value = item.id || '';
document.getElementById('shopManageGroupInput').value = item.group_name || '';
setShopManageGroupLeader(item.leader_user_id || '', item.leader_username || '', item.member_user_ids || []);
document.getElementById('msgShopManageGroup').textContent = '';
document.getElementById('msgShopManageGroup').className = 'msg';
};
});
document.querySelectorAll('[data-shop-group-delete]').forEach(function (btn) {
btn.onclick = function () {
var gid = btn.dataset.shopGroupDelete;
var gname = (btn.dataset.shopGroupName || '').replace(/&quot;/g, '"');
if (!confirm('确定删除分组“' + gname + '”吗?')) return;
fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
alert(res.error || '删除失败');
return;
}
loadShopManageGroups(null, null, true).then(function () {
renderShopManageGroupRows();
renderDedupeGroupSummary();
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('btnManageDedupeGroups').onclick = function () {
setShopManageGroupGrantRoutes(['dedupe-total-data']);
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();
renderDedupeGroupSummary();
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 = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无店铺</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (chooseSkipPriceAsinShopPage - 1) * chooseSkipPriceAsinShopPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.mall_name || '') + '</td><td>' + (item.account || '') + '</td><td>' +
'<button class="btn btn-sm" type="button" data-choose-skip-price-asin-shop="' + item.id + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '" data-group-id="' + (item.group_id || '') + '">选择</button>' +
'</td></tr>';
}).join('');
}
renderPagination('chooseSkipPriceAsinShopPagination', res.total, res.page, res.page_size, loadChooseSkipPriceAsinShops);
bindChooseSkipPriceAsinShopActions();
})
.catch(function () {
document.getElementById('chooseSkipPriceAsinShopListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
});
}
function bindChooseSkipPriceAsinShopActions() {
document.querySelectorAll('[data-choose-skip-price-asin-shop]').forEach(function (btn) {
btn.onclick = function () {
document.getElementById('skipPriceAsinShopName').value = (btn.dataset.shopName || '').replace(/&quot;/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 = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN 和最低价</div>';
return;
}
container.innerHTML = selectedCountries.map(function (countryCode) {
var label = getSkipPriceCountryLabel(countryCode);
var value = existingValues[countryCode] || '';
var minimumPrice = existingMinimumPrices[countryCode] || '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span style="min-width:56px;color:#555;">' + label + '</span>' +
'<input type="text" data-skip-price-country-input="' + countryCode + '" value="' + value.replace(/"/g, '&quot;') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
'<input type="number" data-skip-price-country-minimum-price-input="' + countryCode + '" value="' + minimumPrice.replace(/"/g, '&quot;') + '" min="0" step="0.01" placeholder="最低价" style="width:140px;">' +
'</div>';
}).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 buildSkipPriceAsinFilterQuery() {
var query = '';
var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim();
var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim();
var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim();
if (groupId) query += (query ? '&' : '') + 'group_id=' + encodeURIComponent(groupId);
if (shopName) query += (query ? '&' : '') + 'shop_name=' + encodeURIComponent(shopName);
if (asin) query += (query ? '&' : '') + 'asin=' + encodeURIComponent(asin);
return query;
}
function buildSkipPriceAsinOperatorQuery() {
var query = '';
if (currentUserId) query += 'operator_id=' + encodeURIComponent(currentUserId);
if (currentUserRole === 'super_admin') query += (query ? '&' : '') + 'super_admin=true';
return query;
}
function buildSkipPriceAsinQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize;
var filterQuery = buildSkipPriceAsinFilterQuery();
if (filterQuery) query += '&' + filterQuery;
var operatorQuery = buildSkipPriceAsinOperatorQuery();
if (operatorQuery) query += '&' + operatorQuery;
return query;
}
function extractDownloadFilename(disposition, fallbackName) {
var fallback = fallbackName || 'download.xlsx';
if (!disposition) return fallback;
var utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match && utf8Match[1]) {
try {
return decodeURIComponent(utf8Match[1]);
} catch (e) { }
}
var plainMatch = disposition.match(/filename=\"?([^\";]+)\"?/i);
if (plainMatch && plainMatch[1]) return plainMatch[1];
return fallback;
}
function triggerBrowserDownload(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename || 'download.xlsx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
}
function exportSkipPriceAsin() {
var query = buildSkipPriceAsinFilterQuery();
var operatorQuery = buildSkipPriceAsinOperatorQuery();
if (operatorQuery) query += (query ? '&' : '') + operatorQuery;
var url = '/api/admin/skip-price-asins/export' + (query ? ('?' + query) : '');
fetch(url)
.then(function (response) {
var contentType = response.headers.get('content-type') || '';
if (!response.ok || contentType.indexOf('application/json') >= 0) {
return response.json().then(function (res) {
throw new Error((res && (res.error || res.msg)) || '导出失败');
}).catch(function (err) {
throw err instanceof Error ? err : new Error('导出失败');
});
}
return response.blob().then(function (blob) {
return {
blob: blob,
filename: extractDownloadFilename(response.headers.get('content-disposition'), 'skip-price-asin.xlsx')
};
});
})
.then(function (payload) {
triggerBrowserDownload(payload.blob, payload.filename);
})
.catch(function (err) {
alert((err && err.message) || '导出失败');
});
}
function renderSkipPriceAsinCell(item, country) {
var asinValue = item[country.field] || '';
var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]);
var hasValue = !!asinValue || !!minimumPriceValue;
var infoHtml = hasValue
? ('<div style="display:flex;flex-direction:column;gap:4px;min-width:0;">' +
'<span>' + (asinValue || '<span style="color:#999;">-</span>') + '</span>' +
'<span style="color:#666;font-size:12px;">最低价:' + (minimumPriceValue || '-') + '</span>' +
'</div>')
: '<span style="color:#999;">-</span>';
var deleteHtml = hasValue
? ('<button class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>')
: '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
infoHtml +
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-minimum-price="' + minimumPriceValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
deleteHtml +
'</div>';
}
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(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
var currentAsin = (btn.dataset.asin || '').replace(/&quot;/g, '"');
var currentMinimumPrice = (btn.dataset.minimumPrice || '').replace(/&quot;/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(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return;
var operatorQuery = buildSkipPriceAsinOperatorQuery();
fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode + (operatorQuery ? ('?' + operatorQuery) : ''), {
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 = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td>' +
skipPriceCountryColumns.map(function (country) {
return '<td>' + renderSkipPriceAsinCell(item, country) + '</td>';
}).join('') +
'</tr>';
}).join('');
}
renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin);
bindSkipPriceAsinActions();
})
.catch(function () {
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
var skipPriceAsinImportPollTimer = null;
var skipPriceAsinDeleteImportPollTimer = null;
var skipPriceAsinImportPollSeq = 0;
var skipPriceAsinDeleteImportPollSeq = 0;
function stopSkipPriceAsinImportProgress() {
skipPriceAsinImportPollSeq += 1;
if (skipPriceAsinImportPollTimer) {
clearTimeout(skipPriceAsinImportPollTimer);
skipPriceAsinImportPollTimer = null;
}
}
function stopSkipPriceAsinDeleteImportProgress() {
skipPriceAsinDeleteImportPollSeq += 1;
if (skipPriceAsinDeleteImportPollTimer) {
clearTimeout(skipPriceAsinDeleteImportPollTimer);
skipPriceAsinDeleteImportPollTimer = null;
}
}
function setSkipPriceAsinImportProgress(percent, text) {
var wrap = document.getElementById('skipPriceAsinImportProgressWrap');
var fill = document.getElementById('skipPriceAsinImportProgressFill');
var textEl = document.getElementById('skipPriceAsinImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function setSkipPriceAsinDeleteImportProgress(percent, text) {
var wrap = document.getElementById('skipPriceAsinDeleteImportProgressWrap');
var fill = document.getElementById('skipPriceAsinDeleteImportProgressFill');
var textEl = document.getElementById('skipPriceAsinDeleteImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function pollSkipPriceAsinImport(importId) {
stopSkipPriceAsinImportProgress();
var pollSeq = ++skipPriceAsinImportPollSeq;
var finished = false;
var inFlight = false;
var attempts = 0;
var maxAttempts = 300;
function isActive() {
return !finished && pollSeq === skipPriceAsinImportPollSeq;
}
function finish() {
finished = true;
if (skipPriceAsinImportPollTimer) {
clearTimeout(skipPriceAsinImportPollTimer);
skipPriceAsinImportPollTimer = null;
}
}
function schedule() {
if (isActive()) {
skipPriceAsinImportPollTimer = setTimeout(tick, 1500);
}
}
function tick() {
if (!isActive() || inFlight) {
return;
}
attempts += 1;
if (attempts > maxAttempts) {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度超时,请稍后刷新列表确认结果';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
return;
}
inFlight = true;
fetch('/api/admin/skip-price-asins/import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!isActive()) {
return;
}
if (!res.success) {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = res.error || '查询导入进度失败';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setSkipPriceAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = '导入成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgSkipPriceAsinImport').className = 'msg ok';
loadSkipPriceAsin(1);
} else if (progress.status === 'failed') {
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = progress.error_message || '导入失败';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
} else {
schedule();
}
})
.catch(function () {
if (!isActive()) {
return;
}
finish();
document.getElementById('msgSkipPriceAsinImport').textContent = '查询导入进度失败';
document.getElementById('msgSkipPriceAsinImport').className = 'msg err';
})
.finally(function () {
inFlight = false;
});
}
tick();
}
function pollSkipPriceAsinDeleteImport(importId) {
stopSkipPriceAsinDeleteImportProgress();
var pollSeq = ++skipPriceAsinDeleteImportPollSeq;
var finished = false;
var inFlight = false;
var attempts = 0;
var maxAttempts = 300;
function isActive() {
return !finished && pollSeq === skipPriceAsinDeleteImportPollSeq;
}
function finish() {
finished = true;
if (skipPriceAsinDeleteImportPollTimer) {
clearTimeout(skipPriceAsinDeleteImportPollTimer);
skipPriceAsinDeleteImportPollTimer = null;
}
}
function schedule() {
if (isActive()) {
skipPriceAsinDeleteImportPollTimer = setTimeout(tick, 1500);
}
}
function tick() {
if (!isActive() || inFlight) {
return;
}
attempts += 1;
if (attempts > maxAttempts) {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度超时,请稍后刷新列表确认结果';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
return;
}
inFlight = true;
fetch('/api/admin/skip-price-asins/delete-import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!isActive()) {
return;
}
if (!res.success) {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = res.error || '查询删除进度失败';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setSkipPriceAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg ok';
loadSkipPriceAsin(1);
} else if (progress.status === 'failed') {
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = progress.error_message || '删除失败';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
} else {
schedule();
}
})
.catch(function () {
if (!isActive()) {
return;
}
finish();
document.getElementById('msgSkipPriceAsinDeleteImport').textContent = '查询删除进度失败';
document.getElementById('msgSkipPriceAsinDeleteImport').className = 'msg err';
})
.finally(function () {
inFlight = false;
});
}
tick();
}
function uploadSkipPriceAsinImport(deleteMode) {
var fileInput = document.getElementById(deleteMode ? 'skipPriceAsinDeleteImportFile' : 'skipPriceAsinImportFile');
var msgEl = document.getElementById(deleteMode ? 'msgSkipPriceAsinDeleteImport' : 'msgSkipPriceAsinImport');
msgEl.textContent = '';
msgEl.className = 'msg';
if (deleteMode) {
stopSkipPriceAsinDeleteImportProgress();
document.getElementById('skipPriceAsinDeleteImportProgressWrap').style.display = 'none';
} else {
stopSkipPriceAsinImportProgress();
document.getElementById('skipPriceAsinImportProgressWrap').style.display = 'none';
}
var groupId = (document.getElementById('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);
if (currentUserId) formData.append('operator_id', currentUserId);
if (currentUserRole === 'super_admin') formData.append('super_admin', 'true');
var xhr = new XMLHttpRequest();
xhr.open('POST', deleteMode ? '/api/admin/skip-price-asins/delete-import' : '/api/admin/skip-price-asins/import', true);
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var percent = Math.round(event.loaded * 100 / event.total);
if (deleteMode) setSkipPriceAsinDeleteImportProgress(percent, '上传中:' + percent + '%');
else setSkipPriceAsinImportProgress(percent, '上传中:' + percent + '%');
}
};
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status < 200 || xhr.status >= 300) {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
return;
}
var res;
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; }
if (!res.success) {
msgEl.textContent = res.error || (deleteMode ? '删除失败' : '导入失败');
msgEl.className = 'msg err';
return;
}
if (deleteMode) {
setSkipPriceAsinDeleteImportProgress(100, '上传完成,后端处理中...');
pollSkipPriceAsinDeleteImport(res.import_id);
} else {
setSkipPriceAsinImportProgress(100, '上传完成,后端处理中...');
pollSkipPriceAsinImport(res.import_id);
}
fileInput.value = '';
};
xhr.onerror = function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
};
xhr.send(formData);
}
document.getElementById('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;
}
}
var operatorQuery = buildSkipPriceAsinOperatorQuery();
fetch('/api/admin/skip-price-asin/' + itemId + '/country/' + countryCode + (operatorQuery ? ('?' + operatorQuery) : ''), {
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('btnExportSkipPriceAsin').onclick = function () {
exportSkipPriceAsin();
};
document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadSkipPriceAsin(1);
}
});
document.getElementById('skipPriceAsinFilterAsin').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadSkipPriceAsin(1);
}
});
document.getElementById('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;
});
var operatorQuery = buildSkipPriceAsinOperatorQuery();
fetch('/api/admin/skip-price-asin' + (operatorQuery ? ('?' + operatorQuery) : ''), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
group_id: Number(groupId),
shop_name: shopName,
countries: 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 = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN</div>';
return;
}
container.innerHTML = selectedCountries.map(function (countryCode) {
var label = getQueryAsinCountryLabel(countryCode);
var value = existingValues[countryCode] || '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span style="min-width:56px;color:#555;">' + label + '</span>' +
'<input type="text" data-query-asin-country-input="' + countryCode + '" value="' + value.replace(/"/g, '&quot;') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
'</div>';
}).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 = '<tr><td colspan="6" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无店铺</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (chooseQueryAsinShopPage - 1) * chooseQueryAsinShopPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.mall_name || '') + '</td><td>' + (item.username || '') + '</td><td>' +
'<button class="btn btn-sm" type="button" data-choose-query-asin-shop="' + item.id + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '" data-group-id="' + (item.group_id || '') + '">选择</button>' +
'</td></tr>';
}).join('');
}
renderPagination('chooseQueryAsinShopPagination', res.total, res.page, res.page_size, loadChooseQueryAsinShops);
bindChooseQueryAsinShopActions();
})
.catch(function () {
document.getElementById('chooseQueryAsinShopListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
});
}
function bindChooseQueryAsinShopActions() {
document.querySelectorAll('[data-choose-query-asin-shop]').forEach(function (btn) {
btn.onclick = function () {
document.getElementById('queryAsinShopName').value = (btn.dataset.shopName || '').replace(/&quot;/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 filterQuery = buildQueryAsinFilterQuery();
if (filterQuery) query += '&' + filterQuery;
return query;
}
function buildQueryAsinFilterQuery() {
var query = '';
var groupId = (document.getElementById('queryAsinFilterGroupId').value || '').trim();
var shopName = (document.getElementById('queryAsinFilterShopName').value || '').trim();
var asin = (document.getElementById('queryAsinFilterAsin').value || '').trim();
if (groupId) query += (query ? '&' : '') + 'group_id=' + encodeURIComponent(groupId);
if (shopName) query += (query ? '&' : '') + 'shop_name=' + encodeURIComponent(shopName);
if (asin) query += (query ? '&' : '') + 'asin=' + encodeURIComponent(asin);
return query;
}
function exportQueryAsin() {
var query = buildQueryAsinFilterQuery();
var url = '/api/admin/query-asins/export' + (query ? ('?' + query) : '');
fetch(url)
.then(function (response) {
var contentType = response.headers.get('content-type') || '';
if (!response.ok || contentType.indexOf('application/json') >= 0) {
return response.json().then(function (res) {
throw new Error((res && (res.error || res.msg)) || '导出失败');
}).catch(function (err) {
throw err instanceof Error ? err : new Error('导出失败');
});
}
return response.blob().then(function (blob) {
return {
blob: blob,
filename: extractDownloadFilename(response.headers.get('content-disposition'), 'query-asin.xlsx')
};
});
})
.then(function (payload) {
triggerBrowserDownload(payload.blob, payload.filename);
})
.catch(function (err) {
alert((err && err.message) || '导出失败');
});
}
function renderQueryAsinCell(item, country) {
var asinValue = item[country.field] || '';
var infoHtml = asinValue ? '<span>' + asinValue + '</span>' : '<span style="color:#999;">-</span>';
var deleteHtml = asinValue
? ('<button class="btn btn-sm btn-danger" data-query-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>')
: '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
infoHtml +
'<button class="btn btn-sm" data-query-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
deleteHtml +
'</div>';
}
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(/&quot;/g, '"'),
(btn.dataset.asin || '').replace(/&quot;/g, '"')
);
};
});
document.querySelectorAll('[data-query-asin-delete]').forEach(function (btn) {
btn.onclick = function () {
var shopName = (btn.dataset.shopName || '').replace(/&quot;/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 = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无数据</td></tr>';
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (queryAsinPage - 1) * queryAsinPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td>' +
queryAsinCountryColumns.map(function (country) {
return '<td>' + renderQueryAsinCell(item, country) + '</td>';
}).join('') +
'</tr>';
}).join('');
}
renderPagination('queryAsinPagination', res.total, res.page, res.page_size, loadQueryAsin);
bindQueryAsinActions();
})
.catch(function () {
document.getElementById('queryAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
var queryAsinImportPollTimer = null;
var queryAsinDeleteImportPollTimer = null;
function stopQueryAsinImportProgress() {
if (queryAsinImportPollTimer) {
clearInterval(queryAsinImportPollTimer);
queryAsinImportPollTimer = null;
}
}
function stopQueryAsinDeleteImportProgress() {
if (queryAsinDeleteImportPollTimer) {
clearInterval(queryAsinDeleteImportPollTimer);
queryAsinDeleteImportPollTimer = null;
}
}
function setQueryAsinImportProgress(percent, text) {
var wrap = document.getElementById('queryAsinImportProgressWrap');
var fill = document.getElementById('queryAsinImportProgressFill');
var textEl = document.getElementById('queryAsinImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function setQueryAsinDeleteImportProgress(percent, text) {
var wrap = document.getElementById('queryAsinDeleteImportProgressWrap');
var fill = document.getElementById('queryAsinDeleteImportProgressFill');
var textEl = document.getElementById('queryAsinDeleteImportProgressText');
wrap.style.display = 'block';
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
textEl.textContent = text || '';
}
function pollQueryAsinImport(importId) {
stopQueryAsinImportProgress();
function tick() {
fetch('/api/admin/query-asins/import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = res.error || '查询导入进度失败';
document.getElementById('msgQueryAsinImport').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setQueryAsinImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = '导入成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',添加 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgQueryAsinImport').className = 'msg ok';
loadQueryAsin(1);
} else if (progress.status === 'failed') {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = progress.error_message || '导入失败';
document.getElementById('msgQueryAsinImport').className = 'msg err';
}
})
.catch(function () {
stopQueryAsinImportProgress();
document.getElementById('msgQueryAsinImport').textContent = '查询导入进度失败';
document.getElementById('msgQueryAsinImport').className = 'msg err';
});
}
tick();
queryAsinImportPollTimer = setInterval(tick, 1000);
}
function pollQueryAsinDeleteImport(importId) {
stopQueryAsinDeleteImportProgress();
function tick() {
fetch('/api/admin/query-asins/delete-import/' + encodeURIComponent(importId))
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = res.error || '查询删除进度失败';
document.getElementById('msgQueryAsinDeleteImport').className = 'msg err';
return;
}
var progress = res.progress || {};
var totalRows = progress.total_rows || 0;
var processedRows = progress.processed_rows || 0;
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
setQueryAsinDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
if (progress.status === 'success') {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = '删除成功:总行数 ' + totalRows + 'ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
document.getElementById('msgQueryAsinDeleteImport').className = 'msg ok';
loadQueryAsin(1);
} else if (progress.status === 'failed') {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = progress.error_message || '删除失败';
document.getElementById('msgQueryAsinDeleteImport').className = 'msg err';
}
})
.catch(function () {
stopQueryAsinDeleteImportProgress();
document.getElementById('msgQueryAsinDeleteImport').textContent = '查询删除进度失败';
document.getElementById('msgQueryAsinDeleteImport').className = 'msg err';
});
}
tick();
queryAsinDeleteImportPollTimer = setInterval(tick, 1000);
}
function uploadQueryAsinImport(deleteMode) {
var fileInput = document.getElementById(deleteMode ? 'queryAsinDeleteImportFile' : 'queryAsinImportFile');
var msgEl = document.getElementById(deleteMode ? 'msgQueryAsinDeleteImport' : 'msgQueryAsinImport');
msgEl.textContent = '';
msgEl.className = 'msg';
if (deleteMode) {
stopQueryAsinDeleteImportProgress();
document.getElementById('queryAsinDeleteImportProgressWrap').style.display = 'none';
} else {
stopQueryAsinImportProgress();
document.getElementById('queryAsinImportProgressWrap').style.display = 'none';
}
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 Excel 文件';
msgEl.className = 'msg err';
return;
}
var file = fileInput.files[0];
var lowerName = (file.name || '').toLowerCase();
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
msgEl.className = 'msg err';
return;
}
if (deleteMode && !confirm('确定按 Excel 中的分组、店铺名和国家 ASIN 批量删除吗?')) {
return;
}
var formData = new FormData();
formData.append('file', file);
var groupId = (document.getElementById('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('btnExportQueryAsin').onclick = function () {
exportQueryAsin();
};
document.getElementById('queryAsinFilterShopName').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadQueryAsin(1);
}
});
document.getElementById('queryAsinFilterAsin').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
loadQueryAsin(1);
}
});
document.getElementById('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 productCategoryNodeMap = {};
var productCategoryPageSize = 20;
var productCategoryRootState = { children: [], page: 0, total: 0, hasMore: true, loaded: false, loading: false };
var productCategorySearchState = { items: [], page: 0, total: 0, hasMore: false, loading: false };
var productCategoryKeyword = '';
var editingProductCategoryId = null;
function resetProductCategoryForm() {
editingProductCategoryId = null;
document.getElementById('productCategoryParentId').value = '';
document.getElementById('productCategoryName').value = '';
document.getElementById('productCategorySortOrder').value = '';
document.getElementById('productCategoryDescription').value = '';
document.getElementById('btnCreateProductCategory').textContent = '新增类目';
document.getElementById('btnCancelProductCategoryEdit').style.display = 'none';
renderProductCategoryParentOptions();
}
function renderProductCategoryParentOptions(selectedValue, excludedId) {
var select = document.getElementById('productCategoryParentId');
if (!select) return;
var selected = selectedValue == null ? select.value : String(selectedValue || '');
var excluded = excludedId == null ? editingProductCategoryId : excludedId;
var options = ['<option value="">顶级类目</option>'];
productCategoryItems.slice().sort(function (a, b) {
return String(a.path || a.name || '').localeCompare(String(b.path || b.name || ''), 'zh-CN');
}).forEach(function (item) {
if (excluded && String(item.id) === String(excluded)) return;
var prefix = new Array((item.level || 0) + 1).join('  ');
options.push('<option value="' + escapeHtml(item.id) + '">' + prefix + escapeHtml(item.name || '') + '</option>');
});
if (selected && !productCategoryItems.some(function (item) { return String(item.id) === selected; })) {
options.push('<option value="' + escapeHtml(selected) + '">当前父级 #' + escapeHtml(selected) + '</option>');
}
select.innerHTML = options.join('');
select.value = selected;
}
function normalizeProductCategoryItem(item, parent) {
var id = String(item.id || '');
var existing = productCategoryNodeMap[id] || {};
var level = item.level != null ? item.level : (parent ? (parent.level || 0) + 1 : 0);
var path = item.path || (parent && parent.path ? parent.path + ' / ' + (item.name || '') : (item.name || ''));
var normalized = Object.assign(existing, item, {
id: item.id,
parent_id: item.parent_id == null ? null : item.parent_id,
child_count: item.child_count || 0,
level: level,
path: path,
children: existing.children || [],
page: existing.page || 0,
total: existing.total || 0,
hasMore: existing.hasMore !== false,
loaded: existing.loaded || false,
loading: false,
expanded: existing.expanded || false
});
productCategoryNodeMap[id] = normalized;
productCategoryItems = Object.keys(productCategoryNodeMap).map(function (key) { return productCategoryNodeMap[key]; });
return normalized;
}
function flattenVisibleProductCategories(items, output) {
(items || []).forEach(function (item) {
output.push({ type: 'item', item: item });
if ((item.child_count || 0) > 0 && item.expanded) {
flattenVisibleProductCategories(item.children || [], output);
if (item.hasMore) {
output.push({ type: 'more', parent: item });
}
}
});
return output;
}
function renderProductCategoryRows() {
var tbody = document.getElementById('productCategoryListBody');
if (!tbody) return;
var rows = productCategoryKeyword
? productCategorySearchState.items.map(function (item) { return { type: 'item', item: item, search: true }; })
: flattenVisibleProductCategories(productCategoryRootState.children, []);
if (!productCategoryKeyword && productCategoryRootState.hasMore) {
rows.push({ type: 'more', parent: null });
}
if (productCategoryKeyword && productCategorySearchState.hasMore) {
rows.push({ type: 'searchMore' });
}
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无商品类目</td></tr>';
return;
}
tbody.innerHTML = rows.map(function (row) {
if (row.type === 'more') {
var parent = row.parent;
var level = parent ? (parent.level || 0) + 1 : 0;
var indentMore = Math.max(0, level) * 24;
var loading = parent ? parent.loading : productCategoryRootState.loading;
var loadedCount = parent ? (parent.children || []).length : (productCategoryRootState.children || []).length;
var totalCount = parent ? (parent.total || 0) : (productCategoryRootState.total || 0);
return '<tr><td colspan="6"><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indentMore + 'px;"></span>' +
'<button class="btn btn-sm btn-secondary" type="button" data-product-category-load-more="' + escapeHtml(parent ? parent.id : '') + '"' + (loading ? ' disabled' : '') + '>' +
(loading ? '加载中...' : '加载更多') + '</button>' +
'<span class="empty-tip" style="padding:0;">已加载 ' + escapeHtml(loadedCount) + ' / ' + escapeHtml(totalCount) + '</span></div></td></tr>';
}
if (row.type === 'searchMore') {
var searchLoadedCount = (productCategorySearchState.items || []).length;
var searchTotalCount = productCategorySearchState.total || 0;
return '<tr><td colspan="6"><button class="btn btn-sm btn-secondary" type="button" data-product-category-search-more' + (productCategorySearchState.loading ? ' disabled' : '') + '>' +
(productCategorySearchState.loading ? '加载中...' : '加载更多搜索结果') + '</button> ' +
'<span class="empty-tip" style="padding:0;">已加载 ' + escapeHtml(searchLoadedCount) + ' / ' + escapeHtml(searchTotalCount) + '</span></td></tr>';
}
var item = row.item;
var indent = Math.max(0, item.level || 0) * 24;
var hasChildren = (item.child_count || 0) > 0;
var expanded = !!item.expanded;
var mark = hasChildren ? (expanded ? '-' : '+') : '';
return '<tr>' +
'<td><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indent + 'px;"></span>' +
'<button type="button" class="tree-node-mark' + (hasChildren ? '' : ' is-leaf') + '" data-product-category-toggle="' + escapeHtml(item.id) + '"' + (hasChildren ? '' : ' disabled') + '>' + mark + '</button>' +
'<strong>' + escapeHtml(item.name || '') + '</strong></div></td>' +
'<td><div>' + escapeHtml(item.path || item.name || '') + '</div><div class="category-path">' + escapeHtml(item.category_key || '') + '</div></td>' +
'<td>' + escapeHtml(item.sort_order || 0) + '</td>' +
'<td><span class="category-tag">' + (item.is_builtin ? '内置' : '自定义') + '</span></td>' +
'<td>' + escapeHtml(item.description || '') + '</td>' +
'<td><button class="btn btn-sm" data-product-category-edit="' + escapeHtml(item.id) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-product-category-delete="' + escapeHtml(item.id) + '" data-product-category-name="' + escapeHtml(item.name || '') + '"' + (item.child_count > 0 ? ' disabled' : '') + '>删除</button></td>' +
'</tr>';
}).join('');
document.querySelectorAll('[data-product-category-toggle]').forEach(function (btn) {
btn.onclick = function () {
var id = String(btn.dataset.productCategoryToggle || '');
if (!id || btn.disabled) return;
toggleProductCategoryNode(id);
};
});
document.querySelectorAll('[data-product-category-load-more]').forEach(function (btn) {
btn.onclick = function () {
var parentId = (btn.dataset.productCategoryLoadMore || '').trim();
loadProductCategoryChildren(parentId || null, true);
};
});
document.querySelectorAll('[data-product-category-search-more]').forEach(function (btn) {
btn.onclick = function () {
loadProductCategorySearch(true);
};
});
document.querySelectorAll('[data-product-category-edit]').forEach(function (btn) {
btn.onclick = function () {
var item = productCategoryItems.find(function (row) { return String(row.id) === String(btn.dataset.productCategoryEdit); });
if (!item) return;
editingProductCategoryId = item.id;
renderProductCategoryParentOptions(item.parent_id || '', item.id);
document.getElementById('productCategoryName').value = item.name || '';
document.getElementById('productCategorySortOrder').value = item.sort_order || '';
document.getElementById('productCategoryDescription').value = item.description || '';
document.getElementById('btnCreateProductCategory').textContent = '保存类目';
document.getElementById('btnCancelProductCategoryEdit').style.display = '';
document.getElementById('msgProductCategory').textContent = '';
document.getElementById('msgProductCategory').className = 'msg';
};
});
document.querySelectorAll('[data-product-category-delete]').forEach(function (btn) {
btn.onclick = function () {
var name = btn.dataset.productCategoryName || '该类目';
if (!confirm('确定删除“' + name + '”?')) return;
fetch('/api/admin/product-category/' + encodeURIComponent(btn.dataset.productCategoryDelete), { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
alert(res.error || '删除失败');
return;
}
resetProductCategoryForm();
loadProductCategories();
})
.catch(function () { alert('请求失败'); });
};
});
}
function loadProductCategories() {
if (productCategoryKeyword) {
productCategorySearchState = { items: [], page: 0, total: 0, hasMore: false, loading: false };
loadProductCategorySearch(false);
return;
}
productCategoryRootState = { children: [], page: 0, total: 0, hasMore: true, loaded: false, loading: false };
productCategoryNodeMap = {};
productCategoryItems = [];
renderProductCategoryParentOptions();
loadProductCategoryChildren(null, false);
}
function loadProductCategoryChildren(parentId, append) {
var parent = parentId ? productCategoryNodeMap[String(parentId)] : null;
var state = parent || productCategoryRootState;
if (state.loading) return;
state.loading = true;
var failed = false;
renderProductCategoryRows();
var nextPage = append ? (state.page || 0) + 1 : 1;
var query = '?page=' + nextPage + '&page_size=' + productCategoryPageSize;
if (parentId) query += '&parent_id=' + encodeURIComponent(parentId);
fetch('/api/admin/product-categories/children' + query)
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('productCategoryListBody');
if (!res.success) {
failed = true;
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
return;
}
var children = (res.items || []).map(function (item) {
return normalizeProductCategoryItem(item, parent);
});
if (!append) {
state.children = [];
}
state.children = state.children.concat(children);
state.page = res.page || nextPage;
state.total = res.total || 0;
state.hasMore = !!res.has_more;
state.loaded = true;
renderProductCategoryParentOptions();
renderProductCategoryRows();
})
.catch(function () {
failed = true;
var tbody = document.getElementById('productCategoryListBody');
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
})
.finally(function () {
state.loading = false;
if (!failed) renderProductCategoryRows();
});
}
function loadProductCategorySearch(append) {
if (productCategorySearchState.loading) return;
productCategorySearchState.loading = true;
var failed = false;
renderProductCategoryRows();
var nextPage = append ? (productCategorySearchState.page || 0) + 1 : 1;
fetch('/api/admin/product-categories/search?keyword=' + encodeURIComponent(productCategoryKeyword) +
'&page=' + nextPage + '&page_size=' + productCategoryPageSize)
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('productCategoryListBody');
if (!res.success) {
failed = true;
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
return;
}
var items = (res.items || []).map(function (item) {
var normalized = normalizeProductCategoryItem(item, null);
normalized.expanded = false;
return normalized;
});
productCategorySearchState.items = append
? productCategorySearchState.items.concat(items)
: items;
productCategorySearchState.page = res.page || nextPage;
productCategorySearchState.total = res.total || 0;
productCategorySearchState.hasMore = !!res.has_more;
renderProductCategoryParentOptions();
renderProductCategoryRows();
})
.catch(function () {
failed = true;
var tbody = document.getElementById('productCategoryListBody');
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
})
.finally(function () {
productCategorySearchState.loading = false;
if (!failed) renderProductCategoryRows();
});
}
function toggleProductCategoryNode(id) {
var item = productCategoryNodeMap[String(id)];
if (!item) return;
item.expanded = !item.expanded;
if (item.expanded && !item.loaded) {
loadProductCategoryChildren(id, false);
return;
}
renderProductCategoryRows();
}
function exportProductCategories() {
var keyword = (document.getElementById('productCategoryKeyword').value || '').trim();
var url = '/api/admin/product-categories/export' + (keyword ? ('?keyword=' + encodeURIComponent(keyword)) : '');
fetch(url)
.then(function (response) {
var contentType = response.headers.get('content-type') || '';
if (!response.ok || contentType.indexOf('application/json') >= 0) {
return response.json().then(function (res) {
throw new Error((res && (res.error || res.msg)) || '导出失败');
}).catch(function (err) {
throw err instanceof Error ? err : new Error('导出失败');
});
}
return response.blob().then(function (blob) {
return {
blob: blob,
filename: extractDownloadFilename(response.headers.get('content-disposition'), 'product-categories.xlsx')
};
});
})
.then(function (payload) {
triggerBrowserDownload(payload.blob, payload.filename);
})
.catch(function (err) {
alert(err.message || '导出失败');
});
}
document.getElementById('btnCancelProductCategoryEdit').onclick = function () {
resetProductCategoryForm();
};
document.getElementById('btnSearchProductCategory').onclick = function () {
productCategoryKeyword = (document.getElementById('productCategoryKeyword').value || '').trim();
loadProductCategories();
};
document.getElementById('btnClearProductCategorySearch').onclick = function () {
productCategoryKeyword = '';
document.getElementById('productCategoryKeyword').value = '';
loadProductCategories();
};
document.getElementById('btnExportProductCategory').onclick = function () {
exportProductCategories();
};
document.getElementById('productCategoryKeyword').onkeydown = function (event) {
if (event.key === 'Enter') {
event.preventDefault();
document.getElementById('btnSearchProductCategory').click();
}
};
document.getElementById('btnCreateProductCategory').onclick = function () {
var msgEl = document.getElementById('msgProductCategory');
var name = (document.getElementById('productCategoryName').value || '').trim();
var parentId = (document.getElementById('productCategoryParentId').value || '').trim();
var sortOrder = (document.getElementById('productCategorySortOrder').value || '').trim();
var description = (document.getElementById('productCategoryDescription').value || '').trim();
msgEl.textContent = '';
msgEl.className = 'msg';
if (!name) {
msgEl.textContent = '请填写类目名称';
msgEl.className = 'msg err';
return;
}
var payload = {
parent_id: parentId ? Number(parentId) : null,
name: name,
sort_order: sortOrder ? Number(sortOrder) : null,
description: description
};
var url = editingProductCategoryId
? '/api/admin/product-category/' + encodeURIComponent(editingProductCategoryId)
: '/api/admin/product-category';
fetch(url, {
method: editingProductCategoryId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
msgEl.textContent = res.msg || '保存成功';
msgEl.className = 'msg ok';
resetProductCategoryForm();
loadProductCategories();
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
function loadSoftwareVersions() {
fetch('/api/admin/versions')
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('versionListBody');
var pagination = document.getElementById('versionPagination');
if (pagination) pagination.innerHTML = '';
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
var items = res.items || [];
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-tip">暂无版本记录</td></tr>';
return;
}
tbody.innerHTML = items.map(function (v) {
var url = v.file_url || '';
return '<tr><td>' + (v.version || '') + '</td><td><a href="' + url + '" target="_blank" rel="noopener">' + url + '</a></td><td>' + (v.created_at || '') + '</td><td><a href="' + url + '" download class="btn btn-sm">下载</a></td></tr>';
}).join('');
})
.catch(function () {
document.getElementById('versionListBody').innerHTML = '<tr><td colspan="4" class="empty-tip">请求失败</td></tr>';
});
}
function loadDigitalHumanVersions(page) {
page = page || 1;
fetch('/api/admin/digital-human-versions?page=' + page + '&pageSize=20')
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('digitalHumanVersionListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.message || res.error || '') + '</td></tr>';
return;
}
// 兼容两种返回格式:直接数组 或 分页对象
var data = res.data;
var items = [];
var pages = 1;
var current = 1;
if (Array.isArray(data)) {
items = data;
} else if (data && Array.isArray(data.records)) {
items = data.records;
pages = data.pages || 1;
current = data.current || 1;
} else if (data && Array.isArray(data.items)) {
items = data.items;
}
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无版本记录</td></tr>';
} else {
tbody.innerHTML = items.map(function (v) {
var statusText = v.status === 'DRAFT' ? '草稿' : (v.status === 'RELEASED' ? '已发布' : '已废弃');
var statusColor = v.status === 'DRAFT' ? '#999' : (v.status === 'RELEASED' ? '#52c41a' : '#ff4d4f');
var isLatestBadge = v.isLatest ? '<span style="color:#ff4d4f;font-weight:600;">★</span>' : '';
var fileSize = v.fileSize ? (v.fileSize / 1024 / 1024).toFixed(2) + ' MB' : '-';
var md5Short = (v.md5 || 'unknown').substring(0, 12) + '...';
var changelog = (v.changelog || '-').substring(0, 50);
if ((v.changelog || '').length > 50) changelog += '...';
var releasedAt = v.releasedAt || '-';
var actions = '';
if (v.status === 'DRAFT') {
actions += '<button class="btn btn-sm" onclick="releaseVersion(\'' + v.version + '\')">发布</button> ';
}
if (v.status === 'RELEASED' && !v.isLatest) {
actions += '<button class="btn btn-sm" onclick="setLatestVersion(\'' + v.version + '\')">设为最新</button> ';
}
if (v.status === 'RELEASED') {
actions += '<button class="btn btn-sm" onclick="downloadDigitalHumanVersion(\'' + encodeURIComponent(v.version || '') + '\')">下载</button> ';
}
if (!v.isLatest) {
actions += '<button class="btn btn-sm btn-danger" onclick="deleteVersion(\'' + v.version + '\')">删除</button>';
}
return '<tr>' +
'<td>' + (v.version || '') + '</td>' +
'<td style="color:' + statusColor + ';">' + statusText + '</td>' +
'<td>' + isLatestBadge + '</td>' +
'<td>' + fileSize + '</td>' +
'<td title="' + (v.md5 || '') + '">' + md5Short + '</td>' +
'<td>' + changelog + '</td>' +
'<td>' + releasedAt + '</td>' +
'<td>' + actions + '</td>' +
'</tr>';
}).join('');
}
// 分页
var pagination = document.getElementById('digitalHumanVersionPagination');
if (pages > 1) {
var html = '';
for (var i = 1; i <= pages; i++) {
var active = i === current ? ' active' : '';
html += '<span class="page-item' + active + '" onclick="loadDigitalHumanVersions(' + i + ')">' + i + '</span>';
}
pagination.innerHTML = html;
} else {
pagination.innerHTML = '';
}
})
.catch(function (err) {
document.getElementById('digitalHumanVersionListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败: ' + (err.message || '') + '</td></tr>';
});
}
function downloadDigitalHumanVersion(encodedVersion) {
var version = decodeURIComponent(encodedVersion || '');
if (!version) {
alert('版本号为空');
return;
}
fetch('/api/admin/digital-human-versions/' + encodeURIComponent(version) + '/download-url')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
throw new Error(res.message || res.error || '获取下载链接失败');
}
var data = res.data || {};
var downloadUrl = typeof data === 'string' ? data : (data.downloadUrl || data.url || '');
if (!downloadUrl) {
throw new Error('Java 后端未返回下载链接');
}
var a = document.createElement('a');
a.href = downloadUrl;
a.download = 'ShuFuDigitalHuman-' + version + '.zip';
a.target = '_blank';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
})
.catch(function (err) {
alert(err.message || '下载失败');
});
}
window.loadDigitalHumanVersions = loadDigitalHumanVersions;
window.downloadDigitalHumanVersion = downloadDigitalHumanVersion;
document.getElementById('btnUploadVersion').onclick = function () {
var version = (document.getElementById('versionNumber').value || '').trim();
var fileInput = document.getElementById('versionZip');
var msgEl = document.getElementById('msgVersion');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!version) {
msgEl.textContent = '请填写版本号';
msgEl.classList.add('err');
return;
}
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 zip 压缩包';
msgEl.classList.add('err');
return;
}
var file = fileInput.files[0];
if (!(file.name || '').toLowerCase().endsWith('.zip')) {
msgEl.textContent = '仅支持 .zip 格式';
msgEl.classList.add('err');
return;
}
var formData = new FormData();
formData.append('file', file);
formData.append('version', version);
msgEl.textContent = '上传中,请稍候...';
msgEl.classList.remove('err', 'ok');
fetch('/api/admin/version', {
method: 'POST',
body: formData
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
msgEl.textContent = '发布成功。版本:' + res.version + ',链接:' + (res.file_url || '');
msgEl.classList.add('ok');
document.getElementById('versionNumber').value = '';
fileInput.value = '';
loadSoftwareVersions();
} else {
msgEl.textContent = res.error || res.message || '上传失败';
msgEl.classList.add('err');
}
})
.catch(function (err) {
msgEl.textContent = '请求失败: ' + (err.message || '');
msgEl.classList.add('err');
});
};
document.getElementById('btnUploadDigitalHumanVersion').onclick = function () {
var version = (document.getElementById('digitalHumanVersionNumber').value || '').trim();
var minClientVersion = (document.getElementById('digitalHumanMinClientVersion').value || '').trim();
var changelog = (document.getElementById('digitalHumanVersionChangelog').value || '').trim();
var fileInput = document.getElementById('digitalHumanVersionZip');
var msgEl = document.getElementById('msgDigitalHumanVersion');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!version) {
msgEl.textContent = '请填写版本号';
msgEl.classList.add('err');
return;
}
if (!fileInput.files || fileInput.files.length === 0) {
msgEl.textContent = '请选择 zip 压缩包';
msgEl.classList.add('err');
return;
}
var file = fileInput.files[0];
if (!(file.name || '').toLowerCase().endsWith('.zip')) {
msgEl.textContent = '仅支持 .zip 格式';
msgEl.classList.add('err');
return;
}
var formData = new FormData();
formData.append('file', file);
formData.append('version', version);
if (minClientVersion) formData.append('minClientVersion', minClientVersion);
if (changelog) formData.append('changelog', changelog);
msgEl.textContent = '上传中,请稍候...';
msgEl.classList.remove('err', 'ok');
var uploadUrl = window.DIGITAL_HUMAN_JAVA_UPLOAD_URL;
if (!uploadUrl) {
uploadUrl = '/api/admin/digital-human-versions/upload';
}
var xhr = new XMLHttpRequest();
xhr.open('POST', uploadUrl, true);
xhr.timeout = 1800000;
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var percent = Math.floor((event.loaded / event.total) * 100);
msgEl.textContent = '正在上传:' + percent + '%';
} else {
msgEl.textContent = '正在上传,请稍候...';
}
};
xhr.onload = function () {
var res = null;
try {
res = JSON.parse(xhr.responseText || '{}');
} catch (e) {
msgEl.textContent = '上传失败Java 返回不是 JSON';
msgEl.classList.add('err');
return;
}
if (xhr.status >= 200 && xhr.status < 300 && res.success && res.data) {
msgEl.textContent = '上传成功!版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)';
msgEl.classList.add('ok');
document.getElementById('digitalHumanVersionNumber').value = '';
document.getElementById('digitalHumanMinClientVersion').value = '';
document.getElementById('digitalHumanVersionChangelog').value = '';
fileInput.value = '';
loadDigitalHumanVersions();
} else {
msgEl.textContent = (res && (res.message || res.error)) || ('上传失败HTTP ' + xhr.status);
msgEl.classList.add('err');
}
};
xhr.onerror = function () {
msgEl.textContent = '请求失败:无法连接上传服务 ' + uploadUrl;
msgEl.classList.add('err');
};
xhr.ontimeout = function () {
msgEl.textContent = '上传超时,请检查 Java 服务或网关超时配置';
msgEl.classList.add('err');
};
xhr.send(formData);
};
window.releaseVersion = function(version) {
if (!confirm('确认发布版本 ' + version + ' 吗?')) return;
fetch('/api/admin/digital-human-versions/' + version + '/release', {
method: 'POST'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
alert('发布成功!');
loadDigitalHumanVersions();
} else {
alert('发布失败:' + (res.message || res.error || ''));
}
})
.catch(function (err) {
alert('请求失败:' + (err.message || ''));
});
};
window.setLatestVersion = function(version) {
if (!confirm('确认将版本 ' + version + ' 设为最新吗?客户端将自动检测并更新到此版本。')) return;
fetch('/api/admin/digital-human-versions/' + version + '/set-latest', {
method: 'POST'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
alert('设置成功!版本 ' + version + ' 已标记为最新。');
loadDigitalHumanVersions();
} else {
alert('设置失败:' + (res.message || res.error || ''));
}
})
.catch(function (err) {
alert('请求失败:' + (err.message || ''));
});
};
window.deleteVersion = function(version) {
if (!confirm('确认删除版本 ' + version + ' 吗?此操作将删除 OSS 文件,不可恢复!')) return;
fetch('/api/admin/digital-human-versions/' + version, {
method: 'DELETE'
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
alert('删除成功!');
loadDigitalHumanVersions();
} else {
alert('删除失败:' + (res.message || res.error || ''));
}
})
.catch(function (err) {
alert('请求失败:' + (err.message || ''));
});
};
// ========== 栏目权限配置 ==========
function loadColumns() {
fetch('/api/admin/columns')
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('columnListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
allColumnsList = res.items || [];
if (allColumnsList.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无菜单,请在上方新增</td></tr>';
} else {
tbody.innerHTML = allColumnsList.map(function (c) {
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + (c.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-column-edit="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '" data-key="' + (c.column_key || '').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-column-delete="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '">删除</button></td></tr>';
}).join('');
}
bindColumnActions();
})
.catch(function () {
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
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(/&quot;/g, '"');
document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/&quot;/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(/&quot;/g, '"') + '”吗?')) return;
fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); }
else { alert(res.error || '删除失败'); }
});
};
});
}
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 = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
allColumnsList = res.items || [];
if (allColumnsList.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无菜单,请先在上方新增</td></tr>';
} else {
tbody.innerHTML = allColumnsList.map(function (c, index) {
var moveButtons =
'<button class="btn btn-sm btn-secondary" data-column-move-up="' + c.id + '"' + (index === 0 ? ' disabled' : '') + '>上移</button> ' +
'<button class="btn btn-sm btn-secondary" data-column-move-down="' + c.id + '"' + (index === allColumnsList.length - 1 ? ' disabled' : '') + '>下移</button> ';
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + ((c.menu_type || '') === 'admin' ? '后台(admin)' : '软件(app)') + '</td><td>' + (c.sort_order != null ? c.sort_order : 0) + '</td><td>' + (c.route_path || '') + '</td><td>' + (c.created_at || '') + '</td><td>' +
moveButtons +
'<button class="btn btn-sm" data-column-edit="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '" data-key="' + (c.column_key || '').replace(/"/g, '&quot;') + '" data-route="' + (c.route_path || '').replace(/"/g, '&quot;') + '" data-menu-type="' + (c.menu_type || 'app').replace(/"/g, '&quot;') + '" data-sort-order="' + (c.sort_order != null ? String(c.sort_order) : '0').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-column-delete="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '">删除</button></td></tr>';
}).join('');
}
bindColumnActions();
})
.catch(function () {
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
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(/&quot;/g, '"');
document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/&quot;/g, '"');
document.getElementById('editColumnRoutePath').value = (btn.dataset.route || '').replace(/&quot;/g, '"');
document.getElementById('editColumnMenuType').value = ((btn.dataset.menuType || 'app').replace(/&quot;/g, '"') || 'app');
document.getElementById('editColumnSortOrder').value = ((btn.dataset.sortOrder || '0').replace(/&quot;/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(/&quot;/g, '"') + '”吗?')) return;
fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); }
else { alert(res.error || '删除失败'); }
});
};
});
}
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 = '<span>共' + total + ' 条</span>' +
'<button ' + (page <= 1 ? 'disabled' : '') + ' data-p="' + (page - 1) + '">上一页</button>' +
'<span>第' + page + ' / ' + totalPages + ' 页</span>' +
'<button ' + (page >= totalPages ? 'disabled' : '') + ' data-p="' + (page + 1) + '">下一页</button>';
el.querySelectorAll('[data-p]').forEach(function (b) {
if (!b.disabled) b.onclick = function () { onPage(parseInt(b.dataset.p, 10)); };
});
}
// 初始化
var adminCurrentUserPromise = null;
function loadAdminCurrentUser() {
if (adminCurrentUserPromise) return adminCurrentUserPromise;
adminCurrentUserPromise = fetch('/api/admin/current-user')
.then(function (r) {
if (r.status === 404) {
return fetch('/api/auth/check')
.then(function (fallbackResp) { return fallbackResp.json(); })
.then(function (fallbackRes) {
if (fallbackRes && fallbackRes.logged_in) {
return {
success: true,
item: {
username: '当前用户',
role: ''
}
};
}
return { success: false };
});
}
return r.json();
})
.then(function (res) {
var nameEl = document.getElementById('adminCurrentUsername');
var roleEl = document.getElementById('adminCurrentUserRole');
if (!res.success) {
nameEl.textContent = '未登录';
roleEl.style.display = 'none';
return;
}
var item = res.item || {};
currentUserId = item.id || currentUserId;
currentUserRole = item.role || currentUserRole;
currentUserUsername = item.username || currentUserUsername;
nameEl.textContent = item.username || '管理员';
if (item.role) {
roleEl.textContent = item.role === 'super_admin'
? '超级管理员'
: (item.role === 'admin' ? '管理员' : '普通账号');
roleEl.style.display = 'inline-block';
} else {
roleEl.style.display = 'none';
}
updateShopManageGroupButtonsAccess();
updateImageVideoPermissionAccess();
})
.catch(function () {
return fetch('/api/auth/check')
.then(function (r) { return r.json(); })
.then(function (res) {
document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录';
document.getElementById('adminCurrentUserRole').style.display = 'none';
updateShopManageGroupButtonsAccess();
updateImageVideoPermissionAccess();
})
.catch(function () {
document.getElementById('adminCurrentUsername').textContent = '当前用户';
document.getElementById('adminCurrentUserRole').style.display = 'none';
updateShopManageGroupButtonsAccess();
updateImageVideoPermissionAccess();
});
})
.finally(function () {
if (!currentUserId && currentUserRole !== 'super_admin') {
adminCurrentUserPromise = null;
}
});
return adminCurrentUserPromise;
}
document.getElementById('btnAdminLogout').onclick = function () {
fetch('/api/admin/logout', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) {
if (r.status === 404) {
window.location.href = '/logout';
return null;
}
return r.json();
})
.then(function (res) {
if (!res) return;
if (!res.success) {
alert(res.error || '退出失败');
return;
}
window.location.replace(res.redirect || '/login?logout=1');
})
.catch(function () {
window.location.replace('/logout');
});
};
loadAdminCurrentUser().finally(function () {
loadUserOptions();
loadShopManageGroups();
loadAdminMenus();
});
}) ();