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);
|
||||
})();
|
||||
+224
-61
@@ -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, '"');
|
||||
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, '"') + '" 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, '"')) + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-shop-key-delete="' + item.id + '" data-ziniao-account-name="' + (item.ziniao_account_name || '').replace(/"/g, '"') + '">删除</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, '"')) + '">编辑</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, '"')) + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-shop-manage-delete="' + item.id + '" data-shop-manage-name="' + (item.shop_name || '').replace(/"/g, '"') + '">删除</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, '"')) + '">编辑</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, '"') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
|
||||
'<input type="number" data-skip-price-country-minimum-price-input="' + countryCode + '" value="' + minimumPrice.replace(/"/g, '"') + '" 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, '"') + '">删除</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, '"') + '" data-minimum-price="' + minimumPriceValue.replace(/"/g, '"') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '"') + '">编辑</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, '"') + '">删除</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, '"') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '"') + '">编辑</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, '"') + '" 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>';
|
||||
@@ -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'
|
||||
? '超级管理员'
|
||||
|
||||
+2645
-412
File diff suppressed because it is too large
Load Diff
+797
-116
@@ -3,166 +3,847 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="data:,">
|
||||
<meta name="theme-color" content="#0b1220">
|
||||
<title>登录 - 数富AI</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--auth-bg: #0b1220;
|
||||
--auth-surface: #141e31;
|
||||
--auth-surface-raised: #1a2740;
|
||||
--auth-border: rgba(148, 163, 184, 0.2);
|
||||
--auth-border-strong: rgba(148, 163, 184, 0.34);
|
||||
--auth-text: #f1f5f9;
|
||||
--auth-text-muted: #b8c4d8;
|
||||
--auth-text-subtle: #8795ad;
|
||||
--auth-primary: #818cf8;
|
||||
--auth-primary-strong: #a5b4fc;
|
||||
--auth-success: #4ade80;
|
||||
--auth-danger: #fb7185;
|
||||
--auth-radius: 18px;
|
||||
--auth-fast: 160ms;
|
||||
--auth-base: 220ms;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html {
|
||||
min-width: 320px;
|
||||
min-height: 100%;
|
||||
background: var(--auth-bg);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||
background: #d8e4ec;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
font-family: Inter, "SF Pro Display", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--auth-text);
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgba(99, 102, 241, 0.19), transparent 32rem),
|
||||
radial-gradient(circle at 92% 100%, rgba(34, 197, 94, 0.06), transparent 28rem),
|
||||
var(--auth-bg);
|
||||
}
|
||||
|
||||
button,
|
||||
input { font: inherit; }
|
||||
|
||||
a { color: inherit; }
|
||||
|
||||
.auth-skip-link {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 20;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: var(--auth-primary);
|
||||
color: #0b1220;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transform: translateY(-160%);
|
||||
transition: transform var(--auth-fast) ease;
|
||||
}
|
||||
|
||||
.auth-skip-link:focus { transform: translateY(0); }
|
||||
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(420px, 0.9fr) minmax(420px, 1.1fr);
|
||||
}
|
||||
|
||||
.auth-visual {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 100vh;
|
||||
padding: 36px clamp(32px, 5vw, 84px) 34px;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid rgba(148, 163, 184, 0.13);
|
||||
background:
|
||||
linear-gradient(160deg, rgba(16, 26, 46, 0.92), rgba(8, 15, 29, 0.98)),
|
||||
radial-gradient(circle at 20% 8%, rgba(129, 140, 248, 0.18), transparent 28rem);
|
||||
}
|
||||
|
||||
.auth-visual::before,
|
||||
.auth-visual::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.auth-visual::before {
|
||||
inset: 0;
|
||||
opacity: 0.24;
|
||||
background-image: linear-gradient(rgba(148, 163, 184, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px);
|
||||
background-size: 42px 42px;
|
||||
mask-image: linear-gradient(to bottom, black, transparent 78%);
|
||||
}
|
||||
|
||||
.auth-visual::after {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
right: -150px;
|
||||
bottom: -160px;
|
||||
border: 1px solid rgba(129, 140, 248, 0.18);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 32px rgba(129, 140, 248, 0.035), 0 0 0 64px rgba(129, 140, 248, 0.025);
|
||||
}
|
||||
|
||||
.auth-visual-content,
|
||||
.auth-visual-footer { position: relative; z-index: 1; }
|
||||
|
||||
.auth-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: fit-content;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-brand-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.28);
|
||||
border-radius: 13px;
|
||||
background: linear-gradient(135deg, #818cf8, #4f46e5);
|
||||
color: #eef2ff;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 10px 28px rgba(79, 70, 229, 0.34);
|
||||
}
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
|
||||
.auth-brand-copy { display: grid; gap: 1px; }
|
||||
|
||||
.auth-brand-name {
|
||||
color: #f8fafc;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.auth-brand-sub {
|
||||
color: var(--auth-text-subtle);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.auth-visual-content {
|
||||
max-width: 520px;
|
||||
margin: auto 0;
|
||||
padding: 72px 0 96px;
|
||||
}
|
||||
|
||||
.auth-visual-copy {
|
||||
padding-top: 72px;
|
||||
}
|
||||
|
||||
.auth-kicker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--auth-primary-strong);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.8px;
|
||||
}
|
||||
|
||||
.auth-kicker::before {
|
||||
content: "";
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
background: var(--auth-primary);
|
||||
}
|
||||
|
||||
.auth-visual-title {
|
||||
max-width: 560px;
|
||||
margin: 18px 0 18px;
|
||||
color: #f8fafc;
|
||||
font-size: clamp(30px, 4vw, 52px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -1.8px;
|
||||
line-height: 1.12;
|
||||
}
|
||||
|
||||
.auth-visual-description {
|
||||
max-width: 470px;
|
||||
margin: 0;
|
||||
color: var(--auth-text-muted);
|
||||
font-size: 16px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.auth-feature-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 34px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.auth-feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #dbe4f1;
|
||||
}
|
||||
|
||||
.auth-feature-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 30px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid rgba(129, 140, 248, 0.25);
|
||||
border-radius: 9px;
|
||||
background: rgba(129, 140, 248, 0.11);
|
||||
color: var(--auth-primary-strong);
|
||||
}
|
||||
|
||||
.auth-feature-icon svg { width: 16px; height: 16px; }
|
||||
|
||||
.auth-visual-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: rgba(255,255,255,0.5);
|
||||
gap: 16px;
|
||||
color: #72819a;
|
||||
font-size: 12px;
|
||||
}
|
||||
.header-title { font-size: 18px; font-weight: 600; color: #333; }
|
||||
.login-box {
|
||||
background: #fff;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
|
||||
.auth-system-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.auth-system-status::before {
|
||||
content: "";
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--auth-success);
|
||||
box-shadow: 0 0 0 4px rgba(74, 222, 128, 0.12);
|
||||
}
|
||||
|
||||
.auth-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
padding: 40px clamp(24px, 6vw, 96px);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: min(100%, 452px);
|
||||
padding: clamp(28px, 4vw, 48px);
|
||||
border: 1px solid var(--auth-border);
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(145deg, rgba(20, 30, 49, 0.98), rgba(16, 26, 46, 0.96));
|
||||
box-shadow: 0 26px 70px -38px rgba(2, 6, 23, 0.95);
|
||||
}
|
||||
|
||||
.login-card-header { margin-bottom: 30px; }
|
||||
|
||||
.login-card-eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--auth-text-subtle);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
margin: 0;
|
||||
color: var(--auth-text);
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.8px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.login-subtitle {
|
||||
margin: 10px 0 0;
|
||||
color: var(--auth-text-muted);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.form-group { margin-bottom: 20px; }
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
color: var(--auth-text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.input-shell { position: relative; }
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 14px;
|
||||
display: inline-flex;
|
||||
color: #8190a8;
|
||||
pointer-events: none;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.input-icon svg { width: 18px; height: 18px; }
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 8px;
|
||||
min-height: 48px;
|
||||
padding: 12px 52px 12px 44px;
|
||||
border: 1px solid var(--auth-border);
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background: #fff;
|
||||
background: rgba(11, 18, 32, 0.72);
|
||||
color: var(--auth-text);
|
||||
font-size: 14px;
|
||||
color-scheme: dark;
|
||||
transition: border-color var(--auth-fast) ease, box-shadow var(--auth-fast) ease, background var(--auth-fast) ease;
|
||||
}
|
||||
|
||||
.form-group input:hover { border-color: var(--auth-border-strong); }
|
||||
|
||||
.form-group input:focus {
|
||||
border-color: #3498db;
|
||||
border-color: var(--auth-primary);
|
||||
background: rgba(11, 18, 32, 0.92);
|
||||
box-shadow: 0 0 0 4px rgba(129, 140, 248, 0.15);
|
||||
}
|
||||
|
||||
.form-group input::placeholder { color: #71809a; }
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
color: #8190a8;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
transition: background var(--auth-fast) ease, color var(--auth-fast) ease;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
background: rgba(129, 140, 248, 0.12);
|
||||
color: var(--auth-primary-strong);
|
||||
}
|
||||
|
||||
.password-toggle svg { width: 18px; height: 18px; }
|
||||
|
||||
.error-msg {
|
||||
color: #e74c3c;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
margin: -4px 0 18px;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid rgba(251, 113, 133, 0.28);
|
||||
border-radius: 11px;
|
||||
background: rgba(251, 113, 133, 0.1);
|
||||
color: #fda4af;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.error-msg::before {
|
||||
content: "!";
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 12px 18px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.32);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #818cf8, #6366f1);
|
||||
color: #0b1220;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 12px 24px -16px rgba(129, 140, 248, 0.95);
|
||||
transition: transform var(--auth-fast) ease, box-shadow var(--auth-fast) ease, background var(--auth-fast) ease, opacity var(--auth-fast) ease;
|
||||
}
|
||||
|
||||
.btn-login:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #a5b4fc, #818cf8);
|
||||
box-shadow: 0 16px 28px -15px rgba(129, 140, 248, 0.98);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-login:active:not(:disabled) { transform: translateY(1px) scale(0.99); }
|
||||
|
||||
.btn-login:disabled { cursor: wait; opacity: 0.7; }
|
||||
|
||||
.btn-login-spinner {
|
||||
display: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(11, 18, 32, 0.28);
|
||||
border-top-color: #0b1220;
|
||||
border-radius: 50%;
|
||||
animation: login-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.btn-login.is-loading .btn-login-spinner { display: inline-block; }
|
||||
|
||||
@keyframes login-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.login-security-note {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
margin: 22px 0 0;
|
||||
color: var(--auth-text-subtle);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.login-security-note svg {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 1px;
|
||||
color: var(--auth-success);
|
||||
}
|
||||
|
||||
.login-card-footer {
|
||||
margin-top: 34px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.13);
|
||||
color: #72819a;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--auth-primary-strong);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.auth-page { display: block; }
|
||||
.auth-visual {
|
||||
min-height: auto;
|
||||
padding: 22px 24px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.13);
|
||||
}
|
||||
.auth-visual-content { display: block; max-width: none; margin: 0; padding: 0; }
|
||||
.auth-visual-copy { display: none; }
|
||||
.auth-visual-footer { display: none; }
|
||||
.auth-content { min-height: calc(100vh - 87px); padding: 32px 24px 44px; }
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.auth-visual { padding: 18px 16px; }
|
||||
.auth-content { padding: 24px 14px 32px; }
|
||||
.login-card { padding: 26px 20px; border-radius: 18px; }
|
||||
.login-title { font-size: 27px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
scroll-behavior: auto !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 登录页莫兰迪亮色主题 ===== */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--auth-bg: #f2f4f1;
|
||||
--auth-surface: #ffffff;
|
||||
--auth-surface-raised: #f8faf8;
|
||||
--auth-border: #dbe3dd;
|
||||
--auth-border-strong: #b9c9bd;
|
||||
--auth-text: #29362f;
|
||||
--auth-text-muted: #5d6c63;
|
||||
--auth-text-subtle: #7f8d84;
|
||||
--auth-primary: #607a6d;
|
||||
--auth-primary-strong: #456052;
|
||||
--auth-success: #4e8068;
|
||||
--auth-danger: #a9545d;
|
||||
}
|
||||
|
||||
html { background: var(--auth-bg); }
|
||||
|
||||
body {
|
||||
color: var(--auth-text);
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgba(177, 198, 185, 0.34), transparent 34rem),
|
||||
radial-gradient(circle at 96% 100%, rgba(213, 190, 178, 0.2), transparent 28rem),
|
||||
var(--auth-bg);
|
||||
}
|
||||
|
||||
.auth-skip-link {
|
||||
background: var(--auth-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.auth-visual {
|
||||
border-right-color: #d5ded7;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(232, 239, 234, 0.96), rgba(246, 248, 245, 0.98)),
|
||||
radial-gradient(circle at 20% 8%, rgba(127, 153, 138, 0.16), transparent 28rem);
|
||||
}
|
||||
|
||||
.auth-visual::before {
|
||||
opacity: 0.32;
|
||||
background-image: linear-gradient(rgba(96, 122, 109, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(96, 122, 109, 0.1) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.auth-visual::after {
|
||||
border-color: rgba(96, 122, 109, 0.22);
|
||||
box-shadow: 0 0 0 32px rgba(96, 122, 109, 0.055), 0 0 0 64px rgba(96, 122, 109, 0.035);
|
||||
}
|
||||
|
||||
.auth-brand-mark {
|
||||
border-color: rgba(69, 96, 82, 0.18);
|
||||
background: linear-gradient(135deg, #829b8b, #607a6d);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 10px 24px rgba(96, 122, 109, 0.24);
|
||||
}
|
||||
|
||||
.auth-brand-name,
|
||||
.auth-visual-title { color: var(--auth-text); }
|
||||
.auth-brand-sub { color: #76857b; }
|
||||
.auth-kicker { color: var(--auth-primary-strong); }
|
||||
.auth-kicker::before { background: var(--auth-primary); }
|
||||
.auth-visual-description { color: #607069; }
|
||||
.auth-feature-item { color: #46564d; }
|
||||
.auth-feature-icon { border-color: #c5d5c9; background: #edf3ee; color: var(--auth-primary-strong); }
|
||||
.auth-visual-footer { color: #77857d; }
|
||||
.auth-system-status { color: #3f7258; }
|
||||
.auth-system-status::before { background: var(--auth-success); box-shadow: 0 0 0 4px rgba(78, 128, 104, 0.13); }
|
||||
|
||||
.auth-content {
|
||||
background: rgba(248, 250, 248, 0.45);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border-color: var(--auth-border);
|
||||
background: linear-gradient(145deg, #ffffff, #f9fbf9);
|
||||
box-shadow: 0 26px 70px -38px rgba(60, 77, 67, 0.34);
|
||||
}
|
||||
|
||||
.login-card-eyebrow { color: #718078; }
|
||||
.login-title { color: var(--auth-text); }
|
||||
.login-subtitle { color: var(--auth-text-muted); }
|
||||
.form-group label { color: var(--auth-text-muted); }
|
||||
.input-icon { color: #82938a; }
|
||||
|
||||
.form-group input {
|
||||
background: #f7faf7;
|
||||
border-color: #cfdad2;
|
||||
color: var(--auth-text);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.form-group input:hover { border-color: #aebfb3; }
|
||||
.form-group input:focus { background: #ffffff; border-color: #6f8b7b; box-shadow: 0 0 0 4px rgba(111, 139, 123, 0.16); }
|
||||
.form-group input::placeholder { color: #87958c; }
|
||||
|
||||
.password-toggle { color: #82938a; }
|
||||
.password-toggle:hover { background: #edf4ee; color: var(--auth-primary-strong); }
|
||||
|
||||
.error-msg { border-color: #e4c2c5; background: #f8ebeb; color: #91474f; }
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-color: #7d9988;
|
||||
background: linear-gradient(135deg, #718b7c, #607a6d);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 12px 24px -16px rgba(96, 122, 109, 0.82);
|
||||
}
|
||||
.btn-login:hover {
|
||||
opacity: 0.9;
|
||||
|
||||
.btn-login:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #829b8b, #6b8577);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 16px 28px -15px rgba(96, 122, 109, 0.86);
|
||||
}
|
||||
.btn-login:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
.btn-login-spinner { border-color: rgba(255, 255, 255, 0.35); border-top-color: #ffffff; }
|
||||
.login-security-note { color: #718078; }
|
||||
.login-security-note svg { color: var(--auth-success); }
|
||||
.login-card-footer { border-top-color: #dce5de; color: #77857d; }
|
||||
:focus-visible { outline-color: #607a6d; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.auth-visual { border-bottom-color: #d5ded7; }
|
||||
}
|
||||
|
||||
|
||||
/* ===== 登录页统一蓝白色调 ===== */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--auth-bg: #f4f7fb;
|
||||
--auth-surface: #ffffff;
|
||||
--auth-surface-raised: #f9fbfd;
|
||||
--auth-border: #d8e3ee;
|
||||
--auth-border-strong: #b7c9db;
|
||||
--auth-text: #24384d;
|
||||
--auth-text-muted: #5b6f83;
|
||||
--auth-text-subtle: #8293a5;
|
||||
--auth-primary: #4f78a5;
|
||||
--auth-primary-strong: #2f5d8b;
|
||||
--auth-success: #4e806d;
|
||||
--auth-danger: #b35f6a;
|
||||
}
|
||||
|
||||
html, body { background: var(--auth-bg); }
|
||||
body {
|
||||
color: var(--auth-text);
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgba(178, 205, 229, 0.32), transparent 34rem),
|
||||
radial-gradient(circle at 96% 100%, rgba(214, 226, 239, 0.28), transparent 28rem),
|
||||
var(--auth-bg);
|
||||
}
|
||||
.auth-skip-link { background: var(--auth-primary); color: #ffffff; }
|
||||
.auth-visual {
|
||||
border-right-color: #d4e0eb;
|
||||
background:
|
||||
linear-gradient(160deg, rgba(232, 240, 248, 0.97), rgba(248, 251, 254, 0.99)),
|
||||
radial-gradient(circle at 20% 8%, rgba(112, 148, 186, 0.16), transparent 28rem);
|
||||
}
|
||||
.auth-visual::before { opacity: 0.3; background-image: linear-gradient(rgba(79, 120, 165, 0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(79, 120, 165, 0.1) 1px, transparent 1px); }
|
||||
.auth-visual::after { border-color: rgba(79, 120, 165, 0.22); box-shadow: 0 0 0 32px rgba(79, 120, 165, 0.055), 0 0 0 64px rgba(79, 120, 165, 0.035); }
|
||||
.auth-brand-mark { border-color: rgba(47, 93, 139, 0.18); background: linear-gradient(135deg, #7094ba, #4f78a5); color: #ffffff; box-shadow: 0 10px 24px rgba(79, 120, 165, 0.24); }
|
||||
.auth-brand-name, .auth-visual-title { color: var(--auth-text); }
|
||||
.auth-brand-sub { color: #77899b; }
|
||||
.auth-kicker { color: var(--auth-primary-strong); }
|
||||
.auth-kicker::before { background: var(--auth-primary); }
|
||||
.auth-visual-description { color: #60748a; }
|
||||
.auth-feature-item { color: #465d73; }
|
||||
.auth-feature-icon { border-color: #c2d3e3; background: #edf5fb; color: var(--auth-primary-strong); }
|
||||
.auth-visual-footer { color: #778b9f; }
|
||||
.auth-system-status { color: #3d7158; }
|
||||
.auth-system-status::before { background: var(--auth-success); box-shadow: 0 0 0 4px rgba(78, 128, 104, 0.13); }
|
||||
.auth-content { background: rgba(249, 251, 253, 0.5); }
|
||||
.login-card { border-color: var(--auth-border); background: linear-gradient(145deg, #ffffff, #f9fbfd); box-shadow: 0 26px 70px -38px rgba(39, 67, 94, 0.34); }
|
||||
.login-card-eyebrow { color: #71859a; }
|
||||
.login-title { color: var(--auth-text); }
|
||||
.login-subtitle, .form-group label { color: var(--auth-text-muted); }
|
||||
.input-icon, .password-toggle { color: #8298ad; }
|
||||
.form-group input { background: #f8fbfd; border-color: #cbd9e6; color: var(--auth-text); color-scheme: light; }
|
||||
.form-group input:hover { border-color: #9fb7cd; }
|
||||
.form-group input:focus { background: #ffffff; border-color: #5f85ad; box-shadow: 0 0 0 4px rgba(95, 133, 173, 0.16); }
|
||||
.form-group input::placeholder { color: #8293a5; }
|
||||
.password-toggle:hover { background: #edf5fb; color: var(--auth-primary-strong); }
|
||||
.error-msg { border-color: #e4c2c5; background: #f8ebeb; color: #91474f; }
|
||||
.btn-login { border-color: #7196ba; background: linear-gradient(135deg, #5f85ad, #4f78a5); color: #ffffff; box-shadow: 0 12px 24px -16px rgba(79, 120, 165, 0.82); }
|
||||
.btn-login:hover:not(:disabled) { background: linear-gradient(135deg, #7094ba, #5d83ac); color: #ffffff; box-shadow: 0 16px 28px -15px rgba(79, 120, 165, 0.86); }
|
||||
.btn-login-spinner { border-color: rgba(255,255,255,0.35); border-top-color: #ffffff; }
|
||||
.login-security-note { color: #71859a; }
|
||||
.login-security-note svg { color: var(--auth-success); }
|
||||
.login-card-footer { border-top-color: #dce5ee; color: #778b9f; }
|
||||
:focus-visible { outline-color: #4f78a5; }
|
||||
@media (max-width: 900px) { .auth-visual { border-bottom-color: #d4e0eb; } }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<span class="header-title">数富AI</span>
|
||||
</header>
|
||||
<a class="auth-skip-link" href="#loginMain">跳转到登录表单</a>
|
||||
<div class="auth-page">
|
||||
<aside class="auth-visual" aria-label="数富AI产品信息">
|
||||
<div class="auth-visual-content">
|
||||
<a class="auth-brand" href="/login" aria-label="数富AI 登录页">
|
||||
<span class="auth-brand-mark" aria-hidden="true">数</span>
|
||||
<span class="auth-brand-copy">
|
||||
<span class="auth-brand-name">数富AI</span>
|
||||
<span class="auth-brand-sub">电商运营管理后台</span>
|
||||
</span>
|
||||
</a>
|
||||
<div class="auth-visual-copy">
|
||||
<span class="auth-kicker">数富AI · 运营工作台</span>
|
||||
<h2 class="auth-visual-title">让每一次运营动作,都有清晰的工作流。</h2>
|
||||
<p class="auth-visual-description">统一管理数据、店铺、任务与版本,让团队在一个可靠的运营工作台中快速协作。</p>
|
||||
<ul class="auth-feature-list" aria-label="工作台能力">
|
||||
<li class="auth-feature-item">
|
||||
<span class="auth-feature-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 4 7v5c0 4.5 3.4 7.7 8 9 4.6-1.3 8-4.5 8-9V7l-8-4Z"></path><path d="m8.5 12 2.2 2.2 4.8-5"></path></svg></span>
|
||||
<span>权限隔离,操作边界清晰可控</span>
|
||||
</li>
|
||||
<li class="auth-feature-item">
|
||||
<span class="auth-feature-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8v4l3 2"></path><circle cx="12" cy="12" r="9"></circle><path d="M3 4v5h5"></path><path d="M3.5 9A9 9 0 0 1 19 5.5"></path></svg></span>
|
||||
<span>任务状态,进度反馈及时透明</span>
|
||||
</li>
|
||||
<li class="auth-feature-item">
|
||||
<span class="auth-feature-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5"></path><path d="M4 19h16"></path><path d="m7 15 3-4 3 2 4-6"></path><path d="M17 7h3v3"></path></svg></span>
|
||||
<span>数据工具,支撑日常电商运营</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-visual-footer">
|
||||
<span>数富AI · 管理控制台</span>
|
||||
<span class="auth-system-status">本地服务已就绪</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="login-box">
|
||||
<h1 class="login-title">登录</h1>
|
||||
{% if error %}
|
||||
<p class="error-msg">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form id="loginForm" method="POST" action="/login">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" name="username" placeholder="请输入用户名" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-login" id="btnLogin">登录</button>
|
||||
</form>
|
||||
<main class="auth-content" id="loginMain">
|
||||
<section class="login-card" aria-labelledby="loginTitle">
|
||||
<header class="login-card-header">
|
||||
<p class="login-card-eyebrow">欢迎回来</p>
|
||||
<h1 class="login-title" id="loginTitle">登录工作台</h1>
|
||||
<p class="login-subtitle">使用管理员账号进入数富AI运营后台。</p>
|
||||
</header>
|
||||
<form id="loginForm" method="POST" action="/login" novalidate>
|
||||
{% if error %}
|
||||
<p class="error-msg" id="loginError" role="alert" aria-live="assertive">{{ error }}</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">用户名</label>
|
||||
<div class="input-shell">
|
||||
<span class="input-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"></circle><path d="M4 21a8 8 0 0 1 16 0"></path></svg></span>
|
||||
<input type="text" id="loginUsername" name="username" autocomplete="username" placeholder="请输入用户名" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">密码</label>
|
||||
<div class="input-shell">
|
||||
<span class="input-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="10" x="5" y="11" rx="2"></rect><path d="M8 11V7a4 4 0 0 1 8 0v4"></path></svg></span>
|
||||
<input type="password" id="loginPassword" name="password" autocomplete="current-password" placeholder="请输入密码" required>
|
||||
<button type="button" class="password-toggle" id="togglePassword" aria-label="显示密码" aria-pressed="false" title="显示密码">
|
||||
<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.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"></path><circle cx="12" cy="12" r="2.5"></circle></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn-login" id="btnLogin" aria-busy="false">
|
||||
<span class="btn-login-label">登录</span>
|
||||
<span class="btn-login-spinner" aria-hidden="true"></span>
|
||||
</button>
|
||||
</form>
|
||||
<p class="login-security-note">
|
||||
<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 3 4 7v5c0 4.5 3.4 7.7 8 9 4.6-1.3 8-4.5 8-9V7l-8-4Z"></path><path d="m8.5 12 2.2 2.2 4.8-5"></path></svg>
|
||||
<span>请勿在公共设备保存账号凭据。登录状态由本地安全会话管理。</span>
|
||||
</p>
|
||||
<footer class="login-card-footer">数富AI · 电商运营管理工作台</footer>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var params = new URLSearchParams(window.location.search || '');
|
||||
if (params.get('logout') === '1' || params.get('switch') === '1') {
|
||||
return;
|
||||
}
|
||||
fetch('/api/auth/check', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.logged_in && res.redirect) {
|
||||
window.location.href = res.redirect;
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
})();
|
||||
document.getElementById('loginForm').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
(function () {
|
||||
var form = document.getElementById('loginForm');
|
||||
var btn = document.getElementById('btnLogin');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '登录中...';
|
||||
var form = e.target;
|
||||
var fd = new FormData(form);
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
window.location.href = res.redirect || '/admin';
|
||||
} else {
|
||||
var errEl = document.querySelector('.error-msg');
|
||||
if (!errEl) {
|
||||
errEl = document.createElement('p');
|
||||
errEl.className = 'error-msg';
|
||||
form.insertBefore(errEl, form.firstChild);
|
||||
}
|
||||
errEl.textContent = res.error || '登录失败';
|
||||
btn.disabled = false;
|
||||
btn.textContent = '登录';
|
||||
var label = btn ? btn.querySelector('.btn-login-label') : null;
|
||||
var password = document.getElementById('loginPassword');
|
||||
var togglePassword = document.getElementById('togglePassword');
|
||||
if (!form || !btn) return;
|
||||
|
||||
if (password && togglePassword) {
|
||||
togglePassword.addEventListener('click', function () {
|
||||
var visible = password.type === 'password';
|
||||
password.type = visible ? 'text' : 'password';
|
||||
togglePassword.setAttribute('aria-pressed', visible ? 'true' : 'false');
|
||||
togglePassword.setAttribute('aria-label', visible ? '隐藏密码' : '显示密码');
|
||||
togglePassword.setAttribute('title', visible ? '隐藏密码' : '显示密码');
|
||||
});
|
||||
}
|
||||
|
||||
function setError(message) {
|
||||
var errEl = document.getElementById('loginError') || document.querySelector('.error-msg');
|
||||
if (!errEl) {
|
||||
errEl = document.createElement('p');
|
||||
errEl.id = 'loginError';
|
||||
errEl.className = 'error-msg';
|
||||
errEl.setAttribute('role', 'alert');
|
||||
errEl.setAttribute('aria-live', 'assertive');
|
||||
form.insertBefore(errEl, form.firstChild);
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
form.submit();
|
||||
errEl.textContent = message || '登录失败';
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
btn.disabled = loading;
|
||||
btn.classList.toggle('is-loading', loading);
|
||||
btn.setAttribute('aria-busy', loading ? 'true' : 'false');
|
||||
if (label) label.textContent = loading ? '登录中...' : '登录';
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
if (btn.disabled) return;
|
||||
setLoading(true);
|
||||
var data = new FormData(form);
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(function (response) {
|
||||
return response.json().catch(function () {
|
||||
return { success: false, error: '登录响应异常,请重试' };
|
||||
});
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.success) {
|
||||
window.location.href = result.redirect || '/admin';
|
||||
return;
|
||||
}
|
||||
setError(result.error || '用户名或密码错误');
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(function () {
|
||||
// API 不可用时保留原生表单提交作为兜底路径。
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user