Files
crawler-plugin/backend/static/admin-interactions.js
T
huangzd1997 42bbf4904e
Build Backend JAR / build (push) Has been cancelled
feat-admin-ui-interaction
2026-08-30 22:09:22 +08:00

334 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
'use strict';
if (window.__adminInteractionLayerInstalled) return;
window.__adminInteractionLayerInstalled = true;
var toastRegion = document.getElementById('adminToastRegion');
var confirmMask = document.getElementById('adminConfirmModal');
var confirmTitle = document.getElementById('adminConfirmTitle');
var confirmMessage = document.getElementById('adminConfirmMessage');
var confirmAccept = document.getElementById('adminConfirmAccept');
var confirmCancel = document.getElementById('adminConfirmCancel');
var guide = document.getElementById('adminOperationGuide');
var guideText = document.getElementById('adminOperationGuideText');
var guideSteps = document.getElementById('adminOperationGuideSteps');
var guideToggle = document.getElementById('adminOperationGuideToggle');
var pendingConfirmButton = null;
var previousFocus = null;
var busyButton = null;
var busyWasDisabled = false;
var activeFetches = 0;
var activeXhrs = 0;
var mainContent = document.getElementById('adminContent');
var guides = {
users: { text: '先用筛选定位账号,再编辑角色和菜单权限;删除账号会要求二次确认。', steps: ['筛选账号', '编辑权限', '确认保存'] },
columns: { text: '菜单会影响后台和软件端的可见范围。建议先填写名称、标识和路由,再设置父菜单。', steps: ['新增或调整菜单', '设置层级', '检查权限'] },
'dedupe-total-data': { text: '支持按关键词、用户、分组和国家筛选;导入前先确认所选分组,导出会按日期范围生成文件。', steps: ['选择分组', '筛选或导入', '核对并导出'] },
'invalid-asin-data': { text: '维护不符合规则的 ASIN 或品牌。添加后可使用上方筛选快速回查。', steps: ['填写 ASIN/品牌', '选择分组', '保存并回查'] },
'shop-keys': { text: '紫鸟令牌属于敏感配置。白名单状态可悬停查看检测详情,编辑前请先核对账号名称。', steps: ['新增或筛选密钥', '查看白名单状态', '编辑或删除'] },
'shop-manage': { text: '店铺信息按分组管理。长商城名会自动缩略,悬停即可查看完整内容。', steps: ['选择分组', '维护店铺信息', '筛选核对结果'] },
'skip-price-asin': { text: '最低价 ASIN 以店铺和国家为单位维护。每个国家可单独编辑 ASIN 与最低价。', steps: ['选择店铺和国家', '填写 ASIN/最低价', '保存或批量导入'] },
'query-asin': { text: '查询 ASIN 按店铺和国家独立维护。空国家列可直接编辑,已有记录可修改或删除。', steps: ['选择店铺和国家', '维护各国 ASIN', '筛选或导出'] },
'product-categories': { text: '类目树支持展开查看层级。搜索、编辑和删除都在同一列表中完成。', steps: ['搜索类目', '展开层级', '新增或编辑'] },
'image-video-tasks': { text: '可先使用筛选缩小任务范围,再查看任务状态、结果和权限范围。', steps: ['设置筛选', '查看任务结果', '按需处理任务'] },
'shop-data-crawl-tasks': { text: '店铺数据任务按状态和时间筛选。批量操作前请核对已选任务。', steps: ['筛选任务', '检查状态', '执行批量操作'] },
history: { text: '生成记录可按用户和时间范围回溯,用于核对结果文件和执行时间。', steps: ['设置时间范围', '筛选记录', '查看结果预览'] },
version: { text: '上传版本后请核对版本号和下载链接,再通知用户更新。', steps: ['上传压缩包', '检查版本记录', '维护历史版本'] },
'digital-human-version': { text: '数字人版本需先上传草稿,再发布并标记最新版本。', steps: ['上传草稿', '确认更新日志', '发布或设为最新'] }
};
function cleanText(value) {
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
}
function showToast(message, type) {
var value = cleanText(message);
if (!value || !toastRegion) return;
var item = document.createElement('div');
item.className = 'admin-toast' + (type === 'error' ? ' is-error' : type === 'info' ? ' is-info' : '');
var content = document.createElement('span');
content.className = 'admin-toast__text';
content.textContent = value;
item.appendChild(content);
toastRegion.appendChild(item);
window.setTimeout(function () {
item.style.opacity = '0';
item.style.transform = 'translateY(-6px)';
item.style.transition = 'opacity 160ms ease, transform 160ms ease';
window.setTimeout(function () { item.remove(); }, 180);
}, type === 'error' ? 5200 : 3200);
}
window.__adminToast = showToast;
function activeTabName() {
var tab = document.querySelector('#adminMenu .tab.active');
return tab ? (tab.dataset.tab || '') : '';
}
function updateGuide(tabName) {
if (!guide || !guideText || !guideSteps) return;
var config = guides[tabName] || { text: '先使用筛选定位记录,再进行新增、编辑、导出等操作。涉及删除的数据会要求二次确认。', steps: ['选择筛选条件', '处理记录', '核对反馈'] };
guideText.textContent = config.text;
guideSteps.innerHTML = (config.steps || []).map(function (step) { return '<li>' + step + '</li>'; }).join('');
guide.dataset.tab = tabName || '';
}
window.__adminUpdateOperationGuide = updateGuide;
function setGuideCollapsed(collapsed) {
if (!guide || !guideToggle) return;
guide.classList.toggle('is-collapsed', collapsed);
guideToggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
guideToggle.textContent = collapsed ? '展开提示' : '收起提示';
try { localStorage.setItem('shufuAdminGuideCollapsed', collapsed ? '1' : '0'); } catch (error) {}
}
if (guideToggle) {
var collapsed = false;
try { collapsed = localStorage.getItem('shufuAdminGuideCollapsed') === '1'; } catch (error) {}
setGuideCollapsed(collapsed);
guideToggle.addEventListener('click', function () {
setGuideCollapsed(!guide.classList.contains('is-collapsed'));
});
}
function hashTabName() {
var raw = (window.location.hash || '').replace(/^#/, '');
var match = raw.match(/(?:^|&)tab=([^&]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
function syncTabHash(tabName) {
if (!tabName || !window.history || !window.history.replaceState) return;
var next = '#tab=' + encodeURIComponent(tabName);
if (window.location.hash !== next) window.history.replaceState(null, '', next);
}
function navigateToHash(attemptsLeft) {
var tabName = hashTabName();
if (!tabName) {
updateGuide(activeTabName());
return;
}
var tab = document.querySelector('#adminMenu .tab[data-tab="' + tabName + '"]');
if (tab && typeof tab.onclick === 'function') {
if (!tab.classList.contains('active')) tab.click();
else updateGuide(tabName);
return;
}
if (attemptsLeft > 0) {
window.setTimeout(function () { navigateToHash(attemptsLeft - 1); }, 80);
} else {
updateGuide(activeTabName());
}
}
function closeConfirm() {
if (!confirmMask) return;
confirmMask.classList.remove('show');
confirmMask.setAttribute('aria-hidden', 'true');
document.body.classList.remove('admin-confirm-open');
var focus = previousFocus;
pendingConfirmButton = null;
previousFocus = null;
if (focus && focus.isConnected) focus.focus();
}
function openConfirm(button) {
if (!confirmMask || !confirmMessage || !confirmAccept) return;
pendingConfirmButton = button;
previousFocus = document.activeElement;
var customMessage = cleanText(button.dataset.confirmMessage);
var label = cleanText(button.getAttribute('aria-label') || button.textContent || '删除');
var subject = cleanText(button.dataset.name || button.dataset.value || button.dataset.shopManageName || button.dataset.shopName || button.dataset.ziniaoAccountName || '');
var country = cleanText(button.dataset.country || '');
if (!customMessage && country && subject) subject += '' + country + '';
if (!customMessage && subject) customMessage = (button.classList.contains('btn-danger') ? '确认删除“' : '确认执行“') + subject + '”吗?此操作可能影响已有数据。';
if (confirmTitle) confirmTitle.textContent = button.classList.contains('btn-danger') ? '删除前确认' : '请确认操作';
confirmMessage.textContent = customMessage || ('确认执行“' + label + '”吗?此操作可能影响已有数据。');
confirmAccept.textContent = button.dataset.confirmActionLabel || (button.classList.contains('btn-danger') ? '确认删除' : '确认操作');
confirmMask.classList.add('show');
confirmMask.setAttribute('aria-hidden', 'false');
document.body.classList.add('admin-confirm-open');
window.setTimeout(function () { confirmAccept.focus(); }, 0);
}
function keepConfirmFocus(event) {
if (!confirmMask || !confirmMask.classList.contains('show') || event.key !== 'Tab') return;
var focusable = Array.prototype.filter.call(confirmMask.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'), function (el) {
return !el.disabled && el.offsetParent !== null;
});
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
}
if (confirmCancel) confirmCancel.addEventListener('click', closeConfirm);
if (confirmMask) confirmMask.addEventListener('click', function (event) {
if (event.target === confirmMask) closeConfirm();
});
if (confirmAccept) confirmAccept.addEventListener('click', function () {
var target = pendingConfirmButton;
closeConfirm();
if (!target) return;
window.__adminConfirmBypass = true;
try { target.click(); }
finally { window.setTimeout(function () { window.__adminConfirmBypass = false; }, 0); }
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Enter' && event.target && event.target.matches && event.target.matches('input:not([type="file"]), select') && !event.target.closest('textarea')) {
var searchScope = event.target.closest('.form-row, .form-box');
var searchButton = searchScope && searchScope.querySelector('button[id^="btnSearch"], button[id*="Search"]');
if (searchButton && !searchButton.disabled) {
event.preventDefault();
searchButton.click();
return;
}
}
if (event.key === 'Escape' && confirmMask && confirmMask.classList.contains('show')) {
event.preventDefault();
closeConfirm();
return;
}
keepConfirmFocus(event);
});
var nativeAlert = window.alert ? window.alert.bind(window) : null;
window.alert = function (message) {
showToast(message, /失败|错误|无权|不能为空|不正确|异常/.test(String(message || '')) ? 'error' : 'info');
};
var nativeConfirm = window.confirm ? window.confirm.bind(window) : null;
window.confirm = function (message) {
if (window.__adminConfirmBypass) return true;
return nativeConfirm ? nativeConfirm(message) : false;
};
function updatePageBusy() {
if (!mainContent) return;
mainContent.setAttribute('aria-busy', activeFetches || activeXhrs ? 'true' : 'false');
}
function startBusy() {
var button = window.__adminLastActionButton;
if (!button || !button.isConnected || button.disabled || button.classList.contains('tab') || button.classList.contains('menu-group-title') || button === confirmAccept) return;
busyButton = button;
busyWasDisabled = button.disabled;
button.classList.add('is-busy');
button.setAttribute('aria-busy', 'true');
button.disabled = true;
}
function finishBusy() {
var finishedButton = busyButton;
if (finishedButton && finishedButton.isConnected) {
finishedButton.classList.remove('is-busy');
finishedButton.removeAttribute('aria-busy');
if (!busyWasDisabled) finishedButton.disabled = false;
}
if (window.__adminLastActionButton === finishedButton) window.__adminLastActionButton = null;
busyButton = null;
busyWasDisabled = false;
}
if (window.fetch) {
var nativeFetch = window.fetch.bind(window);
window.fetch = function () {
activeFetches += 1;
updatePageBusy();
if (activeFetches === 1) startBusy();
var request;
try { request = nativeFetch.apply(window, arguments); }
catch (error) { activeFetches = Math.max(0, activeFetches - 1); if (!activeFetches && !activeXhrs) finishBusy(); throw error; }
return Promise.resolve(request).finally(function () {
activeFetches = Math.max(0, activeFetches - 1);
updatePageBusy();
if (!activeFetches && !activeXhrs) finishBusy();
});
};
}
if (window.XMLHttpRequest) {
var nativeSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
activeXhrs += 1;
updatePageBusy();
if (activeXhrs === 1) startBusy();
this.addEventListener('loadend', function () {
activeXhrs = Math.max(0, activeXhrs - 1);
updatePageBusy();
if (!activeFetches && !activeXhrs) finishBusy();
}, { once: true });
try { return nativeSend.apply(this, arguments); }
catch (error) { activeXhrs = Math.max(0, activeXhrs - 1); if (!activeFetches && !activeXhrs) finishBusy(); throw error; }
};
}
function addButtonHint(button) {
if (!button || button.title) return;
var label = cleanText(button.textContent);
if (label === '编辑') button.title = '编辑当前记录';
else if (label === '删除') button.title = '删除当前记录,需二次确认';
else if (label === '查询') button.title = '按当前筛选条件查询';
else if (/^导出/.test(label)) button.title = '导出当前筛选结果';
else if (/^上传并/.test(label)) button.title = '上传文件并执行相应操作';
else if (label === '管理分组') button.title = '新增、编辑或删除分组';
else if (label === '选择店铺') button.title = '从店铺列表选择并回填';
}
function enhance(root) {
var scope = root && root.querySelectorAll ? root : document;
scope.querySelectorAll('button').forEach(addButtonHint);
scope.querySelectorAll('.table-ellipsis').forEach(function (element) {
if (!element.title) element.title = cleanText(element.textContent);
});
scope.querySelectorAll('.empty-tip').forEach(function (element) { element.setAttribute('role', 'status'); });
scope.querySelectorAll('.msg').forEach(function (element) {
var value = cleanText(element.textContent);
if (!value || (!element.classList.contains('ok') && !element.classList.contains('err'))) return;
var key = value + '|' + element.className;
if (element.dataset.adminToastKey === key) return;
element.dataset.adminToastKey = key;
showToast(value, element.classList.contains('err') ? 'error' : 'success');
});
}
document.addEventListener('click', function (event) {
var button = event.target && event.target.closest ? event.target.closest('button') : null;
if (!button || button.disabled) return;
if (button.classList.contains('tab') || button.classList.contains('menu-group-title')) {
window.__adminLastActionButton = null;
} else if (button !== confirmAccept) {
window.__adminLastActionButton = button;
}
if (button.classList.contains('tab')) {
window.setTimeout(function () {
var tabName = activeTabName();
updateGuide(tabName);
syncTabHash(tabName);
}, 0);
}
if ((!button.matches('.btn-danger') && !button.hasAttribute('data-admin-confirm')) || button === confirmAccept || window.__adminConfirmBypass) return;
event.preventDefault();
event.stopImmediatePropagation();
openConfirm(button);
}, true);
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
enhance(mutation.target && mutation.target.nodeType === 1 ? mutation.target : document);
if (mutation.type === 'attributes' && mutation.target.matches && mutation.target.matches('#adminMenu .tab')) {
window.setTimeout(function () { updateGuide(activeTabName()); }, 0);
}
});
});
enhance(document);
observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class'] });
window.addEventListener('hashchange', function () { navigateToHash(0); });
navigateToHash(25);
})();