6087 lines
356 KiB
JavaScript
6087 lines
356 KiB
JavaScript
(function () {
|
||
// 全局请求动画:拦截本页所有 fetch 和 XMLHttpRequest 请求。
|
||
(function installRequestLoadingInterceptor() {
|
||
if (window.__adminRequestLoadingInstalled) return;
|
||
window.__adminRequestLoadingInstalled = true;
|
||
var activeRequests = 0;
|
||
var showTimer = null;
|
||
var hideTimer = null;
|
||
var showDelayMs = 180;
|
||
var loadingEl = document.getElementById('requestLoading');
|
||
var loadingBarEl = document.getElementById('requestLoadingBar');
|
||
|
||
function showRequestLoading() {
|
||
if (hideTimer) {
|
||
clearTimeout(hideTimer);
|
||
hideTimer = null;
|
||
}
|
||
if (!loadingEl || !loadingBarEl) return;
|
||
loadingBarEl.classList.remove('finishing');
|
||
loadingBarEl.classList.add('show');
|
||
loadingEl.classList.add('show');
|
||
}
|
||
|
||
function hideRequestLoading() {
|
||
if (showTimer) {
|
||
clearTimeout(showTimer);
|
||
showTimer = null;
|
||
}
|
||
if (!loadingEl || !loadingBarEl) return;
|
||
loadingBarEl.classList.remove('show');
|
||
loadingBarEl.classList.add('finishing');
|
||
loadingEl.classList.remove('show');
|
||
hideTimer = setTimeout(function () {
|
||
loadingBarEl.classList.remove('finishing');
|
||
}, 220);
|
||
}
|
||
|
||
function beginRequest() {
|
||
activeRequests += 1;
|
||
if (activeRequests === 1) {
|
||
showTimer = setTimeout(showRequestLoading, showDelayMs);
|
||
}
|
||
}
|
||
|
||
function endRequest() {
|
||
activeRequests = Math.max(0, activeRequests - 1);
|
||
if (activeRequests === 0) {
|
||
hideRequestLoading();
|
||
}
|
||
}
|
||
|
||
if (window.fetch) {
|
||
var nativeFetch = window.fetch.bind(window);
|
||
window.fetch = function (input, init) {
|
||
var options = init || {};
|
||
var skipLoading = !!options.__skipLoading;
|
||
if (skipLoading) {
|
||
options = Object.assign({}, options);
|
||
delete options.__skipLoading;
|
||
} else {
|
||
beginRequest();
|
||
}
|
||
try {
|
||
return nativeFetch(input, options).finally(function () {
|
||
if (!skipLoading) endRequest();
|
||
});
|
||
} catch (err) {
|
||
if (!skipLoading) endRequest();
|
||
throw err;
|
||
}
|
||
};
|
||
}
|
||
|
||
if (window.XMLHttpRequest) {
|
||
var nativeOpen = XMLHttpRequest.prototype.open;
|
||
var nativeSend = XMLHttpRequest.prototype.send;
|
||
XMLHttpRequest.prototype.open = function () {
|
||
this.__adminSkipLoading = false;
|
||
return nativeOpen.apply(this, arguments);
|
||
};
|
||
XMLHttpRequest.prototype.send = function () {
|
||
if (!this.__adminSkipLoading) {
|
||
beginRequest();
|
||
this.addEventListener('loadend', endRequest, { once: true });
|
||
}
|
||
try {
|
||
return nativeSend.apply(this, arguments);
|
||
} catch (err) {
|
||
if (!this.__adminSkipLoading) endRequest();
|
||
throw err;
|
||
}
|
||
};
|
||
}
|
||
})();
|
||
|
||
// ===== 菜单分组(一级分类 + 二级菜单)=====
|
||
var ADMIN_MENU_GROUPS = [
|
||
{ key: 'account', title: '账号与权限', items: ['users', 'columns'] },
|
||
{ key: 'data', title: '数据管理', items: ['dedupe-total-data', 'invalid-asin-data', 'query-asin', 'product-categories'] },
|
||
{ key: 'shop', title: '店铺管理', items: ['shop-keys', 'shop-manage', 'skip-price-asin'] },
|
||
{ key: 'tasks', title: '任务中心', items: ['image-video-tasks', 'shop-data-crawl-tasks'] },
|
||
{ key: 'record', title: '记录与版本', items: ['history', 'version', 'digital-human-version'] }
|
||
];
|
||
var ADMIN_MENU_ICONS = {
|
||
'users': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>',
|
||
'columns': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="7" height="7" x="3" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="3" rx="1"></rect><rect width="7" height="7" x="14" y="14" rx="1"></rect><rect width="7" height="7" x="3" y="14" rx="1"></rect></svg>',
|
||
'dedupe-total-data': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"></path><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"></path><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"></path></svg>',
|
||
'invalid-asin-data': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>',
|
||
'shop-keys': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"></path><path d="m21 2-9.6 9.6"></path><circle cx="7.5" cy="15.5" r="5.5"></circle></svg>',
|
||
'shop-manage': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"></path><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"></path><path d="M2 7h20"></path><path d="M22 7v3a2 2 0 0 1-2 2 2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"></path></svg>',
|
||
'skip-price-asin': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="m4.9 4.9 14.2 14.2"></path></svg>',
|
||
'query-asin': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>',
|
||
'product-categories': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"></path></svg>',
|
||
'image-video-tasks': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m22 8-6 4 6 4V8Z"></path><rect width="14" height="12" x="2" y="6" rx="2" ry="2"></rect></svg>',
|
||
'shop-data-crawl-tasks': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M3 5v14a9 3 0 0 0 18 0V5"></path><path d="M3 12a9 3 0 0 0 18 0"></path></svg>',
|
||
'history': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path><path d="M12 7v5l4 2"></path></svg>',
|
||
'version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7.5 4.27 9 5.15"></path><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"></path><path d="M3.3 7 12 12l8.7-5"></path><path d="M12 22V12"></path></svg>',
|
||
'digital-human-version': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>'
|
||
};
|
||
var ADMIN_MENU_FALLBACK_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 8v8"></path><path d="M8 12h8"></path></svg>';
|
||
function adminMenuIcon(route) {
|
||
return ADMIN_MENU_ICONS[route] || ADMIN_MENU_FALLBACK_ICON;
|
||
}
|
||
|
||
// Tab 切换
|
||
var adminMenuEl = document.getElementById('adminMenu');
|
||
var activeAdminTabName = '';
|
||
var adminMenuByRoute = {};
|
||
var ADMIN_PANEL_MAP = {
|
||
'users': 'panel-users',
|
||
'columns': 'panel-columns',
|
||
'dedupe-total-data': 'panel-dedupe-total-data',
|
||
'invalid-asin-data': 'panel-invalid-asin-data',
|
||
'shop-keys': 'panel-shop-keys',
|
||
'shop-manage': 'panel-shop-manage',
|
||
'skip-price-asin': 'panel-skip-price-asin',
|
||
'query-asin': 'panel-query-asin',
|
||
'product-categories': 'panel-product-categories',
|
||
'image-video-tasks': 'panel-image-video-tasks',
|
||
'shop-data-crawl-tasks': 'panel-shop-data-crawl-tasks',
|
||
'history': 'panel-history',
|
||
'version': 'panel-version',
|
||
'digital-human-version': 'panel-digital-human-version'
|
||
};
|
||
function runTabLoader(tabName) {
|
||
if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); }
|
||
else if (tabName === 'columns') loadColumns();
|
||
else if (tabName === 'dedupe-total-data') {
|
||
loadDedupeTotalData(1);
|
||
loadDedupeGroupSummary();
|
||
}
|
||
else if (tabName === 'invalid-asin-data') {
|
||
loadShopManageGroups();
|
||
loadInvalidAsinData(1);
|
||
}
|
||
else if (tabName === 'shop-keys') loadShopKeys(1);
|
||
else if (tabName === 'shop-manage') loadShopManage(1);
|
||
else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1);
|
||
else if (tabName === 'query-asin') loadQueryAsin(1);
|
||
else if (tabName === 'product-categories') loadProductCategories();
|
||
else if (tabName === 'image-video-tasks') loadImageVideoTasks(1);
|
||
else if (tabName === 'shop-data-crawl-tasks') loadShopDataCrawlTasks(1);
|
||
else if (tabName === 'history') loadHistory(1);
|
||
else if (tabName === 'version') loadSoftwareVersions();
|
||
else if (tabName === 'digital-human-version') loadDigitalHumanVersions();
|
||
}
|
||
function hideAllAdminPanels() {
|
||
document.querySelectorAll('.tab-panel').forEach(function (panel) {
|
||
panel.classList.remove('active');
|
||
panel.style.display = 'none';
|
||
});
|
||
}
|
||
function getActiveAdminTabName() {
|
||
return activeAdminTabName;
|
||
}
|
||
function activateAdminTab(tabName) {
|
||
var panelId = ADMIN_PANEL_MAP[tabName];
|
||
var panel = panelId ? document.getElementById(panelId) : null;
|
||
if (!panel) return;
|
||
activeAdminTabName = tabName;
|
||
document.querySelectorAll('#adminMenu .tab').forEach(function (tab) {
|
||
var isActive = tab.dataset.tab === tabName;
|
||
tab.classList.toggle('active', isActive);
|
||
if (isActive) tab.setAttribute('aria-current', 'page');
|
||
else tab.removeAttribute('aria-current');
|
||
});
|
||
// 激活页面时自动展开所属分组(折叠由用户显式控制)
|
||
if (adminMenuEl) {
|
||
adminMenuEl.querySelectorAll('.menu-group').forEach(function (group) {
|
||
if (group.querySelector('.tab[data-tab="' + tabName + '"]')) {
|
||
group.classList.remove('is-collapsed');
|
||
var groupTitle = group.querySelector('.menu-group-title');
|
||
if (groupTitle) groupTitle.setAttribute('aria-expanded', 'true');
|
||
}
|
||
});
|
||
}
|
||
hideAllAdminPanels();
|
||
panel.style.display = '';
|
||
panel.classList.add('active');
|
||
var nameItem = adminMenuByRoute[tabName];
|
||
var titleEl = document.getElementById('adminPageTitle');
|
||
if (titleEl) titleEl.textContent = (nameItem && nameItem.name) || tabName;
|
||
runTabLoader(tabName);
|
||
}
|
||
function renderAdminTabs(items) {
|
||
var knownItems = (items || []).filter(function (item) {
|
||
return !!ADMIN_PANEL_MAP[item.route_path];
|
||
});
|
||
if (!knownItems.length) {
|
||
activeAdminTabName = '';
|
||
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">暂无可用菜单</div>';
|
||
hideAllAdminPanels();
|
||
return;
|
||
}
|
||
adminMenuByRoute = {};
|
||
knownItems.forEach(function (item) {
|
||
adminMenuByRoute[item.route_path] = item;
|
||
});
|
||
var groupsHtml = '';
|
||
ADMIN_MENU_GROUPS.forEach(function (group) {
|
||
var groupRoutes = group.items.filter(function (route) {
|
||
return !!adminMenuByRoute[route];
|
||
});
|
||
if (!groupRoutes.length) return;
|
||
var itemsHtml = groupRoutes.map(function (route) {
|
||
var item = adminMenuByRoute[route];
|
||
var label = (item.name || route).replace(/"/g, '"');
|
||
return '<button class="tab" type="button" data-tab="' + route + '" aria-controls="panel-' + route + '" title="' + label + '">' +
|
||
'<span class="adm-icon">' + adminMenuIcon(route) + '</span>' +
|
||
'<span class="adm-label">' + label + '</span></button>';
|
||
}).join('');
|
||
groupsHtml += '<div class="menu-group" data-menu-group="' + group.key + '">' +
|
||
'<button class="menu-group-title" type="button" aria-expanded="true">' + group.title +
|
||
'<span class="menu-group-chevron" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span>' +
|
||
'</button>' +
|
||
'<div class="menu-group-body">' + itemsHtml + '</div></div>';
|
||
});
|
||
if (!groupsHtml) {
|
||
activeAdminTabName = '';
|
||
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">暂无可用菜单</div>';
|
||
hideAllAdminPanels();
|
||
return;
|
||
}
|
||
if (adminMenuEl) adminMenuEl.innerHTML = groupsHtml;
|
||
document.querySelectorAll('#adminMenu .tab').forEach(function (tab) {
|
||
tab.onclick = function () {
|
||
activateAdminTab(tab.dataset.tab);
|
||
};
|
||
});
|
||
if (window.__initAdminMenuCollapse) window.__initAdminMenuCollapse();
|
||
var fallbackTab = activeAdminTabName && knownItems.some(function (item) { return item.route_path === activeAdminTabName; })
|
||
? activeAdminTabName
|
||
: knownItems[0].route_path;
|
||
activateAdminTab(fallbackTab);
|
||
}
|
||
function syncAdminMenuGroups() {
|
||
if (!adminMenuEl) return;
|
||
adminMenuEl.querySelectorAll('.menu-group').forEach(function (group) {
|
||
var visible = false;
|
||
group.querySelectorAll('.tab').forEach(function (tab) {
|
||
if (tab.style.display !== 'none') visible = true;
|
||
});
|
||
group.style.display = visible ? '' : 'none';
|
||
});
|
||
}
|
||
function loadAdminMenus(preferredTab) {
|
||
if (preferredTab) activeAdminTabName = preferredTab;
|
||
fetch('/api/admin/current-user/menus')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) {
|
||
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">菜单加载失败</div>';
|
||
hideAllAdminPanels();
|
||
return;
|
||
}
|
||
renderAdminTabs(res.items || []);
|
||
})
|
||
.catch(function () {
|
||
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">菜单加载失败</div>';
|
||
hideAllAdminPanels();
|
||
});
|
||
}
|
||
hideAllAdminPanels();
|
||
|
||
// ========== 用户管理 ==========
|
||
var userPage = 1, userPageSize = 15;
|
||
var currentUserId = null;
|
||
var currentUserRole = 'admin';
|
||
var currentUserUsername = '';
|
||
var adminsList = [];
|
||
function roleLabel(role) {
|
||
if (role === 'super_admin') return '超级管理员';
|
||
if (role === 'admin') return '管理员';
|
||
return '普通账号';
|
||
}
|
||
function buildUserListQuery(page) {
|
||
var q = 'page=' + (page || 1) + '&page_size=' + userPageSize;
|
||
var kw = (document.getElementById('searchUsername').value || '').trim();
|
||
if (kw) q += '&username=' + encodeURIComponent(kw);
|
||
var cby = document.getElementById('filterCreatedBy').value;
|
||
if (cby) q += '&created_by_id=' + encodeURIComponent(cby);
|
||
return q;
|
||
}
|
||
function loadUsers(page) {
|
||
userPage = page || 1;
|
||
fetch('/api/admin/users?' + buildUserListQuery(userPage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var tbody = document.getElementById('userListBody');
|
||
if (!res.success) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||
return;
|
||
}
|
||
currentUserId = res.current_user_id || null;
|
||
currentUserRole = res.current_user_role || 'admin';
|
||
currentUserUsername = (res.current_user_username || '').trim();
|
||
adminsList = res.admins || [];
|
||
var items = res.items || [];
|
||
if (items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无用户</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = items.map(function (u) {
|
||
return '<tr><td>' + u.id + '</td><td>' + (u.username || '') + '</td><td>' +
|
||
roleLabel(u.role || 'normal') + '</td><td>' + (u.creator_username || '-') + '</td><td>' + (u.created_at || '') + '</td><td>' +
|
||
'<button class="btn btn-sm" data-edit="' + u.id + '" data-user="' + (JSON.stringify(u).replace(/"/g, '"')) + '">编辑</button> ' +
|
||
'<button class="btn btn-sm btn-danger" data-delete="' + u.id + '" data-name="' + (u.username || '').replace(/"/g, '"') + '">删除</button>' +
|
||
'</td></tr>';
|
||
}).join('');
|
||
}
|
||
renderPagination('userPagination', res.total, res.page, res.page_size, loadUsers);
|
||
bindUserActions();
|
||
updateCreateFormByRole();
|
||
updateUserFilterByRole();
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('userListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
|
||
});
|
||
}
|
||
function updateUserFilterByRole() {
|
||
var grp = document.getElementById('filterCreatedByGroup');
|
||
var sel = document.getElementById('filterCreatedBy');
|
||
if (currentUserRole === 'super_admin') {
|
||
grp.style.display = 'block';
|
||
var cur = sel.value;
|
||
sel.innerHTML = '<option value="">全部</option>';
|
||
adminsList.forEach(function (a) {
|
||
var opt = document.createElement('option');
|
||
opt.value = a.id;
|
||
opt.textContent = a.username;
|
||
sel.appendChild(opt);
|
||
});
|
||
sel.value = cur || '';
|
||
} else {
|
||
grp.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function updateDedupeTotalDataAccess() {
|
||
var tab = document.querySelector('.tab[data-tab="dedupe-total-data"]');
|
||
var panel = document.getElementById('panel-dedupe-total-data');
|
||
var canUse = currentUserRole === 'super_admin' || (currentUserRole === 'admin' && currentUserUsername === '');
|
||
if (tab) tab.style.display = canUse ? '' : 'none';
|
||
if (panel) panel.style.display = canUse ? '' : 'none';
|
||
if (!canUse && tab && tab.classList.contains('active')) {
|
||
tab.classList.remove('active');
|
||
|
||
tab.removeAttribute('aria-current');
|
||
if (panel) panel.classList.remove('active');
|
||
var usersTab = document.querySelector('.tab[data-tab="users"]');
|
||
var usersPanel = document.getElementById('panel-users');
|
||
if (usersTab) { usersTab.classList.add('active'); usersTab.setAttribute('aria-current', 'page'); }
|
||
if (usersPanel) usersPanel.classList.add('active');
|
||
}
|
||
syncAdminMenuGroups();
|
||
}
|
||
function updateShopManageAccess() {
|
||
var tab = document.querySelector('.tab[data-tab="shop-manage"]');
|
||
var panel = document.getElementById('panel-shop-manage');
|
||
if (!tab || !panel) return;
|
||
var canUse = currentUserRole === 'super_admin' ||
|
||
!!currentUserAdminPermissionKeys['admin_shop_manage'] ||
|
||
!!currentUserAdminPermissionRoutes['shop-manage'];
|
||
tab.style.display = canUse ? '' : 'none';
|
||
panel.style.display = canUse ? '' : 'none';
|
||
if (!canUse && tab.classList.contains('active')) {
|
||
tab.classList.remove('active');
|
||
|
||
tab.removeAttribute('aria-current');
|
||
panel.classList.remove('active');
|
||
var usersTab = document.querySelector('.tab[data-tab="users"]');
|
||
var usersPanel = document.getElementById('panel-users');
|
||
if (usersTab) { usersTab.classList.add('active'); usersTab.setAttribute('aria-current', 'page'); }
|
||
if (usersPanel) usersPanel.classList.add('active');
|
||
}
|
||
syncAdminMenuGroups();
|
||
}
|
||
function loadCurrentUserAdminPermissions() {
|
||
currentUserAdminPermissionKeys = {};
|
||
currentUserAdminPermissionRoutes = {};
|
||
if (!currentUserId || currentUserRole === 'super_admin') {
|
||
updateShopManageAccess();
|
||
return;
|
||
}
|
||
fetch('/api/admin/user/' + currentUserId + '/column-permissions')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) {
|
||
updateShopManageAccess();
|
||
return;
|
||
}
|
||
(res.items || []).forEach(function (item) {
|
||
var columnKey = (item.column_key || '').trim();
|
||
var routePath = (item.route_path || '').trim();
|
||
if (columnKey) currentUserAdminPermissionKeys[columnKey] = true;
|
||
if (routePath) currentUserAdminPermissionRoutes[routePath] = true;
|
||
});
|
||
updateShopManageAccess();
|
||
})
|
||
.catch(function () {
|
||
updateShopManageAccess();
|
||
});
|
||
}
|
||
function applyTabAccess(tabName, canUse) {
|
||
var tab = document.querySelector('.tab[data-tab="' + tabName + '"]');
|
||
var panel = document.getElementById('panel-' + tabName);
|
||
if (tab) tab.style.display = canUse ? '' : 'none';
|
||
if (panel) panel.style.display = canUse ? '' : 'none';
|
||
if (!canUse && tab && tab.classList.contains('active')) {
|
||
tab.classList.remove('active');
|
||
|
||
tab.removeAttribute('aria-current');
|
||
if (panel) panel.classList.remove('active');
|
||
var usersTab = document.querySelector('.tab[data-tab="users"]');
|
||
var usersPanel = document.getElementById('panel-users');
|
||
if (usersTab) { usersTab.classList.add('active'); usersTab.setAttribute('aria-current', 'page'); }
|
||
if (usersPanel) usersPanel.classList.add('active');
|
||
}
|
||
syncAdminMenuGroups();
|
||
}
|
||
function hasAdminTabAccess(tabName) {
|
||
var config = ADMIN_TAB_ACCESS_CONFIG[tabName];
|
||
if (!config) return true;
|
||
if (currentUserRole === 'super_admin') return true;
|
||
if (config.superAdminOnly) return false;
|
||
return !!currentUserAdminPermissionKeys[config.columnKey] ||
|
||
!!currentUserAdminPermissionRoutes[config.routePath];
|
||
}
|
||
function refreshAdminTabAccess() {
|
||
Object.keys(ADMIN_TAB_ACCESS_CONFIG).forEach(function (tabName) {
|
||
applyTabAccess(tabName, hasAdminTabAccess(tabName));
|
||
});
|
||
}
|
||
function loadCurrentUserAdminPermissions() {
|
||
currentUserAdminPermissionKeys = {};
|
||
currentUserAdminPermissionRoutes = {};
|
||
if (!currentUserId || currentUserRole === 'super_admin') {
|
||
refreshAdminTabAccess();
|
||
return;
|
||
}
|
||
fetch('/api/admin/user/' + currentUserId + '/column-permissions')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) {
|
||
refreshAdminTabAccess();
|
||
return;
|
||
}
|
||
(res.items || []).forEach(function (item) {
|
||
var columnKey = (item.column_key || '').trim();
|
||
var routePath = (item.route_path || '').trim();
|
||
if (columnKey) currentUserAdminPermissionKeys[columnKey] = true;
|
||
if (routePath) currentUserAdminPermissionRoutes[routePath] = true;
|
||
});
|
||
refreshAdminTabAccess();
|
||
})
|
||
.catch(function () {
|
||
refreshAdminTabAccess();
|
||
});
|
||
}
|
||
var allColumnsList = [];
|
||
var columnPermissionCatalogReady = false;
|
||
var columnPermissionCatalogState = 'idle'; // idle | loading | ready | failed
|
||
var columnPermissionCatalogError = '';
|
||
var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' };
|
||
function fetchCatalogWithRetry(url, attempts) {
|
||
return fetch(url).then(function (r) {
|
||
if (!r.ok) throw new Error('请求失败(' + r.status + ')');
|
||
return r.json();
|
||
}).catch(function (error) {
|
||
if (attempts <= 1) throw error;
|
||
return new Promise(function (resolve) { setTimeout(resolve, 700); })
|
||
.then(function () { return fetchCatalogWithRetry(url, attempts - 1); });
|
||
});
|
||
}
|
||
function renderPermissionCatalogStatus() {
|
||
var wrap = document.getElementById('createColumnPermissionWrap');
|
||
if (!wrap) return;
|
||
if (columnPermissionCatalogState === 'loading') {
|
||
wrap.textContent = '菜单权限加载中…';
|
||
} else if (columnPermissionCatalogState === 'failed') {
|
||
wrap.innerHTML = '';
|
||
var tip = document.createElement('div');
|
||
tip.style.color = '#e5484d';
|
||
tip.style.fontSize = '13px';
|
||
tip.style.padding = '8px 0';
|
||
tip.textContent = '菜单权限加载失败' +
|
||
(columnPermissionCatalogError ? '(' + columnPermissionCatalogError + ')' : '') +
|
||
':';
|
||
var retryBtn = document.createElement('button');
|
||
retryBtn.type = 'button';
|
||
retryBtn.className = 'col-perm-retry';
|
||
retryBtn.textContent = '重新加载';
|
||
retryBtn.style.marginLeft = '6px';
|
||
retryBtn.style.padding = '2px 10px';
|
||
retryBtn.onclick = loadColumnsForPermission;
|
||
tip.appendChild(retryBtn);
|
||
wrap.appendChild(tip);
|
||
}
|
||
}
|
||
// Hierarchical permission editor: only direct IDs are submitted; descendants are visual inheritance.
|
||
function columnParentId(item) {
|
||
var value = item && item.parent_id != null ? item.parent_id : item && item.parentId;
|
||
var parsed = value == null || value === '' ? null : Number(value);
|
||
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||
}
|
||
function columnId(item) { return Number(item && item.id); }
|
||
function columnMenuType(item) {
|
||
return String(item && item.menu_type || 'app').toLowerCase() === 'admin' ? 'admin' : 'app';
|
||
}
|
||
function columnDescendantIds(id) {
|
||
var result = [], pending = [Number(id)];
|
||
while (pending.length) {
|
||
var parent = pending.shift();
|
||
allColumnsList.forEach(function (item) {
|
||
if (columnParentId(item) !== parent) return;
|
||
var childId = columnId(item);
|
||
if (result.indexOf(childId) < 0) {
|
||
result.push(childId);
|
||
pending.push(childId);
|
||
}
|
||
});
|
||
}
|
||
return result;
|
||
}
|
||
function columnTreeHasDirectDescendant(id, directIds) {
|
||
return columnDescendantIds(id).some(function (descendantId) { return !!directIds[descendantId]; });
|
||
}
|
||
function columnTreeFullyCovered(id, directIds) {
|
||
if (directIds[id]) return true;
|
||
var children = allColumnsList.filter(function (item) { return columnParentId(item) === Number(id); });
|
||
return children.length > 0 && children.every(function (child) {
|
||
return columnTreeFullyCovered(columnId(child), directIds);
|
||
});
|
||
}
|
||
var columnPermissionCatalogPending = null;
|
||
function loadColumnsForPermission() {
|
||
if (columnPermissionCatalogPending) return columnPermissionCatalogPending;
|
||
columnPermissionCatalogReady = false;
|
||
columnPermissionCatalogState = 'loading';
|
||
columnPermissionCatalogError = '';
|
||
renderPermissionCatalogStatus();
|
||
var ownedKeys = {}, ownedRoutes = {};
|
||
var ownPermissionRequest = (currentUserRole === 'admin' && currentUserId)
|
||
? fetchCatalogWithRetry('/api/admin/user/' + currentUserId + '/column-permissions', 3)
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '当前用户权限加载失败');
|
||
(res.items || []).forEach(function (item) {
|
||
if (item.column_key) ownedKeys[String(item.column_key).trim()] = true;
|
||
if (item.route_path) ownedRoutes[String(item.route_path).trim()] = true;
|
||
});
|
||
})
|
||
: Promise.resolve();
|
||
var catalogChain = ownPermissionRequest.then(function () { return fetchCatalogWithRetry('/api/admin/columns', 3); })
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '菜单列表加载失败');
|
||
var availableColumns = (res.items || []).filter(function (item) {
|
||
if (item.column_key === 'admin_image_video_task_data' ||
|
||
item.column_key === 'admin_shop_data_crawl_task_data') return false;
|
||
return true;
|
||
});
|
||
if (currentUserRole === 'admin') {
|
||
var columnsById = {}, grantableIds = {}, visibleIds = {};
|
||
availableColumns.forEach(function (item) {
|
||
var id = columnId(item);
|
||
columnsById[id] = item;
|
||
if (ownedKeys[String(item.column_key || '').trim()] || ownedRoutes[String(item.route_path || '').trim()]) {
|
||
grantableIds[id] = true;
|
||
}
|
||
});
|
||
Object.keys(grantableIds).forEach(function (id) {
|
||
var currentId = Number(id), visited = {};
|
||
while (currentId > 0 && !visited[currentId] && columnsById[currentId]) {
|
||
visited[currentId] = true;
|
||
visibleIds[currentId] = true;
|
||
currentId = columnParentId(columnsById[currentId]);
|
||
}
|
||
});
|
||
allColumnsList = availableColumns.filter(function (item) {
|
||
return !!visibleIds[columnId(item)];
|
||
}).map(function (item) {
|
||
item._structureOnly = !grantableIds[columnId(item)];
|
||
return item;
|
||
});
|
||
} else {
|
||
allColumnsList = availableColumns.map(function (item) {
|
||
item._structureOnly = false;
|
||
return item;
|
||
});
|
||
}
|
||
columnPermissionCatalogReady = true;
|
||
columnPermissionCatalogState = 'ready';
|
||
renderColumnPermissionWrap('createColumnPermissionWrap');
|
||
renderColumnPermissionWrap('editColumnPermissionWrap');
|
||
populateColumnParentSelects();
|
||
}).catch(function (error) {
|
||
columnPermissionCatalogReady = false;
|
||
columnPermissionCatalogState = 'failed';
|
||
columnPermissionCatalogError = (error && error.message) ? error.message : '';
|
||
allColumnsList = [];
|
||
var editPermissionWrap = document.getElementById('editColumnPermissionWrap');
|
||
if (editPermissionWrap) editPermissionWrap._permissionsReady = false;
|
||
renderColumnPermissionWrap('createColumnPermissionWrap');
|
||
renderColumnPermissionWrap('editColumnPermissionWrap');
|
||
renderPermissionCatalogStatus();
|
||
}).finally(function () {
|
||
columnPermissionCatalogPending = null;
|
||
});
|
||
columnPermissionCatalogPending = catalogChain;
|
||
return catalogChain;
|
||
}
|
||
function renderColumnCards(wrapId) {
|
||
var wrap = document.getElementById(wrapId);
|
||
var cardsEl = document.getElementById(columnCardsContainerMap[wrapId]);
|
||
if (!wrap || !cardsEl) return;
|
||
var directIds = wrap._directIds || {};
|
||
cardsEl.innerHTML = '';
|
||
allColumnsList.forEach(function (item) {
|
||
var id = columnId(item);
|
||
if (!directIds[id] || item._structureOnly) return;
|
||
var card = document.createElement('span');
|
||
card.className = 'column-permission-card';
|
||
card.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')';
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'col-card-remove';
|
||
btn.setAttribute('aria-label', '移除');
|
||
btn.textContent = '×';
|
||
btn.onclick = function () {
|
||
delete directIds[id];
|
||
renderColumnPermissionWrap(wrapId);
|
||
};
|
||
card.appendChild(btn);
|
||
cardsEl.appendChild(card);
|
||
});
|
||
}
|
||
function renderColumnPermissionWrap(wrapId) {
|
||
var wrap = document.getElementById(wrapId);
|
||
if (!wrap) return;
|
||
var directIds = wrap._directIds || {};
|
||
var collapsedIds = wrap._collapsedIds || {};
|
||
var collapsedMenuTypes = wrap._collapsedMenuTypes || {};
|
||
var previousScrollTop = wrap.scrollTop;
|
||
wrap._directIds = directIds;
|
||
wrap._collapsedIds = collapsedIds;
|
||
wrap._collapsedMenuTypes = collapsedMenuTypes;
|
||
wrap.innerHTML = '';
|
||
if (!allColumnsList.length) {
|
||
wrap.textContent = '暂无可分配菜单';
|
||
renderColumnCards(wrapId);
|
||
return;
|
||
}
|
||
var byParent = {};
|
||
allColumnsList.forEach(function (item) {
|
||
var parent = columnParentId(item);
|
||
var key = parent == null ? 'root' : String(parent);
|
||
(byParent[key] || (byParent[key] = [])).push(item);
|
||
});
|
||
if (!wrap._collapseStateInitialized) {
|
||
allColumnsList.forEach(function (item) {
|
||
var id = columnId(item);
|
||
if ((byParent[String(id)] || []).length) collapsedIds[id] = true;
|
||
});
|
||
wrap._collapseStateInitialized = true;
|
||
}
|
||
if (!wrap._menuTypeCollapseInitialized) {
|
||
collapsedMenuTypes.admin = true;
|
||
collapsedMenuTypes.app = true;
|
||
wrap._menuTypeCollapseInitialized = true;
|
||
}
|
||
function renderNode(item, depth, inherited) {
|
||
var id = columnId(item);
|
||
var itemMenuType = columnMenuType(item);
|
||
var children = (byParent[String(id)] || []).filter(function (child) {
|
||
return columnMenuType(child) === itemMenuType;
|
||
});
|
||
var structureOnly = item._structureOnly === true;
|
||
var effective = inherited || !!directIds[id];
|
||
var covered = effective || columnTreeFullyCovered(id, directIds);
|
||
var node = document.createElement('div');
|
||
node.className = 'column-tree-node';
|
||
node.setAttribute('data-depth', String(depth));
|
||
var row = document.createElement('div');
|
||
row.className = 'column-tree-row';
|
||
if (children.length) {
|
||
var toggle = document.createElement('button');
|
||
var collapsed = !!collapsedIds[id];
|
||
toggle.type = 'button';
|
||
toggle.className = 'column-tree-toggle';
|
||
toggle.setAttribute('aria-expanded', String(!collapsed));
|
||
toggle.setAttribute('aria-label', (collapsed ? '展开' : '收起') + (item.name || '菜单'));
|
||
toggle.title = collapsed ? '展开子菜单' : '收起子菜单';
|
||
toggle.onclick = function () {
|
||
if (collapsedIds[id]) delete collapsedIds[id];
|
||
else collapsedIds[id] = true;
|
||
renderColumnPermissionWrap(wrapId);
|
||
};
|
||
row.appendChild(toggle);
|
||
} else {
|
||
var spacer = document.createElement('span');
|
||
spacer.className = 'column-tree-toggle-spacer';
|
||
spacer.setAttribute('aria-hidden', 'true');
|
||
row.appendChild(spacer);
|
||
}
|
||
var label = document.createElement('label');
|
||
var checkbox = document.createElement('input');
|
||
checkbox.type = 'checkbox';
|
||
checkbox.checked = covered;
|
||
checkbox.indeterminate = !covered && columnTreeHasDirectDescendant(id, directIds);
|
||
checkbox.disabled = inherited || structureOnly;
|
||
checkbox.onchange = function () {
|
||
if (structureOnly) return;
|
||
if (checkbox.checked) {
|
||
directIds[id] = true;
|
||
columnDescendantIds(id).forEach(function (descendantId) { delete directIds[descendantId]; });
|
||
} else {
|
||
delete directIds[id];
|
||
columnDescendantIds(id).forEach(function (descendantId) { delete directIds[descendantId]; });
|
||
}
|
||
renderColumnPermissionWrap(wrapId);
|
||
};
|
||
var text = document.createElement('span');
|
||
text.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')';
|
||
if (inherited || structureOnly) text.className = 'inherited-label';
|
||
label.appendChild(checkbox);
|
||
label.appendChild(text);
|
||
row.appendChild(label);
|
||
node.appendChild(row);
|
||
if (children.length) {
|
||
var childrenWrap = document.createElement('div');
|
||
childrenWrap.className = 'column-tree-children';
|
||
childrenWrap.hidden = !!collapsedIds[id];
|
||
children.forEach(function (child) {
|
||
childrenWrap.appendChild(renderNode(child, depth + 1, effective));
|
||
});
|
||
node.appendChild(childrenWrap);
|
||
}
|
||
return node;
|
||
}
|
||
[
|
||
{ key: 'admin', label: '后台菜单' },
|
||
{ key: 'app', label: 'APP(软件)菜单' }
|
||
].forEach(function (group) {
|
||
var groupItems = allColumnsList.filter(function (item) {
|
||
return columnMenuType(item) === group.key;
|
||
});
|
||
if (!groupItems.length) return;
|
||
var section = document.createElement('div');
|
||
section.className = 'column-tree-group';
|
||
var groupToggle = document.createElement('button');
|
||
var groupCollapsed = !!collapsedMenuTypes[group.key];
|
||
groupToggle.type = 'button';
|
||
groupToggle.className = 'column-tree-group-toggle';
|
||
groupToggle.setAttribute('aria-expanded', String(!groupCollapsed));
|
||
groupToggle.setAttribute('aria-label', (groupCollapsed ? '展开' : '收起') + group.label);
|
||
var groupName = document.createElement('span');
|
||
groupName.className = 'column-tree-group-name';
|
||
groupName.textContent = group.label;
|
||
var groupCount = document.createElement('span');
|
||
groupCount.className = 'column-tree-group-count';
|
||
groupCount.textContent = groupItems.length + ' 项';
|
||
groupToggle.appendChild(groupName);
|
||
groupToggle.appendChild(groupCount);
|
||
groupToggle.onclick = function () {
|
||
if (collapsedMenuTypes[group.key]) delete collapsedMenuTypes[group.key];
|
||
else collapsedMenuTypes[group.key] = true;
|
||
renderColumnPermissionWrap(wrapId);
|
||
};
|
||
section.appendChild(groupToggle);
|
||
var groupBody = document.createElement('div');
|
||
groupBody.className = 'column-tree-group-body';
|
||
groupBody.hidden = groupCollapsed;
|
||
groupItems.filter(function (item) {
|
||
var parentId = columnParentId(item);
|
||
if (parentId == null) return true;
|
||
var parent = allColumnsList.find(function (candidate) {
|
||
return columnId(candidate) === parentId;
|
||
});
|
||
return !parent || columnMenuType(parent) !== group.key;
|
||
}).forEach(function (item) {
|
||
groupBody.appendChild(renderNode(item, 0, false));
|
||
});
|
||
section.appendChild(groupBody);
|
||
wrap.appendChild(section);
|
||
});
|
||
renderColumnCards(wrapId);
|
||
wrap.scrollTop = previousScrollTop;
|
||
}
|
||
function getSelectedColumnIds(wrapId) {
|
||
var wrap = document.getElementById(wrapId);
|
||
return wrap && wrap._directIds ? Object.keys(wrap._directIds).map(Number).filter(function (id) {
|
||
var item = allColumnsList.find(function (column) { return columnId(column) === id; });
|
||
return id > 0 && item && !item._structureOnly;
|
||
}) : [];
|
||
}
|
||
function setColumnPermissionCheckboxes(wrapId, columnIds) {
|
||
var wrap = document.getElementById(wrapId);
|
||
if (!wrap) return;
|
||
wrap._directIds = {};
|
||
(columnIds || []).forEach(function (id) {
|
||
var parsed = Number(id);
|
||
if (Number.isFinite(parsed) && parsed > 0) wrap._directIds[parsed] = true;
|
||
});
|
||
wrap._permissionsReady = true;
|
||
renderColumnPermissionWrap(wrapId);
|
||
}
|
||
function populateColumnParentSelects() {
|
||
['columnParentId', 'editColumnParentId'].forEach(function (selectId) {
|
||
var select = document.getElementById(selectId);
|
||
if (!select) return;
|
||
var current = select.value;
|
||
var typeSelect = document.getElementById(selectId === 'editColumnParentId' ? 'editColumnMenuType' : 'columnMenuType');
|
||
var menuType = typeSelect ? (typeSelect.value || 'app') : 'app';
|
||
var editingId = selectId === 'editColumnParentId' ? Number(document.getElementById('editColumnId').value || 0) : 0;
|
||
var blockedIds = editingId > 0 ? [editingId].concat(columnDescendantIds(editingId)) : [];
|
||
select.innerHTML = '<option value="">无(根菜单)</option>';
|
||
allColumnsList.filter(function (item) {
|
||
return !item._structureOnly && String(item.menu_type || 'app') === String(menuType) && blockedIds.indexOf(columnId(item)) < 0;
|
||
}).forEach(function (item) {
|
||
var option = document.createElement('option');
|
||
option.value = item.id;
|
||
option.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')';
|
||
select.appendChild(option);
|
||
});
|
||
select.value = current;
|
||
});
|
||
}
|
||
function updateCreateFormByRole() {
|
||
var roleSel = document.getElementById('createRole');
|
||
var optAdmin = document.getElementById('optAdmin');
|
||
var formCreatedBy = document.getElementById('formGroupCreatedBy');
|
||
var selCreatedBy = document.getElementById('createCreatedBy');
|
||
if (currentUserRole === 'super_admin') {
|
||
if (optAdmin) optAdmin.style.display = '';
|
||
formCreatedBy.style.display = (roleSel.value === 'normal') ? 'block' : 'none';
|
||
selCreatedBy.innerHTML = '<option value="">请选择管理员</option>';
|
||
adminsList.forEach(function (a) {
|
||
var opt = document.createElement('option');
|
||
opt.value = a.id;
|
||
opt.textContent = a.username;
|
||
selCreatedBy.appendChild(opt);
|
||
});
|
||
} else {
|
||
if (optAdmin) optAdmin.style.display = 'none';
|
||
roleSel.value = 'normal';
|
||
formCreatedBy.style.display = 'none';
|
||
}
|
||
}
|
||
function bindUserActions() {
|
||
document.querySelectorAll('[data-edit]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var raw = (btn.getAttribute('data-user') || '{}').replace(/"/g, '"');
|
||
var u;
|
||
try { u = JSON.parse(raw); } catch (e) { u = {}; }
|
||
document.getElementById('editUserId').value = u.id || '';
|
||
document.getElementById('editUsername').value = u.username || '';
|
||
document.getElementById('editPassword').value = '';
|
||
var editRole = document.getElementById('editRole');
|
||
var editFormGroupRole = document.getElementById('editFormGroupRole');
|
||
var editFormGroupCreator = document.getElementById('editFormGroupCreator');
|
||
var editCreatorName = document.getElementById('editCreatorName');
|
||
editFormGroupRole.style.display = (currentUserRole === 'super_admin' && u.role !== 'super_admin') ? 'block' : 'none';
|
||
editFormGroupCreator.style.display = (u.role === 'normal' && u.creator_username) ? 'block' : 'none';
|
||
editCreatorName.value = u.creator_username || '';
|
||
if (u.role !== 'super_admin') { editRole.value = u.role || 'normal'; }
|
||
document.getElementById('msgEdit').textContent = '';
|
||
var editPermissionWrap = document.getElementById('editColumnPermissionWrap');
|
||
editPermissionWrap._directIds = {};
|
||
editPermissionWrap._permissionsReady = false;
|
||
renderColumnPermissionWrap('editColumnPermissionWrap');
|
||
if (!columnPermissionCatalogReady && (columnPermissionCatalogState === 'failed' || columnPermissionCatalogState === 'idle')) {
|
||
loadColumnsForPermission();
|
||
}
|
||
fetch('/api/admin/user/' + (u.id) + '/columns')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (String(document.getElementById('editUserId').value || '') !== String(u.id || '')) return;
|
||
if (!res.success || !Array.isArray(res.column_ids)) {
|
||
throw new Error(res.error || '权限加载失败');
|
||
}
|
||
setColumnPermissionCheckboxes('editColumnPermissionWrap', res.column_ids);
|
||
})
|
||
.catch(function (error) {
|
||
if (String(document.getElementById('editUserId').value || '') !== String(u.id || '')) return;
|
||
var msgEl = document.getElementById('msgEdit');
|
||
msgEl.textContent = error.message || '权限加载失败';
|
||
msgEl.className = 'msg err';
|
||
});
|
||
document.getElementById('editUserModal').classList.add('show');
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
if (!confirm('确定删除用户 "' + (btn.dataset.name || '') + '" 吗?')) return;
|
||
fetch('/api/admin/user/' + btn.dataset.delete, { method: 'DELETE' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) { loadUsers(userPage); }
|
||
else { alert(res.error || '删除失败'); }
|
||
});
|
||
};
|
||
});
|
||
}
|
||
|
||
document.getElementById('btnSearchUsers').onclick = function () { loadUsers(1); };
|
||
document.getElementById('createRole').onchange = function () { updateCreateFormByRole(); };
|
||
document.getElementById('btnCreate').onclick = function () {
|
||
var username = (document.getElementById('username').value || '').trim();
|
||
var password = document.getElementById('password').value || '';
|
||
var role = document.getElementById('createRole').value || 'normal';
|
||
var createdById = document.getElementById('createCreatedBy').value ? parseInt(document.getElementById('createCreatedBy').value, 10) : null;
|
||
var msgEl = document.getElementById('msgCreate');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!username || username.length < 2) {
|
||
msgEl.textContent = '用户名至少 2 个字符';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (!password || password.length < 6) {
|
||
msgEl.textContent = '密码至少 6 个字符';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (!columnPermissionCatalogReady) {
|
||
if (columnPermissionCatalogState === 'failed' || columnPermissionCatalogState === 'idle') {
|
||
loadColumnsForPermission();
|
||
msgEl.textContent = '菜单权限加载失败,已重新加载,请稍后重试';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
msgEl.textContent = '菜单权限尚未加载(加载中),请稍后重试';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var body = { username: username, password: password, role: role };
|
||
if (role === 'normal' && currentUserRole === 'super_admin' && createdById) body.created_by_id = createdById;
|
||
body.column_ids = getSelectedColumnIds('createColumnPermissionWrap');
|
||
fetch('/api/admin/user', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
msgEl.textContent = res.msg || '创建成功';
|
||
msgEl.classList.add('ok');
|
||
document.getElementById('username').value = '';
|
||
document.getElementById('password').value = '';
|
||
loadUsers(1);
|
||
} else {
|
||
msgEl.textContent = res.error || '创建失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.classList.add('err');
|
||
});
|
||
};
|
||
|
||
document.getElementById('btnSaveUser').onclick = function () {
|
||
var uid = document.getElementById('editUserId').value;
|
||
var password = document.getElementById('editPassword').value;
|
||
var editRoleEl = document.getElementById('editRole');
|
||
var msgEl = document.getElementById('msgEdit');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
var editPermissionWrap = document.getElementById('editColumnPermissionWrap');
|
||
if (!columnPermissionCatalogReady || !editPermissionWrap._permissionsReady) {
|
||
msgEl.textContent = '用户权限尚未加载,不能保存';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var body = {};
|
||
if (password) body.password = password;
|
||
if (currentUserRole === 'super_admin' && editRoleEl && editRoleEl.offsetParent !== null)
|
||
body.role = editRoleEl.value || 'normal';
|
||
body.column_ids = getSelectedColumnIds('editColumnPermissionWrap');
|
||
fetch('/api/admin/user/' + uid, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
msgEl.textContent = res.msg || '保存成功';
|
||
msgEl.classList.add('ok');
|
||
document.getElementById('editUserModal').classList.remove('show');
|
||
loadUsers(userPage);
|
||
if (String(uid || '') === String(currentUserId || '')) {
|
||
loadAdminMenus(getActiveAdminTabName());
|
||
}
|
||
} else {
|
||
msgEl.textContent = res.error || '保存失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
});
|
||
};
|
||
document.getElementById('btnCloseEdit').onclick = function () {
|
||
document.getElementById('editUserModal').classList.remove('show');
|
||
};
|
||
|
||
// ========== 视频任务管理 ==========
|
||
var imageVideoTaskPage = 1, imageVideoTaskPageSize = 20;
|
||
var imageVideoCards = [];
|
||
var selectedImageVideoKeys = new Set();
|
||
var imageVideoDownloadInProgress = false;
|
||
var imageVideoPermissionUsers = [];
|
||
var imageVideoPermissionInitialUserIds = new Set();
|
||
var selectedImageVideoPermissionUserIds = new Set();
|
||
var imageVideoPermissionView = 'granted';
|
||
function updateImageVideoPermissionAccess() {
|
||
var button = document.getElementById('btnOpenImageVideoPermissions');
|
||
if (button) button.style.display = currentUserRole === 'super_admin' ? 'inline-flex' : 'none';
|
||
var shopButton = document.getElementById('btnOpenShopDataTaskPermissions');
|
||
if (shopButton) shopButton.style.display = currentUserRole === 'super_admin' ? 'inline-flex' : 'none';
|
||
}
|
||
function imageVideoPermissionUsersForView() {
|
||
if (imageVideoPermissionView === 'granted') {
|
||
return imageVideoPermissionUsers.filter(function (user) {
|
||
return imageVideoPermissionInitialUserIds.has(Number(user.id));
|
||
});
|
||
}
|
||
return imageVideoPermissionUsers;
|
||
}
|
||
function syncImageVideoPermissionTabs() {
|
||
document.getElementById('imageVideoPermissionGrantedCount').textContent = '(' + imageVideoPermissionInitialUserIds.size + ')';
|
||
document.getElementById('imageVideoPermissionAllCount').textContent = '(' + imageVideoPermissionUsers.length + ')';
|
||
document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) {
|
||
var active = tab.dataset.imageVideoPermissionView === imageVideoPermissionView;
|
||
tab.classList.toggle('active', active);
|
||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
});
|
||
}
|
||
function filteredImageVideoPermissionUsers() {
|
||
var users = imageVideoPermissionUsersForView();
|
||
var keyword = (document.getElementById('imageVideoPermissionSearch').value || '').trim().toLowerCase();
|
||
if (!keyword) return users;
|
||
return users.filter(function (user) {
|
||
return String(user.username || '').toLowerCase().indexOf(keyword) >= 0;
|
||
});
|
||
}
|
||
function syncImageVideoPermissionSelectAll() {
|
||
var visibleUsers = filteredImageVideoPermissionUsers();
|
||
var selectedCount = visibleUsers.filter(function (user) {
|
||
return selectedImageVideoPermissionUserIds.has(Number(user.id));
|
||
}).length;
|
||
var selectAll = document.getElementById('imageVideoPermissionSelectAll');
|
||
selectAll.checked = visibleUsers.length > 0 && selectedCount === visibleUsers.length;
|
||
selectAll.indeterminate = selectedCount > 0 && selectedCount < visibleUsers.length;
|
||
selectAll.disabled = visibleUsers.length === 0;
|
||
}
|
||
function renderImageVideoPermissionUsers() {
|
||
var list = document.getElementById('imageVideoPermissionList');
|
||
var summary = document.getElementById('imageVideoPermissionSummary');
|
||
var visibleUsers = filteredImageVideoPermissionUsers();
|
||
syncImageVideoPermissionTabs();
|
||
var pendingCount = imageVideoPermissionUsers.filter(function (user) {
|
||
return imageVideoPermissionInitialUserIds.has(Number(user.id)) !== selectedImageVideoPermissionUserIds.has(Number(user.id));
|
||
}).length;
|
||
summary.textContent = imageVideoPermissionView === 'granted'
|
||
? '当前显示已保存分配用户,共 ' + visibleUsers.length + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '')
|
||
: '当前显示全部用户,已保存分配 ' + imageVideoPermissionInitialUserIds.size + ' 人' + (pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '');
|
||
list.innerHTML = visibleUsers.length ? visibleUsers.map(function (user) {
|
||
var userId = Number(user.id);
|
||
var persistedGranted = imageVideoPermissionInitialUserIds.has(userId);
|
||
var pendingGranted = selectedImageVideoPermissionUserIds.has(userId);
|
||
var pendingChanged = persistedGranted !== pendingGranted;
|
||
var actionText = pendingChanged
|
||
? (pendingGranted ? '待保存分配' : '待保存取消')
|
||
: (persistedGranted ? '取消分配' : '分配');
|
||
return '<div class="image-video-permission-row">' +
|
||
'<input type="checkbox" aria-label="' + escapeHtml(user.username || '-') + (persistedGranted ? ' 已保存分配' : ' 未分配') + '" data-image-video-permission-user="' + userId + '"' +
|
||
(pendingGranted ? ' checked' : '') + '>' +
|
||
'<span class="image-video-permission-user">' +
|
||
'<span class="image-video-permission-name" title="' + escapeHtml(user.username || '') + '">' + escapeHtml(user.username || '-') + '</span>' +
|
||
'<span class="image-video-permission-role">' + escapeHtml(roleLabel(user.role)) + '</span>' +
|
||
'</span>' +
|
||
'<button class="image-video-permission-state' + (pendingChanged ? ' pending' : (persistedGranted ? ' granted' : '')) + '" type="button" data-image-video-permission-toggle="' + userId + '">' +
|
||
actionText +
|
||
'</button>' +
|
||
'</div>';
|
||
}).join('') : '<div class="image-video-permission-empty">暂无匹配用户</div>';
|
||
syncImageVideoPermissionSelectAll();
|
||
}
|
||
function setImageVideoPermissionLoading(loading) {
|
||
document.getElementById('btnSaveImageVideoPermissions').disabled = loading;
|
||
document.getElementById('imageVideoPermissionSearch').disabled = loading;
|
||
if (loading) document.getElementById('imageVideoPermissionSelectAll').disabled = true;
|
||
}
|
||
function openImageVideoPermissions() {
|
||
if (currentUserRole !== 'super_admin') return;
|
||
var modal = document.getElementById('imageVideoPermissionModal');
|
||
var message = document.getElementById('imageVideoPermissionMessage');
|
||
modal.classList.add('show');
|
||
message.textContent = '';
|
||
message.className = 'msg';
|
||
imageVideoPermissionView = 'granted';
|
||
syncImageVideoPermissionTabs();
|
||
document.getElementById('imageVideoPermissionSearch').value = '';
|
||
document.getElementById('imageVideoPermissionList').innerHTML = '<div class="image-video-permission-empty">加载中...</div>';
|
||
setImageVideoPermissionLoading(true);
|
||
fetch('/api/admin/image-video-task-permissions')
|
||
.then(function (response) { return response.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '权限加载失败');
|
||
imageVideoPermissionUsers = res.items || [];
|
||
imageVideoPermissionInitialUserIds = new Set(imageVideoPermissionUsers.filter(function (user) {
|
||
return !!user.granted;
|
||
}).map(function (user) { return Number(user.id); }));
|
||
selectedImageVideoPermissionUserIds = new Set(imageVideoPermissionInitialUserIds);
|
||
renderImageVideoPermissionUsers();
|
||
})
|
||
.catch(function (error) {
|
||
imageVideoPermissionUsers = [];
|
||
selectedImageVideoPermissionUserIds.clear();
|
||
document.getElementById('imageVideoPermissionList').innerHTML = '<div class="image-video-permission-empty">' + escapeHtml(error.message || '权限加载失败') + '</div>';
|
||
})
|
||
.finally(function () { setImageVideoPermissionLoading(false); });
|
||
}
|
||
function closeImageVideoPermissions() {
|
||
document.getElementById('imageVideoPermissionModal').classList.remove('show');
|
||
}
|
||
function saveImageVideoPermissions() {
|
||
var message = document.getElementById('imageVideoPermissionMessage');
|
||
message.textContent = '保存中...';
|
||
message.className = 'msg';
|
||
setImageVideoPermissionLoading(true);
|
||
fetch('/api/admin/image-video-task-permissions', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ user_ids: Array.from(selectedImageVideoPermissionUserIds).sort(function (a, b) { return a - b; }) })
|
||
})
|
||
.then(function (response) { return response.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '权限保存失败');
|
||
imageVideoPermissionUsers.forEach(function (user) {
|
||
user.granted = selectedImageVideoPermissionUserIds.has(Number(user.id));
|
||
});
|
||
imageVideoPermissionInitialUserIds = new Set(selectedImageVideoPermissionUserIds);
|
||
renderImageVideoPermissionUsers();
|
||
message.textContent = res.msg || '保存成功';
|
||
message.className = 'msg ok';
|
||
})
|
||
.catch(function (error) {
|
||
message.textContent = error.message || '权限保存失败';
|
||
message.className = 'msg err';
|
||
})
|
||
.finally(function () {
|
||
setImageVideoPermissionLoading(false);
|
||
syncImageVideoPermissionSelectAll();
|
||
});
|
||
}
|
||
function buildImageVideoTaskQuery(page) {
|
||
var params = new URLSearchParams();
|
||
params.set('page', String(page || 1));
|
||
params.set('page_size', String(imageVideoTaskPageSize));
|
||
var values = {
|
||
username: document.getElementById('imageVideoFilterUsername').value.trim(),
|
||
submitted_from: document.getElementById('imageVideoFilterFrom').value,
|
||
submitted_to: document.getElementById('imageVideoFilterTo').value
|
||
};
|
||
Object.keys(values).forEach(function (key) { if (values[key]) params.set(key, values[key]); });
|
||
return params.toString();
|
||
}
|
||
var SHOP_DATA_STATUS_LABELS = {
|
||
'PENDING': '待执行', 'WAITING': '排队中', 'RUNNING': '执行中', 'POLLING': '处理中',
|
||
'SUCCESS': '已完成', 'COMPLETED': '已完成', 'DONE': '已完成',
|
||
'FAILED': '失败', 'ERROR': '失败', 'CANCELLED': '已取消', 'CANCELED': '已取消',
|
||
'DELETED': '已删除', 'STOPPED': '已停止'
|
||
};
|
||
function imageVideoStatusLabel(value) {
|
||
var status = String(value || '-').toUpperCase();
|
||
var label = SHOP_DATA_STATUS_LABELS[status];
|
||
if (label) return label;
|
||
if (status.indexOf('SUCCEED') === 0) return '已完成';
|
||
if (status.indexOf('FAIL') === 0 || status.indexOf('ERROR') === 0) return '失败';
|
||
if (status.indexOf('CANCEL') === 0) return '已取消';
|
||
if (status.indexOf('RUN') === 0 || status.indexOf('WAIT') === 0 || status.indexOf('PROCESS') === 0 || status.indexOf('POLL') === 0) return '处理中';
|
||
return status === '-' ? '-' : status;
|
||
}
|
||
function countryCodeLabel(value) {
|
||
var labels = { DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙' };
|
||
return labels[String(value || '').toUpperCase()] || value || '-';
|
||
}
|
||
function countryListLabel(codes) {
|
||
if (Array.isArray(codes)) {
|
||
return codes.map(countryCodeLabel).join('、') || '-';
|
||
}
|
||
return String(codes || '').split(/[,,]/).map(function (code) {
|
||
return countryCodeLabel(code.trim());
|
||
}).filter(Boolean).join('、') || '-';
|
||
}
|
||
function renderImageVideoStatus(value) {
|
||
var status = String(value || '-').toUpperCase();
|
||
return '<span class="image-video-status ' + escapeHtml(status) + '" title="' + escapeHtml(status) + '">' + escapeHtml(imageVideoStatusLabel(status)) + '</span>';
|
||
}
|
||
function safeAdminUrl(value) {
|
||
var url = String(value || '').trim();
|
||
return /^https?:\/\//i.test(url) ? url : '';
|
||
}
|
||
function imageVideoDownloadIcon() {
|
||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"></path></svg>';
|
||
}
|
||
function imageVideoUnavailableHtml(message) {
|
||
return '<div class="image-video-unavailable">' +
|
||
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 10 4.5-2.5v9L15 14"></path><rect x="3" y="6" width="12" height="12" rx="2"></rect><path d="m4 4 16 16"></path></svg>' +
|
||
'<strong>' + escapeHtml(message || '暂无可用视频') + '</strong>' +
|
||
'</div>';
|
||
}
|
||
function imageVideoCardKey(taskId, videoIndex) {
|
||
return String(taskId) + ':' + String(videoIndex);
|
||
}
|
||
function flattenImageVideoTasks(items) {
|
||
var cards = [];
|
||
(items || []).forEach(function (task) {
|
||
var videos = Array.isArray(task.videos) ? task.videos : [];
|
||
if (!videos.length) {
|
||
cards.push({ key: imageVideoCardKey(task.task_id, 'empty'), task: task, video: null, videoIndex: 0 });
|
||
return;
|
||
}
|
||
videos.forEach(function (video, index) {
|
||
cards.push({
|
||
key: imageVideoCardKey(task.task_id, index),
|
||
task: task,
|
||
video: {
|
||
sourceUrl: safeAdminUrl(video.source_url),
|
||
archivedUrl: safeAdminUrl(video.archived_url),
|
||
displayUrl: safeAdminUrl(video.display_url),
|
||
objectKey: String(video.object_key || ''),
|
||
archiveStatus: String(video.archive_status || ''),
|
||
archiveError: String(video.archive_error || '')
|
||
},
|
||
videoIndex: index
|
||
});
|
||
});
|
||
});
|
||
return cards;
|
||
}
|
||
function renderImageVideoCard(card) {
|
||
var task = card.task;
|
||
var video = card.video;
|
||
var hasVideo = !!(video && video.displayUrl);
|
||
var debugUrl = safeAdminUrl(task.debug_url);
|
||
var generatedAt = task.completed_at || task.submitted_at || '-';
|
||
var mediaHtml = hasVideo
|
||
? '<input class="image-video-card-check" type="checkbox" data-image-video-select="' + escapeHtml(card.key) + '" aria-label="选择任务 ' + escapeHtml(task.task_id) + ' 的视频 ' + (card.videoIndex + 1) + '">' +
|
||
'<video src="' + escapeHtml(video.displayUrl) + '" controls playsinline preload="metadata"></video>'
|
||
: imageVideoUnavailableHtml(task.status === 'FAILED' ? '任务失败,未生成视频' : '视频生成中或暂无结果');
|
||
return '<article class="image-video-card" data-image-video-card="' + escapeHtml(card.key) + '">' +
|
||
'<div class="image-video-media">' + mediaHtml + '</div>' +
|
||
'<div class="image-video-card-body">' +
|
||
'<div class="image-video-card-head">' +
|
||
'<div class="image-video-card-title" title="任务 ' + escapeHtml(task.task_id) + '">任务 ' + escapeHtml(task.task_id) + (video ? ' · 视频 ' + (card.videoIndex + 1) : '') + '</div>' +
|
||
renderImageVideoStatus(task.status) +
|
||
'</div>' +
|
||
'<div class="image-video-card-info">' +
|
||
'<div class="image-video-info-row"><label>用户名</label><span>' + escapeHtml(task.username || '-') + '</span></div>' +
|
||
'<div class="image-video-info-row"><label>所属分组</label><span>' + escapeHtml(task.group_name || '-') + '</span></div>' +
|
||
'<div class="image-video-info-row"><label>任务模式</label><span>' + escapeHtml(task.mode || '-') + '</span></div>' +
|
||
'<div class="image-video-info-row"><label>生成时间</label><span>' + escapeHtml(generatedAt) + '</span></div>' +
|
||
'<div class="image-video-info-row"><label>调试链接</label>' + (debugUrl
|
||
? '<button class="image-video-copy-link" type="button" data-image-video-copy-debug="' + escapeHtml(card.key) + '">复制链接</button>'
|
||
: '<span>-</span>') + '</div>' +
|
||
'</div>' +
|
||
'<div class="image-video-card-actions">' +
|
||
'<button class="image-video-card-action" type="button" data-image-video-download="' + escapeHtml(card.key) + '"' + (hasVideo ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</article>';
|
||
}
|
||
function syncImageVideoSelectionUi() {
|
||
var selectable = imageVideoCards.filter(function (card) { return !!(card.video && card.video.displayUrl); });
|
||
document.querySelectorAll('[data-image-video-card]').forEach(function (cardEl) {
|
||
var selected = selectedImageVideoKeys.has(cardEl.dataset.imageVideoCard);
|
||
cardEl.classList.toggle('selected', selected);
|
||
var checkbox = cardEl.querySelector('[data-image-video-select]');
|
||
if (checkbox) checkbox.checked = selected;
|
||
});
|
||
var selectedCount = selectable.filter(function (card) { return selectedImageVideoKeys.has(card.key); }).length;
|
||
var selectAll = document.getElementById('imageVideoSelectAll');
|
||
selectAll.checked = selectable.length > 0 && selectedCount === selectable.length;
|
||
selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length;
|
||
selectAll.disabled = imageVideoDownloadInProgress || selectable.length === 0;
|
||
var batchButton = document.getElementById('btnBatchDownloadImageVideos');
|
||
batchButton.disabled = imageVideoDownloadInProgress || selectedCount === 0;
|
||
batchButton.innerHTML = imageVideoDownloadIcon() + (imageVideoDownloadInProgress ? '处理中' : '批量下载' + (selectedCount ? ' (' + selectedCount + ')' : ''));
|
||
document.querySelectorAll('[data-image-video-download]').forEach(function (button) {
|
||
var card = imageVideoCards.find(function (item) { return item.key === button.dataset.imageVideoDownload; });
|
||
button.disabled = imageVideoDownloadInProgress || !card || !card.video || !card.video.displayUrl;
|
||
});
|
||
}
|
||
function renderImageVideoCards() {
|
||
var grid = document.getElementById('imageVideoTaskGrid');
|
||
grid.innerHTML = imageVideoCards.length
|
||
? imageVideoCards.map(renderImageVideoCard).join('')
|
||
: '<div class="image-video-empty">暂无符合条件的视频任务</div>';
|
||
grid.querySelectorAll('video').forEach(function (video) {
|
||
video.addEventListener('error', function () {
|
||
var media = video.closest('.image-video-media');
|
||
if (!media || media.querySelector('.image-video-unavailable')) return;
|
||
video.remove();
|
||
media.insertAdjacentHTML('beforeend', imageVideoUnavailableHtml('视频加载失败,可尝试下载'));
|
||
}, { once: true });
|
||
});
|
||
syncImageVideoSelectionUi();
|
||
}
|
||
function resetImageVideoSelection() {
|
||
selectedImageVideoKeys.clear();
|
||
document.getElementById('imageVideoDownloadProgress').textContent = '';
|
||
syncImageVideoSelectionUi();
|
||
}
|
||
function loadImageVideoTasks(page) {
|
||
imageVideoTaskPage = page || 1;
|
||
selectedImageVideoKeys.clear();
|
||
var grid = document.getElementById('imageVideoTaskGrid');
|
||
grid.innerHTML = '<div class="image-video-empty">加载中...</div>';
|
||
document.getElementById('imageVideoDownloadProgress').textContent = '';
|
||
fetch('/api/admin/image-video-tasks?' + buildImageVideoTaskQuery(imageVideoTaskPage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) {
|
||
imageVideoCards = [];
|
||
grid.innerHTML = '<div class="image-video-empty">加载失败:' + escapeHtml(res.error || '') + '</div>';
|
||
document.getElementById('imageVideoTaskTotal').textContent = '';
|
||
syncImageVideoSelectionUi();
|
||
return;
|
||
}
|
||
var items = res.items || [];
|
||
imageVideoCards = flattenImageVideoTasks(items);
|
||
var videoCount = imageVideoCards.filter(function (card) { return !!card.video; }).length;
|
||
document.getElementById('imageVideoTaskTotal').textContent = '共 ' + (res.total || 0) + ' 个任务 · 本页 ' + videoCount + ' 个视频';
|
||
renderImageVideoCards();
|
||
renderPagination('imageVideoTaskPagination', res.total, res.page, res.page_size, loadImageVideoTasks);
|
||
})
|
||
.catch(function () {
|
||
imageVideoCards = [];
|
||
grid.innerHTML = '<div class="image-video-empty">请求失败,请稍后重试</div>';
|
||
document.getElementById('imageVideoTaskTotal').textContent = '';
|
||
syncImageVideoSelectionUi();
|
||
});
|
||
}
|
||
function imageVideoFilename(card) {
|
||
var url = card.video.displayUrl || '';
|
||
var path = url.split(/[?#]/)[0];
|
||
var match = path.match(/\.([a-z0-9]{2,5})$/i);
|
||
return 'task-' + card.task.task_id + '-video-' + (card.videoIndex + 1) + '.' + (match ? match[1].toLowerCase() : 'mp4');
|
||
}
|
||
function triggerImageVideoLink(url, filename, newWindow) {
|
||
var link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = filename;
|
||
if (newWindow) {
|
||
link.target = '_blank';
|
||
link.rel = 'noreferrer';
|
||
}
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
}
|
||
function downloadImageVideo(card) {
|
||
var url = card && card.video ? card.video.displayUrl : '';
|
||
if (!url) return Promise.resolve({ fallback: false, skipped: true });
|
||
var filename = imageVideoFilename(card);
|
||
return fetch(url, { __skipLoading: true })
|
||
.then(function (response) {
|
||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||
return response.blob();
|
||
})
|
||
.then(function (blob) {
|
||
var objectUrl = URL.createObjectURL(blob);
|
||
triggerImageVideoLink(objectUrl, filename, false);
|
||
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
|
||
return { fallback: false, skipped: false };
|
||
})
|
||
.catch(function () {
|
||
triggerImageVideoLink(url, filename, true);
|
||
return { fallback: true, skipped: false };
|
||
});
|
||
}
|
||
function runImageVideoDownloads(cards) {
|
||
if (imageVideoDownloadInProgress || !cards.length) return;
|
||
imageVideoDownloadInProgress = true;
|
||
syncImageVideoSelectionUi();
|
||
var progress = document.getElementById('imageVideoDownloadProgress');
|
||
progress.textContent = '正在下载';
|
||
downloadImageVideo(cards[0]).then(function (result) {
|
||
progress.textContent = result.fallback ? '已在新窗口打开视频' : '下载已开始';
|
||
}).finally(function () {
|
||
imageVideoDownloadInProgress = false;
|
||
syncImageVideoSelectionUi();
|
||
});
|
||
}
|
||
function imageVideoZipFilename(response) {
|
||
var disposition = response.headers.get('Content-Disposition') || '';
|
||
var encoded = disposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||
if (encoded) {
|
||
try { return decodeURIComponent(encoded[1]); } catch (error) { }
|
||
}
|
||
var plain = disposition.match(/filename="?([^";]+)"?/i);
|
||
return plain ? plain[1] : 'video-tasks.zip';
|
||
}
|
||
function downloadImageVideoZip(cards) {
|
||
if (imageVideoDownloadInProgress || !cards.length) return;
|
||
imageVideoDownloadInProgress = true;
|
||
syncImageVideoSelectionUi();
|
||
var progress = document.getElementById('imageVideoDownloadProgress');
|
||
progress.textContent = '正在打包 ' + cards.length + ' 个视频...';
|
||
fetch('/api/admin/image-video-tasks/download-zip', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
items: cards.map(function (card) {
|
||
return { task_id: Number(card.task.task_id), video_index: card.videoIndex };
|
||
})
|
||
}),
|
||
__skipLoading: true
|
||
}).then(function (response) {
|
||
if (!response.ok) {
|
||
return response.json().catch(function () { return {}; }).then(function (data) {
|
||
throw new Error(data.error || '压缩包生成失败');
|
||
});
|
||
}
|
||
var filename = imageVideoZipFilename(response);
|
||
var errorCount = Number(response.headers.get('X-Archive-Error-Count') || 0);
|
||
return response.blob().then(function (blob) {
|
||
return { blob: blob, filename: filename, errorCount: errorCount };
|
||
});
|
||
}).then(function (result) {
|
||
var objectUrl = URL.createObjectURL(result.blob);
|
||
triggerImageVideoLink(objectUrl, result.filename, false);
|
||
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
|
||
progress.textContent = result.errorCount
|
||
? '压缩包已下载,' + result.errorCount + ' 个视频失败,详见包内错误清单'
|
||
: '压缩包下载已开始';
|
||
}).catch(function (error) {
|
||
progress.textContent = error.message || '压缩包下载失败';
|
||
}).finally(function () {
|
||
imageVideoDownloadInProgress = false;
|
||
syncImageVideoSelectionUi();
|
||
});
|
||
}
|
||
function copyImageVideoDebugUrl(card, button) {
|
||
var url = card ? safeAdminUrl(card.task.debug_url) : '';
|
||
if (!url) return;
|
||
var copyPromise;
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
copyPromise = navigator.clipboard.writeText(url);
|
||
} else {
|
||
copyPromise = new Promise(function (resolve, reject) {
|
||
var textarea = document.createElement('textarea');
|
||
textarea.value = url;
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.opacity = '0';
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
var copied = document.execCommand('copy');
|
||
textarea.remove();
|
||
if (copied) resolve();
|
||
else reject(new Error('copy failed'));
|
||
});
|
||
}
|
||
copyPromise.then(function () {
|
||
button.textContent = '已复制';
|
||
button.classList.add('copied');
|
||
setTimeout(function () {
|
||
button.textContent = '复制链接';
|
||
button.classList.remove('copied');
|
||
}, 1600);
|
||
}).catch(function () {
|
||
button.textContent = '复制失败';
|
||
setTimeout(function () { button.textContent = '复制链接'; }, 1600);
|
||
});
|
||
}
|
||
document.getElementById('btnFilterImageVideoTasks').onclick = function () { loadImageVideoTasks(1); };
|
||
document.getElementById('btnOpenImageVideoPermissions').onclick = openImageVideoPermissions;
|
||
document.getElementById('btnCloseImageVideoPermissions').onclick = closeImageVideoPermissions;
|
||
document.getElementById('btnCancelImageVideoPermissions').onclick = closeImageVideoPermissions;
|
||
document.getElementById('btnSaveImageVideoPermissions').onclick = saveImageVideoPermissions;
|
||
document.querySelectorAll('[data-image-video-permission-view]').forEach(function (tab) {
|
||
tab.onclick = function () {
|
||
imageVideoPermissionView = tab.dataset.imageVideoPermissionView || 'granted';
|
||
document.getElementById('imageVideoPermissionSearch').value = '';
|
||
renderImageVideoPermissionUsers();
|
||
};
|
||
});
|
||
document.getElementById('imageVideoPermissionSearch').oninput = renderImageVideoPermissionUsers;
|
||
document.getElementById('imageVideoPermissionSelectAll').onchange = function (event) {
|
||
filteredImageVideoPermissionUsers().forEach(function (user) {
|
||
var userId = Number(user.id);
|
||
if (event.target.checked) selectedImageVideoPermissionUserIds.add(userId);
|
||
else selectedImageVideoPermissionUserIds.delete(userId);
|
||
});
|
||
renderImageVideoPermissionUsers();
|
||
};
|
||
document.getElementById('imageVideoPermissionList').onclick = function (event) {
|
||
var toggle = event.target.closest('[data-image-video-permission-toggle]');
|
||
if (!toggle) return;
|
||
var userId = Number(toggle.dataset.imageVideoPermissionToggle);
|
||
if (selectedImageVideoPermissionUserIds.has(userId)) selectedImageVideoPermissionUserIds.delete(userId);
|
||
else selectedImageVideoPermissionUserIds.add(userId);
|
||
renderImageVideoPermissionUsers();
|
||
};
|
||
document.getElementById('imageVideoPermissionList').onchange = function (event) {
|
||
var checkbox = event.target.closest('[data-image-video-permission-user]');
|
||
if (!checkbox) return;
|
||
var userId = Number(checkbox.dataset.imageVideoPermissionUser);
|
||
if (checkbox.checked) selectedImageVideoPermissionUserIds.add(userId);
|
||
else selectedImageVideoPermissionUserIds.delete(userId);
|
||
renderImageVideoPermissionUsers();
|
||
};
|
||
document.getElementById('imageVideoPermissionModal').onclick = function (event) {
|
||
if (event.target === event.currentTarget) closeImageVideoPermissions();
|
||
};
|
||
document.getElementById('btnResetImageVideoTasks').onclick = function () {
|
||
['imageVideoFilterUsername', 'imageVideoFilterFrom', 'imageVideoFilterTo']
|
||
.forEach(function (id) { document.getElementById(id).value = ''; });
|
||
loadImageVideoTasks(1);
|
||
};
|
||
document.getElementById('imageVideoSelectAll').onchange = function (event) {
|
||
imageVideoCards.forEach(function (card) {
|
||
if (!card.video || !card.video.displayUrl) return;
|
||
if (event.target.checked) selectedImageVideoKeys.add(card.key);
|
||
else selectedImageVideoKeys.delete(card.key);
|
||
});
|
||
syncImageVideoSelectionUi();
|
||
};
|
||
document.getElementById('btnBatchDownloadImageVideos').onclick = function () {
|
||
downloadImageVideoZip(imageVideoCards.filter(function (card) { return selectedImageVideoKeys.has(card.key); }));
|
||
};
|
||
document.getElementById('imageVideoTaskGrid').onchange = function (event) {
|
||
var checkbox = event.target.closest('[data-image-video-select]');
|
||
if (!checkbox) return;
|
||
if (checkbox.checked) selectedImageVideoKeys.add(checkbox.dataset.imageVideoSelect);
|
||
else selectedImageVideoKeys.delete(checkbox.dataset.imageVideoSelect);
|
||
syncImageVideoSelectionUi();
|
||
};
|
||
document.getElementById('imageVideoTaskGrid').onclick = function (event) {
|
||
var copyButton = event.target.closest('[data-image-video-copy-debug]');
|
||
if (copyButton) {
|
||
var copyCard = imageVideoCards.find(function (item) { return item.key === copyButton.dataset.imageVideoCopyDebug; });
|
||
if (copyCard) copyImageVideoDebugUrl(copyCard, copyButton);
|
||
}
|
||
var downloadButton = event.target.closest('[data-image-video-download]');
|
||
if (downloadButton) {
|
||
var card = imageVideoCards.find(function (item) { return item.key === downloadButton.dataset.imageVideoDownload; });
|
||
if (card) runImageVideoDownloads([card]);
|
||
}
|
||
};
|
||
|
||
// ========== 店铺数据任务管理 ==========
|
||
var shopDataTaskPage = 1, shopDataTaskPageSize = 20;
|
||
var shopDataTaskGroups = [];
|
||
var shopDataTasks = [];
|
||
var selectedShopDataResultIds = new Set();
|
||
var shopDataDownloadInProgress = false;
|
||
var shopDataPermissionUsers = [];
|
||
var shopDataPermissionInitialUserIds = new Set();
|
||
var selectedShopDataPermissionUserIds = new Set();
|
||
var shopDataPermissionView = 'granted';
|
||
|
||
function buildShopDataTaskQuery(page) {
|
||
var params = new URLSearchParams();
|
||
params.set('page', String(page || 1));
|
||
params.set('page_size', String(shopDataTaskPageSize));
|
||
var values = {
|
||
shop_name: document.getElementById('shopDataTaskFilterShop').value.trim(),
|
||
group_name: document.getElementById('shopDataTaskFilterGroup').value.trim(),
|
||
created_from: document.getElementById('shopDataTaskFilterFrom').value,
|
||
created_to: document.getElementById('shopDataTaskFilterTo').value
|
||
};
|
||
Object.keys(values).forEach(function (key) {
|
||
if (values[key]) params.set(key, values[key]);
|
||
});
|
||
return params.toString();
|
||
}
|
||
|
||
function shopDataResultId(item) {
|
||
if (!item) return 0;
|
||
var value = item.result_id != null ? item.result_id : item.resultId;
|
||
var id = Number(value);
|
||
return isFinite(id) && id > 0 ? id : 0;
|
||
}
|
||
|
||
function shopDataBoolean(value) {
|
||
if (typeof value === 'string') {
|
||
return ['1', 'true', 'yes', 'y'].indexOf(value.toLowerCase()) >= 0;
|
||
}
|
||
return !!value;
|
||
}
|
||
|
||
function shopDataDateValue(value) {
|
||
if (!value) return 0;
|
||
var timestamp = Date.parse(String(value).replace(' ', 'T'));
|
||
return isNaN(timestamp) ? 0 : timestamp;
|
||
}
|
||
|
||
function shopDataResultSort(a, b) {
|
||
var dateDiff = shopDataDateValue(b.created_at || b.finished_at) - shopDataDateValue(a.created_at || a.finished_at);
|
||
if (dateDiff) return dateDiff;
|
||
return shopDataResultId(b) - shopDataResultId(a);
|
||
}
|
||
|
||
function shopDataGroupKey(item) {
|
||
var name = String((item && (item.shop_name || item.shop || item.source_filename)) || '').trim();
|
||
// Keep the same trimmed, case-insensitive key as the Flask grouping query.
|
||
return 'name:' + name.toLowerCase();
|
||
}
|
||
|
||
function shopDataNormalizeResult(group, raw) {
|
||
var result = {};
|
||
Object.keys(group || {}).forEach(function (key) {
|
||
if (key !== 'results' && key !== 'group_results') result[key] = group[key];
|
||
});
|
||
Object.keys(raw || {}).forEach(function (key) {
|
||
if (key !== 'results' && key !== 'group_results') result[key] = raw[key];
|
||
});
|
||
result.shop_name = result.shop_name || result.shop || result.source_filename || '';
|
||
result.shop_id = result.shop_id || result.shopId || result.source_file_url || '';
|
||
result.group_name = result.group_name || result.group || '';
|
||
result.status = result.status || result.task_status || result.file_status || '';
|
||
result.error = result.error || result.result_error || result.error_message || result.task_error || result.file_error || '';
|
||
result.output_filename = result.output_filename || result.result_filename || result.filename || '';
|
||
result.country_codes = result.country_codes || result.countryCodes || [];
|
||
if (result.file_size == null) result.file_size = result.result_file_size;
|
||
if (result.row_count == null) result.row_count = result.rows;
|
||
if (result.finished_at == null) result.finished_at = result.completed_at;
|
||
if (result.result_id == null && raw && raw.resultId != null) result.result_id = raw.resultId;
|
||
if (result.result_id == null && raw && raw.id != null && !Array.isArray(raw.results) && !Array.isArray(raw.group_results)) result.result_id = raw.id;
|
||
if (result.file_ready == null) {
|
||
result.file_ready = !!String(result.result_file_url || result.resultFileUrl || '').trim();
|
||
} else {
|
||
result.file_ready = shopDataBoolean(result.file_ready);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// The admin API now returns one group per shop. Keep a flat result list
|
||
// for selection/actions while rendering the grouped view.
|
||
function normalizeShopDataTaskGroups(items) {
|
||
var groups = [];
|
||
var byKey = Object.create(null);
|
||
(Array.isArray(items) ? items : []).forEach(function (rawGroup) {
|
||
if (!rawGroup || typeof rawGroup !== 'object') return;
|
||
var rawResults = Array.isArray(rawGroup.results)
|
||
? rawGroup.results
|
||
: (Array.isArray(rawGroup.group_results) ? rawGroup.group_results : [rawGroup]);
|
||
var groupBase = {};
|
||
Object.keys(rawGroup).forEach(function (key) {
|
||
if (key !== 'results' && key !== 'group_results') groupBase[key] = rawGroup[key];
|
||
});
|
||
if (!groupBase.shop_name && rawResults.length) {
|
||
groupBase.shop_name = rawResults[0].shop_name || rawResults[0].shop || rawResults[0].source_filename || '';
|
||
}
|
||
if (!groupBase.shop_id && rawResults.length) {
|
||
groupBase.shop_id = rawResults[0].shop_id || rawResults[0].shopId || rawResults[0].source_file_url || '';
|
||
}
|
||
var key = shopDataGroupKey(groupBase);
|
||
var group = byKey[key];
|
||
if (!group) {
|
||
group = {
|
||
key: key,
|
||
shop_name: groupBase.shop_name || '',
|
||
shop_id: groupBase.shop_id || '',
|
||
group_name: groupBase.group_name || groupBase.group || '',
|
||
latest_created_at: groupBase.latest_created_at || '',
|
||
results: []
|
||
};
|
||
byKey[key] = group;
|
||
groups.push(group);
|
||
}
|
||
rawResults.forEach(function (rawResult) {
|
||
if (!rawResult || typeof rawResult !== 'object') return;
|
||
var result = shopDataNormalizeResult(groupBase, rawResult);
|
||
var resultId = shopDataResultId(result);
|
||
if (!resultId || !result.file_ready) return;
|
||
if (resultId && group.results.some(function (existing) { return shopDataResultId(existing) === resultId; })) return;
|
||
group.results.push(result);
|
||
if (!group.shop_name) group.shop_name = result.shop_name || '';
|
||
if (!group.shop_id) group.shop_id = result.shop_id || '';
|
||
if (!group.group_name) group.group_name = result.group_name || '';
|
||
});
|
||
});
|
||
groups.forEach(function (group) {
|
||
group.results.sort(shopDataResultSort);
|
||
group.results = group.results.slice(0, 1);
|
||
if (!group.latest_created_at && group.results.length) {
|
||
group.latest_created_at = group.results[0].created_at || group.results[0].finished_at || '';
|
||
}
|
||
});
|
||
return groups.filter(function (group) { return group.results.length > 0; });
|
||
}
|
||
|
||
function shopDataDeleteIcon() {
|
||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18"></path><path d="M8 6V4h8v2"></path><path d="M19 6l-1 15H6L5 6"></path><path d="M10 11v6m4-6v6"></path></svg>';
|
||
}
|
||
|
||
function renderShopDataStatus(item, status) {
|
||
var normalized = String(status || '-').toUpperCase();
|
||
var errorTitle = item && item.error ? ' title="' + escapeHtml(item.error) + '"' : '';
|
||
return '<span class="image-video-status ' + escapeHtml(normalized) + '"' + errorTitle + '>' + escapeHtml(imageVideoStatusLabel(normalized)) + '</span>';
|
||
}
|
||
|
||
function renderShopDataTaskResult(item) {
|
||
var resultId = shopDataResultId(item);
|
||
var selected = resultId > 0 && selectedShopDataResultIds.has(resultId);
|
||
var status = String(item.status || item.file_status || '').toUpperCase();
|
||
var terminal = ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(status) >= 0;
|
||
var countryCodes = item.country_codes != null ? item.country_codes : item.countryCodes;
|
||
var countries = countryListLabel(countryCodes);
|
||
var filename = item.output_filename || '-';
|
||
var checkbox = '<input type="checkbox" data-shop-data-select="' + (resultId || '') + '"' +
|
||
(selected ? ' checked' : '') + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>';
|
||
return '<div class="shop-data-result' + (selected ? ' selected' : '') + '" data-shop-data-card="' + (resultId || '') + '">' +
|
||
'<div class="shop-data-result-head">' +
|
||
'<label class="shop-data-task-title">' + checkbox +
|
||
'<span title="任务 ' + escapeHtml(item.task_id || item.task_no || '-') + '">任务 ' + escapeHtml(item.task_id || item.task_no || '-') + '</span>' +
|
||
'</label>' +
|
||
renderShopDataStatus(item, status || item.file_status) +
|
||
'</div>' +
|
||
'<div class="image-video-card-info">' +
|
||
'<div class="image-video-info-row"><label>国家</label><span>' + escapeHtml(countries) + '</span></div>' +
|
||
'<div class="image-video-info-row"><label>文件</label><span title="' + escapeHtml(filename) + '">' + escapeHtml(filename) + '</span></div>' +
|
||
'</div>' +
|
||
'<div class="image-video-card-actions">' +
|
||
'<button class="image-video-card-action" type="button" data-shop-data-download="' + (resultId || '') + '"' + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载文件</button>' +
|
||
'<button class="image-video-card-action shop-data-delete-action" type="button" data-shop-data-delete="' + (resultId || '') + '"' + (terminal && resultId > 0 ? '' : ' disabled') + '>' + shopDataDeleteIcon() + '删除</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function renderShopDataTaskCard(group) {
|
||
var results = Array.isArray(group.results) ? group.results : [];
|
||
var latest = group.latest_created_at || (results[0] && (results[0].created_at || results[0].finished_at)) || '-';
|
||
return '<article class="image-video-card shop-data-task-card" data-shop-data-group="' + escapeHtml(group.key || '') + '">' +
|
||
'<div class="image-video-card-body">' +
|
||
'<div class="image-video-card-head shop-data-group-head">' +
|
||
'<div class="shop-data-task-title"><span title="' + escapeHtml(group.shop_name || '-') + '">' + escapeHtml(group.shop_name || '-') + '</span></div>' +
|
||
'<span class="shop-data-group-meta">' + results.length + '/1 份当日累计文件</span>' +
|
||
'</div>' +
|
||
'<div class="image-video-card-info">' +
|
||
'<div class="image-video-info-row"><label>分组</label><span>' + escapeHtml(group.group_name || '-') + '</span></div>' +
|
||
'<div class="image-video-info-row"><label>最新</label><span>' + escapeHtml(latest) + '</span></div>' +
|
||
'</div>' +
|
||
'<div class="shop-data-result-list">' +
|
||
(results.length ? results.map(renderShopDataTaskResult).join('') : '<div class="image-video-empty">暂无结果</div>') +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</article>';
|
||
}
|
||
|
||
function syncShopDataSelectionUi() {
|
||
document.querySelectorAll('[data-shop-data-card]').forEach(function (card) {
|
||
var resultId = Number(card.dataset.shopDataCard);
|
||
var selected = selectedShopDataResultIds.has(resultId);
|
||
card.classList.toggle('selected', selected);
|
||
var checkbox = card.querySelector('[data-shop-data-select]');
|
||
if (checkbox) checkbox.checked = selected && !checkbox.disabled;
|
||
});
|
||
var selectable = shopDataTasks.filter(function (item) { return !!item.file_ready && shopDataResultId(item) > 0; });
|
||
var selectedCount = selectable.filter(function (item) {
|
||
return selectedShopDataResultIds.has(shopDataResultId(item));
|
||
}).length;
|
||
var selectAll = document.getElementById('shopDataTaskSelectAll');
|
||
selectAll.checked = selectable.length > 0 && selectedCount === selectable.length;
|
||
selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length;
|
||
selectAll.disabled = shopDataDownloadInProgress || selectable.length === 0;
|
||
var batch = document.getElementById('btnBatchDownloadShopDataTasks');
|
||
batch.disabled = shopDataDownloadInProgress || selectedCount === 0;
|
||
batch.innerHTML = imageVideoDownloadIcon() + (shopDataDownloadInProgress
|
||
? '处理中'
|
||
: '批量下载' + (selectedCount ? ' (' + selectedCount + ')' : ''));
|
||
}
|
||
|
||
function renderShopDataTasks() {
|
||
var grid = document.getElementById('shopDataTaskGrid');
|
||
grid.innerHTML = shopDataTaskGroups.length
|
||
? shopDataTaskGroups.map(renderShopDataTaskCard).join('')
|
||
: '<div class="image-video-empty">暂无符合条件的店铺数据任务</div>';
|
||
syncShopDataSelectionUi();
|
||
}
|
||
|
||
function loadShopDataCrawlTasks(page) {
|
||
shopDataTaskPage = page || 1;
|
||
selectedShopDataResultIds.clear();
|
||
var grid = document.getElementById('shopDataTaskGrid');
|
||
grid.innerHTML = '<div class="image-video-empty">加载中...</div>';
|
||
document.getElementById('shopDataTaskDownloadProgress').textContent = '';
|
||
fetch('/api/admin/shop-data-crawl-tasks?' + buildShopDataTaskQuery(shopDataTaskPage))
|
||
.then(function (response) { return response.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '加载失败');
|
||
var payload = res.data && typeof res.data === 'object' && !Array.isArray(res.data) ? res.data : res;
|
||
shopDataTaskGroups = normalizeShopDataTaskGroups(payload.items || []);
|
||
shopDataTasks = shopDataTaskGroups.reduce(function (all, group) {
|
||
return all.concat(group.results || []);
|
||
}, []);
|
||
var total = payload.total != null ? Number(payload.total) : shopDataTaskGroups.length;
|
||
var responsePage = payload.page || page;
|
||
var responsePageSize = payload.page_size || shopDataTaskPageSize;
|
||
document.getElementById('shopDataTaskTotal').textContent = '共 ' + (total || 0) + ' 家店铺 · 每家店铺保留 1 份当日累计文件';
|
||
renderShopDataTasks();
|
||
renderPagination('shopDataTaskPagination', total, responsePage, responsePageSize, loadShopDataCrawlTasks);
|
||
})
|
||
.catch(function (error) {
|
||
shopDataTaskGroups = [];
|
||
shopDataTasks = [];
|
||
grid.innerHTML = '<div class="image-video-empty">加载失败:' + escapeHtml(error.message || '') + '</div>';
|
||
document.getElementById('shopDataTaskTotal').textContent = '';
|
||
syncShopDataSelectionUi();
|
||
});
|
||
}
|
||
|
||
function downloadShopDataTask(item) {
|
||
var resultId = shopDataResultId(item);
|
||
if (!item || !item.file_ready || !resultId) return;
|
||
var filename = item.output_filename || ('shop-data-task-' + resultId + '.xlsx');
|
||
triggerImageVideoLink('/api/admin/shop-data-crawl-tasks/' + resultId + '/download', filename, false);
|
||
}
|
||
|
||
function deleteShopDataTask(item) {
|
||
var resultId = shopDataResultId(item);
|
||
if (!item || !resultId || ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(String(item.status || item.file_status || '').toUpperCase()) < 0) return;
|
||
if (!window.confirm('确认删除店铺“' + (item.shop_name || '-') + '”的任务 ' + item.task_id + ' 及结果文件?')) return;
|
||
var progress = document.getElementById('shopDataTaskDownloadProgress');
|
||
progress.textContent = '正在删除任务 ' + item.task_id + '...';
|
||
fetch('/api/admin/shop-data-crawl-tasks/' + resultId, { method: 'DELETE' })
|
||
.then(function (response) { return response.json().then(function (data) { return { ok: response.ok, data: data }; }); })
|
||
.then(function (result) {
|
||
if (!result.ok || !result.data.success) throw new Error(result.data.error || '删除失败');
|
||
progress.textContent = result.data.msg || '删除成功';
|
||
loadShopDataCrawlTasks(shopDataTaskPage);
|
||
})
|
||
.catch(function (error) {
|
||
progress.textContent = error.message || '删除失败';
|
||
});
|
||
}
|
||
|
||
function downloadShopDataTasksZip() {
|
||
var resultIds = Array.from(selectedShopDataResultIds);
|
||
if (shopDataDownloadInProgress || !resultIds.length) return;
|
||
shopDataDownloadInProgress = true;
|
||
syncShopDataSelectionUi();
|
||
var progress = document.getElementById('shopDataTaskDownloadProgress');
|
||
progress.textContent = '正在打包 ' + resultIds.length + ' 个文件...';
|
||
fetch('/api/admin/shop-data-crawl-tasks/download-zip', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ result_ids: resultIds }),
|
||
__skipLoading: true
|
||
}).then(function (response) {
|
||
if (!response.ok) {
|
||
return response.json().catch(function () { return {}; }).then(function (data) {
|
||
throw new Error(data.error || '压缩包生成失败');
|
||
});
|
||
}
|
||
var filename = imageVideoZipFilename(response);
|
||
var errorCount = Number(response.headers.get('X-Archive-Error-Count') || 0);
|
||
return response.blob().then(function (blob) {
|
||
return { blob: blob, filename: filename, errorCount: errorCount };
|
||
});
|
||
}).then(function (result) {
|
||
var objectUrl = URL.createObjectURL(result.blob);
|
||
triggerImageVideoLink(objectUrl, result.filename, false);
|
||
setTimeout(function () { URL.revokeObjectURL(objectUrl); }, 1000);
|
||
progress.textContent = result.errorCount
|
||
? '压缩包已下载,' + result.errorCount + ' 个文件失败,详见包内清单'
|
||
: '压缩包下载已开始';
|
||
}).catch(function (error) {
|
||
progress.textContent = error.message || '批量下载失败';
|
||
}).finally(function () {
|
||
shopDataDownloadInProgress = false;
|
||
syncShopDataSelectionUi();
|
||
});
|
||
}
|
||
|
||
function shopDataPermissionUsersForView() {
|
||
return shopDataPermissionView === 'granted'
|
||
? shopDataPermissionUsers.filter(function (user) {
|
||
return shopDataPermissionInitialUserIds.has(Number(user.id));
|
||
})
|
||
: shopDataPermissionUsers;
|
||
}
|
||
|
||
function filteredShopDataPermissionUsers() {
|
||
var users = shopDataPermissionUsersForView();
|
||
var keyword = (document.getElementById('shopDataTaskPermissionSearch').value || '').trim().toLowerCase();
|
||
return keyword ? users.filter(function (user) {
|
||
return String(user.username || '').toLowerCase().indexOf(keyword) >= 0;
|
||
}) : users;
|
||
}
|
||
|
||
function renderShopDataPermissionUsers() {
|
||
var visibleUsers = filteredShopDataPermissionUsers();
|
||
document.getElementById('shopDataTaskPermissionGrantedCount').textContent = '(' + shopDataPermissionInitialUserIds.size + ')';
|
||
document.getElementById('shopDataTaskPermissionAllCount').textContent = '(' + shopDataPermissionUsers.length + ')';
|
||
document.querySelectorAll('[data-shop-data-permission-view]').forEach(function (tab) {
|
||
var active = tab.dataset.shopDataPermissionView === shopDataPermissionView;
|
||
tab.classList.toggle('active', active);
|
||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
});
|
||
var pendingCount = shopDataPermissionUsers.filter(function (user) {
|
||
var userId = Number(user.id);
|
||
return shopDataPermissionInitialUserIds.has(userId) !== selectedShopDataPermissionUserIds.has(userId);
|
||
}).length;
|
||
document.getElementById('shopDataTaskPermissionSummary').textContent =
|
||
(shopDataPermissionView === 'granted' ? '当前显示已分配用户,共 ' + visibleUsers.length + ' 人' : '当前显示全部用户,已分配 ' + shopDataPermissionInitialUserIds.size + ' 人') +
|
||
(pendingCount ? ' · 待保存变更 ' + pendingCount + ' 项' : '');
|
||
document.getElementById('shopDataTaskPermissionList').innerHTML = visibleUsers.length
|
||
? visibleUsers.map(function (user) {
|
||
var userId = Number(user.id);
|
||
var saved = shopDataPermissionInitialUserIds.has(userId);
|
||
var selected = selectedShopDataPermissionUserIds.has(userId);
|
||
var changed = saved !== selected;
|
||
return '<div class="image-video-permission-row">' +
|
||
'<input type="checkbox" data-shop-data-permission-user="' + userId + '"' + (selected ? ' checked' : '') + '>' +
|
||
'<span class="image-video-permission-user"><span class="image-video-permission-name">' + escapeHtml(user.username || '-') + '</span><span class="image-video-permission-role">' + escapeHtml(roleLabel(user.role)) + '</span></span>' +
|
||
'<button class="image-video-permission-state' + (changed ? ' pending' : (saved ? ' granted' : '')) + '" type="button" data-shop-data-permission-toggle="' + userId + '">' +
|
||
(changed ? (selected ? '待保存分配' : '待保存取消') : (saved ? '取消分配' : '分配')) +
|
||
'</button></div>';
|
||
}).join('')
|
||
: '<div class="image-video-permission-empty">暂无匹配用户</div>';
|
||
var selectedVisibleCount = visibleUsers.filter(function (user) {
|
||
return selectedShopDataPermissionUserIds.has(Number(user.id));
|
||
}).length;
|
||
var selectAll = document.getElementById('shopDataTaskPermissionSelectAll');
|
||
selectAll.checked = visibleUsers.length > 0 && selectedVisibleCount === visibleUsers.length;
|
||
selectAll.indeterminate = selectedVisibleCount > 0 && selectedVisibleCount < visibleUsers.length;
|
||
selectAll.disabled = visibleUsers.length === 0;
|
||
}
|
||
|
||
function openShopDataTaskPermissions() {
|
||
if (currentUserRole !== 'super_admin') return;
|
||
var modal = document.getElementById('shopDataTaskPermissionModal');
|
||
var saveButton = document.getElementById('btnSaveShopDataTaskPermissions');
|
||
var permissionLoaded = false;
|
||
modal.classList.add('show');
|
||
shopDataPermissionView = 'granted';
|
||
document.getElementById('shopDataTaskPermissionSearch').value = '';
|
||
document.getElementById('shopDataTaskPermissionMessage').textContent = '';
|
||
document.getElementById('shopDataTaskPermissionList').innerHTML = '<div class="image-video-permission-empty">加载中...</div>';
|
||
saveButton.disabled = true;
|
||
fetch('/api/admin/shop-data-crawl-task-permissions')
|
||
.then(function (response) { return response.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '权限加载失败');
|
||
shopDataPermissionUsers = res.items || [];
|
||
shopDataPermissionInitialUserIds = new Set(shopDataPermissionUsers.filter(function (user) { return !!user.granted; }).map(function (user) { return Number(user.id); }));
|
||
selectedShopDataPermissionUserIds = new Set(shopDataPermissionInitialUserIds);
|
||
permissionLoaded = true;
|
||
renderShopDataPermissionUsers();
|
||
}).catch(function (error) {
|
||
shopDataPermissionUsers = [];
|
||
shopDataPermissionInitialUserIds = new Set();
|
||
selectedShopDataPermissionUserIds = new Set();
|
||
document.getElementById('shopDataTaskPermissionList').innerHTML = '<div class="image-video-permission-empty">' + escapeHtml(error.message || '权限加载失败') + '</div>';
|
||
document.getElementById('shopDataTaskPermissionMessage').textContent = '权限加载失败,请关闭后重试';
|
||
}).finally(function () {
|
||
saveButton.disabled = !permissionLoaded;
|
||
});
|
||
}
|
||
|
||
function closeShopDataTaskPermissions() {
|
||
document.getElementById('shopDataTaskPermissionModal').classList.remove('show');
|
||
}
|
||
|
||
function saveShopDataTaskPermissions() {
|
||
var message = document.getElementById('shopDataTaskPermissionMessage');
|
||
message.textContent = '保存中...';
|
||
message.className = 'msg';
|
||
document.getElementById('btnSaveShopDataTaskPermissions').disabled = true;
|
||
fetch('/api/admin/shop-data-crawl-task-permissions', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ user_ids: Array.from(selectedShopDataPermissionUserIds).sort(function (a, b) { return a - b; }) })
|
||
}).then(function (response) { return response.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '权限保存失败');
|
||
shopDataPermissionInitialUserIds = new Set(selectedShopDataPermissionUserIds);
|
||
renderShopDataPermissionUsers();
|
||
message.textContent = res.msg || '保存成功';
|
||
message.className = 'msg ok';
|
||
}).catch(function (error) {
|
||
message.textContent = error.message || '权限保存失败';
|
||
message.className = 'msg err';
|
||
}).finally(function () {
|
||
document.getElementById('btnSaveShopDataTaskPermissions').disabled = false;
|
||
});
|
||
}
|
||
|
||
document.getElementById('btnFilterShopDataTasks').onclick = function () { loadShopDataCrawlTasks(1); };
|
||
document.getElementById('btnResetShopDataTasks').onclick = function () {
|
||
['shopDataTaskFilterShop', 'shopDataTaskFilterGroup', 'shopDataTaskFilterFrom', 'shopDataTaskFilterTo']
|
||
.forEach(function (id) { document.getElementById(id).value = ''; });
|
||
loadShopDataCrawlTasks(1);
|
||
};
|
||
document.getElementById('shopDataTaskSelectAll').onchange = function (event) {
|
||
shopDataTasks.forEach(function (item) {
|
||
var resultId = shopDataResultId(item);
|
||
if (!item.file_ready || !resultId) return;
|
||
if (event.target.checked) selectedShopDataResultIds.add(resultId);
|
||
else selectedShopDataResultIds.delete(resultId);
|
||
});
|
||
syncShopDataSelectionUi();
|
||
};
|
||
document.getElementById('btnBatchDownloadShopDataTasks').onclick = downloadShopDataTasksZip;
|
||
document.getElementById('shopDataTaskGrid').onchange = function (event) {
|
||
var checkbox = event.target.closest('[data-shop-data-select]');
|
||
if (!checkbox) return;
|
||
var resultId = Number(checkbox.dataset.shopDataSelect);
|
||
if (!resultId) return;
|
||
if (checkbox.checked) selectedShopDataResultIds.add(resultId);
|
||
else selectedShopDataResultIds.delete(resultId);
|
||
syncShopDataSelectionUi();
|
||
};
|
||
document.getElementById('shopDataTaskGrid').onclick = function (event) {
|
||
var downloadButton = event.target.closest('[data-shop-data-download]');
|
||
if (downloadButton) {
|
||
var downloadItem = shopDataTasks.find(function (task) { return shopDataResultId(task) === Number(downloadButton.dataset.shopDataDownload); });
|
||
downloadShopDataTask(downloadItem);
|
||
return;
|
||
}
|
||
var deleteButton = event.target.closest('[data-shop-data-delete]');
|
||
if (deleteButton) {
|
||
var deleteItem = shopDataTasks.find(function (task) { return shopDataResultId(task) === Number(deleteButton.dataset.shopDataDelete); });
|
||
deleteShopDataTask(deleteItem);
|
||
}
|
||
};
|
||
document.getElementById('btnOpenShopDataTaskPermissions').onclick = openShopDataTaskPermissions;
|
||
document.getElementById('btnCloseShopDataTaskPermissions').onclick = closeShopDataTaskPermissions;
|
||
document.getElementById('btnCancelShopDataTaskPermissions').onclick = closeShopDataTaskPermissions;
|
||
document.getElementById('btnSaveShopDataTaskPermissions').onclick = saveShopDataTaskPermissions;
|
||
document.querySelectorAll('[data-shop-data-permission-view]').forEach(function (tab) {
|
||
tab.onclick = function () {
|
||
shopDataPermissionView = tab.dataset.shopDataPermissionView || 'granted';
|
||
document.getElementById('shopDataTaskPermissionSearch').value = '';
|
||
renderShopDataPermissionUsers();
|
||
};
|
||
});
|
||
document.getElementById('shopDataTaskPermissionSearch').oninput = renderShopDataPermissionUsers;
|
||
document.getElementById('shopDataTaskPermissionSelectAll').onchange = function (event) {
|
||
filteredShopDataPermissionUsers().forEach(function (user) {
|
||
if (event.target.checked) selectedShopDataPermissionUserIds.add(Number(user.id));
|
||
else selectedShopDataPermissionUserIds.delete(Number(user.id));
|
||
});
|
||
renderShopDataPermissionUsers();
|
||
};
|
||
document.getElementById('shopDataTaskPermissionList').onclick = function (event) {
|
||
var button = event.target.closest('[data-shop-data-permission-toggle]');
|
||
if (!button) return;
|
||
var userId = Number(button.dataset.shopDataPermissionToggle);
|
||
if (selectedShopDataPermissionUserIds.has(userId)) selectedShopDataPermissionUserIds.delete(userId);
|
||
else selectedShopDataPermissionUserIds.add(userId);
|
||
renderShopDataPermissionUsers();
|
||
};
|
||
document.getElementById('shopDataTaskPermissionList').onchange = function (event) {
|
||
var checkbox = event.target.closest('[data-shop-data-permission-user]');
|
||
if (!checkbox) return;
|
||
var userId = Number(checkbox.dataset.shopDataPermissionUser);
|
||
if (checkbox.checked) selectedShopDataPermissionUserIds.add(userId);
|
||
else selectedShopDataPermissionUserIds.delete(userId);
|
||
renderShopDataPermissionUsers();
|
||
};
|
||
document.getElementById('shopDataTaskPermissionModal').onclick = function (event) {
|
||
if (event.target === event.currentTarget) closeShopDataTaskPermissions();
|
||
};
|
||
|
||
var historyPage = 1, historyPageSize = 15;
|
||
function toSqlDatetime(val) {
|
||
if (!val) return '';
|
||
return val.replace('T', ' ');
|
||
}
|
||
function buildHistoryQuery(page) {
|
||
var q = 'page=' + (page || 1) + '&page_size=' + historyPageSize;
|
||
var uid = document.getElementById('filterUser').value;
|
||
var start = toSqlDatetime(document.getElementById('filterTimeStart').value);
|
||
var end = toSqlDatetime(document.getElementById('filterTimeEnd').value);
|
||
if (uid) q += '&user_id=' + uid;
|
||
if (start) q += '&time_start=' + encodeURIComponent(start);
|
||
if (end) q += '&time_end=' + encodeURIComponent(end);
|
||
return q;
|
||
}
|
||
function panelTypeLabel(value) {
|
||
var panelType = String(value || '');
|
||
var labels = {
|
||
textToImage: '反推词', productMainImage: '产品主图', buyerShow: '买家秀',
|
||
productPoster: '产品海报', clonePoster: '克隆海报', randomPoster: '随机海报',
|
||
clothingDetail: '服装详情', productDetail: '产品详情', extremeDetail: '极致详情',
|
||
cloneDetail: '克隆详情', imageEdit: '图片编辑'
|
||
};
|
||
if (labels[panelType]) return labels[panelType];
|
||
return panelType ? panelType : '-';
|
||
}
|
||
function loadHistory(page) {
|
||
historyPage = page || 1;
|
||
fetch('/api/admin/history?' + buildHistoryQuery(historyPage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var tbody = document.getElementById('historyListBody');
|
||
if (!res.success) {
|
||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||
return;
|
||
}
|
||
var items = res.items || [];
|
||
if (items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无记录</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = items.map(function (h) {
|
||
var urls = (h.result_urls || []);
|
||
var thumbUrls = (h.long_image_url ? [h.long_image_url] : []).concat(urls);
|
||
var thumbs = thumbUrls.slice(0, 3).map(function (url) {
|
||
return '<img src="' + (url || '').replace(/"/g, '"') + '" class="thumb" alt="">';
|
||
}).join('');
|
||
return '<tr><td>' + h.id + '</td><td>' + (h.username || '-') + '</td><td>' +
|
||
panelTypeLabel(h.panel_type) + '</td><td>' + (h.created_at || '') + '</td><td>' +
|
||
'<div class="thumb-wrap">' + (thumbs || '-') + '</div></td></tr>';
|
||
}).join('');
|
||
}
|
||
renderPagination('historyPagination', res.total, res.page, res.page_size, loadHistory);
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('historyListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
|
||
});
|
||
}
|
||
var shopManageAllUsers = [];
|
||
function escapeHtml(value) {
|
||
return String(value == null ? '' : value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
var dropdownMultiSelects = {};
|
||
function initDropdownMultiSelect(selectId, placeholder) {
|
||
var select = document.getElementById(selectId);
|
||
if (!select || dropdownMultiSelects[selectId]) return;
|
||
|
||
select.classList.add('multi-select-native');
|
||
var wrapper = document.createElement('div');
|
||
wrapper.className = 'multi-select-dropdown';
|
||
wrapper.setAttribute('data-multi-select-id', selectId);
|
||
|
||
var trigger = document.createElement('button');
|
||
trigger.type = 'button';
|
||
trigger.className = 'multi-select-trigger';
|
||
trigger.textContent = placeholder || '请选择';
|
||
|
||
var panel = document.createElement('div');
|
||
panel.className = 'multi-select-panel';
|
||
|
||
select.parentNode.insertBefore(wrapper, select);
|
||
wrapper.appendChild(trigger);
|
||
wrapper.appendChild(panel);
|
||
wrapper.appendChild(select);
|
||
|
||
function selectedOptions() {
|
||
return Array.from(select.options).filter(function (option) { return option.selected; });
|
||
}
|
||
|
||
function updateSummary() {
|
||
var selected = selectedOptions();
|
||
if (!selected.length) {
|
||
trigger.textContent = placeholder || '请选择';
|
||
trigger.title = '';
|
||
return;
|
||
}
|
||
var labels = selected.map(function (option) { return option.textContent || option.value; });
|
||
trigger.textContent = labels.join('、');
|
||
trigger.title = labels.join('、');
|
||
}
|
||
|
||
function renderOptions() {
|
||
panel.innerHTML = Array.from(select.options).map(function (option, index) {
|
||
var optionId = selectId + '_multi_' + index;
|
||
return '<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 getDedupeTotalDataDateRange() {
|
||
return {
|
||
startDate: document.getElementById('exportDedupeTotalDataStartDate').value || '',
|
||
endDate: document.getElementById('exportDedupeTotalDataEndDate').value || ''
|
||
};
|
||
}
|
||
function validateDedupeTotalDataDateRange() {
|
||
var dateRange = getDedupeTotalDataDateRange();
|
||
if (dateRange.startDate && dateRange.endDate && dateRange.startDate > dateRange.endDate) {
|
||
alert('开始日期不能晚于结束日期');
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
function buildDedupeTotalDataQuery(page) {
|
||
var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize;
|
||
var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim();
|
||
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
|
||
var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim();
|
||
var country = (document.getElementById('dedupeTotalDataCountryFilter').value || '').trim();
|
||
var dateRange = getDedupeTotalDataDateRange();
|
||
if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
|
||
if (username) q += '&username=' + encodeURIComponent(username);
|
||
if (groupId) q += '&group_id=' + encodeURIComponent(groupId);
|
||
if (country) q += '&country=' + encodeURIComponent(country);
|
||
if (dateRange.startDate) q += '&start_date=' + encodeURIComponent(dateRange.startDate);
|
||
if (dateRange.endDate) q += '&end_date=' + encodeURIComponent(dateRange.endDate);
|
||
return q;
|
||
}
|
||
function getDedupeTotalDataCountryLabel(countryCodes) {
|
||
var countryLabels = {
|
||
DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙'
|
||
};
|
||
if (Array.isArray(countryCodes)) {
|
||
return countryCodes.map(function (code) { return countryLabels[code] || code; }).join('、');
|
||
}
|
||
return String(countryCodes || '').split(/[,,]/).map(function (code) {
|
||
code = code.trim();
|
||
return countryLabels[code] || code;
|
||
}).filter(Boolean).join('、');
|
||
}
|
||
function loadDedupeTotalData(page) {
|
||
if (!validateDedupeTotalDataDateRange()) return;
|
||
dedupeTotalDataPage = page || 1;
|
||
fetch('/api/admin/dedupe-total-data?' + buildDedupeTotalDataQuery(dedupeTotalDataPage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var tbody = document.getElementById('dedupeTotalDataListBody');
|
||
if (!res.success) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
|
||
return;
|
||
}
|
||
var items = res.items || [];
|
||
if (items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">暂无总数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = items.map(function (item) {
|
||
return '<tr><td>' + escapeHtml(item.id) + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(getDedupeTotalDataCountryLabel(item.country)) + '</td><td>' + escapeHtml(item.username || '') + '</td><td>' + escapeHtml(item.group_name || '未分组') + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
|
||
'<button class="btn btn-sm" data-dedupe-total-edit="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '" data-group-id="' + escapeHtml(item.group_id || '') + '">编辑</button> ' +
|
||
'<button class="btn btn-sm btn-danger" data-dedupe-total-delete="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">删除</button>' +
|
||
'</td></tr>';
|
||
}).join('');
|
||
}
|
||
renderPagination('dedupeTotalDataPagination', res.total, res.page, res.page_size, loadDedupeTotalData);
|
||
bindDedupeTotalDataActions();
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
|
||
});
|
||
}
|
||
function bindDedupeTotalDataActions() {
|
||
document.querySelectorAll('[data-dedupe-total-edit]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
document.getElementById('editDedupeTotalDataId').value = btn.dataset.dedupeTotalEdit || '';
|
||
document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/"/g, '"');
|
||
document.getElementById('msgEditDedupeTotalData').textContent = '';
|
||
document.getElementById('msgEditDedupeTotalData').className = 'msg';
|
||
var groupId = btn.dataset.groupId || '';
|
||
loadShopManageGroups().then(function () {
|
||
document.getElementById('editDedupeTotalDataGroupId').value = groupId;
|
||
document.getElementById('editDedupeTotalDataModal').classList.add('show');
|
||
});
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var value = (btn.dataset.value || '').replace(/"/g, '"');
|
||
if (!confirm('确定删除总数据“' + value + '”吗?')) return;
|
||
fetch('/api/admin/dedupe-total-data/' + btn.dataset.dedupeTotalDelete, { method: 'DELETE' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) { loadDedupeTotalData(dedupeTotalDataPage); }
|
||
else { alert(res.error || '删除失败'); }
|
||
});
|
||
};
|
||
});
|
||
}
|
||
document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); };
|
||
var dedupeTotalDataExportButton = document.getElementById('btnExportDedupeTotalData');
|
||
var dedupeTotalDataExportWait = document.getElementById('dedupeTotalDataExportWait');
|
||
var dedupeTotalDataExportWaitSeconds = document.getElementById('dedupeTotalDataExportWaitSeconds');
|
||
var dedupeTotalDataExportWaitTimer = null;
|
||
function showDedupeTotalDataExportWait() {
|
||
var startedAt = Date.now();
|
||
dedupeTotalDataExportWaitSeconds.textContent = '0';
|
||
dedupeTotalDataExportWait.classList.add('show');
|
||
dedupeTotalDataExportWait.setAttribute('aria-hidden', 'false');
|
||
dedupeTotalDataExportWaitTimer = setInterval(function () {
|
||
dedupeTotalDataExportWaitSeconds.textContent = String(Math.floor((Date.now() - startedAt) / 1000));
|
||
}, 1000);
|
||
}
|
||
function hideDedupeTotalDataExportWait() {
|
||
clearInterval(dedupeTotalDataExportWaitTimer);
|
||
dedupeTotalDataExportWaitTimer = null;
|
||
dedupeTotalDataExportWait.classList.remove('show');
|
||
dedupeTotalDataExportWait.setAttribute('aria-hidden', 'true');
|
||
}
|
||
dedupeTotalDataExportButton.onclick = function () {
|
||
if (dedupeTotalDataExportButton.disabled) return;
|
||
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
|
||
var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim();
|
||
var country = (document.getElementById('dedupeTotalDataCountryFilter').value || '').trim();
|
||
var dateRange = getDedupeTotalDataDateRange();
|
||
if (!validateDedupeTotalDataDateRange()) return;
|
||
var params = [];
|
||
if (username) params.push('username=' + encodeURIComponent(username));
|
||
if (groupId) params.push('group_id=' + encodeURIComponent(groupId));
|
||
if (country) params.push('country=' + encodeURIComponent(country));
|
||
if (dateRange.startDate) params.push('start_date=' + encodeURIComponent(dateRange.startDate));
|
||
if (dateRange.endDate) params.push('end_date=' + encodeURIComponent(dateRange.endDate));
|
||
var originalButtonText = dedupeTotalDataExportButton.textContent;
|
||
dedupeTotalDataExportButton.disabled = true;
|
||
dedupeTotalDataExportButton.setAttribute('aria-busy', 'true');
|
||
dedupeTotalDataExportButton.textContent = '导出中...';
|
||
showDedupeTotalDataExportWait();
|
||
fetch('/api/admin/dedupe-total-data/export' + (params.length ? ('?' + params.join('&')) : ''))
|
||
.then(function (response) {
|
||
var contentType = response.headers.get('content-type') || '';
|
||
if (!response.ok || contentType.indexOf('application/json') >= 0) {
|
||
return response.json().then(function (res) {
|
||
throw new Error((res && (res.error || res.msg)) || '导出失败');
|
||
});
|
||
}
|
||
return response.blob().then(function (blob) {
|
||
return {
|
||
blob: blob,
|
||
filename: extractDownloadFilename(
|
||
response.headers.get('content-disposition'),
|
||
'dedupe-total-data.xlsx'
|
||
)
|
||
};
|
||
});
|
||
})
|
||
.then(function (payload) {
|
||
triggerBrowserDownload(payload.blob, payload.filename);
|
||
})
|
||
.catch(function (err) {
|
||
alert((err && err.message) || '导出失败');
|
||
})
|
||
.finally(function () {
|
||
dedupeTotalDataExportButton.disabled = false;
|
||
dedupeTotalDataExportButton.removeAttribute('aria-busy');
|
||
dedupeTotalDataExportButton.textContent = originalButtonText;
|
||
hideDedupeTotalDataExportWait();
|
||
});
|
||
};
|
||
var dedupeImportPollTimer = null;
|
||
var dedupeDeleteImportPollTimer = null;
|
||
function stopDedupeImportProgress() {
|
||
if (dedupeImportPollTimer) {
|
||
clearInterval(dedupeImportPollTimer);
|
||
dedupeImportPollTimer = null;
|
||
}
|
||
}
|
||
function stopDedupeDeleteImportProgress() {
|
||
if (dedupeDeleteImportPollTimer) {
|
||
clearInterval(dedupeDeleteImportPollTimer);
|
||
dedupeDeleteImportPollTimer = null;
|
||
}
|
||
}
|
||
function setDedupeImportProgress(percent, text) {
|
||
var wrap = document.getElementById('dedupeTotalDataProgressWrap');
|
||
var fill = document.getElementById('dedupeTotalDataProgressFill');
|
||
var textEl = document.getElementById('dedupeTotalDataProgressText');
|
||
wrap.style.display = 'block';
|
||
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
|
||
textEl.textContent = text || '';
|
||
}
|
||
function setDedupeDeleteImportProgress(percent, text) {
|
||
var wrap = document.getElementById('dedupeTotalDataDeleteProgressWrap');
|
||
var fill = document.getElementById('dedupeTotalDataDeleteProgressFill');
|
||
var textEl = document.getElementById('dedupeTotalDataDeleteProgressText');
|
||
wrap.style.display = 'block';
|
||
fill.style.width = Math.max(0, Math.min(100, percent || 0)) + '%';
|
||
textEl.textContent = text || '';
|
||
}
|
||
function pollDedupeImport(importId) {
|
||
stopDedupeImportProgress();
|
||
function tick() {
|
||
fetch('/api/admin/dedupe-total-data/import/' + encodeURIComponent(importId))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) {
|
||
stopDedupeImportProgress();
|
||
document.getElementById('msgDedupeTotalData').textContent = res.error || '查询导入进度失败';
|
||
document.getElementById('msgDedupeTotalData').className = 'msg err';
|
||
return;
|
||
}
|
||
var progress = res.progress || {};
|
||
var totalRows = progress.total_rows || 0;
|
||
var processedRows = progress.processed_rows || 0;
|
||
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
|
||
setDedupeImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
|
||
if (progress.status === 'success') {
|
||
stopDedupeImportProgress();
|
||
document.getElementById('msgDedupeTotalData').textContent = '导入成功:总行数 ' + (progress.total_rows || 0) + ',ASIN 数量 ' + (progress.asin_count || 0) + ',新增 ' + (progress.inserted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
|
||
document.getElementById('msgDedupeTotalData').className = 'msg ok';
|
||
loadDedupeTotalData(1);
|
||
} else if (progress.status === 'failed') {
|
||
stopDedupeImportProgress();
|
||
document.getElementById('msgDedupeTotalData').textContent = progress.error_message || '导入失败';
|
||
document.getElementById('msgDedupeTotalData').className = 'msg err';
|
||
}
|
||
})
|
||
.catch(function () {
|
||
stopDedupeImportProgress();
|
||
document.getElementById('msgDedupeTotalData').textContent = '查询导入进度失败';
|
||
document.getElementById('msgDedupeTotalData').className = 'msg err';
|
||
});
|
||
}
|
||
tick();
|
||
dedupeImportPollTimer = setInterval(tick, 1000);
|
||
}
|
||
function pollDedupeDeleteImport(importId) {
|
||
stopDedupeDeleteImportProgress();
|
||
function tick() {
|
||
fetch('/api/admin/dedupe-total-data/delete-import/' + encodeURIComponent(importId))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) {
|
||
stopDedupeDeleteImportProgress();
|
||
document.getElementById('msgDeleteDedupeTotalData').textContent = res.error || '查询删除进度失败';
|
||
document.getElementById('msgDeleteDedupeTotalData').className = 'msg err';
|
||
return;
|
||
}
|
||
var progress = res.progress || {};
|
||
var totalRows = progress.total_rows || 0;
|
||
var processedRows = progress.processed_rows || 0;
|
||
var percent = totalRows > 0 ? Math.round(processedRows * 100 / totalRows) : 0;
|
||
setDedupeDeleteImportProgress(percent, '处理中:已处理 ' + processedRows + ' / ' + totalRows + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0));
|
||
if (progress.status === 'success') {
|
||
stopDedupeDeleteImportProgress();
|
||
document.getElementById('msgDeleteDedupeTotalData').textContent = '删除成功:总行数 ' + (progress.total_rows || 0) + ',ASIN 数量 ' + (progress.asin_count || 0) + ',删除 ' + (progress.deleted_count || 0) + ',跳过 ' + (progress.skipped_count || 0);
|
||
document.getElementById('msgDeleteDedupeTotalData').className = 'msg ok';
|
||
loadDedupeTotalData(1);
|
||
} else if (progress.status === 'failed') {
|
||
stopDedupeDeleteImportProgress();
|
||
document.getElementById('msgDeleteDedupeTotalData').textContent = progress.error_message || '删除失败';
|
||
document.getElementById('msgDeleteDedupeTotalData').className = 'msg err';
|
||
}
|
||
})
|
||
.catch(function () {
|
||
stopDedupeDeleteImportProgress();
|
||
document.getElementById('msgDeleteDedupeTotalData').textContent = '查询删除进度失败';
|
||
document.getElementById('msgDeleteDedupeTotalData').className = 'msg err';
|
||
});
|
||
}
|
||
tick();
|
||
dedupeDeleteImportPollTimer = setInterval(tick, 1000);
|
||
}
|
||
|
||
document.getElementById('btnAddDedupeTotalData').onclick = function () {
|
||
var fileInput = document.getElementById('dedupeTotalDataFile');
|
||
var msgEl = document.getElementById('msgDedupeTotalData');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
stopDedupeImportProgress();
|
||
document.getElementById('dedupeTotalDataProgressWrap').style.display = 'none';
|
||
if (!fileInput.files || fileInput.files.length === 0) {
|
||
msgEl.textContent = '请选择 Excel 文件';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var groupId = (document.getElementById('dedupeTotalDataImportGroupId').value || '').trim();
|
||
if (!groupId) {
|
||
msgEl.textContent = '请选择分组';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var file = fileInput.files[0];
|
||
var lowerName = (file.name || '').toLowerCase();
|
||
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
|
||
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('group_id', groupId);
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', '/api/admin/dedupe-total-data/import', true);
|
||
xhr.upload.onprogress = function (event) {
|
||
if (event.lengthComputable) {
|
||
var percent = Math.round(event.loaded * 100 / event.total);
|
||
setDedupeImportProgress(percent, '上传中:' + percent + '%');
|
||
}
|
||
};
|
||
xhr.onreadystatechange = function () {
|
||
if (xhr.readyState !== 4) return;
|
||
if (xhr.status < 200 || xhr.status >= 300) {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.className = 'msg err';
|
||
return;
|
||
}
|
||
var res;
|
||
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; }
|
||
if (!res.success) {
|
||
msgEl.textContent = res.error || '导入失败';
|
||
msgEl.className = 'msg err';
|
||
return;
|
||
}
|
||
setDedupeImportProgress(100, '上传完成,后端处理中...');
|
||
pollDedupeImport(res.import_id);
|
||
fileInput.value = '';
|
||
};
|
||
xhr.onerror = function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.className = 'msg err';
|
||
};
|
||
xhr.send(formData);
|
||
};
|
||
document.getElementById('btnDeleteImportDedupeTotalData').onclick = function () {
|
||
var fileInput = document.getElementById('dedupeTotalDataDeleteFile');
|
||
var msgEl = document.getElementById('msgDeleteDedupeTotalData');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
stopDedupeDeleteImportProgress();
|
||
document.getElementById('dedupeTotalDataDeleteProgressWrap').style.display = 'none';
|
||
if (!fileInput.files || fileInput.files.length === 0) {
|
||
msgEl.textContent = '请选择 Excel 文件';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var file = fileInput.files[0];
|
||
var lowerName = (file.name || '').toLowerCase();
|
||
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
|
||
msgEl.textContent = '仅支持 .xlsx 或 .xls 文件';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (!confirm('确定按 Excel 中的 ASIN 批量删除匹配的总数据吗?')) {
|
||
return;
|
||
}
|
||
var groupId = (document.getElementById('dedupeTotalDataDeleteGroupId').value || '').trim();
|
||
if (!groupId) {
|
||
msgEl.textContent = '请选择分组';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('group_id', groupId);
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', '/api/admin/dedupe-total-data/delete-import', true);
|
||
xhr.upload.onprogress = function (event) {
|
||
if (event.lengthComputable) {
|
||
var percent = Math.round(event.loaded * 100 / event.total);
|
||
setDedupeDeleteImportProgress(percent, '上传中:' + percent + '%');
|
||
}
|
||
};
|
||
xhr.onreadystatechange = function () {
|
||
if (xhr.readyState !== 4) return;
|
||
if (xhr.status < 200 || xhr.status >= 300) {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.className = 'msg err';
|
||
return;
|
||
}
|
||
var res;
|
||
try { res = JSON.parse(xhr.responseText || '{}'); } catch (e) { res = { success: false, error: '返回格式错误' }; }
|
||
if (!res.success) {
|
||
msgEl.textContent = res.error || '删除失败';
|
||
msgEl.className = 'msg err';
|
||
return;
|
||
}
|
||
setDedupeDeleteImportProgress(100, '上传完成,后端处理中...');
|
||
pollDedupeDeleteImport(res.import_id);
|
||
fileInput.value = '';
|
||
};
|
||
xhr.onerror = function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.className = 'msg err';
|
||
};
|
||
xhr.send(formData);
|
||
};
|
||
document.getElementById('btnSaveDedupeTotalData').onclick = function () {
|
||
var itemId = document.getElementById('editDedupeTotalDataId').value;
|
||
var value = (document.getElementById('editDedupeTotalDataValue').value || '').trim();
|
||
var groupId = (document.getElementById('editDedupeTotalDataGroupId').value || '').trim();
|
||
var msgEl = document.getElementById('msgEditDedupeTotalData');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!value) {
|
||
msgEl.textContent = '请填写总数据值';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (!groupId) {
|
||
msgEl.textContent = '请选择分组';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
fetch('/api/admin/dedupe-total-data/' + itemId, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ data_value: value, group_id: Number(groupId) })
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
document.getElementById('editDedupeTotalDataModal').classList.remove('show');
|
||
loadDedupeTotalData(dedupeTotalDataPage);
|
||
} else {
|
||
msgEl.textContent = res.error || '保存失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
});
|
||
};
|
||
document.getElementById('btnCloseEditDedupeTotalData').onclick = function () {
|
||
document.getElementById('editDedupeTotalDataModal').classList.remove('show');
|
||
};
|
||
|
||
// ========== 不符合ASIN数据 ==========
|
||
var invalidAsinDataPage = 1, invalidAsinDataPageSize = 15;
|
||
function buildInvalidAsinDataQuery(page) {
|
||
var q = 'page=' + (page || 1) + '&page_size=' + invalidAsinDataPageSize;
|
||
var keyword = (document.getElementById('searchInvalidAsinData').value || '').trim();
|
||
var groupId = (document.getElementById('invalidAsinDataFilterGroupId').value || '').trim();
|
||
if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
|
||
if (groupId) q += '&group_id=' + encodeURIComponent(groupId);
|
||
return q;
|
||
}
|
||
function loadInvalidAsinData(page) {
|
||
invalidAsinDataPage = page || 1;
|
||
fetch('/api/admin/invalid-asin-data?' + buildInvalidAsinDataQuery(invalidAsinDataPage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var tbody = document.getElementById('invalidAsinDataListBody');
|
||
if (!res.success) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||
return;
|
||
}
|
||
var items = res.items || [];
|
||
if (items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">暂无数据</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = items.map(function (item) {
|
||
var dataValueAttr = (item.data_value || '').replace(/"/g, '"');
|
||
var brandAttr = (item.brand || '').replace(/"/g, '"');
|
||
var groupId = item.group_id == null ? '' : String(item.group_id);
|
||
var source = String(item.record_source || 'AUTO').toUpperCase();
|
||
var sourceLabel = source === 'MANUAL' ? '\u624b\u52a8\u65b0\u589e' : '\u81ea\u52a8\u5bfc\u5165';
|
||
return '<tr><td>' + escapeHtml(item.id || '') + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(item.brand || '') + '</td><td>' + escapeHtml(item.group_name || '') + '</td><td>' + sourceLabel + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
|
||
'<button class="btn btn-sm" data-invalid-asin-edit="' + item.id + '" data-value="' + dataValueAttr + '" data-brand="' + brandAttr + '" data-group-id="' + groupId + '" data-source="' + source + '">编辑</button> ' +
|
||
'<button class="btn btn-sm btn-danger" data-invalid-asin-delete="' + item.id + '" data-value="' + dataValueAttr + '">删除</button>' +
|
||
'</td></tr>';
|
||
}).join('');
|
||
}
|
||
renderPagination('invalidAsinDataPagination', res.total, res.page, res.page_size, loadInvalidAsinData);
|
||
bindInvalidAsinDataActions();
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('invalidAsinDataListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
|
||
});
|
||
}
|
||
function bindInvalidAsinDataActions() {
|
||
document.querySelectorAll('[data-invalid-asin-edit]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
document.getElementById('editInvalidAsinDataId').value = btn.dataset.invalidAsinEdit || '';
|
||
document.getElementById('editInvalidAsinDataValue').value = (btn.dataset.value || '').replace(/"/g, '"');
|
||
document.getElementById('editInvalidAsinDataBrand').value = (btn.dataset.brand || '').replace(/"/g, '"');
|
||
var source = String(btn.dataset.source || 'AUTO').toUpperCase();
|
||
var isManual = source === 'MANUAL';
|
||
document.getElementById('editInvalidAsinDataRecordSource').value = source;
|
||
document.getElementById('editInvalidAsinDataSourceLabel').value = isManual
|
||
? '\u624b\u52a8\u65b0\u589e'
|
||
: '\u81ea\u52a8\u5bfc\u5165';
|
||
document.getElementById('editInvalidAsinDataGroupForm').style.display = isManual ? '' : 'none';
|
||
if (isManual) {
|
||
loadShopManageGroups().then(function () {
|
||
document.getElementById('editInvalidAsinDataGroupSelect').value = currentUserRole === 'super_admin'
|
||
? (btn.dataset.groupId || '')
|
||
: getInvalidAsinDataLockedGroupId();
|
||
});
|
||
}
|
||
document.getElementById('msgEditInvalidAsinData').textContent = '';
|
||
document.getElementById('msgEditInvalidAsinData').className = 'msg';
|
||
document.getElementById('editInvalidAsinDataModal').classList.add('show');
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-invalid-asin-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var value = (btn.dataset.value || '').replace(/"/g, '"');
|
||
if (!confirm('确定删除“' + value + '”吗?')) return;
|
||
fetch('/api/admin/invalid-asin-data/' + btn.dataset.invalidAsinDelete, { method: 'DELETE' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) { loadInvalidAsinData(invalidAsinDataPage); }
|
||
else { alert(res.error || '删除失败'); }
|
||
});
|
||
};
|
||
});
|
||
}
|
||
document.getElementById('btnSearchInvalidAsinData').onclick = function () { loadInvalidAsinData(1); };
|
||
document.getElementById('btnAddInvalidAsinData').onclick = function () {
|
||
var dataValue = (document.getElementById('invalidAsinDataValue').value || '').trim();
|
||
var brand = (document.getElementById('invalidAsinDataBrand').value || '').trim();
|
||
var groupId = (document.getElementById('invalidAsinDataGroupSelect').value || '').trim();
|
||
var msgEl = document.getElementById('msgInvalidAsinData');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!dataValue) {
|
||
msgEl.textContent = '请填写 ASIN';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (!groupId) {
|
||
msgEl.textContent = '请选择分组';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
fetch('/api/admin/invalid-asin-data', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ data_value: dataValue, brand: brand, group_id: Number(groupId) })
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
msgEl.textContent = res.msg || '创建成功';
|
||
msgEl.classList.add('ok');
|
||
document.getElementById('invalidAsinDataValue').value = '';
|
||
document.getElementById('invalidAsinDataBrand').value = '';
|
||
document.getElementById('invalidAsinDataGroupSelect').value = currentUserRole === 'super_admin'
|
||
? ''
|
||
: getInvalidAsinDataLockedGroupId();
|
||
loadInvalidAsinData(1);
|
||
} else {
|
||
msgEl.textContent = res.error || '创建失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.classList.add('err');
|
||
});
|
||
};
|
||
document.getElementById('btnSaveInvalidAsinData').onclick = function () {
|
||
var itemId = document.getElementById('editInvalidAsinDataId').value;
|
||
var dataValue = (document.getElementById('editInvalidAsinDataValue').value || '').trim();
|
||
var brand = (document.getElementById('editInvalidAsinDataBrand').value || '').trim();
|
||
var source = String(document.getElementById('editInvalidAsinDataRecordSource').value || 'AUTO').toUpperCase();
|
||
var groupId = (document.getElementById('editInvalidAsinDataGroupSelect').value || '').trim();
|
||
var msgEl = document.getElementById('msgEditInvalidAsinData');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!dataValue) {
|
||
msgEl.textContent = '请填写 ASIN';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (source === 'MANUAL' && !groupId) {
|
||
msgEl.textContent = '请选择分组';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
fetch('/api/admin/invalid-asin-data/' + itemId, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
data_value: dataValue,
|
||
brand: brand,
|
||
group_id: source === 'MANUAL' ? Number(groupId) : null
|
||
})
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
document.getElementById('editInvalidAsinDataModal').classList.remove('show');
|
||
loadInvalidAsinData(invalidAsinDataPage);
|
||
} else {
|
||
msgEl.textContent = res.error || '保存失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
});
|
||
};
|
||
document.getElementById('btnCloseEditInvalidAsinData').onclick = function () {
|
||
document.getElementById('editInvalidAsinDataModal').classList.remove('show');
|
||
};
|
||
|
||
// ========== 店铺密钥管理 ==========
|
||
var shopKeyPage = 1, shopKeyPageSize = 15;
|
||
function buildShopKeyQuery(page) {
|
||
return 'page=' + (page || 1) + '&page_size=' + shopKeyPageSize;
|
||
}
|
||
function renderShopKeyWhitelistStatus(item) {
|
||
var status = String(item.ip_whitelist_status || 'UNKNOWN').toUpperCase();
|
||
var statusMeta = {
|
||
ALLOWED: { label: '正常', className: 'is-allowed' },
|
||
BLOCKED: { label: '未加白名单', className: 'is-blocked' },
|
||
UNKNOWN: { label: '未检测', className: 'is-unknown' }
|
||
};
|
||
var meta = statusMeta[status] || statusMeta.UNKNOWN;
|
||
var details = [];
|
||
if (item.ip_whitelist_checked_at) details.push('检测时间:' + item.ip_whitelist_checked_at);
|
||
if (item.ip_whitelist_message) details.push(item.ip_whitelist_message);
|
||
var detailText = details.join('\n') || ('白名单状态:' + meta.label);
|
||
return '<span class="shop-key-whitelist-status ' + meta.className + '" role="status" aria-label="白名单状态:' + escapeHtml(meta.label) + '" title="' +
|
||
escapeHtml(detailText) + '">' + escapeHtml(meta.label) + '</span>';
|
||
}
|
||
function loadShopKeys(page) {
|
||
shopKeyPage = page || 1;
|
||
fetch('/api/admin/shop-keys?' + buildShopKeyQuery(shopKeyPage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var tbody = document.getElementById('shopKeyListBody');
|
||
if (!res.success) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||
return;
|
||
}
|
||
var items = res.items || [];
|
||
if (items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无店铺密钥</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = items.map(function (item, index) {
|
||
var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1;
|
||
return '<tr><td>' + rowNo + '</td><td>' + renderShopTableText(item.remark_name) + '</td><td>' + renderShopTableText(item.ziniao_account_name) + '</td><td>' + renderShopTableText(item.ziniao_token) + '</td><td>' + renderShopKeyWhitelistStatus(item) + '</td><td>' + renderShopTableText(item.created_at) + '</td><td>' + renderShopTableText(item.updated_at) + '</td><td>' +
|
||
'<button type="button" class="btn btn-sm" data-shop-key-edit="' + escapeHtml(item.id) + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '"')) + '">编辑</button> ' +
|
||
'<button type="button" class="btn btn-sm btn-danger" data-shop-key-delete="' + escapeHtml(item.id) + '" data-ziniao-account-name="' + escapeHtml(item.ziniao_account_name || '') + '">删除</button>' +
|
||
'</td></tr>';
|
||
}).join('');
|
||
}
|
||
renderPagination('shopKeyPagination', res.total, res.page, res.page_size, loadShopKeys);
|
||
bindShopKeyActions();
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('shopKeyListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
|
||
});
|
||
}
|
||
function bindShopKeyActions() {
|
||
document.querySelectorAll('[data-shop-key-edit]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var item = {};
|
||
try { item = JSON.parse((btn.dataset.shopKey || '').replace(/"/g, '"')); } catch (e) { item = {}; }
|
||
document.getElementById('editShopKeyId').value = item.id || '';
|
||
document.getElementById('editShopKeyRemarkName').value = item.remark_name || '';
|
||
document.getElementById('editShopKeyZiniaoAccountName').value = item.ziniao_account_name || '';
|
||
document.getElementById('editShopKeyZiniaoToken').value = item.ziniao_token || '';
|
||
document.getElementById('msgEditShopKey').textContent = '';
|
||
document.getElementById('msgEditShopKey').className = 'msg';
|
||
document.getElementById('editShopKeyModal').classList.add('show');
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-shop-key-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var name = (btn.dataset.ziniaoAccountName || '').replace(/"/g, '"');
|
||
if (!confirm('确定删除店铺密钥“' + name + '”吗?')) return;
|
||
fetch('/api/admin/shop-key/' + btn.dataset.shopKeyDelete, { method: 'DELETE' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) { loadShopKeys(shopKeyPage); }
|
||
else { alert(res.error || '删除失败'); }
|
||
});
|
||
};
|
||
});
|
||
}
|
||
document.getElementById('btnCreateShopKey').onclick = function () {
|
||
var remarkName = (document.getElementById('shopKeyRemarkName').value || '').trim();
|
||
var ziniaoAccountName = (document.getElementById('shopKeyZiniaoAccountName').value || '').trim();
|
||
var ziniaoToken = (document.getElementById('shopKeyZiniaoToken').value || '').trim();
|
||
var msgEl = document.getElementById('msgShopKey');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!ziniaoAccountName || !ziniaoToken) {
|
||
msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
fetch('/api/admin/shop-key', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
remark_name: remarkName,
|
||
ziniao_account_name: ziniaoAccountName,
|
||
ziniao_token: ziniaoToken
|
||
})
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
document.getElementById('shopKeyRemarkName').value = '';
|
||
document.getElementById('shopKeyZiniaoAccountName').value = '';
|
||
document.getElementById('shopKeyZiniaoToken').value = '';
|
||
msgEl.textContent = res.msg || '创建成功';
|
||
msgEl.className = 'msg ok';
|
||
loadShopKeys(1);
|
||
} else {
|
||
msgEl.textContent = res.error || '创建失败';
|
||
msgEl.className = 'msg err';
|
||
}
|
||
})
|
||
.catch(function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.className = 'msg err';
|
||
});
|
||
};
|
||
document.getElementById('btnSaveShopKey').onclick = function () {
|
||
var itemId = document.getElementById('editShopKeyId').value;
|
||
var remarkName = (document.getElementById('editShopKeyRemarkName').value || '').trim();
|
||
var ziniaoAccountName = (document.getElementById('editShopKeyZiniaoAccountName').value || '').trim();
|
||
var ziniaoToken = (document.getElementById('editShopKeyZiniaoToken').value || '').trim();
|
||
var msgEl = document.getElementById('msgEditShopKey');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!ziniaoAccountName || !ziniaoToken) {
|
||
msgEl.textContent = '请完整填写紫鸟账号名称、紫鸟令牌';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
fetch('/api/admin/shop-key/' + itemId, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
remark_name: remarkName,
|
||
ziniao_account_name: ziniaoAccountName,
|
||
ziniao_token: ziniaoToken
|
||
})
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
document.getElementById('editShopKeyModal').classList.remove('show');
|
||
loadShopKeys(shopKeyPage);
|
||
} else {
|
||
msgEl.textContent = res.error || '保存失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.classList.add('err');
|
||
});
|
||
};
|
||
document.getElementById('btnCloseEditShopKey').onclick = function () {
|
||
document.getElementById('editShopKeyModal').classList.remove('show');
|
||
};
|
||
|
||
// ========== 店铺管理 ==========
|
||
var shopManagePage = 1, shopManagePageSize = 15;
|
||
var shopManageGroups = [];
|
||
var shopManageGroupsLoadedAt = 0;
|
||
var shopManageLockedGroupId = null;
|
||
var currentShopManageGroupGrantRoutes = [];
|
||
|
||
function buildShopManageQuery(page) {
|
||
var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize;
|
||
var groupId = (document.getElementById('shopManageFilterGroupId').value || '').trim();
|
||
var shopName = (document.getElementById('shopManageFilterShopName').value || '').trim();
|
||
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
|
||
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
|
||
return query;
|
||
}
|
||
|
||
function shopPasswordIcon(revealed) {
|
||
if (revealed) {
|
||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m2 2 20 20"></path><path d="M6.71 6.71C4.7 8.1 3.34 10.08 2 12c2.12 3.04 5.5 6 10 6 1.67 0 3.17-.41 4.47-1.05"></path><path d="M10.73 5.08A9.36 9.36 0 0 1 12 5c4.5 0 7.88 2.96 10 7a15.82 15.82 0 0 1-2.12 2.91"></path><path d="M14.12 14.12A3 3 0 0 1 9.88 9.88"></path></svg>';
|
||
}
|
||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.06 12.35a1 1 0 0 1 0-.7C3.54 8.04 7.06 5.5 12 5.5s8.46 2.54 9.94 6.15a1 1 0 0 1 0 .7C20.46 15.96 16.94 18.5 12 18.5S3.54 15.96 2.06 12.35"></path><circle cx="12" cy="12" r="3"></circle></svg>';
|
||
}
|
||
|
||
function renderShopPasswordCell(item) {
|
||
var maskedPassword = item.password || '******';
|
||
return '<span class="shop-password-cell">' +
|
||
'<span class="shop-password-value" data-shop-password-value>' + escapeHtml(maskedPassword) + '</span>' +
|
||
'<button type="button" class="shop-password-toggle" data-shop-password-toggle="' + escapeHtml(item.id) + '" data-shop-name="' + escapeHtml(item.shop_name || '') + '" data-masked-password="' + escapeHtml(maskedPassword) + '" aria-label="显示密码" aria-pressed="false" title="显示密码">' +
|
||
shopPasswordIcon(false) + '</button></span>';
|
||
}
|
||
|
||
function renderShopTableText(value, fallback) {
|
||
var text = String(value == null ? '' : value).trim();
|
||
var shown = text || fallback || '-';
|
||
return '<span class="table-ellipsis" title="' + escapeHtml(shown) + '">' + escapeHtml(shown) + '</span>';
|
||
}
|
||
|
||
function loadShopManage(page) {
|
||
shopManagePage = page || 1;
|
||
fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var tbody = document.getElementById('shopManageListBody');
|
||
if (!res.success) {
|
||
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||
return;
|
||
}
|
||
var items = res.items || [];
|
||
if (items.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="10" class="empty-tip">暂无店铺</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = items.map(function (item, index) {
|
||
var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1;
|
||
return '<tr>' +
|
||
'<td class="shop-col-index">' + rowNo + '</td>' +
|
||
'<td class="shop-col-group">' + renderShopTableText(item.group_name) + '</td>' +
|
||
'<td class="shop-col-name">' + renderShopTableText(item.shop_name) + '</td>' +
|
||
'<td class="shop-col-mall">' + renderShopTableText(item.mall_name) + '</td>' +
|
||
'<td class="shop-col-automation">' + renderShopTableText(item.zn_username) + '</td>' +
|
||
'<td class="shop-col-account">' + renderShopTableText(item.account) + '</td>' +
|
||
'<td class="shop-col-password">' + renderShopPasswordCell(item) + '</td>' +
|
||
'<td class="shop-col-created">' + renderShopTableText(item.created_at) + '</td>' +
|
||
'<td class="shop-col-updated">' + renderShopTableText(item.updated_at) + '</td>' +
|
||
'<td class="shop-col-actions">' +
|
||
'<button type="button" class="btn btn-sm" data-shop-manage-edit="' + escapeHtml(item.id) + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '"')) + '">编辑</button> ' +
|
||
'<button type="button" class="btn btn-sm btn-danger" data-shop-manage-delete="' + escapeHtml(item.id) + '" data-shop-manage-name="' + escapeHtml(item.shop_name || '') + '">删除</button>' +
|
||
'</td></tr>';
|
||
}).join('');
|
||
}
|
||
renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage);
|
||
bindShopManageActions();
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('shopManageListBody').innerHTML = '<tr><td colspan="10" class="empty-tip">请求失败</td></tr>';
|
||
});
|
||
}
|
||
|
||
function bindShopManageActions() {
|
||
document.querySelectorAll('[data-shop-password-toggle]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var valueEl = btn.parentElement.querySelector('[data-shop-password-value]');
|
||
var revealed = btn.dataset.revealed === 'true';
|
||
if (revealed) {
|
||
valueEl.textContent = btn.dataset.maskedPassword || '******';
|
||
btn.dataset.revealed = 'false';
|
||
btn.setAttribute('aria-label', '显示密码');
|
||
btn.setAttribute('aria-pressed', 'false');
|
||
btn.title = '显示密码';
|
||
btn.innerHTML = shopPasswordIcon(false);
|
||
return;
|
||
}
|
||
|
||
btn.disabled = true;
|
||
valueEl.textContent = '读取中...';
|
||
fetch('/api/admin/shop-manage/' + encodeURIComponent(btn.dataset.shopPasswordToggle) + '/credential?shop_name=' + encodeURIComponent(btn.dataset.shopName || ''))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) throw new Error(res.error || '读取密码失败');
|
||
valueEl.textContent = res.password || '';
|
||
btn.dataset.revealed = 'true';
|
||
btn.setAttribute('aria-label', '隐藏密码');
|
||
btn.setAttribute('aria-pressed', 'true');
|
||
btn.title = '隐藏密码';
|
||
btn.innerHTML = shopPasswordIcon(true);
|
||
})
|
||
.catch(function (error) {
|
||
valueEl.textContent = btn.dataset.maskedPassword || '******';
|
||
alert(error.message || '读取密码失败');
|
||
})
|
||
.finally(function () {
|
||
btn.disabled = false;
|
||
});
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-shop-manage-edit]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var item = {};
|
||
try { item = JSON.parse((btn.dataset.shopManage || '').replace(/"/g, '"')); } catch (e) { item = {}; }
|
||
document.getElementById('editShopManageId').value = item.id || '';
|
||
document.getElementById('editShopManageShopName').value = item.shop_name || '';
|
||
document.getElementById('editShopManageMallName').value = item.mall_name || '';
|
||
document.getElementById('editShopManageZnUsername').value = item.zn_username || '';
|
||
document.getElementById('editShopManageAccount').value = item.account || '';
|
||
document.getElementById('editShopManagePassword').value = item.password || '';
|
||
document.getElementById('msgEditShopManage').textContent = '';
|
||
document.getElementById('msgEditShopManage').className = 'msg';
|
||
loadShopManageGroups(null, item.group_id).then(function () {
|
||
document.getElementById('editShopManageModal').classList.add('show');
|
||
});
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-shop-manage-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var name = (btn.dataset.shopManageName || '').replace(/"/g, '"');
|
||
if (!confirm('确定删除店铺“' + name + '”吗?')) return;
|
||
fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) { loadShopManage(shopManagePage); }
|
||
else { alert(res.error || '删除失败'); }
|
||
});
|
||
};
|
||
});
|
||
}
|
||
|
||
function getInvalidAsinDataLockedGroupId() {
|
||
if (currentUserRole === 'super_admin') return '';
|
||
if (shopManageLockedGroupId) return String(shopManageLockedGroupId);
|
||
return shopManageGroups.length ? String(shopManageGroups[0].id || '') : '';
|
||
}
|
||
|
||
function refreshShopGroupSelects(selectedCreateId, selectedEditId) {
|
||
var createSel = document.getElementById('shopManageGroupSelect');
|
||
var editSel = document.getElementById('editShopManageGroupSelect');
|
||
var invalidCreateSel = document.getElementById('invalidAsinDataGroupSelect');
|
||
var invalidEditSel = document.getElementById('editInvalidAsinDataGroupSelect');
|
||
var invalidFilterSel = document.getElementById('invalidAsinDataFilterGroupId');
|
||
var dedupeImportSel = document.getElementById('dedupeTotalDataImportGroupId');
|
||
var dedupeDeleteSel = document.getElementById('dedupeTotalDataDeleteGroupId');
|
||
var dedupeFilterSel = document.getElementById('dedupeTotalDataGroupFilterId');
|
||
var dedupeEditSel = document.getElementById('editDedupeTotalDataGroupId');
|
||
var filterSel = document.getElementById('shopManageFilterGroupId');
|
||
var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect');
|
||
var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId');
|
||
var chooseSkipShopSel = document.getElementById('chooseSkipPriceAsinShopGroupId');
|
||
var queryCreateSel = document.getElementById('queryAsinGroupSelect');
|
||
var queryFilterSel = document.getElementById('queryAsinFilterGroupId');
|
||
var chooseQueryShopSel = document.getElementById('chooseQueryAsinShopGroupId');
|
||
var selectedInvalidCreateId = invalidCreateSel ? invalidCreateSel.value : '';
|
||
var selectedInvalidEditId = invalidEditSel ? invalidEditSel.value : '';
|
||
var selectedInvalidFilterId = invalidFilterSel ? invalidFilterSel.value : '';
|
||
var selectedDedupeImportId = dedupeImportSel ? dedupeImportSel.value : '';
|
||
var selectedDedupeDeleteId = dedupeDeleteSel ? dedupeDeleteSel.value : '';
|
||
var selectedDedupeFilterId = dedupeFilterSel ? dedupeFilterSel.value : '';
|
||
var selectedDedupeEditId = dedupeEditSel ? dedupeEditSel.value : '';
|
||
var selectedFilterId = filterSel ? filterSel.value : '';
|
||
var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : '';
|
||
var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : '';
|
||
var 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('');
|
||
var lockedInvalidGroupId = getInvalidAsinDataLockedGroupId();
|
||
var lockedGroup = shopManageGroups.find(function (group) {
|
||
return String(group.id || '') === lockedInvalidGroupId;
|
||
});
|
||
var invalidGroupOpts = createOpts;
|
||
if (currentUserRole !== 'super_admin') {
|
||
invalidGroupOpts = lockedGroup
|
||
? ['<option value="' + lockedGroup.id + '">' + (lockedGroup.group_name || '') + '</option>']
|
||
: [createOpts[0]];
|
||
}
|
||
var invalidFilterOpts = currentUserRole === 'super_admin'
|
||
? filterOpts
|
||
: (lockedGroup
|
||
? ['<option value="' + lockedGroup.id + '">' + (lockedGroup.group_name || '') + '</option>']
|
||
: [createOpts[0]]);
|
||
if (invalidCreateSel) {
|
||
invalidCreateSel.innerHTML = invalidGroupOpts.join('');
|
||
invalidCreateSel.disabled = currentUserRole !== 'super_admin';
|
||
}
|
||
if (invalidEditSel) {
|
||
invalidEditSel.innerHTML = invalidGroupOpts.join('');
|
||
invalidEditSel.disabled = currentUserRole !== 'super_admin';
|
||
}
|
||
if (invalidFilterSel) {
|
||
invalidFilterSel.innerHTML = invalidFilterOpts.join('');
|
||
invalidFilterSel.disabled = currentUserRole !== 'super_admin';
|
||
}
|
||
if (dedupeImportSel) dedupeImportSel.innerHTML = createOpts.join('');
|
||
if (dedupeDeleteSel) dedupeDeleteSel.innerHTML = createOpts.join('');
|
||
if (dedupeFilterSel) dedupeFilterSel.innerHTML = filterOpts.join('');
|
||
if (dedupeEditSel) dedupeEditSel.innerHTML = createOpts.join('');
|
||
if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join('');
|
||
if (queryCreateSel) queryCreateSel.innerHTML = createOpts.join('');
|
||
if (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 (invalidCreateSel) {
|
||
invalidCreateSel.value = currentUserRole === 'super_admin'
|
||
? selectedInvalidCreateId
|
||
: lockedInvalidGroupId;
|
||
}
|
||
if (invalidEditSel) {
|
||
invalidEditSel.value = currentUserRole === 'super_admin'
|
||
? selectedInvalidEditId
|
||
: lockedInvalidGroupId;
|
||
}
|
||
if (invalidFilterSel) {
|
||
invalidFilterSel.value = currentUserRole === 'super_admin'
|
||
? selectedInvalidFilterId
|
||
: lockedInvalidGroupId;
|
||
}
|
||
if (dedupeImportSel && selectedDedupeImportId) dedupeImportSel.value = selectedDedupeImportId;
|
||
if (dedupeDeleteSel && selectedDedupeDeleteId) dedupeDeleteSel.value = selectedDedupeDeleteId;
|
||
if (dedupeFilterSel && selectedDedupeFilterId) dedupeFilterSel.value = selectedDedupeFilterId;
|
||
if (dedupeEditSel && selectedDedupeEditId) dedupeEditSel.value = selectedDedupeEditId;
|
||
if (filterSel && selectedFilterId) filterSel.value = selectedFilterId;
|
||
if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId;
|
||
if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId;
|
||
if (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 || [];
|
||
shopManageLockedGroupId = res.locked_group_id || null;
|
||
shopManageGroupsLoadedAt = Date.now();
|
||
refreshShopGroupSelects(selectedCreateId, selectedEditId);
|
||
return shopManageGroups;
|
||
})
|
||
.catch(function () {
|
||
shopManageGroups = [];
|
||
shopManageLockedGroupId = null;
|
||
shopManageGroupsLoadedAt = 0;
|
||
refreshShopGroupSelects();
|
||
return [];
|
||
});
|
||
}
|
||
|
||
function renderDedupeGroupSummary() {
|
||
var summaryEl = document.getElementById('dedupeGroupSummary');
|
||
if (!summaryEl) return;
|
||
if (currentUserRole === 'super_admin') {
|
||
summaryEl.innerHTML = '<span class="dedupe-group-summary-chip">全部分组 · ' +
|
||
escapeHtml(shopManageGroups.length) + ' 个</span>';
|
||
return;
|
||
}
|
||
if (!shopManageGroups.length) {
|
||
summaryEl.textContent = '未加入分组';
|
||
return;
|
||
}
|
||
var visibleGroups = shopManageGroups.slice(0, 4);
|
||
summaryEl.innerHTML = visibleGroups.map(function (group) {
|
||
var relation = String(group.leader_user_id || '') === String(currentUserId || '') ? '组长' : '组员';
|
||
return '<span class="dedupe-group-summary-chip" title="' + escapeHtml(group.group_name || '') + '">' +
|
||
escapeHtml(group.group_name || '') + ' · ' + relation + '</span>';
|
||
}).join('') + (shopManageGroups.length > visibleGroups.length
|
||
? '<span>另 ' + escapeHtml(shopManageGroups.length - visibleGroups.length) + ' 个</span>'
|
||
: '');
|
||
}
|
||
|
||
function loadDedupeGroupSummary(force) {
|
||
var summaryEl = document.getElementById('dedupeGroupSummary');
|
||
if (!summaryEl) return Promise.resolve([]);
|
||
summaryEl.textContent = '正在加载...';
|
||
return loadShopManageGroups(null, null, force).then(function (groups) {
|
||
renderDedupeGroupSummary();
|
||
return groups;
|
||
});
|
||
}
|
||
|
||
function resetShopManageGroupForm() {
|
||
document.getElementById('shopManageGroupEditId').value = '';
|
||
document.getElementById('shopManageGroupLeaderUserId').value = currentUserId || '';
|
||
document.getElementById('shopManageGroupLeaderName').value = currentUserUsername || '';
|
||
document.getElementById('shopManageGroupInput').value = '';
|
||
document.getElementById('msgShopManageGroup').textContent = '';
|
||
document.getElementById('msgShopManageGroup').className = 'msg';
|
||
refreshShopManageGroupMemberSelect(currentUserId, []);
|
||
}
|
||
|
||
function setShopManageGroupGrantRoutes(routes) {
|
||
currentShopManageGroupGrantRoutes = Array.isArray(routes) ? routes.slice() : [];
|
||
}
|
||
|
||
function updateShopManageGroupButtonsAccess() {
|
||
var canManageGroups = !!currentUserId;
|
||
['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups', 'btnManageQueryAsinGroups', 'btnManageDedupeGroups', 'btnManageInvalidAsinDataGroups', 'btnManageInvalidAsinDataGroupsFromEdit'].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(/"/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);
|
||
if (getActiveAdminTabName() === 'dedupe-total-data') loadDedupeTotalData(dedupeTotalDataPage);
|
||
if (getActiveAdminTabName() === 'invalid-asin-data') loadInvalidAsinData(invalidAsinDataPage);
|
||
});
|
||
});
|
||
};
|
||
});
|
||
}
|
||
|
||
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('btnManageInvalidAsinDataGroups').onclick = function () {
|
||
setShopManageGroupGrantRoutes(['invalid-asin-data']);
|
||
openShopManageGroupModal();
|
||
};
|
||
document.getElementById('btnManageInvalidAsinDataGroupsFromEdit').onclick = function () {
|
||
setShopManageGroupGrantRoutes(['invalid-asin-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);
|
||
if (getActiveAdminTabName() === 'dedupe-total-data') loadDedupeTotalData(dedupeTotalDataPage);
|
||
if (getActiveAdminTabName() === 'invalid-asin-data') loadInvalidAsinData(invalidAsinDataPage);
|
||
});
|
||
})
|
||
.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 znUsername = (document.getElementById('shopManageZnUsername').value || '').trim();
|
||
var account = (document.getElementById('shopManageAccount').value || '').trim();
|
||
var password = (document.getElementById('shopManagePassword').value || '').trim();
|
||
var msgEl = document.getElementById('msgShopManage');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!groupId || !shopName || !mallName || !account || !password) {
|
||
msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
fetch('/api/admin/shop-manage', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, zn_username: znUsername, account: account, password: password })
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
document.getElementById('shopManageGroupSelect').value = '';
|
||
document.getElementById('shopManageShopName').value = '';
|
||
document.getElementById('shopManageMallName').value = '';
|
||
document.getElementById('shopManageZnUsername').value = '';
|
||
document.getElementById('shopManageAccount').value = '';
|
||
document.getElementById('shopManagePassword').value = '';
|
||
msgEl.textContent = res.msg || '创建成功';
|
||
msgEl.className = 'msg ok';
|
||
loadShopManage(1);
|
||
} else {
|
||
msgEl.textContent = res.error || '创建失败';
|
||
msgEl.className = 'msg err';
|
||
}
|
||
})
|
||
.catch(function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.className = 'msg err';
|
||
});
|
||
};
|
||
|
||
document.getElementById('btnSaveShopManage').onclick = function () {
|
||
var itemId = document.getElementById('editShopManageId').value;
|
||
var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim();
|
||
var shopName = (document.getElementById('editShopManageShopName').value || '').trim();
|
||
var mallName = (document.getElementById('editShopManageMallName').value || '').trim();
|
||
var znUsername = (document.getElementById('editShopManageZnUsername').value || '').trim();
|
||
var account = (document.getElementById('editShopManageAccount').value || '').trim();
|
||
var password = (document.getElementById('editShopManagePassword').value || '').trim();
|
||
var msgEl = document.getElementById('msgEditShopManage');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!groupId || !shopName || !mallName || !account || !password) {
|
||
msgEl.textContent = '请完整填写分组、店铺名、店铺商城名、账号、密码';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
fetch('/api/admin/shop-manage/' + itemId, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, zn_username: znUsername, account: account, password: password })
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
document.getElementById('editShopManageModal').classList.remove('show');
|
||
loadShopManage(shopManagePage);
|
||
} else {
|
||
msgEl.textContent = res.error || '保存失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
msgEl.textContent = '请求失败';
|
||
msgEl.classList.add('err');
|
||
});
|
||
};
|
||
document.getElementById('btnCloseEditShopManage').onclick = function () {
|
||
document.getElementById('editShopManageModal').classList.remove('show');
|
||
};
|
||
|
||
// ========== 版本管理 ==========
|
||
// ========== 跳过跟价 ASIN ==========
|
||
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, '"') + '" 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(/"/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');
|
||
if (!container) return;
|
||
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 class="asin-empty-hint">请选择国家后输入 ASIN 和最低价</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = selectedCountries.map(function (countryCode) {
|
||
var label = getSkipPriceCountryLabel(countryCode);
|
||
var value = existingValues[countryCode] || '';
|
||
var minimumPrice = existingMinimumPrices[countryCode] || '';
|
||
return '<div class="skip-price-entry">' +
|
||
'<span class="skip-price-country">' + escapeHtml(label) + '</span>' +
|
||
'<input class="skip-price-asin-input" type="text" data-skip-price-country-input="' + escapeHtml(countryCode) + '" value="' + escapeHtml(value) + '" placeholder="请输入' + escapeHtml(label) + ' ASIN">' +
|
||
'<input class="skip-price-minimum-input" type="number" data-skip-price-country-minimum-price-input="' + escapeHtml(countryCode) + '" value="' + escapeHtml(minimumPrice) + '" min="0" step="0.01" placeholder="最低价">' +
|
||
'</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 = String(item[country.field] || '').trim();
|
||
var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]);
|
||
var hasValue = !!asinValue || !!minimumPriceValue;
|
||
var safeAsin = escapeHtml(asinValue);
|
||
var safeMinimumPrice = escapeHtml(minimumPriceValue || '-');
|
||
var countryLabel = escapeHtml(country.label || country.code || '国家');
|
||
var infoHtml = hasValue
|
||
? '<div class="asin-cell-content">' +
|
||
(asinValue ? '<span class="asin-cell-value" title="' + safeAsin + '">' + safeAsin + '</span>' : '<span class="asin-empty-value">-</span>') +
|
||
'<span class="asin-cell-meta">最低价:' + safeMinimumPrice + '</span>' +
|
||
'</div>'
|
||
: '<span class="asin-empty-value">-</span>';
|
||
var deleteHtml = hasValue
|
||
? '<button type="button" class="btn btn-sm btn-danger" aria-label="删除' + countryLabel + ' ASIN" data-skip-price-asin-delete="' + escapeHtml(item.id) + '" data-country="' + escapeHtml(country.code) + '" data-shop-name="' + escapeHtml(item.shop_name || '') + '">删除</button>'
|
||
: '';
|
||
return '<div class="asin-cell-layout">' +
|
||
infoHtml +
|
||
'<div class="asin-cell-actions">' +
|
||
'<button type="button" class="btn btn-sm" aria-label="编辑' + countryLabel + ' ASIN" data-skip-price-asin-edit="' + escapeHtml(item.id) + '" data-country="' + escapeHtml(country.code) + '" data-asin="' + safeAsin + '" data-minimum-price="' + escapeHtml(minimumPriceValue) + '" data-shop-name="' + escapeHtml(item.shop_name || '') + '">编辑</button>' +
|
||
deleteHtml +
|
||
'</div></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(/"/g, '"');
|
||
var countryCode = btn.dataset.country || '';
|
||
var currentAsin = (btn.dataset.asin || '').replace(/"/g, '"');
|
||
var currentMinimumPrice = (btn.dataset.minimumPrice || '').replace(/"/g, '"');
|
||
openEditSkipPriceAsinModal(
|
||
btn.dataset.skipPriceAsinEdit,
|
||
countryCode,
|
||
shopName,
|
||
currentAsin,
|
||
currentMinimumPrice
|
||
);
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var shopName = (btn.dataset.shopName || '').replace(/"/g, '"');
|
||
var countryCode = btn.dataset.country || '';
|
||
if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return;
|
||
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 class="asin-col-index">' + rowNo + '</td>' +
|
||
'<td class="asin-col-group">' + renderShopTableText(item.group_name) + '</td>' +
|
||
'<td class="asin-col-shop">' + renderShopTableText(item.shop_name) + '</td>' +
|
||
skipPriceCountryColumns.map(function (country) {
|
||
return '<td class="asin-col-country">' + 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, '"') + '" 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, '"') + '" 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(/"/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 = String(item[country.field] || '').trim();
|
||
var countryLabel = escapeHtml(country.label || country.code || '国家');
|
||
var infoHtml = asinValue
|
||
? '<span class="asin-cell-value" title="' + escapeHtml(asinValue) + '">' + escapeHtml(asinValue) + '</span>'
|
||
: '<span class="asin-empty-value">-</span>';
|
||
var deleteHtml = asinValue
|
||
? '<button type="button" class="btn btn-sm btn-danger" aria-label="删除' + countryLabel + ' ASIN" data-query-asin-delete="' + escapeHtml(item.id) + '" data-country="' + escapeHtml(country.code) + '" data-shop-name="' + escapeHtml(item.shop_name || '') + '">删除</button>'
|
||
: '';
|
||
return '<div class="asin-cell-layout">' +
|
||
'<div class="asin-cell-content">' + infoHtml + '</div>' +
|
||
'<div class="asin-cell-actions">' +
|
||
'<button type="button" class="btn btn-sm" aria-label="编辑' + countryLabel + ' ASIN" data-query-asin-edit="' + escapeHtml(item.id) + '" data-country="' + escapeHtml(country.code) + '" data-asin="' + escapeHtml(asinValue) + '" data-shop-name="' + escapeHtml(item.shop_name || '') + '">编辑</button>' +
|
||
deleteHtml +
|
||
'</div></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(/"/g, '"'),
|
||
(btn.dataset.asin || '').replace(/"/g, '"')
|
||
);
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-query-asin-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var shopName = (btn.dataset.shopName || '').replace(/"/g, '"');
|
||
var countryCode = btn.dataset.country || '';
|
||
if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return;
|
||
fetch('/api/admin/query-asin/' + btn.dataset.queryAsinDelete + '/country/' + countryCode, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) loadQueryAsin(queryAsinPage);
|
||
else alert(res.error || '删除失败');
|
||
});
|
||
};
|
||
});
|
||
}
|
||
function loadQueryAsin(page) {
|
||
queryAsinPage = page || 1;
|
||
fetch('/api/admin/query-asins?' + buildQueryAsinQuery(queryAsinPage))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
var tbody = document.getElementById('queryAsinListBody');
|
||
if (!res.success) {
|
||
tbody.innerHTML = '<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 class="asin-col-index">' + rowNo + '</td>' +
|
||
'<td class="asin-col-group">' + renderShopTableText(item.group_name) + '</td>' +
|
||
'<td class="asin-col-shop">' + renderShopTableText(item.shop_name) + '</td>' +
|
||
queryAsinCountryColumns.map(function (country) {
|
||
return '<td class="asin-col-country">' + 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><span class="category-path-value table-ellipsis" title="' + escapeHtml(item.path || item.name || '') + '">' + escapeHtml(item.path || item.name || '') + '</span></td>' +
|
||
'<td>' + escapeHtml(item.sort_order || 0) + '</td>' +
|
||
'<td><span class="category-tag">' + (item.is_builtin ? '内置' : '自定义') + '</span></td>' +
|
||
'<td>' + escapeHtml(item.description || '') + '</td>' +
|
||
'<td><button class="btn btn-sm" data-product-category-edit="' + escapeHtml(item.id) + '">编辑</button> ' +
|
||
'<button class="btn btn-sm btn-danger" data-product-category-delete="' + escapeHtml(item.id) + '" data-product-category-name="' + escapeHtml(item.name || '') + '"' + (item.child_count > 0 ? ' disabled' : '') + '>删除</button></td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
document.querySelectorAll('[data-product-category-toggle]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var id = String(btn.dataset.productCategoryToggle || '');
|
||
if (!id || btn.disabled) return;
|
||
toggleProductCategoryNode(id);
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-product-category-load-more]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var parentId = (btn.dataset.productCategoryLoadMore || '').trim();
|
||
loadProductCategoryChildren(parentId || null, true);
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-product-category-search-more]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
loadProductCategorySearch(true);
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-product-category-edit]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
var item = productCategoryItems.find(function (row) { return String(row.id) === String(btn.dataset.productCategoryEdit); });
|
||
if (!item) return;
|
||
editingProductCategoryId = item.id;
|
||
renderProductCategoryParentOptions(item.parent_id || '', item.id);
|
||
document.getElementById('productCategoryName').value = item.name || '';
|
||
document.getElementById('productCategorySortOrder').value = item.sort_order || '';
|
||
document.getElementById('productCategoryDescription').value = item.description || '';
|
||
document.getElementById('btnCreateProductCategory').textContent = '保存类目';
|
||
document.getElementById('btnCancelProductCategoryEdit').style.display = '';
|
||
document.getElementById('msgProductCategory').textContent = '';
|
||
document.getElementById('msgProductCategory').className = 'msg';
|
||
};
|
||
});
|
||
document.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 type="button" class="btn btn-sm" data-admin-confirm data-confirm-message="确认发布版本 ' + v.version + ' 吗?" onclick="releaseVersion(\'' + v.version + '\')">发布</button> ';
|
||
}
|
||
if (v.status === 'RELEASED' && !v.isLatest) {
|
||
actions += '<button type="button" class="btn btn-sm" data-admin-confirm data-confirm-message="确认将版本 ' + v.version + ' 设为最新吗?客户端会自动检测更新。" onclick="setLatestVersion(\'' + v.version + '\')">设为最新</button> ';
|
||
}
|
||
if (v.status === 'RELEASED') {
|
||
actions += '<button class="btn btn-sm" onclick="downloadDigitalHumanVersion(\'' + encodeURIComponent(v.version || '') + '\')">下载</button> ';
|
||
}
|
||
if (!v.isLatest) {
|
||
actions += '<button type="button" class="btn btn-sm btn-danger" data-admin-confirm data-confirm-message="确认删除版本 ' + v.version + ' 吗?该操作会删除对应文件,无法恢复。" onclick="deleteVersion(\'' + v.version + '\')">删除</button>';
|
||
}
|
||
|
||
return '<tr>' +
|
||
'<td>' + (v.version || '') + '</td>' +
|
||
'<td style="color:' + statusColor + ';">' + statusText + '</td>' +
|
||
'<td>' + isLatestBadge + '</td>' +
|
||
'<td>' + fileSize + '</td>' +
|
||
'<td title="' + (v.md5 || '') + '">' + md5Short + '</td>' +
|
||
'<td>' + changelog + '</td>' +
|
||
'<td>' + releasedAt + '</td>' +
|
||
'<td>' + actions + '</td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
}
|
||
|
||
// 分页
|
||
var pagination = document.getElementById('digitalHumanVersionPagination');
|
||
if (pages > 1) {
|
||
var html = '';
|
||
for (var i = 1; i <= pages; i++) {
|
||
var active = i === current ? ' active' : '';
|
||
html += '<span class="page-item' + active + '" onclick="loadDigitalHumanVersions(' + i + ')">' + i + '</span>';
|
||
}
|
||
pagination.innerHTML = html;
|
||
appendPaginationQuickJump(pagination, pages, current, loadDigitalHumanVersions);
|
||
} else {
|
||
pagination.innerHTML = '';
|
||
}
|
||
})
|
||
.catch(function (err) {
|
||
document.getElementById('digitalHumanVersionListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败: ' + (err.message || '') + '</td></tr>';
|
||
});
|
||
}
|
||
function downloadDigitalHumanVersion(encodedVersion) {
|
||
var version = decodeURIComponent(encodedVersion || '');
|
||
if (!version) {
|
||
alert('版本号为空');
|
||
return;
|
||
}
|
||
fetch('/api/admin/digital-human-versions/' + encodeURIComponent(version) + '/download-url')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (!res.success) {
|
||
throw new Error(res.message || res.error || '获取下载链接失败');
|
||
}
|
||
var data = res.data || {};
|
||
var downloadUrl = typeof data === 'string' ? data : (data.downloadUrl || data.url || '');
|
||
if (!downloadUrl) {
|
||
throw new Error('Java 后端未返回下载链接');
|
||
}
|
||
var a = document.createElement('a');
|
||
a.href = downloadUrl;
|
||
a.download = 'ShuFuDigitalHuman-' + version + '.zip';
|
||
a.target = '_blank';
|
||
a.rel = 'noopener';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
})
|
||
.catch(function (err) {
|
||
alert(err.message || '下载失败');
|
||
});
|
||
}
|
||
|
||
window.loadDigitalHumanVersions = loadDigitalHumanVersions;
|
||
window.downloadDigitalHumanVersion = downloadDigitalHumanVersion;
|
||
|
||
document.getElementById('btnUploadVersion').onclick = function () {
|
||
var version = (document.getElementById('versionNumber').value || '').trim();
|
||
var fileInput = document.getElementById('versionZip');
|
||
var msgEl = document.getElementById('msgVersion');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
|
||
if (!version) {
|
||
msgEl.textContent = '请填写版本号';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (!fileInput.files || fileInput.files.length === 0) {
|
||
msgEl.textContent = '请选择 zip 压缩包';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var file = fileInput.files[0];
|
||
if (!(file.name || '').toLowerCase().endsWith('.zip')) {
|
||
msgEl.textContent = '仅支持 .zip 格式';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
|
||
var formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('version', version);
|
||
|
||
msgEl.textContent = '上传中,请稍候...';
|
||
msgEl.classList.remove('err', 'ok');
|
||
|
||
fetch('/api/admin/version', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
msgEl.textContent = '发布成功。版本:' + res.version + ',链接:' + (res.file_url || '');
|
||
msgEl.classList.add('ok');
|
||
document.getElementById('versionNumber').value = '';
|
||
fileInput.value = '';
|
||
loadSoftwareVersions();
|
||
} else {
|
||
msgEl.textContent = res.error || res.message || '上传失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
})
|
||
.catch(function (err) {
|
||
msgEl.textContent = '请求失败: ' + (err.message || '');
|
||
msgEl.classList.add('err');
|
||
});
|
||
};
|
||
|
||
document.getElementById('btnUploadDigitalHumanVersion').onclick = function () {
|
||
var version = (document.getElementById('digitalHumanVersionNumber').value || '').trim();
|
||
var minClientVersion = (document.getElementById('digitalHumanMinClientVersion').value || '').trim();
|
||
var changelog = (document.getElementById('digitalHumanVersionChangelog').value || '').trim();
|
||
var fileInput = document.getElementById('digitalHumanVersionZip');
|
||
var msgEl = document.getElementById('msgDigitalHumanVersion');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
|
||
if (!version) {
|
||
msgEl.textContent = '请填写版本号';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
if (!fileInput.files || fileInput.files.length === 0) {
|
||
msgEl.textContent = '请选择 zip 压缩包';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
var file = fileInput.files[0];
|
||
if (!(file.name || '').toLowerCase().endsWith('.zip')) {
|
||
msgEl.textContent = '仅支持 .zip 格式';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
|
||
var formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('version', version);
|
||
if (minClientVersion) formData.append('minClientVersion', minClientVersion);
|
||
if (changelog) formData.append('changelog', changelog);
|
||
|
||
msgEl.textContent = '上传中,请稍候...';
|
||
msgEl.classList.remove('err', 'ok');
|
||
|
||
var uploadUrl = window.DIGITAL_HUMAN_JAVA_UPLOAD_URL;
|
||
if (!uploadUrl) {
|
||
uploadUrl = '/api/admin/digital-human-versions/upload';
|
||
}
|
||
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open('POST', uploadUrl, true);
|
||
xhr.timeout = 1800000;
|
||
xhr.upload.onprogress = function (event) {
|
||
if (event.lengthComputable) {
|
||
var percent = Math.floor((event.loaded / event.total) * 100);
|
||
msgEl.textContent = '正在上传:' + percent + '%';
|
||
} else {
|
||
msgEl.textContent = '正在上传,请稍候...';
|
||
}
|
||
};
|
||
xhr.onload = function () {
|
||
var res = null;
|
||
try {
|
||
res = JSON.parse(xhr.responseText || '{}');
|
||
} catch (e) {
|
||
msgEl.textContent = '上传失败:Java 返回不是 JSON';
|
||
msgEl.classList.add('err');
|
||
return;
|
||
}
|
||
|
||
if (xhr.status >= 200 && xhr.status < 300 && res.success && res.data) {
|
||
msgEl.textContent = '上传成功!版本:' + res.data.version + '(状态:草稿,请在列表中点击"发布"按钮)';
|
||
msgEl.classList.add('ok');
|
||
document.getElementById('digitalHumanVersionNumber').value = '';
|
||
document.getElementById('digitalHumanMinClientVersion').value = '';
|
||
document.getElementById('digitalHumanVersionChangelog').value = '';
|
||
fileInput.value = '';
|
||
loadDigitalHumanVersions();
|
||
} else {
|
||
msgEl.textContent = (res && (res.message || res.error)) || ('上传失败:HTTP ' + xhr.status);
|
||
msgEl.classList.add('err');
|
||
}
|
||
};
|
||
xhr.onerror = function () {
|
||
msgEl.textContent = '请求失败:无法连接上传服务 ' + uploadUrl;
|
||
msgEl.classList.add('err');
|
||
};
|
||
xhr.ontimeout = function () {
|
||
msgEl.textContent = '上传超时,请检查 Java 服务或网关超时配置';
|
||
msgEl.classList.add('err');
|
||
};
|
||
xhr.send(formData);
|
||
};
|
||
|
||
window.releaseVersion = function(version) {
|
||
if (!confirm('确认发布版本 ' + version + ' 吗?')) return;
|
||
fetch('/api/admin/digital-human-versions/' + version + '/release', {
|
||
method: 'POST'
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
alert('发布成功!');
|
||
loadDigitalHumanVersions();
|
||
} else {
|
||
alert('发布失败:' + (res.message || res.error || ''));
|
||
}
|
||
})
|
||
.catch(function (err) {
|
||
alert('请求失败:' + (err.message || ''));
|
||
});
|
||
};
|
||
|
||
window.setLatestVersion = function(version) {
|
||
if (!confirm('确认将版本 ' + version + ' 设为最新吗?客户端将自动检测并更新到此版本。')) return;
|
||
fetch('/api/admin/digital-human-versions/' + version + '/set-latest', {
|
||
method: 'POST'
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
alert('设置成功!版本 ' + version + ' 已标记为最新。');
|
||
loadDigitalHumanVersions();
|
||
} else {
|
||
alert('设置失败:' + (res.message || res.error || ''));
|
||
}
|
||
})
|
||
.catch(function (err) {
|
||
alert('请求失败:' + (err.message || ''));
|
||
});
|
||
};
|
||
|
||
window.deleteVersion = function(version) {
|
||
if (!confirm('确认删除版本 ' + version + ' 吗?此操作将删除 OSS 文件,不可恢复!')) return;
|
||
fetch('/api/admin/digital-human-versions/' + version, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
alert('删除成功!');
|
||
loadDigitalHumanVersions();
|
||
} else {
|
||
alert('删除失败:' + (res.message || res.error || ''));
|
||
}
|
||
})
|
||
.catch(function (err) {
|
||
alert('请求失败:' + (err.message || ''));
|
||
});
|
||
};
|
||
|
||
|
||
// ========== 栏目权限配置 ==========
|
||
document.getElementById('btnCloseEditColumn').onclick = function () {
|
||
document.getElementById('editColumnModal').classList.remove('show');
|
||
};
|
||
|
||
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="9" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||
return;
|
||
}
|
||
allColumnsList = res.items || [];
|
||
if (allColumnsList.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" class="empty-tip">暂无菜单,请先在上方新增</td></tr>';
|
||
} else {
|
||
tbody.innerHTML = allColumnsList.map(function (c) {
|
||
var siblings = columnSiblingItems(c);
|
||
var siblingIndex = siblings.findIndex(function (item) { return String(item.id) === String(c.id); });
|
||
var moveButtons =
|
||
'<button class="btn btn-sm btn-secondary" data-column-move-up="' + c.id + '"' + (siblingIndex <= 0 ? ' disabled' : '') + '>上移</button> ' +
|
||
'<button class="btn btn-sm btn-secondary" data-column-move-down="' + c.id + '"' + (siblingIndex < 0 || siblingIndex === siblings.length - 1 ? ' disabled' : '') + '>下移</button> ';
|
||
var parent = allColumnsList.find(function (item) { return String(item.id) === String(c.parent_id || c.parentId || ''); });
|
||
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + ((c.menu_type || '') === 'admin' ? '后台' : '软件') + '</td><td>' + (parent ? (parent.name || '') : '-') + '</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, '"') + '" data-key="' + (c.column_key || '').replace(/"/g, '"') + '" data-route="' + (c.route_path || '').replace(/"/g, '"') + '" data-menu-type="' + (c.menu_type || 'app').replace(/"/g, '"') + '" data-sort-order="' + (c.sort_order != null ? String(c.sort_order) : '0').replace(/"/g, '"') + '" data-parent-id="' + String(c.parent_id || c.parentId || '').replace(/"/g, '"') + '">编辑</button> ' +
|
||
'<button class="btn btn-sm btn-danger" data-column-delete="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '"') + '">删除</button></td></tr>';
|
||
}).join('');
|
||
}
|
||
bindColumnActions();
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="9" class="empty-tip">请求失败</td></tr>';
|
||
});
|
||
}
|
||
function columnSiblingItems(item) {
|
||
var menuType = String(item && item.menu_type || 'app');
|
||
var parentId = columnParentId(item);
|
||
return allColumnsList.filter(function (candidate) {
|
||
return String(candidate.menu_type || 'app') === menuType && columnParentId(candidate) === parentId;
|
||
});
|
||
}
|
||
function bindColumnActions() {
|
||
document.querySelectorAll('[data-column-move-up]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
if (btn.disabled) return;
|
||
moveColumnItem(btn.dataset.columnMoveUp, -1, btn);
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-column-move-down]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
if (btn.disabled) return;
|
||
moveColumnItem(btn.dataset.columnMoveDown, 1, btn);
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-column-edit]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
document.getElementById('editColumnId').value = btn.dataset.columnEdit || '';
|
||
document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/"/g, '"');
|
||
document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/"/g, '"');
|
||
document.getElementById('editColumnRoutePath').value = (btn.dataset.route || '').replace(/"/g, '"');
|
||
document.getElementById('editColumnMenuType').value = ((btn.dataset.menuType || 'app').replace(/"/g, '"') || 'app');
|
||
document.getElementById('editColumnSortOrder').value = ((btn.dataset.sortOrder || '0').replace(/"/g, '"') || '0');
|
||
populateColumnParentSelects();
|
||
document.getElementById('editColumnParentId').value = btn.dataset.parentId || '';
|
||
document.getElementById('msgEditColumn').textContent = '';
|
||
document.getElementById('editColumnModal').classList.add('show');
|
||
};
|
||
});
|
||
document.querySelectorAll('[data-column-delete]').forEach(function (btn) {
|
||
btn.onclick = function () {
|
||
if (!confirm('确定删除菜单“' + (btn.dataset.name || '').replace(/"/g, '"') + '”吗?')) return;
|
||
fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); }
|
||
else { alert(res.error || '删除失败'); }
|
||
});
|
||
};
|
||
});
|
||
}
|
||
function moveColumnItem(columnId, delta, triggerBtn) {
|
||
var currentItem = allColumnsList.find(function (item) { return String(item.id) === String(columnId); });
|
||
if (!currentItem) return;
|
||
var siblings = columnSiblingItems(currentItem);
|
||
var currentIndex = siblings.findIndex(function (item) { return String(item.id) === String(columnId); });
|
||
if (currentIndex < 0) return;
|
||
var targetIndex = currentIndex + delta;
|
||
if (targetIndex < 0 || targetIndex >= siblings.length) return;
|
||
var targetItem = siblings[targetIndex];
|
||
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,
|
||
menu_type: currentItem.menu_type || 'app'
|
||
})
|
||
}).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 parentIdValue = (document.getElementById('columnParentId').value || '').trim();
|
||
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, parent_id: parentIdValue ? Number(parentIdValue) : null })
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
msgEl.textContent = res.msg || '新增成功';
|
||
msgEl.classList.add('ok');
|
||
document.getElementById('columnName').value = '';
|
||
document.getElementById('columnKey').value = '';
|
||
document.getElementById('columnRoutePath').value = '';
|
||
document.getElementById('columnSortOrder').value = '';
|
||
document.getElementById('columnMenuType').value = 'admin';
|
||
document.getElementById('columnParentId').value = '';
|
||
loadColumns();
|
||
loadColumnsForPermission();
|
||
loadAdminMenus(getActiveAdminTabName());
|
||
} else {
|
||
msgEl.textContent = res.error || '新增失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
})
|
||
.catch(function () { msgEl.textContent = '请求失败'; msgEl.classList.add('err'); });
|
||
};
|
||
document.getElementById('btnSaveColumn').onclick = function () {
|
||
var cid = document.getElementById('editColumnId').value;
|
||
var name = (document.getElementById('editColumnName').value || '').trim();
|
||
var 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 parentIdValue = (document.getElementById('editColumnParentId').value || '').trim();
|
||
var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10);
|
||
var msgEl = document.getElementById('msgEditColumn');
|
||
msgEl.textContent = '';
|
||
msgEl.className = 'msg';
|
||
if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; }
|
||
if (!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, parent_id: parentIdValue ? Number(parentIdValue) : null })
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
if (res.success) {
|
||
document.getElementById('editColumnModal').classList.remove('show');
|
||
loadColumns();
|
||
loadColumnsForPermission();
|
||
loadAdminMenus(getActiveAdminTabName());
|
||
} else {
|
||
msgEl.textContent = res.error || '保存失败';
|
||
msgEl.classList.add('err');
|
||
}
|
||
});
|
||
};
|
||
if (document.getElementById('columnMenuType')) document.getElementById('columnMenuType').onchange = populateColumnParentSelects;
|
||
if (document.getElementById('editColumnMenuType')) document.getElementById('editColumnMenuType').onchange = populateColumnParentSelects;
|
||
|
||
// ========== 分页 ==========
|
||
function appendPaginationQuickJump(el, totalPages, page, onPage) {
|
||
el.insertAdjacentHTML('beforeend',
|
||
'<label class="pagination-jump">跳至<input type="number" min="1" max="' + totalPages + '" step="1" inputmode="numeric" aria-label="跳转页码" data-page-jump-input>页</label>' +
|
||
'<button type="button" data-page-jump>跳转</button>');
|
||
var jumpInput = el.querySelector('[data-page-jump-input]');
|
||
var jumpToPage = function () {
|
||
var targetPage = parseInt(jumpInput.value, 10);
|
||
if (isNaN(targetPage)) {
|
||
jumpInput.focus();
|
||
return;
|
||
}
|
||
targetPage = Math.min(Math.max(targetPage, 1), totalPages);
|
||
jumpInput.value = targetPage;
|
||
if (targetPage !== page) onPage(targetPage);
|
||
};
|
||
el.querySelector('[data-page-jump]').onclick = jumpToPage;
|
||
jumpInput.onkeydown = function (event) {
|
||
if (event.key === 'Enter') jumpToPage();
|
||
};
|
||
}
|
||
|
||
function renderPagination(elId, total, page, pageSize, onPage) {
|
||
var el = document.getElementById(elId);
|
||
if (!el) return;
|
||
var totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||
el.innerHTML = '<span>共' + total + ' 条</span>' +
|
||
'<button ' + (page <= 1 ? 'disabled' : '') + ' data-p="' + (page - 1) + '">上一页</button>' +
|
||
'<span>第' + page + ' / ' + totalPages + ' 页</span>' +
|
||
'<button ' + (page >= totalPages ? 'disabled' : '') + ' data-p="' + (page + 1) + '">下一页</button>';
|
||
el.querySelectorAll('[data-p]').forEach(function (b) {
|
||
if (!b.disabled) b.onclick = function () { onPage(parseInt(b.dataset.p, 10)); };
|
||
});
|
||
appendPaginationQuickJump(el, totalPages, page, onPage);
|
||
}
|
||
|
||
// 初始化
|
||
var adminCurrentUserPromise = null;
|
||
function loadAdminCurrentUser() {
|
||
if (adminCurrentUserPromise) return adminCurrentUserPromise;
|
||
adminCurrentUserPromise = fetch('/api/admin/current-user')
|
||
.then(function (r) {
|
||
if (r.status === 404) {
|
||
return fetch('/api/auth/check')
|
||
.then(function (fallbackResp) { return fallbackResp.json(); })
|
||
.then(function (fallbackRes) {
|
||
if (fallbackRes && fallbackRes.logged_in) {
|
||
return {
|
||
success: true,
|
||
item: {
|
||
username: '当前用户',
|
||
role: ''
|
||
}
|
||
};
|
||
}
|
||
return { success: false };
|
||
});
|
||
}
|
||
return r.json();
|
||
})
|
||
.then(function (res) {
|
||
var nameEl = document.getElementById('adminCurrentUsername');
|
||
var roleEl = document.getElementById('adminCurrentUserRole');
|
||
if (!res.success) {
|
||
nameEl.textContent = '未登录';
|
||
roleEl.style.display = 'none';
|
||
return;
|
||
}
|
||
var item = res.item || {};
|
||
currentUserId = item.id || currentUserId;
|
||
currentUserRole = item.role || currentUserRole;
|
||
currentUserUsername = item.username || currentUserUsername;
|
||
nameEl.textContent = item.username || '管理员';
|
||
var avatarEl = document.getElementById('adminUserAvatar');
|
||
if (avatarEl) avatarEl.textContent = (item.username || '管').charAt(0).toUpperCase();
|
||
if (item.role) {
|
||
roleEl.textContent = item.role === 'super_admin'
|
||
? '超级管理员'
|
||
: (item.role === 'admin' ? '管理员' : '普通账号');
|
||
roleEl.style.display = 'inline-block';
|
||
} else {
|
||
roleEl.style.display = 'none';
|
||
}
|
||
updateShopManageGroupButtonsAccess();
|
||
updateImageVideoPermissionAccess();
|
||
})
|
||
.catch(function () {
|
||
return fetch('/api/auth/check')
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (res) {
|
||
document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录';
|
||
document.getElementById('adminCurrentUserRole').style.display = 'none';
|
||
updateShopManageGroupButtonsAccess();
|
||
updateImageVideoPermissionAccess();
|
||
})
|
||
.catch(function () {
|
||
document.getElementById('adminCurrentUsername').textContent = '当前用户';
|
||
document.getElementById('adminCurrentUserRole').style.display = 'none';
|
||
updateShopManageGroupButtonsAccess();
|
||
updateImageVideoPermissionAccess();
|
||
});
|
||
})
|
||
.finally(function () {
|
||
if (!currentUserId && currentUserRole !== 'super_admin') {
|
||
adminCurrentUserPromise = null;
|
||
}
|
||
});
|
||
return adminCurrentUserPromise;
|
||
}
|
||
|
||
document.getElementById('btnAdminLogout').onclick = function () {
|
||
fetch('/api/admin/logout', {
|
||
method: 'POST',
|
||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||
})
|
||
.then(function (r) {
|
||
if (r.status === 404) {
|
||
window.location.href = '/logout';
|
||
return null;
|
||
}
|
||
return r.json();
|
||
})
|
||
.then(function (res) {
|
||
if (!res) return;
|
||
if (!res.success) {
|
||
alert(res.error || '退出失败');
|
||
return;
|
||
}
|
||
window.location.replace(res.redirect || '/login?logout=1');
|
||
})
|
||
.catch(function () {
|
||
window.location.replace('/logout');
|
||
});
|
||
};
|
||
|
||
loadAdminCurrentUser().finally(function () {
|
||
loadUserOptions();
|
||
loadShopManageGroups();
|
||
loadAdminMenus();
|
||
});
|
||
}) ();
|