企业级通用后台管理系统的基础

<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
    <meta name="color-scheme" content="light dark">
    <meta name="theme-color" content="#6d5efc">
    <meta name="description" content="Nova Admin — 企业级通用后台管理系统">
    <title>Nova Admin · 用户管理</title>

    <style>
        /* =====================================================================
   Nova Admin · 样式总纲
   分层顺序 = 优先级顺序(低 → 高)
   reset < tokens < base < layout < components < utilities < motion
   ===================================================================== */
        @layer reset, tokens, base, layout, components, utilities, motion;


        /* =====================================================================
   1. RESET —— 现代化最小重置
   ===================================================================== */
        @layer reset {

            *,
            *::before,
            *::after {
                box-sizing: border-box;
            }

            * {
                margin: 0;
            }

            html {
                -webkit-text-size-adjust: 100%;
                scrollbar-gutter: stable;
            }

            body {
                min-height: 100dvh;
            }

            img,
            picture,
            svg,
            video,
            canvas {
                display: block;
                max-width: 100%;
            }

            input,
            button,
            textarea,
            select {
                font: inherit;
                color: inherit;
                letter-spacing: inherit;
            }

            button,
            [role="button"] {
                background: none;
                border: 0;
                cursor: pointer;
            }

            ul,
            ol {
                list-style: none;
                padding: 0;
            }

            a {
                color: inherit;
                text-decoration: none;
            }

            table {
                border-collapse: collapse;
                border-spacing: 0;
            }

            dialog {
                padding: 0;
                border: 0;
                margin: auto;
                background: transparent;
                max-width: none;
                max-height: none;
            }

            input[type="search"]::-webkit-search-cancel-button {
                -webkit-appearance: none;
            }

            [hidden] {
                display: none !important;
            }

            :focus-visible {
                outline: 2px solid var(--accent);
                outline-offset: 2px;
                border-radius: 6px;
            }
        }


        /* =====================================================================
   2. TOKENS —— 设计令牌
   ===================================================================== */
        @layer tokens {
            :root {
                color-scheme: light dark;

                /* 品牌 & 语义色 */
                --accent: light-dark(oklch(0.55 0.20 268), oklch(0.74 0.16 268));
                --accent-hover: light-dark(oklch(0.48 0.21 268), oklch(0.80 0.14 268));
                --accent-soft: color-mix(in oklab, var(--accent) 14%, transparent);
                --accent-softer: color-mix(in oklab, var(--accent) 7%, transparent);
                --accent-ring: color-mix(in oklab, var(--accent) 34%, transparent);
                --on-accent: light-dark(#fff, #0b0e14);

                --success: light-dark(oklch(0.60 0.15 155), oklch(0.78 0.16 155));
                --warn: light-dark(oklch(0.72 0.16 78), oklch(0.84 0.15 80));
                --danger: light-dark(oklch(0.58 0.21 25), oklch(0.72 0.18 25));
                /* ✅ 已修复:闭合 light-dark() 的右括号(上一版漏写导致 --info 及其派生变量失效) */
                --info: light-dark(oklch(0.62 0.14 225), oklch(0.78 0.13 225));

                --success-soft: color-mix(in oklab, var(--success) 16%, transparent);
                --warn-soft: color-mix(in oklab, var(--warn) 20%, transparent);
                --danger-soft: color-mix(in oklab, var(--danger) 15%, transparent);
                --info-soft: color-mix(in oklab, var(--info) 16%, transparent);

                /* 表面 / 背景 */
                --bg-app: light-dark(#f2f4f9, #0a0d13);
                --bg-surface: light-dark(#ffffff, #121721);
                --bg-surface-2: light-dark(#f7f9fc, #171d29);
                --bg-elevated: light-dark(#ffffff, #1c2331);
                --bg-hover: light-dark(#eef2f8, #212a3a);
                --bg-active: light-dark(#e6ecfa, #27324a);
                --bg-sunken: light-dark(#eceff5, #0e131c);

                /* 描边 */
                --border: light-dark(#e3e8f0, #232b39);
                --border-strong: light-dark(#ccd5e1, #35405480);

                /* 文本 */
                --text: light-dark(#0f172a, #e9eef7);
                --text-muted: light-dark(#5c6b82, #94a3b8);
                --text-faint: light-dark(#93a1b5, #64748b);

                /* 形状 */
                --r-xs: 6px;
                --r-sm: 9px;
                --r-md: 13px;
                --r-lg: 18px;
                --r-xl: 24px;
                --r-full: 999px;

                /* 阴影 */
                --sh-xs: 0 1px 2px light-dark(oklch(0.2 0.02 260 / .06), oklch(0 0 0 / .5));
                --sh-sm: 0 2px 8px light-dark(oklch(0.2 0.02 260 / .07), oklch(0 0 0 / .45));
                --sh-md: 0 8px 26px light-dark(oklch(0.2 0.02 260 / .10), oklch(0 0 0 / .55));
                --sh-lg: 0 24px 60px light-dark(oklch(0.2 0.02 260 / .16), oklch(0 0 0 / .68));
                --sh-inner: inset 0 1px 0 light-dark(#fff, oklch(1 0 0 / .05));

                /* 尺寸 */
                --sidebar-w: 262px;
                --sidebar-w-collapsed: 76px;
                --topbar-h: 62px;
                --content-max: 1560px;

                /* 动效 */
                --ease: cubic-bezier(.4, 0, .2, 1);
                --ease-out-back: cubic-bezier(.34, 1.56, .64, 1);
                --dur-fast: .14s;
                --dur: .24s;
                --dur-slow: .42s;

                interpolate-size: allow-keywords;
            }

            :root[data-theme="light"] {
                color-scheme: light;
            }

            :root[data-theme="dark"] {
                color-scheme: dark;
            }

            @property --ring-angle {
                syntax: '<angle>';
                initial-value: 0deg;
                inherits: false;
            }

            @property --glow-x {
                syntax: '<percentage>';
                initial-value: 50%;
                inherits: false;
            }
        }


        /* =====================================================================
   3. BASE
   ===================================================================== */
        @layer base {
            html {
                font-size: 14px;
            }

            body {
                font-family: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI",
                    "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
                line-height: 1.55;
                color: var(--text);
                background: var(--bg-app);
                -webkit-font-smoothing: antialiased;
                -moz-osx-font-smoothing: grayscale;
                font-variant-numeric: tabular-nums;
                transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
                overflow-x: hidden;
            }

            h1,
            h2,
            h3,
            h4 {
                line-height: 1.28;
                text-wrap: balance;
                font-weight: 650;
            }

            p {
                text-wrap: pretty;
            }

            small {
                font-size: .82em;
            }

            code,
            kbd,
            .mono {
                font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
            }

            ::selection {
                background: var(--accent-soft);
                color: var(--text);
            }

            * {
                scrollbar-width: thin;
                scrollbar-color: var(--border-strong) transparent;
            }

            ::-webkit-scrollbar {
                width: 10px;
                height: 10px;
            }

            ::-webkit-scrollbar-track {
                background: transparent;
            }

            ::-webkit-scrollbar-thumb {
                background: var(--border-strong);
                border-radius: var(--r-full);
                border: 3px solid transparent;
                background-clip: content-box;
            }

            ::-webkit-scrollbar-thumb:hover {
                background: var(--text-faint);
                background-clip: content-box;
            }

            .sr-only {
                position: absolute;
                width: 1px;
                height: 1px;
                padding: 0;
                margin: -1px;
                overflow: hidden;
                clip-path: inset(50%);
                white-space: nowrap;
                border: 0;
            }

            .sprite {
                position: absolute;
                width: 0;
                height: 0;
                overflow: hidden;
            }

            svg.icon {
                width: 1.15em;
                height: 1.15em;
                flex: none;
            }
        }


        /* =====================================================================
   4. LAYOUT —— 应用骨架
   ===================================================================== */
        @layer layout {

            /* 顶部滚动进度条(滚动驱动动画) */
            .scroll-progress {
                position: fixed;
                inset: 0 0 auto 0;
                height: 3px;
                z-index: 9999;
                background: linear-gradient(90deg, var(--accent), var(--info), var(--success));
                transform-origin: 0 50%;
                transform: scaleX(0);
                pointer-events: none;
                animation: grow-x linear both;
                animation-timeline: scroll(root block);
            }

            @keyframes grow-x {
                from {
                    transform: scaleX(0);
                }

                to {
                    transform: scaleX(1);
                }
            }

            /* 应用网格 */
            .app {
                display: grid;
                grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
                min-height: 100dvh;
                transition: grid-template-columns var(--dur-slow) var(--ease);
            }

            /* ---------------- 侧边栏 ---------------- */
            .sidebar {
                position: sticky;
                top: 0;
                z-index: 40;
                height: 100dvh;
                display: flex;
                flex-direction: column;
                background: var(--bg-surface);
                border-inline-end: 1px solid var(--border);
                overflow: hidden auto;
                overscroll-behavior: contain;
                transition: width var(--dur-slow) var(--ease);
            }

            .sidebar__brand {
                display: flex;
                align-items: center;
                gap: 12px;
                padding: 0 18px;
                height: var(--topbar-h);
                flex: none;
                border-block-end: 1px solid var(--border);
                transition: padding var(--dur) var(--ease);
            }

            .brand__logo {
                width: 36px;
                height: 36px;
                flex: none;
                display: grid;
                place-items: center;
                border-radius: 11px;
                background: linear-gradient(140deg, var(--accent),
                        color-mix(in oklab, var(--accent) 45%, var(--info)));
                color: #fff;
                box-shadow: var(--sh-sm), var(--sh-inner);
            }

            .brand__logo svg {
                width: 20px;
                height: 20px;
            }

            .brand__text {
                display: flex;
                flex-direction: column;
                line-height: 1.15;
                white-space: nowrap;
                transition: opacity var(--dur) var(--ease);
            }

            .brand__text strong {
                font-size: 1.02rem;
                letter-spacing: .2px;
            }

            .brand__text small {
                font-size: .72rem;
                color: var(--text-muted);
            }

            .sidebar__nav {
                flex: 1;
                padding: 14px 12px;
                overflow-y: auto;
                display: flex;
                flex-direction: column;
                gap: 4px;
                transition: padding var(--dur) var(--ease);
            }

            .nav__label {
                font-size: .68rem;
                font-weight: 700;
                letter-spacing: .14em;
                text-transform: uppercase;
                color: var(--text-faint);
                padding: 14px 12px 6px;
                transition: opacity var(--dur) var(--ease);
            }

            .nav__item {
                position: relative;
                display: flex;
                align-items: center;
                gap: 12px;
                width: 100%;
                padding: 10px 12px;
                border-radius: var(--r-sm);
                color: var(--text-muted);
                font-weight: 500;
                white-space: nowrap;
                transition:
                    background-color var(--dur-fast) var(--ease),
                    color var(--dur-fast) var(--ease),
                    padding var(--dur) var(--ease),
                    gap var(--dur) var(--ease);
            }

            .nav__item:hover {
                background: var(--bg-hover);
                color: var(--text);
            }

            .nav__item:active {
                transform: scale(.985);
            }

            .nav__item[aria-current="page"],
            .nav__item.is-active {
                background: linear-gradient(90deg, var(--accent-soft), var(--accent-softer) 70%, transparent);
                color: var(--accent);
                font-weight: 620;
            }

            .nav__item[aria-current="page"]::before,
            .nav__item.is-active::before {
                content: "";
                position: absolute;
                inset-inline-start: -12px;
                inset-block: 8px;
                width: 3px;
                border-radius: var(--r-full);
                background: var(--accent);
                box-shadow: 0 0 12px var(--accent-ring);
                transition: inset-inline-start var(--dur) var(--ease);
            }

            .nav__icon {
                width: 19px;
                height: 19px;
                flex: none;
            }

            .nav__text {
                flex: 1;
                transition: opacity var(--dur) var(--ease);
            }

            .nav__badge {
                font-size: .68rem;
                font-weight: 700;
                padding: 1px 7px;
                border-radius: var(--r-full);
                background: var(--danger-soft);
                color: var(--danger);
                transition: opacity var(--dur) var(--ease);
            }

            .nav__chevron {
                width: 15px;
                height: 15px;
                transition: rotate var(--dur) var(--ease), opacity var(--dur) var(--ease);
            }

            details[open]>summary .nav__chevron {
                rotate: 180deg;
            }

            .nav__group>summary {
                list-style: none;
                cursor: pointer;
            }

            .nav__group>summary::-webkit-details-marker {
                display: none;
            }

            .nav__sublist {
                display: flex;
                flex-direction: column;
                gap: 2px;
                padding-inline-start: 30px;
                margin-block-start: 2px;
                border-inline-start: 1px dashed var(--border);
                margin-inline-start: 21px;
            }

            .nav__sublist .nav__item {
                padding-block: 7px;
                font-size: .92rem;
            }

            .nav__sublist .nav__item::before {
                display: none;
            }

            .sidebar__footer {
                flex: none;
                padding: 12px;
                border-block-start: 1px solid var(--border);
                transition: padding var(--dur) var(--ease);
            }

            .sidebar__user {
                display: flex;
                align-items: center;
                gap: 10px;
                padding: 8px;
                border-radius: var(--r-md);
                transition: background-color var(--dur-fast) var(--ease), padding var(--dur) var(--ease);
            }

            .sidebar__user:hover {
                background: var(--bg-hover);
            }

            .sidebar__foot-text {
                display: flex;
                flex-direction: column;
                min-width: 0;
                transition: opacity var(--dur) var(--ease);
            }

            .sidebar__foot-text strong {
                font-size: .88rem;
            }

            .sidebar__foot-text small {
                color: var(--text-muted);
                font-size: .74rem;
            }

            /* ----------------------------------------------------------------
     ⭐⭐⭐ 折叠状态(BUG 修复重点)
     ----------------------------------------------------------------
     上一版的问题:隐藏元素用 opacity:0 + visibility:hidden,
     仍然占据布局宽度,导致 76px 的列被内部 200px+ 的内容撑破,
     出现左侧菜单"悬浮"、"错位"的现象。

     修复方案:
     1. 用 display:none 彻底从布局移除(不再占位)
     2. 导航项改为居中对齐(只剩图标)
     3. 品牌区、用户区居中
     4. 用户区的退出按钮也隐藏(折叠状态下不显示)
     ---------------------------------------------------------------- */
            .app[data-collapsed="true"] {
                grid-template-columns: var(--sidebar-w-collapsed) minmax(0, 1fr);
            }

            .app[data-collapsed="true"] :is(.brand__text,
                .nav__label,
                .nav__text,
                .nav__badge,
                .nav__chevron,
                .sidebar__foot-text,
                .nav__sublist) {
                /* ⭐ 关键:display:none 而非 opacity/visibility,
       彻底移除布局占位,避免撑破侧边栏宽度 */
                display: none;
            }

            .app[data-collapsed="true"] .sidebar__brand {
                justify-content: center;
                padding-inline: 0;
            }

            .app[data-collapsed="true"] .sidebar__nav {
                padding-inline: 10px;
                align-items: center;
            }

            .app[data-collapsed="true"] .nav__item {
                justify-content: center;
                gap: 0;
                padding: 10px 0;
            }

            /* 折叠时高亮条位置微调(避免贴到侧边栏边缘外) */
            .app[data-collapsed="true"] .nav__item[aria-current="page"]::before,
            .app[data-collapsed="true"] .nav__item.is-active::before {
                inset-inline-start: -10px;
                inset-block: 10px;
            }

            .app[data-collapsed="true"] .sidebar__footer {
                padding-inline: 10px;
            }

            .app[data-collapsed="true"] .sidebar__user {
                justify-content: center;
                padding-inline: 0;
            }

            .app[data-collapsed="true"] .sidebar__user .icon-btn {
                display: none;
            }

            /* ---------------- 主区域 ---------------- */
            .main {
                display: flex;
                flex-direction: column;
                min-width: 0;
            }

            .topbar {
                position: sticky;
                top: 0;
                z-index: 30;
                height: var(--topbar-h);
                flex: none;
                display: flex;
                align-items: center;
                gap: 10px;
                padding-inline: 18px;
                background: color-mix(in oklab, var(--bg-surface) 78%, transparent);
                backdrop-filter: blur(16px) saturate(150%);
                -webkit-backdrop-filter: blur(16px) saturate(150%);
                border-block-end: 1px solid var(--border);
            }

            .topbar__spacer {
                flex: 1;
            }

            .global-search {
                position: relative;
                display: flex;
                align-items: center;
                width: clamp(180px, 30vw, 380px);
            }

            .global-search input {
                width: 100%;
                padding: 8px 52px 8px 36px;
                border: 1px solid var(--border);
                border-radius: var(--r-full);
                background: var(--bg-surface-2);
                font-size: .9rem;
                transition: border-color var(--dur-fast) var(--ease),
                    box-shadow var(--dur-fast) var(--ease),
                    background-color var(--dur-fast) var(--ease);
            }

            .global-search input::placeholder {
                color: var(--text-faint);
            }

            .global-search input:focus {
                outline: none;
                background: var(--bg-surface);
                border-color: var(--accent);
                box-shadow: 0 0 0 3.5px var(--accent-ring);
            }

            .global-search .icon {
                position: absolute;
                inset-inline-start: 12px;
                color: var(--text-faint);
                pointer-events: none;
            }

            .global-search kbd {
                position: absolute;
                inset-inline-end: 8px;
                font-size: .68rem;
                padding: 2px 7px;
                border: 1px solid var(--border);
                border-radius: var(--r-xs);
                background: var(--bg-sunken);
                color: var(--text-faint);
                pointer-events: none;
            }

            .content {
                flex: 1;
                padding: 22px clamp(14px, 2vw, 28px) 48px;
                display: flex;
                flex-direction: column;
                gap: 20px;
                width: 100%;
                max-width: var(--content-max);
                margin-inline: auto;
                container-type: inline-size;
                container-name: content;
            }
        }


        /* =====================================================================
   5. COMPONENTS
   ===================================================================== */
        @layer components {

            /* ---------- 5.1 头像 ---------- */
            .avatar {
                --size: 34px;
                width: var(--size);
                height: var(--size);
                flex: none;
                display: grid;
                place-items: center;
                border-radius: var(--r-full);
                font-size: calc(var(--size) * .4);
                font-weight: 700;
                color: light-dark(oklch(0.36 0.12 var(--av-h, 265)), oklch(0.92 0.06 var(--av-h, 265)));
                background:
                    linear-gradient(140deg,
                        light-dark(oklch(0.90 0.07 var(--av-h, 265)), oklch(0.30 0.07 var(--av-h, 265))),
                        light-dark(oklch(0.95 0.04 var(--av-h, 265)), oklch(0.24 0.05 var(--av-h, 265))));
                box-shadow: inset 0 0 0 1px light-dark(oklch(0.2 0.02 265 / .07), oklch(1 0 0 / .07));
                user-select: none;
                position: relative;
            }

            /* 光环头像:isolation 保证 z-index:-1 只在内部生效 */
            .avatar--ring {
                isolation: isolate;
            }

            .avatar--ring::after {
                content: "";
                position: absolute;
                inset: -3px;
                border-radius: inherit;
                background: conic-gradient(from var(--ring-angle), var(--accent), var(--info), var(--success), var(--accent));
                z-index: -1;
                animation: spin-ring 4s linear infinite;
                opacity: .85;
            }

            @keyframes spin-ring {
                to {
                    --ring-angle: 360deg;
                }
            }

            .avatar-stack {
                display: flex;
            }

            .avatar-stack>.avatar {
                margin-inline-start: -9px;
                box-shadow: 0 0 0 2px var(--bg-surface);
            }

            .avatar-stack>.avatar:first-child {
                margin-inline-start: 0;
            }

            /* ---------- 5.2 按钮 ---------- */
            .btn {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                gap: 7px;
                padding: 8px 14px;
                border-radius: var(--r-sm);
                font-size: .88rem;
                font-weight: 580;
                white-space: nowrap;
                border: 1px solid transparent;
                transition:
                    background-color var(--dur-fast) var(--ease),
                    border-color var(--dur-fast) var(--ease),
                    color var(--dur-fast) var(--ease),
                    box-shadow var(--dur-fast) var(--ease),
                    transform var(--dur-fast) var(--ease);
                user-select: none;
            }

            .btn:active {
                transform: translateY(1px) scale(.985);
            }

            .btn:disabled {
                opacity: .45;
                pointer-events: none;
            }

            .btn--primary {
                background: linear-gradient(180deg,
                        color-mix(in oklab, var(--accent) 88%, #fff), var(--accent));
                color: var(--on-accent);
                box-shadow: var(--sh-xs),
                    inset 0 1px 0 light-dark(oklch(1 0 0 / .35), oklch(1 0 0 / .12));
            }

            .btn--primary:hover {
                background: var(--accent-hover);
                box-shadow: 0 6px 20px var(--accent-ring);
            }

            .btn--ghost {
                background: var(--bg-surface);
                border-color: var(--border);
                color: var(--text-muted);
            }

            .btn--ghost:hover {
                background: var(--bg-hover);
                color: var(--text);
                border-color: var(--border-strong);
            }

            .btn--danger {
                background: var(--danger-soft);
                color: var(--danger);
            }

            .btn--danger:hover {
                background: color-mix(in oklab, var(--danger) 22%, transparent);
            }

            .btn--sm {
                padding: 5px 10px;
                font-size: .82rem;
                border-radius: var(--r-xs);
            }

            .btn--icon {
                padding: 0;
                width: 34px;
                height: 34px;
            }

            .icon-btn {
                position: relative;
                display: grid;
                place-items: center;
                width: 36px;
                height: 36px;
                flex: none;
                border-radius: var(--r-sm);
                color: var(--text-muted);
                transition:
                    background-color var(--dur-fast) var(--ease),
                    color var(--dur-fast) var(--ease),
                    transform var(--dur-fast) var(--ease);
            }

            .icon-btn:hover {
                background: var(--bg-hover);
                color: var(--text);
            }

            .icon-btn:active {
                transform: scale(.92);
            }

            .icon-btn .icon {
                width: 18px;
                height: 18px;
            }

            .icon-btn__badge {
                position: absolute;
                top: 4px;
                inset-inline-end: 4px;
                min-width: 16px;
                height: 16px;
                padding-inline: 4px;
                display: grid;
                place-items: center;
                font-size: .62rem;
                font-weight: 700;
                line-height: 1;
                color: #fff;
                background: var(--danger);
                border-radius: var(--r-full);
                box-shadow: 0 0 0 2px var(--bg-surface);
                animation: pulse-badge 2.6s var(--ease) infinite;
            }

            @keyframes pulse-badge {

                0%,
                100% {
                    box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 0 var(--danger-soft);
                }

                50% {
                    box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 5px transparent;
                }
            }

            /* ---------- 5.3 表单控件 ---------- */
            .input,
            .select,
            .textarea {
                width: 100%;
                padding: 8px 12px;
                border: 1px solid var(--border);
                border-radius: var(--r-sm);
                background: var(--bg-surface);
                color: var(--text);
                font-size: .9rem;
                transition:
                    border-color var(--dur-fast) var(--ease),
                    box-shadow var(--dur-fast) var(--ease),
                    background-color var(--dur-fast) var(--ease);
            }

            .input::placeholder,
            .textarea::placeholder {
                color: var(--text-faint);
            }

            .input:hover,
            .select:hover,
            .textarea:hover {
                border-color: var(--border-strong);
            }

            .input:focus,
            .select:focus,
            .textarea:focus {
                outline: none;
                border-color: var(--accent);
                box-shadow: 0 0 0 3.5px var(--accent-ring);
            }

            .input:disabled,
            .select:disabled {
                background: var(--bg-sunken);
                color: var(--text-faint);
                cursor: not-allowed;
            }

            .input:user-invalid,
            .select:user-invalid,
            .textarea:user-invalid {
                border-color: var(--danger);
                box-shadow: 0 0 0 3.5px var(--danger-soft);
            }

            .input:user-valid:not(:placeholder-shown) {
                border-color: color-mix(in oklab, var(--success) 55%, var(--border));
            }

            .select {
                appearance: none;
                padding-inline-end: 32px;
                background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
                background-repeat: no-repeat;
                background-position: right 9px center;
                background-size: 14px;
                cursor: pointer;
            }

            .select--sm,
            .input--sm {
                padding-block: 5px;
                font-size: .83rem;
            }

            .textarea {
                resize: vertical;
                min-height: 76px;
                field-sizing: content;
                max-height: 200px;
            }

            .checkbox,
            .radio {
                --sz: 17px;
                appearance: none;
                width: var(--sz);
                height: var(--sz);
                flex: none;
                border: 1.5px solid var(--border-strong);
                border-radius: 5px;
                background: var(--bg-surface);
                cursor: pointer;
                position: relative;
                transition:
                    background-color var(--dur-fast) var(--ease),
                    border-color var(--dur-fast) var(--ease),
                    box-shadow var(--dur-fast) var(--ease),
                    transform var(--dur-fast) var(--ease);
            }

            .radio {
                border-radius: var(--r-full);
            }

            .checkbox:hover,
            .radio:hover {
                border-color: var(--accent);
            }

            .checkbox:active,
            .radio:active {
                transform: scale(.9);
            }

            .checkbox:checked,
            .radio:checked {
                background: var(--accent);
                border-color: var(--accent);
                box-shadow: 0 2px 8px var(--accent-ring);
            }

            .checkbox:checked::after {
                content: "";
                position: absolute;
                inset: 0;
                background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 13 4 4L19 7'/%3E%3C/svg%3E") center / 12px no-repeat;
                animation: pop-in .18s var(--ease-out-back);
            }

            .checkbox:indeterminate {
                background: var(--accent);
                border-color: var(--accent);
            }

            .checkbox:indeterminate::after {
                content: "";
                position: absolute;
                inset: 0;
                background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='3.4' stroke-linecap='round'%3E%3Cpath d='M6 12h12'/%3E%3C/svg%3E") center / 12px no-repeat;
            }

            .radio:checked::after {
                content: "";
                position: absolute;
                inset: 4px;
                border-radius: inherit;
                background: #fff;
                animation: pop-in .18s var(--ease-out-back);
            }

            @keyframes pop-in {
                from {
                    scale: .3;
                    opacity: 0;
                }

                to {
                    scale: 1;
                    opacity: 1;
                }
            }

            .switch {
                position: relative;
                display: inline-flex;
                align-items: center;
                gap: 9px;
                cursor: pointer;
            }

            .switch input {
                position: absolute;
                opacity: 0;
                width: 0;
                height: 0;
            }

            .switch__track {
                width: 40px;
                height: 22px;
                flex: none;
                border-radius: var(--r-full);
                background: var(--border-strong);
                position: relative;
                transition: background-color var(--dur) var(--ease);
            }

            .switch__track::after {
                content: "";
                position: absolute;
                top: 3px;
                inset-inline-start: 3px;
                width: 16px;
                height: 16px;
                border-radius: var(--r-full);
                background: #fff;
                box-shadow: var(--sh-xs);
                transition: translate var(--dur) var(--ease-out-back);
            }

            .switch input:checked+.switch__track {
                background: var(--accent);
            }

            .switch input:checked+.switch__track::after {
                translate: 18px 0;
            }

            .switch input:focus-visible+.switch__track {
                box-shadow: 0 0 0 3.5px var(--accent-ring);
            }

            .switch__label {
                font-size: .87rem;
                color: var(--text-muted);
            }

            .segmented {
                display: inline-flex;
                padding: 3px;
                border-radius: var(--r-sm);
                background: var(--bg-sunken);
                border: 1px solid var(--border);
                gap: 2px;
            }

            .segmented label {
                position: relative;
            }

            .segmented input {
                position: absolute;
                opacity: 0;
                pointer-events: none;
            }

            .segmented span {
                display: block;
                padding: 4px 12px;
                border-radius: var(--r-xs);
                font-size: .84rem;
                color: var(--text-muted);
                cursor: pointer;
                white-space: nowrap;
                transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
            }

            .segmented span:hover {
                color: var(--text);
            }

            .segmented input:checked+span {
                background: var(--bg-surface);
                color: var(--accent);
                font-weight: 620;
                box-shadow: var(--sh-xs);
            }

            .segmented input:focus-visible+span {
                outline: 2px solid var(--accent);
                outline-offset: 1px;
            }

            /* ---------- 5.4 卡片 ---------- */
            .card {
                background: var(--bg-surface);
                border: 1px solid var(--border);
                border-radius: var(--r-lg);
                box-shadow: var(--sh-xs);
                overflow: hidden;
                transition: box-shadow var(--dur) var(--ease), border-color var(--dur) var(--ease);
            }

            .card--hoverable:hover {
                box-shadow: var(--sh-md);
                border-color: var(--border-strong);
            }

            .card__head {
                display: flex;
                align-items: center;
                gap: 12px;
                padding: 16px 20px;
                border-block-end: 1px solid var(--border);
            }

            .card__title {
                font-size: 1rem;
                font-weight: 650;
                display: flex;
                align-items: center;
                gap: 8px;
            }

            .card__subtitle {
                font-size: .8rem;
                color: var(--text-muted);
                font-weight: 400;
            }

            .card__body {
                padding: 20px;
            }

            /* ---------- 5.5 页头 ---------- */
            .page-head {
                display: flex;
                align-items: flex-end;
                justify-content: space-between;
                gap: 18px;
                flex-wrap: wrap;
            }

            .breadcrumb {
                display: flex;
                align-items: center;
                gap: 7px;
                font-size: .8rem;
                color: var(--text-muted);
                margin-block-end: 6px;
            }

            .breadcrumb a {
                transition: color var(--dur-fast) var(--ease);
            }

            .breadcrumb a:hover {
                color: var(--accent);
            }

            .breadcrumb .sep {
                color: var(--text-faint);
            }

            .page-title {
                font-size: clamp(1.28rem, 2.4cqi + .8rem, 1.7rem);
                font-weight: 700;
                letter-spacing: -.015em;
            }

            .page-desc {
                font-size: .86rem;
                color: var(--text-muted);
                margin-block-start: 4px;
            }

            .page-actions {
                display: flex;
                gap: 9px;
                flex-wrap: wrap;
            }

            /* ---------- 5.6 统计卡 ---------- */
            .stats-grid {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
                gap: 16px;
            }

            .stat {
                position: relative;
                padding: 18px;
                border-radius: var(--r-lg);
                background: var(--bg-surface);
                border: 1px solid var(--border);
                overflow: hidden;
                isolation: isolate;
                transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
            }

            .stat::before {
                content: "";
                position: absolute;
                inset: 0;
                background: radial-gradient(420px circle at var(--glow-x) -30%,
                        color-mix(in oklab, var(--tone, var(--accent)) 22%, transparent), transparent 62%);
                opacity: 0;
                transition: opacity var(--dur-slow) var(--ease);
                z-index: -1;
            }

            .stat:hover {
                transform: translateY(-3px);
                box-shadow: var(--sh-md);
            }

            .stat:hover::before {
                opacity: 1;
            }

            .stat__top {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 10px;
            }

            .stat__label {
                font-size: .82rem;
                color: var(--text-muted);
                font-weight: 550;
            }

            .stat__icon {
                width: 36px;
                height: 36px;
                display: grid;
                place-items: center;
                border-radius: 11px;
                background: color-mix(in oklab, var(--tone, var(--accent)) 14%, transparent);
                color: var(--tone, var(--accent));
            }

            .stat__icon .icon {
                width: 18px;
                height: 18px;
            }

            .stat__value {
                font-size: clamp(1.6rem, 3.2cqi + .6rem, 2.1rem);
                font-weight: 720;
                letter-spacing: -.03em;
                margin-block: 10px 4px;
                font-variant-numeric: tabular-nums;
            }

            .stat__foot {
                display: flex;
                align-items: center;
                gap: 6px;
                font-size: .78rem;
                color: var(--text-muted);
            }

            .trend {
                display: inline-flex;
                align-items: center;
                gap: 2px;
                font-weight: 650;
            }

            .trend--up {
                color: var(--success);
            }

            .trend--down {
                color: var(--danger);
            }

            .trend .icon {
                width: 13px;
                height: 13px;
            }

            .stat__spark {
                display: flex;
                align-items: flex-end;
                gap: 3px;
                height: 26px;
                margin-block-start: 10px;
            }

            .stat__spark i {
                flex: 1;
                border-radius: 3px 3px 1px 1px;
                background: color-mix(in oklab, var(--tone, var(--accent)) 40%, transparent);
                height: calc(var(--h) * 1%);
                transition: height var(--dur-slow) var(--ease);
            }

            .stat:hover .stat__spark i {
                background: color-mix(in oklab, var(--tone, var(--accent)) 70%, transparent);
            }

            /* ---------- 5.7 图表 ---------- */
            .chart {
                display: flex;
                align-items: flex-end;
                gap: clamp(6px, 1.4cqi, 16px);
                height: 232px;
                padding-block-start: 14px;
            }

            .bar {
                flex: 1;
                height: 100%;
                display: flex;
                flex-direction: column;
                justify-content: flex-end;
                align-items: center;
                gap: 9px;
                position: relative;
                min-width: 0;
            }

            .bar__fill {
                width: 100%;
                max-width: 46px;
                height: calc(var(--v) * 1%);
                min-height: 6px;
                border-radius: 8px 8px 3px 3px;
                background: linear-gradient(180deg,
                        color-mix(in oklab, var(--accent) 92%, #fff),
                        color-mix(in oklab, var(--accent) 42%, transparent));
                box-shadow: inset 0 1px 0 light-dark(oklch(1 0 0 / .4), oklch(1 0 0 / .1));
                transition: filter var(--dur) var(--ease), transform var(--dur) var(--ease);
                transform-origin: bottom;
            }

            .bar:hover .bar__fill {
                filter: brightness(1.12) saturate(1.15);
                transform: scaleY(1.02);
            }

            .bar__label {
                font-size: .7rem;
                color: var(--text-faint);
                white-space: nowrap;
            }

            .bar::after {
                content: attr(data-val);
                position: absolute;
                bottom: calc(var(--v) * 1% + 34px);
                padding: 3px 8px;
                border-radius: var(--r-xs);
                font-size: .72rem;
                font-weight: 650;
                background: var(--bg-elevated);
                color: var(--text);
                border: 1px solid var(--border);
                box-shadow: var(--sh-sm);
                opacity: 0;
                translate: 0 5px;
                pointer-events: none;
                transition: opacity var(--dur-fast) var(--ease), translate var(--dur-fast) var(--ease);
                white-space: nowrap;
            }

            .bar:hover::after {
                opacity: 1;
                translate: 0 0;
            }

            /* ---------- 5.8 徽章 ---------- */
            .badge {
                display: inline-flex;
                align-items: center;
                gap: 5px;
                padding: 3px 9px;
                border-radius: var(--r-full);
                font-size: .76rem;
                font-weight: 620;
                white-space: nowrap;
                border: 1px solid transparent;
            }

            .badge::before {
                content: "";
                width: 6px;
                height: 6px;
                border-radius: inherit;
                flex: none;
                background: currentColor;
                box-shadow: 0 0 0 2.5px color-mix(in oklab, currentColor 22%, transparent);
            }

            .badge--success {
                color: var(--success);
                background: var(--success-soft);
            }

            .badge--warn {
                color: var(--warn);
                background: var(--warn-soft);
            }

            .badge--danger {
                color: var(--danger);
                background: var(--danger-soft);
            }

            .badge--info {
                color: var(--info);
                background: var(--info-soft);
            }

            .badge--muted {
                color: var(--text-muted);
                background: var(--bg-sunken);
            }

            .badge--plain::before {
                display: none;
            }

            .chip {
                display: inline-flex;
                align-items: center;
                gap: 6px;
                padding: 4px 11px;
                border-radius: var(--r-full);
                font-size: .8rem;
                background: var(--bg-sunken);
                border: 1px solid var(--border);
                color: var(--text-muted);
                transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
                    border-color var(--dur-fast) var(--ease);
            }

            .chip:hover {
                background: var(--bg-hover);
                color: var(--text);
                border-color: var(--border-strong);
            }

            .chip[aria-pressed="true"],
            .chip.is-active {
                background: var(--accent-soft);
                color: var(--accent);
                border-color: color-mix(in oklab, var(--accent) 35%, transparent);
            }

            /* ---------- 5.9 表格 ---------- */
            .table-card {
                display: flex;
                flex-direction: column;
            }

            .table-toolbar {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 12px;
                flex-wrap: wrap;
                padding: 14px 18px;
                border-block-end: 1px solid var(--border);
            }

            .toolbar__group {
                display: flex;
                align-items: center;
                gap: 9px;
                flex-wrap: wrap;
            }

            .search-field {
                position: relative;
                display: flex;
                align-items: center;
                width: clamp(200px, 26cqi, 320px);
            }

            .search-field .icon {
                position: absolute;
                inset-inline-start: 11px;
                color: var(--text-faint);
                pointer-events: none;
                width: 16px;
                height: 16px;
            }

            .search-field input {
                width: 100%;
                padding: 8px 60px 8px 34px;
                border: 1px solid var(--border);
                border-radius: var(--r-sm);
                background: var(--bg-surface-2);
                font-size: .88rem;
                transition:
                    border-color var(--dur-fast) var(--ease),
                    box-shadow var(--dur-fast) var(--ease),
                    background-color var(--dur-fast) var(--ease);
            }

            .search-field input:focus {
                outline: none;
                background: var(--bg-surface);
                border-color: var(--accent);
                box-shadow: 0 0 0 3.5px var(--accent-ring);
            }

            .search-field__hint {
                position: absolute;
                inset-inline-end: 8px;
                font-size: .66rem;
                padding: 2px 6px;
                border: 1px solid var(--border);
                border-radius: 5px;
                color: var(--text-faint);
                background: var(--bg-sunken);
                pointer-events: none;
            }

            .bulkbar {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 12px;
                flex-wrap: wrap;
                padding: 10px 18px;
                background: linear-gradient(90deg, var(--accent-soft), transparent 70%);
                border-block-end: 1px solid var(--border);
                animation: slide-down var(--dur) var(--ease);
            }

            @keyframes slide-down {
                from {
                    opacity: 0;
                    translate: 0 -8px;
                }

                to {
                    opacity: 1;
                    translate: 0 0;
                }
            }

            .bulkbar__count {
                font-size: .86rem;
                color: var(--text-muted);
            }

            .bulkbar__count strong {
                color: var(--accent);
                font-size: 1rem;
            }

            .table-wrap {
                overflow: auto;
                max-height: min(64vh, 720px);
                overscroll-behavior: contain;
                position: relative;
            }

            .data-table {
                width: 100%;
                min-width: 920px;
                font-size: .875rem;
            }

            .data-table thead th {
                position: sticky;
                top: 0;
                z-index: 5;
                background: var(--bg-surface-2);
                backdrop-filter: blur(8px);
                padding: 11px 14px;
                text-align: start;
                font-size: .76rem;
                font-weight: 700;
                letter-spacing: .05em;
                text-transform: uppercase;
                color: var(--text-muted);
                white-space: nowrap;
                border-block-end: 1px solid var(--border);
                user-select: none;
            }

            .th-sortable {
                cursor: pointer;
                transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
            }

            .th-sortable:hover {
                color: var(--text);
                background: var(--bg-hover);
            }

            .th-inner {
                display: inline-flex;
                align-items: center;
                gap: 6px;
            }

            .th-sort-icon {
                width: 13px;
                height: 13px;
                flex: none;
                opacity: .3;
                transition: opacity var(--dur-fast) var(--ease), rotate var(--dur) var(--ease), color var(--dur-fast) var(--ease);
            }

            .th-sortable:hover .th-sort-icon {
                opacity: .65;
            }

            th[aria-sort="ascending"] .th-sort-icon {
                opacity: 1;
                color: var(--accent);
                rotate: 0deg;
            }

            th[aria-sort="descending"] .th-sort-icon {
                opacity: 1;
                color: var(--accent);
                rotate: 180deg;
            }

            th[aria-sort]:not([aria-sort="none"]) {
                color: var(--accent);
            }

            .data-table tbody td {
                padding: 11px 14px;
                border-block-end: 1px solid var(--border);
                vertical-align: middle;
                transition: background-color var(--dur-fast) var(--ease);
            }

            .data-table[data-zebra="true"] tbody tr:nth-child(even) {
                background: color-mix(in oklab, var(--bg-surface-2) 62%, transparent);
            }

            .data-table tbody tr {
                transition: background-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
            }

            .data-table tbody tr:hover {
                background: var(--bg-hover);
            }

            .data-table tbody tr.is-selected {
                background: var(--accent-softer);
                box-shadow: inset 3px 0 0 var(--accent);
            }

            .data-table tbody tr.is-selected:hover {
                background: var(--accent-soft);
            }

            .data-table[data-density="compact"] tbody td {
                padding-block: 6px;
            }

            .data-table[data-density="compact"] thead th {
                padding-block: 8px;
            }

            /* 列显隐:用 [data-hide~="列名"] 精确匹配空格分隔的属性值 */
            .data-table[data-hide~="check"] [data-col="check"],
            .data-table[data-hide~="id"] [data-col="id"],
            .data-table[data-hide~="user"] [data-col="user"],
            .data-table[data-hide~="email"] [data-col="email"],
            .data-table[data-hide~="phone"] [data-col="phone"],
            .data-table[data-hide~="dept"] [data-col="dept"],
            .data-table[data-hide~="role"] [data-col="role"],
            .data-table[data-hide~="status"] [data-col="status"],
            .data-table[data-hide~="lastLogin"] [data-col="lastLogin"],
            .data-table[data-hide~="createdAt"] [data-col="createdAt"] {
                display: none;
            }

            .user-cell {
                display: flex;
                align-items: center;
                gap: 10px;
                min-width: 0;
            }

            .user-cell__meta {
                display: flex;
                flex-direction: column;
                line-height: 1.25;
                min-width: 0;
            }

            .user-cell__name {
                font-weight: 600;
            }

            .user-cell__sub {
                font-size: .76rem;
                color: var(--text-muted);
            }

            .cell-actions {
                display: flex;
                align-items: center;
                gap: 2px;
                justify-content: flex-end;
            }

            .table-empty {
                padding: 60px 20px;
                display: flex;
                flex-direction: column;
                align-items: center;
                gap: 12px;
                color: var(--text-muted);
                animation: fade-in var(--dur) var(--ease);
            }

            .table-empty__art {
                width: 76px;
                height: 76px;
                display: grid;
                place-items: center;
                border-radius: var(--r-full);
                background: var(--bg-sunken);
                color: var(--text-faint);
            }

            .table-empty__art .icon {
                width: 32px;
                height: 32px;
            }

            .skeleton {
                height: 12px;
                border-radius: var(--r-full);
                background: linear-gradient(90deg,
                        var(--bg-sunken) 25%,
                        color-mix(in oklab, var(--bg-sunken) 60%, var(--bg-hover)) 50%,
                        var(--bg-sunken) 75%);
                background-size: 220% 100%;
                animation: shimmer 1.4s linear infinite;
            }

            @keyframes shimmer {
                from {
                    background-position: 220% 0;
                }

                to {
                    background-position: -20% 0;
                }
            }

            /* ---------- 5.10 分页 ---------- */
            .pagination {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 14px;
                flex-wrap: wrap;
                padding: 14px 18px;
                border-block-start: 1px solid var(--border);
                background: var(--bg-surface);
            }

            .pagination__group {
                display: flex;
                align-items: center;
                gap: 10px;
                flex-wrap: wrap;
                font-size: .84rem;
                color: var(--text-muted);
            }

            .pagination__group strong {
                color: var(--text);
                font-variant-numeric: tabular-nums;
            }

            .pager {
                display: flex;
                align-items: center;
                gap: 4px;
            }

            .page-btn {
                min-width: 34px;
                height: 34px;
                padding-inline: 9px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                border-radius: var(--r-sm);
                font-size: .85rem;
                font-weight: 560;
                color: var(--text-muted);
                border: 1px solid transparent;
                font-variant-numeric: tabular-nums;
                transition:
                    background-color var(--dur-fast) var(--ease),
                    color var(--dur-fast) var(--ease),
                    border-color var(--dur-fast) var(--ease),
                    transform var(--dur-fast) var(--ease);
            }

            .page-btn:hover:not(:disabled):not(.is-current) {
                background: var(--bg-hover);
                color: var(--text);
                border-color: var(--border);
            }

            .page-btn:active:not(:disabled) {
                transform: scale(.94);
            }

            .page-btn:disabled {
                opacity: .35;
                cursor: not-allowed;
                pointer-events: none;
            }

            .page-btn.is-current {
                background: var(--accent);
                color: var(--on-accent);
                border-color: var(--accent);
                box-shadow: 0 3px 12px var(--accent-ring);
                font-weight: 680;
            }

            .page-btn .icon {
                width: 15px;
                height: 15px;
            }

            .page-ellipsis {
                min-width: 26px;
                text-align: center;
                color: var(--text-faint);
                user-select: none;
            }

            .jump-input {
                width: 58px;
                padding: 5px 8px;
                text-align: center;
                border: 1px solid var(--border);
                border-radius: var(--r-xs);
                background: var(--bg-surface);
                font-size: .84rem;
                font-variant-numeric: tabular-nums;
            }

            .jump-input:focus {
                outline: none;
                border-color: var(--accent);
                box-shadow: 0 0 0 3px var(--accent-ring);
            }

            .jump-input::-webkit-outer-spin-button,
            .jump-input::-webkit-inner-spin-button {
                appearance: none;
                margin: 0;
            }

            /* ---------- 5.11 Popover ---------- */
            .popover {
                position: fixed;
                inset: auto;
                top: calc(var(--topbar-h) + 10px);
                right: 20px;
                margin: 0;
                width: min(320px, calc(100vw - 32px));
                padding: 0;
                border: 1px solid var(--border);
                border-radius: var(--r-md);
                background: var(--bg-elevated);
                box-shadow: var(--sh-lg);
                color: var(--text);
                opacity: 0;
                transform: translateY(-8px) scale(.97);
                transition:
                    opacity var(--dur) var(--ease),
                    transform var(--dur) var(--ease-out-back),
                    display var(--dur) allow-discrete,
                    overlay var(--dur) allow-discrete;
            }

            .popover:popover-open {
                opacity: 1;
                transform: none;
            }

            @starting-style {
                .popover:popover-open {
                    opacity: 0;
                    transform: translateY(-8px) scale(.97);
                }
            }

            .popover::backdrop {
                background: transparent;
            }

            .popover__head {
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 12px 14px;
                border-block-end: 1px solid var(--border);
                font-size: .88rem;
                font-weight: 650;
            }

            .popover__body {
                padding: 10px;
                max-height: 340px;
                overflow-y: auto;
            }

            .col-list {
                display: flex;
                flex-direction: column;
                gap: 1px;
            }

            .col-list label {
                display: flex;
                align-items: center;
                gap: 10px;
                padding: 7px 9px;
                border-radius: var(--r-xs);
                font-size: .87rem;
                cursor: pointer;
                transition: background-color var(--dur-fast) var(--ease);
            }

            .col-list label:hover {
                background: var(--bg-hover);
            }

            .notif-item {
                display: flex;
                gap: 11px;
                padding: 10px;
                border-radius: var(--r-sm);
                transition: background-color var(--dur-fast) var(--ease);
                cursor: pointer;
            }

            .notif-item:hover {
                background: var(--bg-hover);
            }

            .notif-item__icon {
                width: 34px;
                height: 34px;
                flex: none;
                display: grid;
                place-items: center;
                border-radius: var(--r-sm);
                background: var(--accent-soft);
                color: var(--accent);
            }

            .notif-item__body {
                min-width: 0;
            }

            .notif-item__title {
                font-size: .86rem;
                font-weight: 600;
            }

            .notif-item__desc {
                font-size: .78rem;
                color: var(--text-muted);
            }

            .notif-item__time {
                font-size: .72rem;
                color: var(--text-faint);
                margin-block-start: 3px;
            }

            .notif-unread {
                position: relative;
            }

            .notif-unread::after {
                content: "";
                position: absolute;
                top: 16px;
                left: 34px;
                width: 7px;
                height: 7px;
                border-radius: 50%;
                background: var(--accent);
                box-shadow: 0 0 0 2px var(--bg-elevated);
            }

            /* ---------- 5.12 对话框 ---------- */
            .modal {
                width: min(680px, calc(100vw - 32px));
                max-height: calc(100dvh - 60px);
                border-radius: var(--r-lg);
                background: var(--bg-elevated);
                border: 1px solid var(--border);
                box-shadow: var(--sh-lg);
                color: var(--text);
                opacity: 0;
                transform: translateY(16px) scale(.97);
                transition:
                    opacity var(--dur) var(--ease),
                    transform var(--dur) var(--ease-out-back),
                    display var(--dur) allow-discrete,
                    overlay var(--dur) allow-discrete;
            }

            .modal[open] {
                opacity: 1;
                transform: none;
            }

            @starting-style {
                .modal[open] {
                    opacity: 0;
                    transform: translateY(16px) scale(.97);
                }
            }

            .modal::backdrop {
                background: color-mix(in oklab, #070a10 58%, transparent);
                backdrop-filter: blur(6px);
                opacity: 0;
                transition:
                    opacity var(--dur) var(--ease),
                    display var(--dur) allow-discrete,
                    overlay var(--dur) allow-discrete;
            }

            .modal[open]::backdrop {
                opacity: 1;
            }

            @starting-style {
                .modal[open]::backdrop {
                    opacity: 0;
                }
            }

            .modal--sm {
                width: min(440px, calc(100vw - 32px));
            }

            .modal__head {
                display: flex;
                align-items: flex-start;
                gap: 12px;
                padding: 18px 20px 14px;
                border-block-end: 1px solid var(--border);
            }

            .modal__head h2 {
                font-size: 1.06rem;
            }

            .modal__head p {
                font-size: .82rem;
                color: var(--text-muted);
                margin-block-start: 3px;
            }

            .modal__head .icon-btn {
                margin-inline-start: auto;
                margin-block-start: -4px;
            }

            .modal__body {
                padding: 20px;
                overflow-y: auto;
                max-height: calc(100dvh - 260px);
            }

            .modal__foot {
                display: flex;
                align-items: center;
                justify-content: flex-end;
                gap: 10px;
                padding: 14px 20px;
                border-block-start: 1px solid var(--border);
                background: color-mix(in oklab, var(--bg-surface-2) 60%, transparent);
            }

            .modal__icon {
                width: 42px;
                height: 42px;
                flex: none;
                display: grid;
                place-items: center;
                border-radius: var(--r-md);
                background: var(--danger-soft);
                color: var(--danger);
            }

            .modal__icon .icon {
                width: 21px;
                height: 21px;
            }

            .form-grid {
                display: grid;
                grid-template-columns: repeat(2, minmax(0, 1fr));
                gap: 16px;
            }

            .field {
                display: flex;
                flex-direction: column;
                gap: 6px;
                min-width: 0;
            }

            .field--full {
                grid-column: 1 / -1;
            }

            .field>label,
            .field>legend {
                font-size: .82rem;
                font-weight: 600;
                color: var(--text-muted);
                padding: 0;
            }

            .field .req {
                color: var(--danger);
            }

            .field__hint {
                font-size: .74rem;
                color: var(--text-faint);
            }

            .field__err {
                font-size: .75rem;
                color: var(--danger);
                display: none;
                align-items: center;
                gap: 4px;
                animation: fade-in var(--dur-fast) var(--ease);
            }

            /* :has() 父选择器:字段内有非法子元素时显示错误提示 */
            .field:has(:user-invalid) .field__err {
                display: flex;
            }

            .field:has(:user-invalid) .field__hint {
                display: none;
            }

            /* ---------- 5.13 Toast ---------- */
            .toast-stack {
                position: fixed;
                inset-block-end: 22px;
                inset-inline-end: 22px;
                z-index: 9998;
                display: flex;
                flex-direction: column;
                gap: 10px;
                width: min(360px, calc(100vw - 32px));
                pointer-events: none;
            }

            .toast {
                pointer-events: auto;
                position: relative;
                display: flex;
                align-items: flex-start;
                gap: 11px;
                padding: 13px 14px;
                border-radius: var(--r-md);
                background: var(--bg-elevated);
                border: 1px solid var(--border);
                box-shadow: var(--sh-lg);
                overflow: hidden;
                animation: toast-in .38s var(--ease-out-back);
            }

            .toast.is-leaving {
                animation: toast-out .24s var(--ease) forwards;
            }

            @keyframes toast-in {
                from {
                    opacity: 0;
                    transform: translateX(28px) scale(.94);
                }

                to {
                    opacity: 1;
                    transform: none;
                }
            }

            @keyframes toast-out {
                to {
                    opacity: 0;
                    transform: translateX(28px) scale(.94);
                }
            }

            .toast__icon {
                width: 30px;
                height: 30px;
                flex: none;
                display: grid;
                place-items: center;
                border-radius: var(--r-sm);
            }

            .toast__icon .icon {
                width: 16px;
                height: 16px;
            }

            .toast--success .toast__icon {
                background: var(--success-soft);
                color: var(--success);
            }

            .toast--error .toast__icon {
                background: var(--danger-soft);
                color: var(--danger);
            }

            .toast--info .toast__icon {
                background: var(--info-soft);
                color: var(--info);
            }

            .toast--warn .toast__icon {
                background: var(--warn-soft);
                color: var(--warn);
            }

            .toast__content {
                flex: 1;
                min-width: 0;
            }

            .toast__title {
                font-size: .88rem;
                font-weight: 640;
            }

            .toast__desc {
                font-size: .8rem;
                color: var(--text-muted);
                margin-block-start: 2px;
            }

            .toast__close {
                color: var(--text-faint);
                padding: 2px;
                margin: -2px -2px 0 0;
                border-radius: var(--r-xs);
            }

            .toast__close:hover {
                color: var(--text);
                background: var(--bg-hover);
            }

            .toast__bar {
                position: absolute;
                inset-block-end: 0;
                inset-inline-start: 0;
                height: 2.5px;
                width: 100%;
                background: currentColor;
                opacity: .5;
                transform-origin: 0 50%;
                animation: toast-countdown linear forwards;
            }

            @keyframes toast-countdown {
                from {
                    transform: scaleX(1);
                }

                to {
                    transform: scaleX(0);
                }
            }

            /* ---------- 5.14 进度 / 分割线 / Tabs ---------- */
            .progress {
                height: 7px;
                border-radius: var(--r-full);
                background: var(--bg-sunken);
                overflow: hidden;
            }

            .progress>i {
                display: block;
                height: 100%;
                width: calc(var(--p, 0) * 1%);
                border-radius: inherit;
                background: linear-gradient(90deg, var(--accent),
                        color-mix(in oklab, var(--accent) 50%, var(--info)));
                transition: width var(--dur-slow) var(--ease);
            }

            .divider {
                height: 1px;
                background: var(--border);
                border: 0;
                margin: 0;
            }

            .divider--v {
                width: 1px;
                height: 20px;
                background: var(--border);
            }

            .tabs {
                display: flex;
                gap: 2px;
                position: relative;
                border-block-end: 1px solid var(--border);
                padding-inline: 6px;
                overflow-x: auto;
            }

            .tab {
                position: relative;
                padding: 10px 14px;
                font-size: .87rem;
                font-weight: 560;
                color: var(--text-muted);
                white-space: nowrap;
                transition: color var(--dur-fast) var(--ease);
            }

            .tab:hover {
                color: var(--text);
            }

            .tab[aria-selected="true"] {
                color: var(--accent);
                font-weight: 650;
            }

            .tab[aria-selected="true"]::after {
                content: "";
                position: absolute;
                inset-inline: 10px;
                inset-block-end: -1px;
                height: 2px;
                border-radius: var(--r-full);
                background: var(--accent);
                animation: tab-slide var(--dur) var(--ease);
            }

            @keyframes tab-slide {
                from {
                    transform: scaleX(.3);
                    opacity: 0;
                }

                to {
                    transform: none;
                    opacity: 1;
                }
            }
        }


        /* =====================================================================
   6. UTILITIES
   ===================================================================== */
        @layer utilities {
            .flex {
                display: flex;
            }

            .flex-col {
                flex-direction: column;
            }

            .items-center {
                align-items: center;
            }

            .justify-between {
                justify-content: space-between;
            }

            .gap-1 {
                gap: 4px;
            }

            .gap-2 {
                gap: 8px;
            }

            .gap-3 {
                gap: 12px;
            }

            .gap-4 {
                gap: 16px;
            }

            .wrap {
                flex-wrap: wrap;
            }

            .ml-auto {
                margin-inline-start: auto;
            }

            .grow {
                flex: 1;
                min-width: 0;
            }

            .text-muted {
                color: var(--text-muted);
            }

            .text-faint {
                color: var(--text-faint);
            }

            .text-sm {
                font-size: .82rem;
            }

            .text-xs {
                font-size: .74rem;
            }

            .fw-600 {
                font-weight: 600;
            }

            .nowrap {
                white-space: nowrap;
            }

            .truncate {
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }

            .mono {
                font-variant-numeric: tabular-nums;
            }

            .hidden {
                display: none !important;
            }
        }


        /* =====================================================================
   7. MOTION —— 动画 / 容器查询 / 响应式
   ===================================================================== */
        @layer motion {

            @keyframes fade-in {
                from {
                    opacity: 0;
                }

                to {
                    opacity: 1;
                }
            }

            @keyframes rise-in {
                from {
                    opacity: 0;
                    transform: translateY(10px);
                }

                to {
                    opacity: 1;
                    transform: none;
                }
            }

            .animate-rise {
                animation: rise-in .46s var(--ease) backwards;
            }

            /* 滚动驱动动画:柱状图边滚边生长 */
            @supports (animation-timeline: view()) {
                .bar__fill {
                    animation: bar-grow linear both;
                    animation-timeline: view();
                    animation-range: entry 15% cover 38%;
                }

                @keyframes bar-grow {
                    from {
                        transform: scaleY(.04);
                        opacity: .35;
                    }

                    to {
                        transform: scaleY(1);
                        opacity: 1;
                    }
                }
            }

            /* 视图过渡:主题切换圆形揭示 */
            @supports (view-transition-name: none) {

                ::view-transition-old(root),
                ::view-transition-new(root) {
                    animation-duration: .46s;
                    animation-timing-function: var(--ease);
                    mix-blend-mode: normal;
                }

                ::view-transition-new(root) {
                    animation-name: vtx-reveal;
                    clip-path: circle(0% at var(--vt-x, 50%) var(--vt-y, 50%));
                }

                @keyframes vtx-reveal {
                    to {
                        clip-path: circle(160% at var(--vt-x, 50%) var(--vt-y, 50%));
                    }
                }
            }

            /* ---------- 容器查询 ---------- */
            @container content (max-width: 1080px) {
                .pagination {
                    justify-content: center;
                }
            }

            @container content (max-width: 900px) {
                .stats-grid {
                    grid-template-columns: repeat(2, minmax(0, 1fr));
                }

                .chart {
                    height: 190px;
                }

                .bar__label {
                    font-size: 0;
                }

                .bar__label::first-letter {
                    font-size: .7rem;
                }
            }

            @container content (max-width: 640px) {
                .stats-grid {
                    grid-template-columns: 1fr;
                }

                .table-toolbar {
                    flex-direction: column;
                    align-items: stretch;
                }

                .toolbar__group {
                    width: 100%;
                }

                .search-field {
                    width: 100%;
                }

                .form-grid {
                    grid-template-columns: 1fr;
                }
            }

            /* ---------- 响应式:平板尺寸自动折叠侧边栏 ---------- */
            @media (min-width: 721px) and (max-width: 1024px) {
                .app {
                    grid-template-columns: var(--sidebar-w-collapsed) minmax(0, 1fr);
                }

                .app :is(.brand__text, .nav__label, .nav__text, .nav__badge,
                    .nav__chevron, .sidebar__foot-text, .nav__sublist) {
                    display: none;
                }

                .app .sidebar__brand {
                    justify-content: center;
                    padding-inline: 0;
                }

                .app .sidebar__nav {
                    padding-inline: 10px;
                    align-items: center;
                }

                .app .nav__item {
                    justify-content: center;
                    gap: 0;
                    padding: 10px 0;
                }

                .app .nav__item[aria-current="page"]::before,
                .app .nav__item.is-active::before {
                    inset-inline-start: -10px;
                    inset-block: 10px;
                }

                .app .sidebar__footer {
                    padding-inline: 10px;
                }

                .app .sidebar__user {
                    justify-content: center;
                    padding-inline: 0;
                }

                .app .sidebar__user .icon-btn {
                    display: none;
                }
            }

            /* ---------- 响应式:移动端抽屉侧边栏 ---------- */
            @media (max-width: 720px) {

                .app,
                .app[data-collapsed="true"] {
                    grid-template-columns: minmax(0, 1fr);
                }

                .sidebar {
                    position: fixed;
                    inset-block: 0;
                    inset-inline-start: 0;
                    width: var(--sidebar-w);
                    translate: -100% 0;
                    transition: translate var(--dur-slow) var(--ease);
                    box-shadow: var(--sh-lg);
                    z-index: 100;
                }

                .app[data-mobile-open="true"] .sidebar {
                    translate: 0 0;
                }

                /* 移动端抽屉打开时,强制恢复所有被折叠的元素 */
                .app[data-mobile-open="true"] .brand__text {
                    display: flex;
                }

                .app[data-mobile-open="true"] .nav__label {
                    display: block;
                }

                .app[data-mobile-open="true"] .nav__text {
                    display: inline;
                }

                .app[data-mobile-open="true"] .nav__badge {
                    display: inline-block;
                }

                .app[data-mobile-open="true"] .nav__chevron {
                    display: block;
                }

                .app[data-mobile-open="true"] .sidebar__foot-text {
                    display: flex;
                }

                .app[data-mobile-open="true"] .nav__sublist {
                    display: flex;
                }

                .app[data-mobile-open="true"] .sidebar__user .icon-btn {
                    display: grid;
                }

                .app[data-mobile-open="true"] .sidebar__brand {
                    justify-content: flex-start;
                    padding-inline: 18px;
                }

                .app[data-mobile-open="true"] .sidebar__nav {
                    padding-inline: 12px;
                    align-items: stretch;
                }

                .app[data-mobile-open="true"] .nav__item {
                    justify-content: flex-start;
                    gap: 12px;
                    padding: 10px 12px;
                }

                .app[data-mobile-open="true"] .sidebar__user {
                    justify-content: flex-start;
                    padding-inline: 8px;
                }

                .global-search {
                    display: none;
                }
            }

            /* 遮罩(移动端抽屉打开时) */
            @media (max-width: 720px) {
                .app[data-mobile-open="true"]::before {
                    content: "";
                    position: fixed;
                    inset: 0;
                    background: color-mix(in oklab, #070a10 55%, transparent);
                    backdrop-filter: blur(3px);
                    z-index: 90;
                    animation: fade-in var(--dur) var(--ease);
                }
            }

            /* ---------- 降低动效 ---------- */
            @media (prefers-reduced-motion: reduce) {

                *,
                *::before,
                *::after {
                    animation-duration: .01ms !important;
                    animation-iteration-count: 1 !important;
                    transition-duration: .01ms !important;
                    scroll-behavior: auto !important;
                }
            }

            /* ---------- 高对比度 ---------- */
            @media (prefers-contrast: more) {
                :root {
                    --border: light-dark(#8a94a6, #6b7688);
                    --border-strong: light-dark(#4b5563, #9aa5b8);
                }
            }

            /* ---------- 打印 ---------- */
            @media print {

                .sidebar,
                .topbar,
                .pagination,
                .table-toolbar,
                .bulkbar,
                .scroll-progress {
                    display: none !important;
                }

                .app {
                    grid-template-columns: 1fr;
                }

                .card {
                    box-shadow: none;
                    border-color: #ccc;
                }
            }
        }
    </style>
</head>

<body>

    <!-- ================================================================
     SVG 图标精灵
     ================================================================ -->
    <svg class="sprite" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
        <defs>
            <symbol id="i-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 2 3 7v10l9 5 9-5V7z" />
                <path d="m3 7 9 5 9-5" />
                <path d="M12 12v10" />
            </symbol>
            <symbol id="i-dashboard" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <rect x="3" y="3" width="7" height="9" rx="1.6" />
                <rect x="14" y="3" width="7" height="5" rx="1.6" />
                <rect x="14" y="12" width="7" height="9" rx="1.6" />
                <rect x="3" y="16" width="7" height="5" rx="1.6" />
            </symbol>
            <symbol id="i-chart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M3 3v18h18" />
                <path d="m7 15 3-4 3 3 5-7" />
            </symbol>
            <symbol id="i-users" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
                <circle cx="9" cy="7" r="4" />
                <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
                <path d="M16 3.13a4 4 0 0 1 0 7.75" />
            </symbol>
            <symbol id="i-box" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M21 8 12 3 3 8v8l9 5 9-5z" />
                <path d="m3 8 9 5 9-5" />
                <path d="M12 13v8" />
            </symbol>
            <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
                <path d="M14 2v6h6" />
                <path d="M9 15h6M9 18h4" />
            </symbol>
            <symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
                <path d="m9 12 2 2 4-4" />
            </symbol>
            <symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="3" />
                <path
                    d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
            </symbol>
            <symbol id="i-bell" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
                <path d="M13.7 21a2 2 0 0 1-3.4 0" />
            </symbol>
            <symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                stroke-linecap="round" stroke-linejoin="round">
                <circle cx="11" cy="11" r="7" />
                <path d="m21 21-4.3-4.3" />
            </symbol>
            <symbol id="i-panel" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <rect x="3" y="3" width="18" height="18" rx="2.5" />
                <path d="M9 3v18" />
            </symbol>
            <symbol id="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="4" />
                <path
                    d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
            </symbol>
            <symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
            </symbol>
            <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round">
                <path d="M12 5v14M5 12h14" />
            </symbol>
            <symbol id="i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
                <path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4z" />
            </symbol>
            <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M3 6h18" />
                <path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
                <path d="m19 6-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
                <path d="M10 11v6M14 11v6" />
            </symbol>
            <symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 3v12" />
                <path d="m7 10 5 5 5-5" />
                <path d="M5 21h14" />
            </symbol>
            <symbol id="i-columns" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <rect x="3" y="3" width="18" height="18" rx="2.5" />
                <path d="M9 3v18M15 3v18" />
            </symbol>
            <symbol id="i-refresh" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M21 12a9 9 0 1 1-2.64-6.36" />
                <path d="M21 3v6h-6" />
            </symbol>
            <symbol id="i-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
                stroke-linecap="round">
                <path d="M18 6 6 18M6 6l12 12" />
            </symbol>
            <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m5 13 4 4L19 7" />
            </symbol>
            <symbol id="i-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m15 18-6-6 6-6" />
            </symbol>
            <symbol id="i-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m9 18 6-6-6-6" />
            </symbol>
            <symbol id="i-chevrons-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m11 17-5-5 5-5M18 17l-5-5 5-5" />
            </symbol>
            <symbol id="i-chevrons-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m13 17 5-5-5-5M6 17l5-5-5-5" />
            </symbol>
            <symbol id="i-chevron-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m6 9 6 6 6-6" />
            </symbol>
            <symbol id="i-trend-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m3 17 6-6 4 4 8-8" />
                <path d="M15 7h6v6" />
            </symbol>
            <symbol id="i-trend-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m3 7 6 6 4-4 8 8" />
                <path d="M15 17h6v-6" />
            </symbol>
            <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="9" />
                <path d="M12 7v5l3 2" />
            </symbol>
            <symbol id="i-more" viewBox="0 0 24 24" fill="currentColor" stroke="none">
                <circle cx="5" cy="12" r="1.7" />
                <circle cx="12" cy="12" r="1.7" />
                <circle cx="19" cy="12" r="1.7" />
            </symbol>
            <symbol id="i-logout" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
                <path d="m16 17 5-5-5-5" />
                <path d="M21 12H9" />
            </symbol>
            <symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M12 9v4" />
                <path d="M12 17h.01" />
                <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
            </symbol>
            <symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"
                stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="9" />
                <path d="M12 16v-4M12 8h.01" />
            </symbol>
            <symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="M22 12h-6l-2 3h-4l-2-3H2" />
                <path
                    d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
            </symbol>
            <symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
                stroke-linecap="round" stroke-linejoin="round">
                <rect x="3" y="11" width="18" height="11" rx="2" />
                <path d="M7 11V7a5 5 0 0 1 10 0v4" />
            </symbol>
            <symbol id="i-sort" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m8 9 4-4 4 4" />
                <path d="m16 15-4 4-4-4" />
            </symbol>
            <symbol id="i-arrow-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m5 12 7-7 7 7M12 19V5" />
            </symbol>
            <symbol id="i-arrow-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
                stroke-linecap="round" stroke-linejoin="round">
                <path d="m19 12-7 7-7-7M12 5v14" />
            </symbol>
        </defs>
    </svg>

    <!-- 顶部滚动进度条 -->
    <div class="scroll-progress" aria-hidden="true"></div>

    <!-- ================================================================
     应用主体
     ================================================================ -->
    <div class="app" id="app" data-collapsed="false" data-mobile-open="false">

        <!-- ============ 侧边栏 ============ -->
        <aside class="sidebar" id="sidebar" aria-label="侧边导航">
            <div class="sidebar__brand">
                <span class="brand__logo"><svg class="icon">
                        <use href="#i-logo" />
                    </svg></span>
                <span class="brand__text">
                    <strong>Nova Admin</strong>
                    <small>企业管理平台 v3.0</small>
                </span>
            </div>

            <nav class="sidebar__nav" aria-label="主导航">
                <p class="nav__label">概览</p>
                <a class="nav__item" href="#" data-nav="dashboard">
                    <svg class="nav__icon">
                        <use href="#i-dashboard" />
                    </svg>
                    <span class="nav__text">仪表盘</span>
                </a>
                <a class="nav__item" href="#" data-nav="analytics">
                    <svg class="nav__icon">
                        <use href="#i-chart" />
                    </svg>
                    <span class="nav__text">数据分析</span>
                    <span class="nav__badge">NEW</span>
                </a>

                <p class="nav__label">业务管理</p>
                <a class="nav__item is-active" href="#" data-nav="users" aria-current="page">
                    <svg class="nav__icon">
                        <use href="#i-users" />
                    </svg>
                    <span class="nav__text">用户管理</span>
                    <span class="nav__badge" id="navUserBadge">137</span>
                </a>
                <a class="nav__item" href="#" data-nav="org">
                    <svg class="nav__icon">
                        <use href="#i-shield" />
                    </svg>
                    <span class="nav__text">组织与权限</span>
                </a>
                <a class="nav__item" href="#" data-nav="products">
                    <svg class="nav__icon">
                        <use href="#i-box" />
                    </svg>
                    <span class="nav__text">商品中心</span>
                </a>
                <a class="nav__item" href="#" data-nav="orders">
                    <svg class="nav__icon">
                        <use href="#i-file" />
                    </svg>
                    <span class="nav__text">订单管理</span>
                </a>

                <p class="nav__label">系统</p>
                <details class="nav__group" name="nav-acc">
                    <summary class="nav__item">
                        <svg class="nav__icon">
                            <use href="#i-settings" />
                        </svg>
                        <span class="nav__text">系统设置</span>
                        <svg class="nav__chevron">
                            <use href="#i-chevron-down" />
                        </svg>
                    </summary>
                    <ul class="nav__sublist">
                        <li><a class="nav__item" href="#" data-nav="sys-basic"><span class="nav__text">基础配置</span></a>
                        </li>
                        <li><a class="nav__item" href="#" data-nav="sys-log"><span class="nav__text">操作日志</span></a>
                        </li>
                        <li><a class="nav__item" href="#" data-nav="sys-api"><span class="nav__text">接口密钥</span></a>
                        </li>
                    </ul>
                </details>
                <a class="nav__item" href="#" data-nav="security">
                    <svg class="nav__icon">
                        <use href="#i-lock" />
                    </svg>
                    <span class="nav__text">安全中心</span>
                </a>
            </nav>

            <div class="sidebar__footer">
                <div class="sidebar__user">
                    <span class="avatar" style="--size:34px; --av-h:265">超</span>
                    <span class="sidebar__foot-text">
                        <strong>超级管理员</strong>
                        <small>admin@nova.com</small>
                    </span>
                    <button class="icon-btn" aria-label="退出登录" title="退出登录"
                        style="margin-inline-start:auto; width:30px;height:30px">
                        <svg class="icon">
                            <use href="#i-logout" />
                        </svg>
                    </button>
                </div>
            </div>
        </aside>

        <!-- ============ 主区域 ============ -->
        <div class="main">
            <header class="topbar">
                <button class="icon-btn" id="sidebarToggle" aria-label="收起/展开侧边栏" aria-expanded="true">
                    <svg class="icon">
                        <use href="#i-panel" />
                    </svg>
                </button>

                <div class="global-search">
                    <svg class="icon">
                        <use href="#i-search" />
                    </svg>
                    <input type="search" id="globalSearch" placeholder="全局搜索:用户、订单、文档…" aria-label="全局搜索"
                        autocomplete="off">
                    <kbd>⌘K</kbd>
                </div>

                <div class="topbar__spacer"></div>

                <div class="segmented" role="group" aria-label="主题">
                    <label><input type="radio" name="theme" value="light" checked><span><svg class="icon"
                                style="vertical-align:-3px">
                                <use href="#i-sun" />
                            </svg></span></label>
                    <label><input type="radio" name="theme" value="dark"><span><svg class="icon"
                                style="vertical-align:-3px">
                                <use href="#i-moon" />
                            </svg></span></label>
                    <label><input type="radio" name="theme" value="system"><span>自动</span></label>
                </div>

                <button class="icon-btn" id="refreshAll" aria-label="刷新数据" title="刷新数据">
                    <svg class="icon">
                        <use href="#i-refresh" />
                    </svg>
                </button>

                <button class="icon-btn" popovertarget="notifPop" aria-label="通知" title="通知">
                    <svg class="icon">
                        <use href="#i-bell" />
                    </svg>
                    <span class="icon-btn__badge">3</span>
                </button>

                <button class="icon-btn" id="fullscreenBtn" aria-label="全屏" title="全屏">
                    <svg class="icon">
                        <use href="#i-panel" />
                    </svg>
                </button>

                <span class="avatar avatar--ring" style="--size:34px; --av-h:280; cursor:pointer" title="超级管理员">超</span>
            </header>

            <!-- 通知 Popover -->
            <div id="notifPop" popover class="popover">
                <div class="popover__head">
                    <span>通知中心</span>
                    <button class="btn btn--ghost btn--sm" id="markAllRead">全部已读</button>
                </div>
                <div class="popover__body">
                    <div class="notif-item notif-unread">
                        <span class="notif-item__icon"><svg class="icon">
                                <use href="#i-users" />
                            </svg></span>
                        <div class="notif-item__body">
                            <p class="notif-item__title">新增 12 位注册用户</p>
                            <p class="notif-item__desc">来自「技术研发部」的批量导入任务已完成</p>
                            <p class="notif-item__time">3 分钟前</p>
                        </div>
                    </div>
                    <div class="notif-item notif-unread">
                        <span class="notif-item__icon" style="background:var(--warn-soft);color:var(--warn)"><svg
                                class="icon">
                                <use href="#i-alert" />
                            </svg></span>
                        <div class="notif-item__body">
                            <p class="notif-item__title">账号异常登录告警</p>
                            <p class="notif-item__desc">检测到异地登录行为,请及时核查</p>
                            <p class="notif-item__time">42 分钟前</p>
                        </div>
                    </div>
                    <div class="notif-item notif-unread">
                        <span class="notif-item__icon" style="background:var(--success-soft);color:var(--success)"><svg
                                class="icon">
                                <use href="#i-check" />
                            </svg></span>
                        <div class="notif-item__body">
                            <p class="notif-item__title">数据库备份成功</p>
                            <p class="notif-item__desc">自动备份任务已完成,耗时 2 分 18 秒</p>
                            <p class="notif-item__time">2 小时前</p>
                        </div>
                    </div>
                </div>
            </div>

            <main class="content" id="content">

                <!-- 页头 -->
                <div class="page-head">
                    <div>
                        <nav class="breadcrumb" aria-label="面包屑">
                            <a href="#">首页</a>
                            <span class="sep">/</span>
                            <a href="#">业务管理</a>
                            <span class="sep">/</span>
                            <span aria-current="page">用户管理</span>
                        </nav>
                        <h1 class="page-title">用户管理</h1>
                        <p class="page-desc">管理平台全部账号的创建、权限、状态与生命周期。</p>
                    </div>
                    <div class="page-actions">
                        <button class="btn btn--ghost" id="importBtn"><svg class="icon">
                                <use href="#i-download" />
                            </svg>导入</button>
                        <button class="btn btn--ghost" id="exportBtn2"><svg class="icon">
                                <use href="#i-download" />
                            </svg>导出</button>
                        <button class="btn btn--primary" id="addUserBtn2"><svg class="icon">
                                <use href="#i-plus" />
                            </svg>新建用户</button>
                    </div>
                </div>

                <!-- 统计卡 -->
                <section class="stats-grid" aria-label="数据概览">
                    <article class="stat" style="--tone: var(--accent)">
                        <div class="stat__top">
                            <span class="stat__label">总用户数</span>
                            <span class="stat__icon"><svg class="icon">
                                    <use href="#i-users" />
                                </svg></span>
                        </div>
                        <div class="stat__value" data-count="137">0</div>
                        <div class="stat__foot">
                            <span class="trend trend--up"><svg class="icon">
                                    <use href="#i-trend-up" />
                                </svg>12.4%</span>
                            <span>较上月</span>
                        </div>
                        <div class="stat__spark" aria-hidden="true">
                            <i style="--h:38"></i><i style="--h:52"></i><i style="--h:44"></i><i style="--h:66"></i>
                            <i style="--h:58"></i><i style="--h:80"></i><i style="--h:96"></i>
                        </div>
                    </article>

                    <article class="stat" style="--tone: var(--success)">
                        <div class="stat__top">
                            <span class="stat__label">活跃账号</span>
                            <span class="stat__icon"><svg class="icon">
                                    <use href="#i-check" />
                                </svg></span>
                        </div>
                        <div class="stat__value" data-count="98">0</div>
                        <div class="stat__foot">
                            <span class="trend trend--up"><svg class="icon">
                                    <use href="#i-trend-up" />
                                </svg>6.1%</span>
                            <span>活跃率 71.5%</span>
                        </div>
                        <div class="stat__spark" aria-hidden="true">
                            <i style="--h:60"></i><i style="--h:48"></i><i style="--h:70"></i><i style="--h:62"></i>
                            <i style="--h:84"></i><i style="--h:76"></i><i style="--h:90"></i>
                        </div>
                    </article>

                    <article class="stat" style="--tone: var(--warn)">
                        <div class="stat__top">
                            <span class="stat__label">待审核</span>
                            <span class="stat__icon"><svg class="icon">
                                    <use href="#i-clock" />
                                </svg></span>
                        </div>
                        <div class="stat__value" data-count="14">0</div>
                        <div class="stat__foot">
                            <span class="trend trend--down"><svg class="icon">
                                    <use href="#i-trend-down" />
                                </svg>3.2%</span>
                            <span>需 2 日内处理</span>
                        </div>
                        <div class="stat__spark" aria-hidden="true">
                            <i style="--h:88"></i><i style="--h:72"></i><i style="--h:64"></i><i style="--h:50"></i>
                            <i style="--h:44"></i><i style="--h:36"></i><i style="--h:28"></i>
                        </div>
                    </article>

                    <article class="stat" style="--tone: var(--danger)">
                        <div class="stat__top">
                            <span class="stat__label">异常锁定</span>
                            <span class="stat__icon"><svg class="icon">
                                    <use href="#i-lock" />
                                </svg></span>
                        </div>
                        <div class="stat__value" data-count="5">0</div>
                        <div class="stat__foot">
                            <span class="trend trend--up"><svg class="icon">
                                    <use href="#i-trend-up" />
                                </svg>1 例新增</span>
                            <span>安全策略已生效</span>
                        </div>
                        <div class="stat__spark" aria-hidden="true">
                            <i style="--h:20"></i><i style="--h:32"></i><i style="--h:26"></i><i style="--h:44"></i>
                            <i style="--h:38"></i><i style="--h:56"></i><i style="--h:62"></i>
                        </div>
                    </article>
                </section>

                <!-- 图表 -->
                <section class="card">
                    <div class="card__head">
                        <div>
                            <h2 class="card__title">近 12 个月新增用户趋势</h2>
                            <p class="card__subtitle">数据每 10 分钟自动同步 · 最后更新:刚刚</p>
                        </div>
                        <div class="toolbar__group ml-auto">
                            <div class="segmented">
                                <label><input type="radio" name="range" value="12m" checked><span>12 个月</span></label>
                                <label><input type="radio" name="range" value="6m"><span>6 个月</span></label>
                                <label><input type="radio" name="range" value="30d"><span>30 天</span></label>
                            </div>
                        </div>
                    </div>
                    <div class="card__body">
                        <div class="chart" role="img" aria-label="近 12 个月新增用户柱状图">
                            <div class="bar" style="--v:42" data-val="1月 · 42 人"><span class="bar__fill"></span><span
                                    class="bar__label">1月</span></div>
                            <div class="bar" style="--v:55" data-val="2月 · 55 人"><span class="bar__fill"></span><span
                                    class="bar__label">2月</span></div>
                            <div class="bar" style="--v:38" data-val="3月 · 38 人"><span class="bar__fill"></span><span
                                    class="bar__label">3月</span></div>
                            <div class="bar" style="--v:71" data-val="4月 · 71 人"><span class="bar__fill"></span><span
                                    class="bar__label">4月</span></div>
                            <div class="bar" style="--v:64" data-val="5月 · 64 人"><span class="bar__fill"></span><span
                                    class="bar__label">5月</span></div>
                            <div class="bar" style="--v:88" data-val="6月 · 88 人"><span class="bar__fill"></span><span
                                    class="bar__label">6月</span></div>
                            <div class="bar" style="--v:76" data-val="7月 · 76 人"><span class="bar__fill"></span><span
                                    class="bar__label">7月</span></div>
                            <div class="bar" style="--v:93" data-val="8月 · 93 人"><span class="bar__fill"></span><span
                                    class="bar__label">8月</span></div>
                            <div class="bar" style="--v:82" data-val="9月 · 82 人"><span class="bar__fill"></span><span
                                    class="bar__label">9月</span></div>
                            <div class="bar" style="--v:100" data-val="10月 · 100 人"><span class="bar__fill"></span><span
                                    class="bar__label">10月</span></div>
                            <div class="bar" style="--v:91" data-val="11月 · 91 人"><span class="bar__fill"></span><span
                                    class="bar__label">11月</span></div>
                            <div class="bar" style="--v:96" data-val="12月 · 96 人"><span class="bar__fill"></span><span
                                    class="bar__label">12月</span></div>
                        </div>
                    </div>
                </section>

                <!-- ============ 用户表格 ============ -->
                <section class="card table-card" aria-labelledby="tableTitle">
                    <h2 class="sr-only" id="tableTitle">用户列表</h2>

                    <div class="tabs" role="tablist" aria-label="用户分类">
                        <button class="tab" role="tab" aria-selected="true" data-tab="all">全部用户</button>
                        <button class="tab" role="tab" aria-selected="false" data-tab="active">正常账号</button>
                        <button class="tab" role="tab" aria-selected="false" data-tab="pending">待审核</button>
                        <button class="tab" role="tab" aria-selected="false" data-tab="inactive">已停用</button>
                        <button class="tab" role="tab" aria-selected="false" data-tab="locked">已锁定</button>
                    </div>

                    <div class="table-toolbar">
                        <div class="toolbar__group">
                            <div class="search-field">
                                <svg class="icon">
                                    <use href="#i-search" />
                                </svg>
                                <input type="search" id="searchInput" placeholder="搜索姓名 / 邮箱 / 手机号…" aria-label="搜索用户"
                                    autocomplete="off">
                                <span class="search-field__hint">/</span>
                            </div>

                            <select class="select select--sm" id="deptFilter" aria-label="按部门筛选"
                                style="width:auto;min-width:130px">
                                <option value="all">全部部门</option>
                            </select>
                            <select class="select select--sm" id="roleFilter" aria-label="按角色筛选"
                                style="width:auto;min-width:120px">
                                <option value="all">全部角色</option>
                            </select>
                            <select class="select select--sm" id="statusFilter" aria-label="按状态筛选"
                                style="width:auto;min-width:110px">
                                <option value="all">全部状态</option>
                                <option value="active">正常</option>
                                <option value="pending">待审核</option>
                                <option value="inactive">已停用</option>
                                <option value="locked">已锁定</option>
                            </select>
                            <button class="btn btn--ghost btn--sm" id="resetFilters"><svg class="icon">
                                    <use href="#i-refresh" />
                                </svg>重置</button>
                        </div>

                        <div class="toolbar__group">
                            <label class="switch" title="隔行变色">
                                <input type="checkbox" id="zebraToggle" checked>
                                <span class="switch__track"></span>
                                <span class="switch__label">斑马纹</span>
                            </label>
                            <label class="switch" title="紧凑模式">
                                <input type="checkbox" id="densityToggle">
                                <span class="switch__track"></span>
                                <span class="switch__label">紧凑</span>
                            </label>
                            <button class="btn btn--ghost btn--sm" popovertarget="colPop"><svg class="icon">
                                    <use href="#i-columns" />
                                </svg>列设置</button>
                            <button class="btn btn--ghost btn--sm" id="exportBtn"><svg class="icon">
                                    <use href="#i-download" />
                                </svg>导出 CSV</button>
                            <button class="btn btn--primary btn--sm" id="addUserBtn"><svg class="icon">
                                    <use href="#i-plus" />
                                </svg>新建用户</button>
                        </div>
                    </div>

                    <div id="colPop" popover class="popover" style="width:min(240px,calc(100vw - 32px))">
                        <div class="popover__head">
                            <span>显示列</span>
                            <button class="btn btn--ghost btn--sm" id="colReset">恢复默认</button>
                        </div>
                        <div class="popover__body">
                            <div class="col-list" id="colList">
                                <label><input type="checkbox" class="checkbox" data-col-toggle="id" checked> 用户
                                    ID</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="user" checked>
                                    用户信息</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="email" checked>
                                    邮箱</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="phone" checked>
                                    手机号</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="dept" checked>
                                    部门</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="role" checked>
                                    角色</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="status" checked>
                                    状态</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="lastLogin" checked>
                                    最后登录</label>
                                <label><input type="checkbox" class="checkbox" data-col-toggle="createdAt"> 创建时间</label>
                            </div>
                        </div>
                    </div>

                    <div class="bulkbar" id="bulkBar" hidden>
                        <span class="bulkbar__count">已选择 <strong id="bulkCount">0</strong> 项</span>
                        <div class="toolbar__group">
                            <button class="btn btn--ghost btn--sm" data-bulk="active"><svg class="icon">
                                    <use href="#i-check" />
                                </svg>批量启用</button>
                            <button class="btn btn--ghost btn--sm" data-bulk="inactive"><svg class="icon">
                                    <use href="#i-lock" />
                                </svg>批量停用</button>
                            <button class="btn btn--danger btn--sm" data-bulk="delete"><svg class="icon">
                                    <use href="#i-trash" />
                                </svg>批量删除</button>
                            <button class="btn btn--ghost btn--sm" data-bulk="clear"><svg class="icon">
                                    <use href="#i-close" />
                                </svg>取消选择</button>
                        </div>
                    </div>

                    <div class="table-wrap">
                        <table class="data-table" id="userTable" data-zebra="true" data-density="comfortable"
                            data-hide="">
                            <caption class="sr-only">用户列表,支持排序、筛选与批量操作</caption>
                            <thead>
                                <tr>
                                    <th data-col="check" style="width:46px">
                                        <input type="checkbox" class="checkbox" id="selectAll" aria-label="全选当前页">
                                    </th>
                                    <th data-col="id" class="th-sortable" data-sort="id" aria-sort="ascending"
                                        style="width:90px">
                                        <span class="th-inner">ID<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th data-col="user" class="th-sortable" data-sort="name" aria-sort="none"
                                        style="min-width:200px">
                                        <span class="th-inner">用户<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th data-col="email" class="th-sortable" data-sort="email" aria-sort="none"
                                        style="min-width:190px">
                                        <span class="th-inner">邮箱<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th data-col="phone" style="min-width:130px">手机号</th>
                                    <th data-col="dept" class="th-sortable" data-sort="dept" aria-sort="none"
                                        style="min-width:140px">
                                        <span class="th-inner">部门<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th data-col="role" class="th-sortable" data-sort="role" aria-sort="none"
                                        style="min-width:130px">
                                        <span class="th-inner">角色<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th data-col="status" class="th-sortable" data-sort="status" aria-sort="none"
                                        style="width:110px">
                                        <span class="th-inner">状态<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th data-col="lastLogin" class="th-sortable" data-sort="lastLogin" aria-sort="none"
                                        style="min-width:150px">
                                        <span class="th-inner">最后登录<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th data-col="createdAt" class="th-sortable" data-sort="createdAt" aria-sort="none"
                                        style="min-width:120px">
                                        <span class="th-inner">创建时间<svg class="th-sort-icon">
                                                <use href="#i-sort" />
                                            </svg></span>
                                    </th>
                                    <th style="width:110px; text-align:end">操作</th>
                                </tr>
                            </thead>
                            <tbody id="userTbody"></tbody>
                        </table>
                    </div>

                    <div class="pagination" id="pagination">
                        <div class="pagination__group">
                            <span>共 <strong id="totalCount">0</strong> 条记录</span>
                            <span class="divider--v"></span>
                            <span>第 <strong id="pageInfo">1</strong> / <strong id="pageTotal">1</strong> 页</span>
                            <span class="divider--v"></span>
                            <span id="pageRange" class="text-faint"></span>
                        </div>

                        <div class="pagination__group">
                            <label for="pageSize" class="text-muted">每页</label>
                            <select class="select select--sm" id="pageSize" style="width:auto">
                                <option value="5">5</option>
                                <option value="10" selected>10</option>
                                <option value="20">20</option>
                                <option value="50">50</option>
                                <option value="100">100</option>
                            </select>
                            <span class="text-muted">条</span>
                        </div>

                        <nav class="pager" id="pager" aria-label="分页导航"></nav>

                        <div class="pagination__group">
                            <span>跳至</span>
                            <input type="number" class="jump-input" id="jumpInput" min="1" value="1"
                                aria-label="跳转到指定页">
                            <span>页</span>
                            <button class="btn btn--ghost btn--sm" id="jumpBtn">GO</button>
                        </div>
                    </div>
                </section>

            </main>
        </div>
    </div>

    <!-- ============ 用户表单对话框 ============ -->
    <dialog id="userDialog" class="modal" aria-labelledby="userDialogTitle">
        <form id="userForm" method="dialog" novalidate>
            <header class="modal__head">
                <div>
                    <h2 id="userDialogTitle">新建用户</h2>
                    <p>填写账号基本信息,带 <span class="req">*</span> 为必填项。</p>
                </div>
                <button type="button" class="icon-btn" data-close-dialog aria-label="关闭">
                    <svg class="icon">
                        <use href="#i-close" />
                    </svg>
                </button>
            </header>

            <div class="modal__body">
                <input type="hidden" name="id">
                <div class="form-grid">
                    <div class="field">
                        <label for="f-name">姓名 <span class="req">*</span></label>
                        <input class="input" id="f-name" name="name" required minlength="2" maxlength="20"
                            placeholder="请输入真实姓名" autocomplete="off">
                        <p class="field__err"><svg class="icon" style="width:13px;height:13px">
                                <use href="#i-alert" />
                            </svg>请输入 2–20 个字符的姓名</p>
                    </div>

                    <div class="field">
                        <label for="f-username">用户名 <span class="req">*</span></label>
                        <input class="input" id="f-username" name="username" required
                            pattern="[a-zA-Z][a-zA-Z0-9_]{2,19}" placeholder="字母开头,3–20 位" autocomplete="off">
                        <p class="field__hint">仅支持字母、数字与下划线</p>
                        <p class="field__err"><svg class="icon" style="width:13px;height:13px">
                                <use href="#i-alert" />
                            </svg>格式不正确,需字母开头且 3–20 位</p>
                    </div>

                    <div class="field">
                        <label for="f-email">邮箱 <span class="req">*</span></label>
                        <input class="input" id="f-email" name="email" type="email" required placeholder="name@nova.com"
                            autocomplete="off">
                        <p class="field__err"><svg class="icon" style="width:13px;height:13px">
                                <use href="#i-alert" />
                            </svg>请输入有效的邮箱地址</p>
                    </div>

                    <div class="field">
                        <label for="f-phone">手机号</label>
                        <input class="input" id="f-phone" name="phone" pattern="1[3-9]\d{9}" placeholder="11 位手机号"
                            inputmode="numeric" autocomplete="off">
                        <p class="field__err"><svg class="icon" style="width:13px;height:13px">
                                <use href="#i-alert" />
                            </svg>请输入有效的 11 位手机号</p>
                    </div>

                    <div class="field">
                        <label for="f-dept">所属部门 <span class="req">*</span></label>
                        <select class="select" id="f-dept" name="dept" required></select>
                    </div>

                    <div class="field">
                        <label for="f-role">角色 <span class="req">*</span></label>
                        <select class="select" id="f-role" name="role" required></select>
                    </div>

                    <div class="field field--full">
                        <fieldset style="border:0;padding:0;display:flex;flex-direction:column;gap:6px">
                            <legend>账号状态</legend>
                            <div class="segmented" style="align-self:flex-start">
                                <label><input type="radio" name="status" value="active" checked><span>正常</span></label>
                                <label><input type="radio" name="status" value="pending"><span>待审核</span></label>
                                <label><input type="radio" name="status" value="inactive"><span>停用</span></label>
                                <label><input type="radio" name="status" value="locked"><span>锁定</span></label>
                            </div>
                        </fieldset>
                    </div>

                    <div class="field field--full">
                        <label for="f-note">备注</label>
                        <textarea class="textarea" id="f-note" name="note" maxlength="200"
                            placeholder="可选,最多 200 字"></textarea>
                    </div>
                </div>
            </div>

            <footer class="modal__foot">
                <button type="button" class="btn btn--ghost" data-close-dialog>取消</button>
                <button type="submit" class="btn btn--primary">
                    <svg class="icon">
                        <use href="#i-check" />
                    </svg>
                    <span id="submitLabel">创建用户</span>
                </button>
            </footer>
        </form>
    </dialog>

    <!-- 确认对话框 -->
    <dialog id="confirmDialog" class="modal modal--sm" aria-labelledby="confirmTitle">
        <div class="modal__head">
            <span class="modal__icon"><svg class="icon">
                    <use href="#i-alert" />
                </svg></span>
            <div>
                <h2 id="confirmTitle">确认操作</h2>
                <p id="confirmDesc">此操作不可撤销。</p>
            </div>
        </div>
        <footer class="modal__foot">
            <button class="btn btn--ghost" id="confirmCancel">取消</button>
            <button class="btn btn--danger" id="confirmOk">确认删除</button>
        </footer>
    </dialog>

    <!-- 详情对话框 -->
    <dialog id="detailDialog" class="modal" aria-labelledby="detailTitle">
        <header class="modal__head">
            <div>
                <h2 id="detailTitle">用户详情</h2>
                <p>查看该账号的完整档案与活动信息。</p>
            </div>
            <button type="button" class="icon-btn" data-close-dialog aria-label="关闭">
                <svg class="icon">
                    <use href="#i-close" />
                </svg>
            </button>
        </header>
        <div class="modal__body" id="detailBody"></div>
        <footer class="modal__foot">
            <button class="btn btn--ghost" data-close-dialog>关闭</button>
            <button class="btn btn--primary" id="detailEditBtn"><svg class="icon">
                    <use href="#i-edit" />
                </svg>编辑</button>
        </footer>
    </dialog>

    <!-- Toast 容器 -->
    <div class="toast-stack" id="toastStack" role="region" aria-live="polite" aria-label="系统通知"></div>


    <script>
        /* =====================================================================
           Nova Admin · 应用逻辑
           ===================================================================== */
        (() => {
            'use strict';

            /* ============ 0. 常量与工具 ============ */
            const DEPTS = ['技术研发部', '产品设计部', '市场营销部', '人力资源部', '财务会计部', '客户成功部', '运维保障部', '法务合规部'];
            const ROLES = ['超级管理员', '系统管理员', '部门主管', '普通用户', '只读访客'];
            const STATUSES = ['active', 'inactive', 'pending', 'locked'];

            const STATUS_META = {
                active: { label: '正常', tone: 'success' },
                inactive: { label: '已停用', tone: 'muted' },
                pending: { label: '待审核', tone: 'warn' },
                locked: { label: '已锁定', tone: 'danger' },
            };

            const SURNAMES = '王李张刘陈杨赵黄周吴徐孙马朱胡郭何高林罗郑梁谢宋唐许韩冯邓曹彭曾肖田董袁潘于蒋蔡余杜叶程苏魏吕丁任沈姚卢姜崔钟谭陆汪范金石廖贾夏韦付方白邹孟熊秦邱江尹薛闫段雷侯龙史陶黎贺顾毛郝龚邵万钱严覃武戴莫孔向汤'.split('');
            const GIVEN = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '娟', '涛', '明', '超', '秀英', '霞', '平', '刚', '桂英', '建国', '丹', '雪', '晨', '浩', '宇', '欣', '悦', '一鸣', '思远', '子涵', '雨萱', '梓豪', '嘉怡'];

            const $ = (sel, root = document) => root.querySelector(sel);
            const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];

            const escapeHtml = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => (
                { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
            ));

            function mulberry32(seed) {
                let a = seed;
                return function () {
                    a |= 0; a = (a + 0x6D2B79F5) | 0;
                    let t = Math.imul(a ^ (a >>> 15), 1 | a);
                    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
                    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
                };
            }

            const debounce = (fn, wait = 260) => {
                let t;
                return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
            };

            const DTF_DATE = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
            const DTF_FULL = new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
            const RTF = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' });
            const NUM = new Intl.NumberFormat('zh-CN');

            const fmtDate = (ts) => DTF_DATE.format(new Date(ts));
            const fmtFull = (ts) => DTF_FULL.format(new Date(ts));

            function fmtRelative(ts) {
                const diff = Date.now() - ts;
                const mins = Math.round(diff / 60000);
                if (Math.abs(mins) < 60) return RTF.format(-mins, 'minute');
                const hours = Math.round(mins / 60);
                if (Math.abs(hours) < 24) return RTF.format(-hours, 'hour');
                const days = Math.round(hours / 24);
                if (Math.abs(days) < 30) return RTF.format(-days, 'day');
                return fmtDate(ts);
            }

            /* ============ 1. 数据生成 ============ */
            function generateUsers(count = 137) {
                const rnd = mulberry32(20261016);
                const now = Date.now();
                const list = [];

                for (let i = 1; i <= count; i++) {
                    const name = SURNAMES[Math.floor(rnd() * SURNAMES.length)] + GIVEN[Math.floor(rnd() * GIVEN.length)];
                    const dept = DEPTS[Math.floor(rnd() * DEPTS.length)];
                    const role = ROLES[Math.floor(rnd() * ROLES.length)];
                    const status = STATUSES[Math.floor(rnd() * STATUSES.length)];
                    const createdAt = now - Math.floor(rnd() * 1080 + 20) * 86400000;
                    const lastLogin = createdAt + Math.floor(rnd() * (now - createdAt) * 0.92);
                    const phoneTail = String(Math.floor(rnd() * 1e8)).padStart(8, '0');
                    const username = `user${String(i).padStart(4, '0')}`;

                    list.push({
                        id: i,
                        name,
                        username,
                        email: `${username}@nova.com`,
                        phone: `1${[3, 5, 6, 7, 8, 9][Math.floor(rnd() * 6)]}${phoneTail.slice(0, 1)}${String(Math.floor(rnd() * 1e8)).padStart(8, '0')}`,
                        dept,
                        role,
                        status,
                        lastLogin,
                        createdAt,
                        hue: Math.floor(rnd() * 360),
                    });
                }
                return list;
            }

            /* ============ 2. 状态 ============ */
            const state = {
                users: generateUsers(),
                page: 1,
                pageSize: 10,
                sortKey: 'id',
                sortDir: 'asc',
                search: '',
                dept: 'all',
                role: 'all',
                status: 'all',
                tab: 'all',
                selected: new Set(),
                hideCols: [],
                editingId: null,
                pendingDelete: null,
            };

            let filteredCache = [];

            /* ============ 3. 过滤 & 排序 ============ */
            function computeFiltered() {
                const q = state.search.trim().toLowerCase();

                let rows = state.users.filter((u) => {
                    if (state.status !== 'all' && u.status !== state.status) return false;
                    if (state.dept !== 'all' && u.dept !== state.dept) return false;
                    if (state.role !== 'all' && u.role !== state.role) return false;
                    if (q) {
                        const hay = `${u.name} ${u.username} ${u.email} ${u.phone} ${u.dept} ${u.role} ${u.id}`.toLowerCase();
                        if (!hay.includes(q)) return false;
                    }
                    return true;
                });

                const dir = state.sortDir === 'asc' ? 1 : -1;
                const key = state.sortKey;

                rows.sort((a, b) => {
                    const va = a[key], vb = b[key];
                    if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
                    return String(va).localeCompare(String(vb), 'zh-CN', { numeric: true }) * dir;
                });

                filteredCache = rows;
                return rows;
            }

            /* ============ 4. 渲染 —— 表格 ============ */
            const tbody = $('#userTbody');
            const tableEl = $('#userTable');

            function renderRows() {
                const rows = filteredCache;
                const total = rows.length;
                const maxPage = Math.max(1, Math.ceil(total / state.pageSize));
                if (state.page > maxPage) state.page = maxPage;

                const start = (state.page - 1) * state.pageSize;
                const pageRows = rows.slice(start, start + state.pageSize);

                if (!pageRows.length) {
                    tbody.innerHTML = `
        <tr>
          <td colspan="11">
            <div class="table-empty">
              <span class="table-empty__art"><svg class="icon"><use href="#i-inbox"/></svg></span>
              <p class="fw-600">没有找到匹配的用户</p>
              <p class="text-sm text-muted">试试调整筛选条件或搜索关键词</p>
              <button class="btn btn--ghost btn--sm" id="emptyReset">清除全部筛选</button>
            </div>
          </td>
        </tr>`;
                    const btn = $('#emptyReset');
                    if (btn) btn.addEventListener('click', resetFilters);
                    syncSelectAll([]);
                    renderPagination(total);
                    updateBulkBar();
                    return;
                }

                tbody.innerHTML = pageRows.map((u) => {
                    const meta = STATUS_META[u.status];
                    const checked = state.selected.has(u.id);
                    return `
      <tr data-id="${u.id}" class="${checked ? 'is-selected' : ''}">
        <td data-col="check">
          <input type="checkbox" class="checkbox row-check" data-id="${u.id}"
                 ${checked ? 'checked' : ''} aria-label="选择用户 ${escapeHtml(u.name)}">
        </td>
        <td data-col="id"><span class="mono text-muted">#${String(u.id).padStart(4, '0')}</span></td>
        <td data-col="user">
          <div class="user-cell">
            <span class="avatar" style="--size:34px; --av-h:${u.hue}">${escapeHtml(u.name[0])}</span>
            <div class="user-cell__meta">
              <span class="user-cell__name truncate">${escapeHtml(u.name)}</span>
              <span class="user-cell__sub truncate">@${escapeHtml(u.username)}</span>
            </div>
          </div>
        </td>
        <td data-col="email"><span class="truncate" style="display:block;max-width:220px">${escapeHtml(u.email)}</span></td>
        <td data-col="phone"><span class="mono text-muted">${escapeHtml(u.phone)}</span></td>
        <td data-col="dept"><span class="chip">${escapeHtml(u.dept)}</span></td>
        <td data-col="role">${escapeHtml(u.role)}</td>
        <td data-col="status">
          <span class="badge badge--${meta.tone}">${meta.label}</span>
        </td>
        <td data-col="lastLogin"><span title="${fmtFull(u.lastLogin)}">${fmtRelative(u.lastLogin)}</span></td>
        <td data-col="createdAt"><span class="text-muted">${fmtDate(u.createdAt)}</span></td>
        <td style="text-align:end">
          <div class="cell-actions">
            <button class="icon-btn" data-action="detail" data-id="${u.id}" title="查看详情" aria-label="查看详情">
              <svg class="icon"><use href="#i-search"/></svg>
            </button>
            <button class="icon-btn" data-action="edit" data-id="${u.id}" title="编辑" aria-label="编辑">
              <svg class="icon"><use href="#i-edit"/></svg>
            </button>
            <button class="icon-btn" data-action="delete" data-id="${u.id}" title="删除" aria-label="删除" style="color:var(--danger)">
              <svg class="icon"><use href="#i-trash"/></svg>
            </button>
          </div>
        </td>
      </tr>`;
                }).join('');

                syncSelectAll(pageRows);
                renderPagination(total);
                updateBulkBar();
            }

            function syncSelectAll(pageRows) {
                const all = $('#selectAll');
                if (!all) return;
                const selectable = pageRows.filter((u) => state.selected.has(u.id)).length;
                all.checked = pageRows.length > 0 && selectable === pageRows.length;
                all.indeterminate = selectable > 0 && selectable < pageRows.length;
            }

            /* ============ 5. 渲染 —— 分页 ============ */
            function buildPageList(current, total) {
                const out = [];
                if (total <= 7) { for (let i = 1; i <= total; i++) out.push(i); return out; }

                out.push(1);
                let start = Math.max(2, current - 1);
                let end = Math.min(total - 1, current + 1);
                if (current <= 3) { start = 2; end = 4; }
                if (current >= total - 2) { start = total - 3; end = total - 1; }

                if (start > 2) out.push('…');
                for (let i = start; i <= end; i++) out.push(i);
                if (end < total - 1) out.push('…');
                out.push(total);
                return out;
            }

            function renderPagination(total) {
                const maxPage = Math.max(1, Math.ceil(total / state.pageSize));
                const pager = $('#pager');
                const pages = buildPageList(state.page, maxPage);

                const rangeStart = total === 0 ? 0 : (state.page - 1) * state.pageSize + 1;
                const rangeEnd = Math.min(state.page * state.pageSize, total);

                pager.innerHTML = `
      <button class="page-btn" data-page="first" ${state.page === 1 ? 'disabled' : ''} title="首页" aria-label="首页">
        <svg class="icon"><use href="#i-chevrons-left"/></svg>
      </button>
      <button class="page-btn" data-page="prev" ${state.page === 1 ? 'disabled' : ''} title="上一页" aria-label="上一页">
        <svg class="icon"><use href="#i-chevron-left"/></svg>
      </button>
      ${pages.map((p) => p === '…'
                    ? `<span class="page-ellipsis">…</span>`
                    : `<button class="page-btn ${p === state.page ? 'is-current' : ''}" data-page="${p}"
             ${p === state.page ? 'aria-current="page"' : ''}>${p}</button>`
                ).join('')}
      <button class="page-btn" data-page="next" ${state.page === maxPage ? 'disabled' : ''} title="下一页" aria-label="下一页">
        <svg class="icon"><use href="#i-chevron-right"/></svg>
      </button>
      <button class="page-btn" data-page="last" ${state.page === maxPage ? 'disabled' : ''} title="尾页" aria-label="尾页">
        <svg class="icon"><use href="#i-chevrons-right"/></svg>
      </button>
    `;

                $('#totalCount').textContent = NUM.format(total);
                $('#pageInfo').textContent = state.page;
                $('#pageTotal').textContent = maxPage;
                $('#pageRange').textContent = total ? `显示 ${rangeStart}–${rangeEnd}` : '无数据';

                const jump = $('#jumpInput');
                jump.max = maxPage;
                jump.value = state.page;

                const badge = $('#navUserBadge');
                if (badge) badge.textContent = state.users.length;
            }

            /* ============ 6. 全量刷新 ============ */
            function refresh({ resetPage = false } = {}) {
                if (resetPage) state.page = 1;
                computeFiltered();
                renderRows();
            }

            /* ============ 7. 选择 ============ */
            function updateBulkBar() {
                const bar = $('#bulkBar');
                const count = state.selected.size;
                $('#bulkCount').textContent = count;
                bar.hidden = count === 0;
            }

            function getPageRows() {
                const start = (state.page - 1) * state.pageSize;
                return filteredCache.slice(start, start + state.pageSize);
            }

            /* ============ 8. Toast ============ */
            const ICONS = { success: '#i-check', error: '#i-alert', info: '#i-info', warn: '#i-alert' };

            function toast(title, desc = '', type = 'success', duration = 3600) {
                const stack = $('#toastStack');
                const el = document.createElement('div');
                el.className = `toast toast--${type}`;
                el.innerHTML = `
      <span class="toast__icon"><svg class="icon"><use href="${ICONS[type] || '#i-info'}"/></svg></span>
      <div class="toast__content">
        <p class="toast__title">${escapeHtml(title)}</p>
        ${desc ? `<p class="toast__desc">${escapeHtml(desc)}</p>` : ''}
      </div>
      <button class="toast__close" aria-label="关闭">
        <svg class="icon" style="width:15px;height:15px"><use href="#i-close"/></svg>
      </button>
      <span class="toast__bar" style="animation-duration:${duration}ms"></span>
    `;
                stack.appendChild(el);

                const remove = () => {
                    el.classList.add('is-leaving');
                    el.addEventListener('animationend', () => el.remove(), { once: true });
                };
                const timer = setTimeout(remove, duration);
                el.querySelector('.toast__close').addEventListener('click', () => { clearTimeout(timer); remove(); });
            }

            /* ============ 9. 对话框 ============ */
            const userDialog = $('#userDialog');
            const confirmDialog = $('#confirmDialog');
            const detailDialog = $('#detailDialog');

            function openUserDialog(user = null) {
                const form = $('#userForm');
                form.reset();
                state.editingId = user ? user.id : null;

                $('#userDialogTitle').textContent = user ? '编辑用户' : '新建用户';
                $('#submitLabel').textContent = user ? '保存修改' : '创建用户';

                if (user) {
                    form.id.value = user.id;
                    form.name.value = user.name;
                    form.username.value = user.username;
                    form.email.value = user.email;
                    form.phone.value = user.phone;
                    form.dept.value = user.dept;
                    form.role.value = user.role;
                    const radio = form.querySelector(`input[name="status"][value="${user.status}"]`);
                    if (radio) radio.checked = true;
                } else {
                    form.id.value = '';
                    form.dept.selectedIndex = 0;
                    form.role.selectedIndex = 0;
                }

                userDialog.showModal();
            }

            function confirmAction({ title = '确认操作', desc = '此操作不可撤销。', okText = '确认', danger = true } = {}) {
                return new Promise((resolve) => {
                    $('#confirmTitle').textContent = title;
                    $('#confirmDesc').textContent = desc;
                    const ok = $('#confirmOk');
                    ok.textContent = okText;
                    ok.className = `btn ${danger ? 'btn--danger' : 'btn--primary'}`;

                    const cleanup = () => {
                        ok.removeEventListener('click', onOk);
                        $('#confirmCancel').removeEventListener('click', onCancel);
                        confirmDialog.removeEventListener('close', onCancel);
                    };
                    const onOk = () => { cleanup(); confirmDialog.close(); resolve(true); };
                    const onCancel = () => { cleanup(); if (confirmDialog.open) confirmDialog.close(); resolve(false); };

                    ok.addEventListener('click', onOk);
                    $('#confirmCancel').addEventListener('click', onCancel);
                    confirmDialog.addEventListener('close', onCancel);

                    confirmDialog.showModal();
                });
            }

            function openDetail(user) {
                const meta = STATUS_META[user.status];
                $('#detailBody').innerHTML = `
      <div class="flex items-center gap-4" style="margin-bottom:20px">
        <span class="avatar" style="--size:64px;--av-h:${user.hue};font-size:1.5rem">${escapeHtml(user.name[0])}</span>
        <div>
          <h3 style="font-size:1.15rem">${escapeHtml(user.name)}</h3>
          <p class="text-muted text-sm">@${escapeHtml(user.username)} · ${escapeHtml(user.role)}</p>
          <span class="badge badge--${meta.tone}" style="margin-top:6px">${meta.label}</span>
        </div>
      </div>
      <div class="form-grid">
        <div class="field"><label>用户 ID</label><p class="mono">#${String(user.id).padStart(4, '0')}</p></div>
        <div class="field"><label>所属部门</label><p>${escapeHtml(user.dept)}</p></div>
        <div class="field"><label>邮箱</label><p class="truncate">${escapeHtml(user.email)}</p></div>
        <div class="field"><label>手机号</label><p class="mono">${escapeHtml(user.phone)}</p></div>
        <div class="field"><label>创建时间</label><p>${fmtFull(user.createdAt)}</p></div>
        <div class="field"><label>最后登录</label><p>${fmtFull(user.lastLogin)}</p></div>
      </div>
    `;

                $('#detailEditBtn').onclick = () => {
                    detailDialog.close();
                    openUserDialog(user);
                };
                detailDialog.showModal();
            }

            document.addEventListener('click', (e) => {
                const btn = e.target.closest('[data-close-dialog]');
                if (btn) btn.closest('dialog')?.close();
            });

            /* ============ 10. CRUD ============ */
            function createUser(data) {
                const id = Math.max(0, ...state.users.map((u) => u.id)) + 1;
                const now = Date.now();
                state.users.unshift({
                    id,
                    name: data.name,
                    username: data.username,
                    email: data.email,
                    phone: data.phone || '—',
                    dept: data.dept,
                    role: data.role,
                    status: data.status,
                    createdAt: now,
                    lastLogin: now,
                    hue: Math.floor(Math.random() * 360),
                });
            }

            function updateUser(id, data) {
                const u = state.users.find((x) => x.id === id);
                if (!u) return;
                Object.assign(u, {
                    name: data.name,
                    username: data.username,
                    email: data.email,
                    phone: data.phone || u.phone,
                    dept: data.dept,
                    role: data.role,
                    status: data.status,
                });
            }

            function deleteUsers(ids) {
                const set = new Set(ids);
                state.users = state.users.filter((u) => !set.has(u.id));
                ids.forEach((id) => state.selected.delete(id));
            }

            /* ============ 11. 事件绑定 ============ */
            const app = $('#app');

            /* 侧边栏折叠 */
            $('#sidebarToggle').addEventListener('click', () => {
                if (window.innerWidth <= 720) {
                    app.dataset.mobileOpen = app.dataset.mobileOpen === 'true' ? 'false' : 'true';
                } else {
                    const next = app.dataset.collapsed !== 'true';
                    app.dataset.collapsed = String(next);
                    $('#sidebarToggle').setAttribute('aria-expanded', String(!next));
                }
            });

            /* 搜索防抖 */
            const onSearch = debounce((value) => {
                state.search = value;
                refresh({ resetPage: true });
            }, 280);
            $('#searchInput').addEventListener('input', (e) => onSearch(e.target.value));

            /* 筛选器 */
            $('#deptFilter').addEventListener('change', (e) => {
                state.dept = e.target.value; refresh({ resetPage: true });
            });
            $('#roleFilter').addEventListener('change', (e) => {
                state.role = e.target.value; refresh({ resetPage: true });
            });
            $('#statusFilter').addEventListener('change', (e) => {
                state.status = e.target.value;
                state.tab = e.target.value === 'all' ? 'all' : e.target.value;
                syncTabs();
                refresh({ resetPage: true });
            });

            function resetFilters() {
                state.search = '';
                state.dept = 'all';
                state.role = 'all';
                state.status = 'all';
                state.tab = 'all';
                $('#searchInput').value = '';
                $('#deptFilter').value = 'all';
                $('#roleFilter').value = 'all';
                $('#statusFilter').value = 'all';
                syncTabs();
                refresh({ resetPage: true });
                toast('筛选已重置', '已恢复全部用户视图', 'info', 2200);
            }
            $('#resetFilters').addEventListener('click', resetFilters);

            /* Tab 切换 */
            function syncTabs() {
                $$('.tab').forEach((t) => t.setAttribute('aria-selected', String(t.dataset.tab === state.tab)));
            }
            $$('.tab').forEach((tab) => {
                tab.addEventListener('click', () => {
                    state.tab = tab.dataset.tab;
                    state.status = tab.dataset.tab === 'all' ? 'all' : tab.dataset.tab;
                    $('#statusFilter').value = state.status;
                    syncTabs();
                    refresh({ resetPage: true });
                });
            });

            /* 排序 */
            $$('.th-sortable').forEach((th) => {
                th.addEventListener('click', () => {
                    const key = th.dataset.sort;
                    if (state.sortKey === key) {
                        state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc';
                    } else {
                        state.sortKey = key;
                        state.sortDir = 'asc';
                    }
                    $$('.th-sortable').forEach((x) => {
                        x.setAttribute('aria-sort', x === th ? (state.sortDir === 'asc' ? 'ascending' : 'descending') : 'none');
                    });
                    refresh();
                });
            });

            /* 全选 / 单选 */
            $('#selectAll').addEventListener('change', (e) => {
                const rows = getPageRows();
                if (e.target.checked) rows.forEach((u) => state.selected.add(u.id));
                else rows.forEach((u) => state.selected.delete(u.id));
                renderRows();
            });

            tbody.addEventListener('change', (e) => {
                const cb = e.target.closest('.row-check');
                if (!cb) return;
                const id = Number(cb.dataset.id);
                if (cb.checked) state.selected.add(id); else state.selected.delete(id);
                cb.closest('tr').classList.toggle('is-selected', cb.checked);
                syncSelectAll(getPageRows());
                updateBulkBar();
            });

            /* 行操作 */
            tbody.addEventListener('click', async (e) => {
                const btn = e.target.closest('[data-action]');
                if (!btn) return;
                const id = Number(btn.dataset.id);
                const user = state.users.find((u) => u.id === id);
                if (!user) return;

                switch (btn.dataset.action) {
                    case 'edit': openUserDialog(user); break;
                    case 'detail': openDetail(user); break;
                    case 'delete': {
                        const ok = await confirmAction({
                            title: '删除用户',
                            desc: `确定要删除「${user.name}」吗?该操作不可恢复。`,
                            okText: '删除',
                        });
                        if (ok) {
                            deleteUsers([id]);
                            refresh();
                            toast('删除成功', `用户「${user.name}」已从系统中移除`, 'success');
                        }
                        break;
                    }
                }
            });

            /* 批量操作 */
            $('#bulkBar').addEventListener('click', async (e) => {
                const btn = e.target.closest('[data-bulk]');
                if (!btn) return;
                const ids = [...state.selected];

                switch (btn.dataset.bulk) {
                    case 'active':
                    case 'inactive':
                    case 'locked': {
                        state.users.forEach((u) => { if (state.selected.has(u.id)) u.status = btn.dataset.bulk; });
                        refresh();
                        toast('批量更新成功', `已将 ${ids.length} 个账号状态更新为「${STATUS_META[btn.dataset.bulk].label}」`, 'success');
                        break;
                    }
                    case 'delete': {
                        const ok = await confirmAction({
                            title: '批量删除',
                            desc: `即将删除 ${ids.length} 个用户账号,删除后无法恢复。`,
                            okText: `删除 ${ids.length} 项`,
                        });
                        if (ok) {
                            deleteUsers(ids);
                            state.selected.clear();
                            refresh();
                            toast('批量删除完成', `已删除 ${ids.length} 个用户`, 'success');
                        }
                        break;
                    }
                    case 'clear':
                        state.selected.clear();
                        renderRows();
                        break;
                }
            });

            /* 分页 */
            $('#pager').addEventListener('click', (e) => {
                const btn = e.target.closest('.page-btn');
                if (!btn || btn.disabled) return;
                const maxPage = Math.max(1, Math.ceil(filteredCache.length / state.pageSize));
                const p = btn.dataset.page;

                if (p === 'first') state.page = 1;
                else if (p === 'prev') state.page = Math.max(1, state.page - 1);
                else if (p === 'next') state.page = Math.min(maxPage, state.page + 1);
                else if (p === 'last') state.page = maxPage;
                else state.page = Number(p);

                renderRows();
                $('#tableTitle')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            });

            $('#pageSize').addEventListener('change', (e) => {
                state.pageSize = Number(e.target.value);
                state.page = 1;
                refresh();
                toast('每页条数已更新', `当前每页显示 ${state.pageSize} 条`, 'info', 2000);
            });

            $('#jumpBtn').addEventListener('click', jumpToPage);
            $('#jumpInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') jumpToPage(); });

            function jumpToPage() {
                const maxPage = Math.max(1, Math.ceil(filteredCache.length / state.pageSize));
                let v = Number($('#jumpInput').value);
                if (!Number.isFinite(v)) v = 1;
                state.page = Math.min(maxPage, Math.max(1, Math.round(v)));
                renderRows();
            }

            /* 视图设置 */
            $('#zebraToggle').addEventListener('change', (e) => {
                tableEl.dataset.zebra = String(e.target.checked);
            });
            $('#densityToggle').addEventListener('change', (e) => {
                tableEl.dataset.density = e.target.checked ? 'compact' : 'comfortable';
            });

            /* 列显隐 */
            function syncColumnVisibility() {
                tableEl.dataset.hide = state.hideCols.join(' ');
            }
            $('#colList').addEventListener('change', (e) => {
                const cb = e.target.closest('[data-col-toggle]');
                if (!cb) return;
                const col = cb.dataset.colToggle;
                if (cb.checked) state.hideCols = state.hideCols.filter((c) => c !== col);
                else if (!state.hideCols.includes(col)) state.hideCols.push(col);
                syncColumnVisibility();
            });
            $('#colReset').addEventListener('click', () => {
                state.hideCols = [];
                syncColumnVisibility();
                $$('[data-col-toggle]').forEach((cb) => { cb.checked = cb.dataset.colToggle !== 'createdAt'; });
                toast('列设置已恢复默认', '', 'info', 2000);
            });

            /* 表单提交 */
            const userForm = $('#userForm');
            userForm.addEventListener('submit', (e) => {
                e.preventDefault();

                if (!userForm.checkValidity()) {
                    userForm.reportValidity();
                    const firstInvalid = userForm.querySelector(':invalid');
                    firstInvalid?.focus();
                    toast('表单校验未通过', '请检查标红的输入项', 'error', 3000);
                    return;
                }

                const fd = new FormData(userForm);
                const data = Object.fromEntries(fd.entries());
                data.status = userForm.querySelector('input[name="status"]:checked')?.value || 'active';

                if (state.editingId != null) {
                    updateUser(state.editingId, data);
                    toast('保存成功', `用户「${data.name}」信息已更新`, 'success');
                } else {
                    createUser(data);
                    toast('创建成功', `用户「${data.name}」已加入系统`, 'success');
                }

                userDialog.close();
                refresh();
            });

            $('#addUserBtn').addEventListener('click', () => openUserDialog());
            $('#addUserBtn2').addEventListener('click', () => openUserDialog());

            /* 导出 CSV */
            function exportCSV() {
                const rows = filteredCache;
                if (!rows.length) { toast('没有可导出的数据', '', 'warn'); return; }

                const headers = ['ID', '姓名', '用户名', '邮箱', '手机号', '部门', '角色', '状态', '最后登录', '创建时间'];
                const lines = [headers.join(',')];

                rows.forEach((u) => {
                    lines.push([
                        u.id,
                        `"${u.name}"`,
                        `"${u.username}"`,
                        `"${u.email}"`,
                        `"${u.phone}"`,
                        `"${u.dept}"`,
                        `"${u.role}"`,
                        `"${STATUS_META[u.status].label}"`,
                        `"${fmtFull(u.lastLogin)}"`,
                        `"${fmtFull(u.createdAt)}"`,
                    ].join(','));
                });

                const blob = new Blob(['\uFEFF' + lines.join('\n')], { type: 'text/csv;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `用户数据_${fmtDate(Date.now()).replace(/\//g, '-')}.csv`;
                a.click();
                URL.revokeObjectURL(url);

                toast('导出成功', `已导出 ${rows.length} 条记录`, 'success');
            }
            $('#exportBtn').addEventListener('click', exportCSV);
            $('#exportBtn2').addEventListener('click', exportCSV);

            $('#importBtn').addEventListener('click', () => {
                toast('导入功能演示', '在实际项目中可接入文件选择与解析逻辑', 'info', 3200);
            });

            /* 刷新 */
            function refreshAll() {
                const skeletonRows = 6;
                tbody.innerHTML = Array.from({ length: skeletonRows }, () => `
      <tr>
        <td data-col="check"><span class="skeleton" style="width:17px;height:17px;display:block"></span></td>
        <td data-col="id"><span class="skeleton" style="width:44px;display:block"></span></td>
        <td data-col="user"><span class="skeleton" style="width:150px;display:block"></span></td>
        <td data-col="email"><span class="skeleton" style="width:170px;display:block"></span></td>
        <td data-col="phone"><span class="skeleton" style="width:100px;display:block"></span></td>
        <td data-col="dept"><span class="skeleton" style="width:90px;display:block"></span></td>
        <td data-col="role"><span class="skeleton" style="width:80px;display:block"></span></td>
        <td data-col="status"><span class="skeleton" style="width:60px;display:block"></span></td>
        <td data-col="lastLogin"><span class="skeleton" style="width:90px;display:block"></span></td>
        <td data-col="createdAt"><span class="skeleton" style="width:80px;display:block"></span></td>
        <td></td>
      </tr>`).join('');

                setTimeout(() => {
                    refresh();
                    toast('数据已刷新', `共 ${state.users.length} 条用户记录`, 'info', 2200);
                }, 520);
            }
            $('#refreshAll').addEventListener('click', refreshAll);

            /* 通知已读 */
            $('#markAllRead').addEventListener('click', () => {
                $$('.notif-unread').forEach((n) => n.classList.remove('notif-unread'));
                const badge = $('.icon-btn__badge');
                if (badge) badge.remove();
                toast('已全部标记为已读', '', 'info', 2000);
            });

            /* 全屏 */
            $('#fullscreenBtn').addEventListener('click', async () => {
                try {
                    if (!document.fullscreenElement) await document.documentElement.requestFullscreen();
                    else await document.exitFullscreen();
                } catch {
                    toast('无法进入全屏', '当前环境不支持该操作', 'warn', 2400);
                }
            });

            /* 主题切换 */
            const themeRadios = $$('input[name="theme"]');

            function applyTheme(mode, event) {
                const root = document.documentElement;
                if (mode === 'system') root.removeAttribute('data-theme');
                else root.setAttribute('data-theme', mode);

                try { localStorage.setItem('nova-theme', mode); } catch { /* ignore */ }

                if (event && document.startViewTransition) {
                    const x = event.clientX || window.innerWidth - 60;
                    const y = event.clientY || 40;
                    root.style.setProperty('--vt-x', x + 'px');
                    root.style.setProperty('--vt-y', y + 'px');
                    document.startViewTransition(() => { });
                }
            }

            themeRadios.forEach((r) => {
                r.addEventListener('change', (e) => { if (r.checked) applyTheme(r.value, e); });
            });

            (() => {
                let saved = 'light';
                try { saved = localStorage.getItem('nova-theme') || 'light'; } catch { /* ignore */ }
                if (saved === 'system') document.documentElement.removeAttribute('data-theme');
                else document.documentElement.setAttribute('data-theme', saved);
                const radio = themeRadios.find((r) => r.value === saved);
                if (radio) radio.checked = true;
            })();

            /* 数字滚动 */
            function animateCounters() {
                $$('[data-count]').forEach((el) => {
                    const target = Number(el.dataset.count);
                    const start = performance.now();
                    const dur = 1100;
                    const tick = (now) => {
                        const p = Math.min(1, (now - start) / dur);
                        const eased = 1 - Math.pow(1 - p, 3);
                        el.textContent = NUM.format(Math.round(target * eased));
                        if (p < 1) requestAnimationFrame(tick);
                    };
                    requestAnimationFrame(tick);
                });
            }

            /* 卡片光晕跟随鼠标 */
            $$('.stat').forEach((card) => {
                card.addEventListener('pointermove', (e) => {
                    const r = card.getBoundingClientRect();
                    const pct = ((e.clientX - r.left) / r.width) * 100;
                    card.style.setProperty('--glow-x', pct + '%');
                });
            });

            /* 键盘快捷键 */
            document.addEventListener('keydown', (e) => {
                const tag = e.target.tagName;
                const typing = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || e.target.isContentEditable;

                if (e.key === '/' && !typing) {
                    e.preventDefault();
                    $('#searchInput').focus();
                }
                if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
                    e.preventDefault();
                    $('#globalSearch').focus();
                }
                if (e.key === 'Escape' && !typing && state.selected.size && !document.querySelector('dialog[open]')) {
                    state.selected.clear();
                    renderRows();
                    toast('已取消选择', '', 'info', 1600);
                }
                if (!typing && !document.querySelector('dialog[open]')) {
                    if (e.key === 'PageDown' || (e.altKey && e.key === 'ArrowRight')) {
                        e.preventDefault();
                        $('#pager').querySelector('[data-page="next"]:not([disabled])')?.click();
                    }
                    if (e.key === 'PageUp' || (e.altKey && e.key === 'ArrowLeft')) {
                        e.preventDefault();
                        $('#pager').querySelector('[data-page="prev"]:not([disabled])')?.click();
                    }
                }
            });

            /* 全局搜索联动 */
            $('#globalSearch').addEventListener('input', debounce((e) => {
                const v = e.target.value.trim();
                if (!v) return;
                $('#searchInput').value = v;
                state.search = v;
                refresh({ resetPage: true });
            }, 420));

            /* 导航项激活态 */
            $$('.nav__item[data-nav]').forEach((item) => {
                item.addEventListener('click', (e) => {
                    e.preventDefault();
                    $$('.nav__item').forEach((n) => { n.removeAttribute('aria-current'); n.classList.remove('is-active'); });
                    item.setAttribute('aria-current', 'page');
                    if (item.dataset.nav !== 'users') {
                        toast('模块演示', `「${item.textContent.trim()}」模块在完整版本中可用`, 'info', 2600);
                    }
                });
            });

            /* 移动端点击遮罩关闭侧边栏 */
            document.addEventListener('click', (e) => {
                if (window.innerWidth > 720) return;
                if (app.dataset.mobileOpen !== 'true') return;
                if (!e.target.closest('.sidebar') && !e.target.closest('#sidebarToggle')) {
                    app.dataset.mobileOpen = 'false';
                }
            });

            /* ============ 12. 初始化 ============ */
            function init() {
                /* 填充部门 / 角色下拉 */
                const deptSelect = $('#deptFilter');
                const roleSelect = $('#roleFilter');
                const formDept = $('#f-dept');
                const formRole = $('#f-role');

                DEPTS.forEach((d) => {
                    deptSelect.insertAdjacentHTML('beforeend', `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`);
                    formDept.insertAdjacentHTML('beforeend', `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`);
                });
                ROLES.forEach((r) => {
                    roleSelect.insertAdjacentHTML('beforeend', `<option value="${escapeHtml(r)}">${escapeHtml(r)}</option>`);
                    formRole.insertAdjacentHTML('beforeend', `<option value="${escapeHtml(r)}">${escapeHtml(r)}</option>`);
                });

                syncColumnVisibility();
                refresh();
                requestAnimationFrame(animateCounters);

                setTimeout(() => {
                    toast('欢迎使用 Nova Admin', `当前共有 ${state.users.length} 位用户,数据已就绪`, 'success', 4200);
                }, 620);

                window.addEventListener('resize', debounce(() => {
                    if (window.innerWidth > 720) app.dataset.mobileOpen = 'false';
                }, 200));
            }

            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', init);
            } else {
                init();
            }

            /* 调试接口 */
            window.__nova = { state, refresh, toast };
        })();
    </script>
</body>

</html>

  

posted @ 2026-10-16 15:52  华腾智算  阅读(4)  评论(0)    收藏  举报
https://damo.alibaba.com/ https://tianchi.aliyun.com/course?spm=5176.21206777.J_3941670930.5.87dc17c9BZNvLL