feat-admin-ui-interaction
Build Backend JAR / build (push) Has been cancelled

This commit is contained in:
2026-08-30 22:09:22 +08:00
parent 2b613a8e5a
commit 42bbf4904e
14 changed files with 7720 additions and 592 deletions
+224 -61
View File
@@ -93,10 +93,39 @@
}
})();
// ===== 菜单分组(一级分类 + 二级菜单)=====
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 adminTabsEl = document.getElementById('adminTabs');
if (adminTabsEl) adminTabsEl.innerHTML = '';
var adminMenuEl = document.getElementById('adminMenu');
var activeAdminTabName = '';
var adminMenuByRoute = {};
var ADMIN_PANEL_MAP = {
'users': 'panel-users',
'columns': 'panel-columns',
@@ -149,12 +178,28 @@
var panel = panelId ? document.getElementById(panelId) : null;
if (!panel) return;
activeAdminTabName = tabName;
document.querySelectorAll('#adminTabs .tab').forEach(function (tab) {
tab.classList.toggle('active', tab.dataset.tab === tabName);
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) {
@@ -163,39 +208,75 @@
});
if (!knownItems.length) {
activeAdminTabName = '';
if (adminTabsEl) adminTabsEl.innerHTML = '<div class="tab active" style="cursor:default;">暂无可用菜单</div>';
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">暂无可用菜单</div>';
hideAllAdminPanels();
return;
}
if (adminTabsEl) {
adminTabsEl.innerHTML = knownItems.map(function (item) {
return '<div class="tab" data-tab="' + item.route_path + '">' + (item.name || item.route_path) + '</div>';
adminMenuByRoute = {};
knownItems.forEach(function (item) {
adminMenuByRoute[item.route_path] = item;
});
var groupsHtml = '';
ADMIN_MENU_GROUPS.forEach(function (group) {
var groupRoutes = group.items.filter(function (route) {
return !!adminMenuByRoute[route];
});
if (!groupRoutes.length) return;
var itemsHtml = groupRoutes.map(function (route) {
var item = adminMenuByRoute[route];
var label = (item.name || route).replace(/"/g, '&quot;');
return '<button class="tab" type="button" data-tab="' + route + '" aria-controls="panel-' + route + '" title="' + label + '">' +
'<span class="adm-icon">' + adminMenuIcon(route) + '</span>' +
'<span class="adm-label">' + label + '</span></button>';
}).join('');
groupsHtml += '<div class="menu-group" data-menu-group="' + group.key + '">' +
'<button class="menu-group-title" type="button" aria-expanded="true">' + group.title +
'<span class="menu-group-chevron" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span>' +
'</button>' +
'<div class="menu-group-body">' + itemsHtml + '</div></div>';
});
if (!groupsHtml) {
activeAdminTabName = '';
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">暂无可用菜单</div>';
hideAllAdminPanels();
return;
}
document.querySelectorAll('#adminTabs .tab').forEach(function (tab) {
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 (adminTabsEl) adminTabsEl.innerHTML = '<div class="tab active" style="cursor:default;">菜单加载失败</div>';
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">菜单加载失败</div>';
hideAllAdminPanels();
return;
}
renderAdminTabs(res.items || []);
})
.catch(function () {
if (adminTabsEl) adminTabsEl.innerHTML = '<div class="tab active" style="cursor:default;">菜单加载失败</div>';
if (adminMenuEl) adminMenuEl.innerHTML = '<div class="menu-empty">菜单加载失败</div>';
hideAllAdminPanels();
});
}
@@ -282,12 +363,15 @@
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');
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"]');
@@ -300,12 +384,15 @@
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');
if (usersTab) { usersTab.classList.add('active'); usersTab.setAttribute('aria-current', 'page'); }
if (usersPanel) usersPanel.classList.add('active');
}
syncAdminMenuGroups();
}
function loadCurrentUserAdminPermissions() {
currentUserAdminPermissionKeys = {};
@@ -340,12 +427,15 @@
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');
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];
@@ -1094,9 +1184,37 @@
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) + '">' + escapeHtml(status) + '</span>';
return '<span class="image-video-status ' + escapeHtml(status) + '" title="' + escapeHtml(status) + '">' + escapeHtml(imageVideoStatusLabel(status)) + '</span>';
}
function safeAdminUrl(value) {
var url = String(value || '').trim();
@@ -1154,7 +1272,7 @@
'<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>' +
'<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">' +
@@ -1601,7 +1719,7 @@
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(normalized) + '</span>';
return '<span class="image-video-status ' + escapeHtml(normalized) + '"' + errorTitle + '>' + escapeHtml(imageVideoStatusLabel(normalized)) + '</span>';
}
function renderShopDataTaskResult(item) {
@@ -1610,14 +1728,14 @@
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 = Array.isArray(countryCodes) ? countryCodes.join('、') : (String(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>' +
'<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>' +
@@ -1724,9 +1842,9 @@
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;
if (!window.confirm('确认删除店铺“' + (item.shop_name || '-') + '”的任务 ' + item.task_id + ' 及结果文件?')) return;
var progress = document.getElementById('shopDataTaskDownloadProgress');
progress.textContent = '正在删除任务 #' + item.task_id + '...';
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) {
@@ -1983,6 +2101,17 @@
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))
@@ -2004,7 +2133,7 @@
return '<img src="' + (url || '').replace(/"/g, '&quot;') + '" class="thumb" alt="">';
}).join('');
return '<tr><td>' + h.id + '</td><td>' + (h.username || '-') + '</td><td>' +
(h.panel_type || '-') + '</td><td>' + (h.created_at || '') + '</td><td>' +
panelTypeLabel(h.panel_type) + '</td><td>' + (h.created_at || '') + '</td><td>' +
'<div class="thumb-wrap">' + (thumbs || '-') + '</div></td></tr>';
}).join('');
}
@@ -2833,8 +2962,9 @@
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);
return '<span class="shop-key-whitelist-status ' + meta.className + '" title="' +
escapeHtml(details.join('\n')) + '">' + escapeHtml(meta.label) + '</span>';
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;
@@ -2852,9 +2982,9 @@
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (shopKeyPage - 1) * shopKeyPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + escapeHtml(item.remark_name || '') + '</td><td>' + escapeHtml(item.ziniao_account_name || '') + '</td><td>' + escapeHtml(item.ziniao_token || '') + '</td><td>' + renderShopKeyWhitelistStatus(item) + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' + escapeHtml(item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-key-edit="' + item.id + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-key-delete="' + item.id + '" data-ziniao-account-name="' + (item.ziniao_account_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
return '<tr><td>' + rowNo + '</td><td>' + renderShopTableText(item.remark_name) + '</td><td>' + renderShopTableText(item.ziniao_account_name) + '</td><td>' + renderShopTableText(item.ziniao_token) + '</td><td>' + renderShopKeyWhitelistStatus(item) + '</td><td>' + renderShopTableText(item.created_at) + '</td><td>' + renderShopTableText(item.updated_at) + '</td><td>' +
'<button type="button" class="btn btn-sm" data-shop-key-edit="' + escapeHtml(item.id) + '" data-shop-key="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button type="button" class="btn btn-sm btn-danger" data-shop-key-delete="' + escapeHtml(item.id) + '" data-ziniao-account-name="' + escapeHtml(item.ziniao_account_name || '') + '">删除</button>' +
'</td></tr>';
}).join('');
}
@@ -3004,6 +3134,12 @@
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))
@@ -3020,9 +3156,19 @@
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.mall_name || '') + '</td><td>' + (item.zn_username || '') + '</td><td>' + (item.account || '') + '</td><td>' + renderShopPasswordCell(item) + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-manage-edit="' + item.id + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-manage-delete="' + item.id + '" data-shop-manage-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
return '<tr>' +
'<td class="shop-col-index">' + rowNo + '</td>' +
'<td class="shop-col-group">' + renderShopTableText(item.group_name) + '</td>' +
'<td class="shop-col-name">' + renderShopTableText(item.shop_name) + '</td>' +
'<td class="shop-col-mall">' + renderShopTableText(item.mall_name) + '</td>' +
'<td class="shop-col-automation">' + renderShopTableText(item.zn_username) + '</td>' +
'<td class="shop-col-account">' + renderShopTableText(item.account) + '</td>' +
'<td class="shop-col-password">' + renderShopPasswordCell(item) + '</td>' +
'<td class="shop-col-created">' + renderShopTableText(item.created_at) + '</td>' +
'<td class="shop-col-updated">' + renderShopTableText(item.updated_at) + '</td>' +
'<td class="shop-col-actions">' +
'<button type="button" class="btn btn-sm" data-shop-manage-edit="' + escapeHtml(item.id) + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button type="button" class="btn btn-sm btn-danger" data-shop-manage-delete="' + escapeHtml(item.id) + '" data-shop-manage-name="' + escapeHtml(item.shop_name || '') + '">删除</button>' +
'</td></tr>';
}).join('');
}
@@ -3655,6 +3801,7 @@
}
function renderSkipPriceAsinInputs() {
var container = document.getElementById('skipPriceAsinInputs');
if (!container) return;
var selectedCountries = getSelectedSkipPriceCountries();
var existingValues = {};
var existingMinimumPrices = {};
@@ -3665,17 +3812,17 @@
existingMinimumPrices[input.getAttribute('data-skip-price-country-minimum-price-input')] = input.value;
});
if (!selectedCountries.length) {
container.innerHTML = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN 和最低价</div>';
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 style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span style="min-width:56px;color:#555;">' + label + '</span>' +
'<input type="text" data-skip-price-country-input="' + countryCode + '" value="' + value.replace(/"/g, '&quot;') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
'<input type="number" data-skip-price-country-minimum-price-input="' + countryCode + '" value="' + minimumPrice.replace(/"/g, '&quot;') + '" min="0" step="0.01" placeholder="最低价" style="width:140px;">' +
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('');
}
@@ -3783,23 +3930,27 @@
});
}
function renderSkipPriceAsinCell(item, country) {
var asinValue = item[country.field] || '';
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 style="display:flex;flex-direction:column;gap:4px;min-width:0;">' +
'<span>' + (asinValue || '<span style="color:#999;">-</span>') + '</span>' +
'<span style="color:#666;font-size:12px;">最低价:' + (minimumPriceValue || '-') + '</span>' +
'</div>')
: '<span style="color:#999;">-</span>';
? '<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 class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>')
? '<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 style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
return '<div class="asin-cell-layout">' +
infoHtml +
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-minimum-price="' + minimumPriceValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
'<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></div>';
}
function openEditSkipPriceAsinModal(itemId, countryCode, shopName, asinValue, minimumPriceValue) {
document.getElementById('editSkipPriceAsinId').value = itemId || '';
@@ -3861,9 +4012,12 @@
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td>' +
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>' + renderSkipPriceAsinCell(item, country) + '</td>';
return '<td class="asin-col-country">' + renderSkipPriceAsinCell(item, country) + '</td>';
}).join('') +
'</tr>';
}).join('');
@@ -4455,16 +4609,20 @@
});
}
function renderQueryAsinCell(item, country) {
var asinValue = item[country.field] || '';
var infoHtml = asinValue ? '<span>' + asinValue + '</span>' : '<span style="color:#999;">-</span>';
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 class="btn btn-sm btn-danger" data-query-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>')
? '<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 style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
infoHtml +
'<button class="btn btn-sm" data-query-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
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></div>';
}
function openEditQueryAsinModal(itemId, countryCode, shopName, asinValue) {
document.getElementById('editQueryAsinId').value = itemId || '';
@@ -4519,9 +4677,12 @@
} else {
tbody.innerHTML = items.map(function (item, index) {
var rowNo = (queryAsinPage - 1) * queryAsinPageSize + index + 1;
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td>' +
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>' + renderQueryAsinCell(item, country) + '</td>';
return '<td class="asin-col-country">' + renderQueryAsinCell(item, country) + '</td>';
}).join('') +
'</tr>';
}).join('');
@@ -4970,7 +5131,7 @@
'<td><div class="tree-name-cell"><span class="tree-indent" style="--indent:' + indent + 'px;"></span>' +
'<button type="button" class="tree-node-mark' + (hasChildren ? '' : ' is-leaf') + '" data-product-category-toggle="' + escapeHtml(item.id) + '"' + (hasChildren ? '' : ' disabled') + '>' + mark + '</button>' +
'<strong>' + escapeHtml(item.name || '') + '</strong></div></td>' +
'<td><div>' + escapeHtml(item.path || item.name || '') + '</div><div class="category-path">' + escapeHtml(item.category_key || '') + '</div></td>' +
'<td><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>' +
@@ -5302,16 +5463,16 @@
var releasedAt = v.releasedAt || '-';
var actions = '';
if (v.status === 'DRAFT') {
actions += '<button class="btn btn-sm" onclick="releaseVersion(\'' + v.version + '\')">发布</button> ';
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 class="btn btn-sm" onclick="setLatestVersion(\'' + v.version + '\')">设为最新</button> ';
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 class="btn btn-sm btn-danger" onclick="deleteVersion(\'' + v.version + '\')">删除</button>';
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>' +
@@ -5601,7 +5762,7 @@
'<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' ? '后台(admin)' : '软件(app)') + '</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>' +
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, '&quot;') + '" data-key="' + (c.column_key || '').replace(/"/g, '&quot;') + '" data-route="' + (c.route_path || '').replace(/"/g, '&quot;') + '" data-menu-type="' + (c.menu_type || 'app').replace(/"/g, '&quot;') + '" data-sort-order="' + (c.sort_order != null ? String(c.sort_order) : '0').replace(/"/g, '&quot;') + '" data-parent-id="' + String(c.parent_id || c.parentId || '').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-column-delete="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '">删除</button></td></tr>';
@@ -5855,6 +6016,8 @@
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'
? '超级管理员'