feat(admin): 管理后台页面同源托管到 Java(classpath:/static)并接入 Java 登录

把 Flask web_source/admin.html 与 static/* 拷入 backend-java resources/static,
经 Spring 默认静态映射同源服务于 api.aishufu.top:18080;/static 根相对引用通过
static/static 子目录镜像零改动保持。页面登录改直连 Java POST /login(cookie+
浏览器持久 deviceId),登出走 Java POST /logout。新增 AdminConsoleController:
GET /、/admin、/login、/logout 页面转发,及 /api/admin/current-user、
/current-user/menus(管理员角色,返回旧前端契约的 item/items 形状)。admin.js
增加全局 fetch 响应解包(ApiResponse data 展开且保留 data 键,401 跳登录)与
URL 兼容映射层(把 Flask 别名统一到 Java 规范路径)。
This commit is contained in:
2026-09-04 10:21:24 +08:00
parent e5909c9977
commit bc5d5b3d2e
8 changed files with 15032 additions and 0 deletions
@@ -0,0 +1,88 @@
package com.nanri.aiimage.modules.admin.controller;
import com.nanri.aiimage.common.api.ApiResponse;
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
import com.nanri.aiimage.modules.permission.model.vo.PermissionMenuItemVo;
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 管理后台页面与"当前管理员"自描述接口(Java 收敛自 Flask 侧 web_source/admin.html 的依赖)。
*
* <p>页面经静态资源托管于同源(classpath:/static 下的 admin.html / login.html),
* 登录/登出沿用 auth 模块(POST /login、POST /logout)。这里提供 GET 页面转发与
* /api/admin/current-user、/current-user/menus,保持与旧前端契约一致({item}、{items})。
*/
@RestController
@RequiredArgsConstructor
@Tag(name = "管理后台页面", description = "管理后台静态页路由与当前管理员信息")
public class AdminConsoleController {
private final AdminAuthSupport adminAuthSupport;
private final PermissionMenuService permissionMenuService;
@GetMapping("/")
@Operation(summary = "根路径跳管理后台", hidden = true)
public String root() {
return "redirect:/admin.html";
}
@GetMapping("/admin")
@Operation(summary = "管理后台入口", hidden = true)
public String adminPage() {
return "redirect:/admin.html";
}
@GetMapping("/login")
@Operation(summary = "登录页入口", hidden = true)
public String loginPage() {
return "forward:/login.html";
}
@GetMapping("/logout")
@Operation(summary = "登出 GET 兜底(POST 走 auth 模块)", hidden = true)
public String logoutPage() {
return "redirect:/login.html";
}
@GetMapping("/api/admin/current-user")
@Operation(summary = "当前登录管理员概要")
public ApiResponse<Map<String, Object>> currentUser(HttpServletRequest request) {
AdminUserEntity operator = adminAuthSupport.requireAdminOrInternal(request);
Map<String, Object> item = new LinkedHashMap<>();
item.put("id", operator.getId());
item.put("username", operator.getUsername() == null ? "" : operator.getUsername());
item.put("role", adminAuthSupport.currentRole(operator));
return ApiResponse.success(Map.of("item", item));
}
@GetMapping("/api/admin/current-user/menus")
@Operation(summary = "当前登录管理员的可见菜单(扁平 route_path/name 列表)")
public ApiResponse<Map<String, Object>> currentUserMenus(HttpServletRequest request) {
AdminUserEntity operator = adminAuthSupport.requireAdminOrInternal(request);
List<PermissionMenuItemVo> menus = permissionMenuService.getUserColumnPermissions(
operator, operator.getId(), PermissionMenuService.MENU_TYPE_ADMIN);
List<Map<String, String>> items = new ArrayList<>(menus.size());
for (PermissionMenuItemVo menu : menus) {
if (menu.getRoutePath() == null || menu.getRoutePath().isBlank()) {
continue;
}
Map<String, String> item = new LinkedHashMap<>();
item.put("route_path", menu.getRoutePath());
item.put("name", menu.getName() == null ? menu.getRoutePath() : menu.getName());
items.add(item);
}
return ApiResponse.success(Map.of("items", items));
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,887 @@
<!DOCTYPE html>
<html lang="zh-CN">
<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>
: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 {
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;
width: 46px;
height: 46px;
border-radius: 13px;
background: #ffffff;
box-shadow: 0 10px 28px rgba(79, 70, 229, 0.20);
overflow: hidden;
}
.auth-brand-mark img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.login-logo {
display: block;
width: 56px;
height: 56px;
margin-bottom: 16px;
border-radius: 14px;
box-shadow: 0 10px 24px -12px rgba(39, 67, 94, 0.45);
}
.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;
gap: 16px;
color: #72819a;
font-size: 12px;
}
.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 {
margin: 0;
color: var(--auth-text);
font-size: 30px;
font-weight: 700;
letter-spacing: -0.8px;
line-height: 1.2;
}
.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;
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%;
min-height: 48px;
padding: 12px 52px 12px 44px;
border: 1px solid var(--auth-border);
border-radius: 12px;
outline: none;
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: 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 {
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;
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-radius: 13px;
background: #ffffff;
box-shadow: 0 10px 24px rgba(96, 122, 109, 0.18);
}
.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 {
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:not(:disabled) {
background: linear-gradient(135deg, #829b8b, #6b8577);
color: #ffffff;
box-shadow: 0 16px 28px -15px rgba(96, 122, 109, 0.86);
}
.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-radius: 13px; background: #ffffff; box-shadow: 0 10px 24px rgba(79, 120, 165, 0.18); }
.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>
<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"><img src="/static/logo_thumb.png" alt=""></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>
<main class="auth-content" id="loginMain">
<section class="login-card" aria-labelledby="loginTitle">
<header class="login-card-header">
<img class="login-logo" src="/static/logo_thumb.png" alt="数富AI">
<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>
<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 form = document.getElementById('loginForm');
var btn = document.getElementById('btnLogin');
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);
}
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 ? '登录中...' : '登录';
}
function deviceId() {
try {
var key = 'aiimage_console_device_id';
var value = localStorage.getItem(key);
if (!value) {
value = 'web-' + Math.random().toString(36).slice(2, 10) + '-' + Date.now().toString(36);
localStorage.setItem(key, value);
}
return value;
} catch (e) {
return 'web-console-fallback';
}
}
// 已登录(Java cookie 有效)时直接进入工作台。
fetch('/check_login', { method: 'GET', credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res && res.success) window.location.replace('/admin.html');
})
.catch(function () { /* 忽略,保持登录页 */ });
form.addEventListener('submit', function (event) {
event.preventDefault();
if (btn.disabled) return;
var username = (document.getElementById('loginUsername').value || '').trim();
var password = document.getElementById('loginPassword').value || '';
if (!username || !password) {
setError('请输入用户名和密码');
return;
}
var did = deviceId();
setLoading(true);
fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Device-Id': did },
body: JSON.stringify({ username: username, password: password, deviceId: did }),
credentials: 'same-origin'
})
.then(function (response) {
return response.json().catch(function () {
return { success: false, message: '登录响应异常,请重试' };
});
})
.then(function (result) {
if (result && result.success) {
window.location.replace('/admin.html');
return;
}
setError((result && (result.error || result.message)) || '用户名或密码错误');
setLoading(false);
})
.catch(function () {
setError('无法连接服务,请稍后重试');
setLoading(false);
});
});
})();
</script>
</body>
</html>
@@ -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 即可复制,点右侧「配置」可一次维护该店铺所有站点的 ASIN 与最低价。', steps: ['筛选店铺', '打开配置抽屉', '保存或批量导入'] },
'query-asin': { text: '查询 ASIN 以店铺为单位维护。列表里点击 ASIN 即可复制,点右侧「配置」可一次维护该店铺所有站点的 ASIN。', steps: ['筛选店铺', '打开配置抽屉', '保存或导出'] },
'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);
})();
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB