This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
(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);
|
||||
})();
|
||||
Reference in New Issue
Block a user