基本功能复刻完成
This commit is contained in:
@@ -1,37 +1,36 @@
|
||||
:root {
|
||||
/* Nebula Dark Palette */
|
||||
--bg: #07101b;
|
||||
--panel: rgba(9, 20, 35, 0.78);
|
||||
--panel-strong: rgba(11, 24, 41, 0.92);
|
||||
--text: #eef6ff;
|
||||
--text-dim: rgba(227, 239, 255, 0.72);
|
||||
--text-faint: rgba(227, 239, 255, 0.54);
|
||||
--border: rgba(152, 192, 255, 0.16);
|
||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
|
||||
--shadow-soft: 0 12px 32px rgba(0, 0, 0, 0.22);
|
||||
/* Xboard/Ant Design Professional Palette */
|
||||
--bg: #f0f2f5;
|
||||
--panel: #ffffff;
|
||||
--text: #000000d9;
|
||||
--text-dim: #000000a6;
|
||||
--text-faint: #00000073;
|
||||
--border: #d9d9d9;
|
||||
--border-light: #f0f0f0;
|
||||
--shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
--shadow-card: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
|
||||
|
||||
/* Brand/Accent */
|
||||
--accent: #6ce5ff;
|
||||
--accent-strong: #21b8ff;
|
||||
--accent-soft: rgba(108, 229, 255, 0.12);
|
||||
--accent-rgb: 108, 229, 255;
|
||||
/* Brand/Accent (Ant Design Blue) */
|
||||
--accent: #1677ff;
|
||||
--accent-hover: #4096ff;
|
||||
--accent-active: #0958d9;
|
||||
--accent-soft: #e6f4ff;
|
||||
|
||||
/* Indicators */
|
||||
--success: #65f0b7;
|
||||
--warn: #ffca7a;
|
||||
--danger: #ff8db5;
|
||||
--success: #52c41a;
|
||||
--warn: #faad14;
|
||||
--danger: #ff4d4f;
|
||||
|
||||
/* Radius */
|
||||
--radius-xl: 28px;
|
||||
--radius-lg: 20px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 8px;
|
||||
--radius-md: 6px;
|
||||
--radius-sm: 4px;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: #091221;
|
||||
--sidebar-active: #ffffff;
|
||||
--sidebar-muted: rgba(227, 239, 255, 0.5);
|
||||
--sidebar-bg: #ffffff;
|
||||
--sidebar-item-active: #e6f4ff;
|
||||
|
||||
font-family: "Avenir Next", "Segoe UI Variable Display", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@@ -41,257 +40,165 @@
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.12), transparent 30%),
|
||||
radial-gradient(circle at top right, rgba(255, 201, 122, 0.08), transparent 22%),
|
||||
linear-gradient(180deg, #050d16 0%, #07101b 100%);
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(155, 192, 255, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(155, 192, 255, 0.04) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
mask-image: radial-gradient(circle at top center, black, transparent 80%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
button, input, select, textarea {
|
||||
font: inherit;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Glass Component Base */
|
||||
.glass-card {
|
||||
background: var(--panel);
|
||||
backdrop-filter: blur(18px);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
font-size: 14px;
|
||||
line-height: 1.5715;
|
||||
}
|
||||
|
||||
/* Main Layout */
|
||||
.admin-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.admin-sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
background: var(--sidebar);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 32px 20px;
|
||||
width: 260px;
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.sidebar-brand-mark {
|
||||
display: inline-flex;
|
||||
padding: 6px 12px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.2);
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
.sidebar-header {
|
||||
padding: 24px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-brand strong {
|
||||
display: block;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sidebar-brand span {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sidebar-nav-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--text-faint);
|
||||
margin: 24px 12px 12px;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
padding: 12px 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-nav-label {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-faint);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.sidebar-item-wrap {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: block;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--sidebar-muted);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
color: var(--text-dim);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--text);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
background: var(--accent-soft);
|
||||
background: var(--sidebar-item-active);
|
||||
color: var(--accent);
|
||||
border-color: rgba(var(--accent-rgb), 0.24);
|
||||
box-shadow: inset 0 0 12px rgba(var(--accent-rgb), 0.08);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-item strong {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar-item span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-top: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.admin-main {
|
||||
padding: 32px 40px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin-left: 260px;
|
||||
padding: 24px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
padding: 24px 28px;
|
||||
border-radius: var(--radius-xl);
|
||||
margin-bottom: 32px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.topbar-copy h1 {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.1;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.topbar-copy p {
|
||||
margin: 10px 0 0;
|
||||
font-size: 15px;
|
||||
margin: 4px 0 0;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.topbar-eyebrow {
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--accent);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.topbar-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.topbar-chip {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* Cards & Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
padding: 28px;
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
border: 1px solid var(--border-light);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--accent);
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-faint);
|
||||
font-size: 14px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* Table Design */
|
||||
.stat-card strong {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table-wrap {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -300,56 +207,45 @@ table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 16px 20px;
|
||||
th {
|
||||
background: #fafafa;
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
th {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-faint);
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
color: var(--text);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
/* Node Hierarchy */
|
||||
.node-child {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* Forms & Inputs */
|
||||
.field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
.node-child td:first-child {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.field input, .field select, .field textarea {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.field input:focus, .field select:focus {
|
||||
border-color: var(--accent);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.1);
|
||||
outline: none;
|
||||
.node-child td:first-child::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 0;
|
||||
bottom: 50%;
|
||||
width: 10px;
|
||||
border-left: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
@@ -357,131 +253,251 @@ tbody tr:last-child td {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 700;
|
||||
height: 32px;
|
||||
padding: 4px 15px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||
color: #050d16;
|
||||
box-shadow: 0 12px 32px rgba(var(--accent-rgb), 0.24);
|
||||
background-color: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--accent-hover);
|
||||
border-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255,255,255,0.05);
|
||||
background-color: #fff;
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
.btn-secondary:hover {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
color: var(--accent);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
/* Form Elements */
|
||||
.field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.field input, .field select, .field textarea {
|
||||
width: 100%;
|
||||
padding: 4px 11px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.field input:focus, .field select:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Toggle Switch (Ant Design Style) */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 22px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
transition: .2s;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
transition: .2s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
|
||||
/* Status Pills */
|
||||
.status-pill {
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
display: inline-flex;
|
||||
padding: 0 7px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-ok { background: rgba(101, 240, 183, 0.12); color: var(--success); border: 1px solid rgba(101, 240, 183, 0.2); }
|
||||
.status-warn { background: rgba(255, 202, 122, 0.12); color: var(--warn); border: 1px solid rgba(255, 202, 122, 0.2); }
|
||||
.status-danger { background: rgba(255, 141, 181, 0.12); color: var(--danger); border: 1px solid rgba(255, 141, 181, 0.2); }
|
||||
|
||||
/* Progress */
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent-strong));
|
||||
box-shadow: 0 0 12px var(--accent-soft);
|
||||
}
|
||||
.status-ok { background: #f6ffed; border: 1px solid #b7eb8f; color: #52c41a; }
|
||||
.status-warn { background: #fffbe6; border: 1px solid #ffe58f; color: #faad14; }
|
||||
.status-danger { background: #fff1f0; border: 1px solid #ffa39e; color: #ff4d4f; }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 100;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 100px;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: min(100%, 640px);
|
||||
animation: modalIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
width: 520px;
|
||||
overflow: hidden;
|
||||
animation: modalIn 0.2s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
@keyframes modalIn {
|
||||
from { opacity: 0; transform: scale(0.9) translateY(20px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.pagination-shell {
|
||||
/* Loading busy state */
|
||||
.btn.busy {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Sidebar Submenus */
|
||||
.sidebar-submenu {
|
||||
margin-top: 2px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.sidebar-submenu.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-nav-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
width: 40px;
|
||||
.sidebar-nav-item-header:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Login Page Styling */
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
text-align: center;
|
||||
color: var(--text-faint);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-field label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.input-wrapper input {
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: var(--accent);
|
||||
color: #050d16;
|
||||
border-color: var(--accent);
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.admin-shell { grid-template-columns: 1fr; }
|
||||
.admin-sidebar { position: static; height: auto; display: none; } /* Mobile sidebar handled differently */
|
||||
.admin-main { padding: 24px; }
|
||||
.grid-3 { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Animation Utilities */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
@media (max-width: 900px) {
|
||||
.admin-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.admin-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
.grid-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
realname: null,
|
||||
devices: null,
|
||||
nodes: null,
|
||||
expandedNodes: new Set(), // Track IDs of expanded parent nodes
|
||||
sidebarGroups: {
|
||||
infrastructure: true,
|
||||
financials: true,
|
||||
system: false
|
||||
},
|
||||
groups: null,
|
||||
routes: null,
|
||||
plans: null,
|
||||
@@ -56,26 +62,27 @@
|
||||
async function loadBootstrap() {
|
||||
try {
|
||||
state.busy = true;
|
||||
render();
|
||||
var loginCheck = unwrap(await request("/api/v1/user/checkLogin", { method: "GET" }));
|
||||
if (!loginCheck || !loginCheck.is_login || !loginCheck.is_admin) {
|
||||
clearSession();
|
||||
return;
|
||||
if (loginCheck && loginCheck.is_admin) {
|
||||
state.user = loginCheck;
|
||||
var results = await Promise.all([
|
||||
request(api.adminConfig, { method: "GET" }),
|
||||
request(api.systemStatus, { method: "GET" })
|
||||
]);
|
||||
state.config = unwrap(results[0]) || {};
|
||||
state.system = unwrap(results[1]) || {};
|
||||
} else {
|
||||
state.user = null;
|
||||
}
|
||||
|
||||
state.user = loginCheck;
|
||||
|
||||
var results = await Promise.all([
|
||||
request(api.adminConfig, { method: "GET" }),
|
||||
request(api.systemStatus, { method: "GET" })
|
||||
]);
|
||||
|
||||
state.config = unwrap(results[0]) || {};
|
||||
state.system = unwrap(results[1]) || {};
|
||||
} catch (error) {
|
||||
console.error("Bootstrap failed:", error);
|
||||
clearSession();
|
||||
show(error.message || "Failed to load admin data.", "error");
|
||||
if (state.token) show("Session expired or configuration error.", "error");
|
||||
} finally {
|
||||
state.busy = false;
|
||||
render();
|
||||
if (state.user) hydrateRoute();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +134,7 @@
|
||||
}
|
||||
|
||||
function onClick(event) {
|
||||
if (state.busy) return;
|
||||
var actionEl = event.target.closest("[data-action]");
|
||||
if (!actionEl) return;
|
||||
|
||||
@@ -141,6 +149,7 @@
|
||||
// Handlers
|
||||
if (action === "plan-add") { state.modal = { type: "plan", data: {} }; render(); return; }
|
||||
if (action === "plan-edit") {
|
||||
if (!state.plans) return;
|
||||
var plan = state.plans.find(p => p.id == actionEl.getAttribute("data-id"));
|
||||
state.modal = { type: "plan", data: plan }; render(); return;
|
||||
}
|
||||
@@ -167,6 +176,7 @@
|
||||
}
|
||||
|
||||
if (action === "user-edit") {
|
||||
if (!state.users || !state.users.list) return;
|
||||
var user = state.users.list.find(u => u.id == actionEl.getAttribute("data-id"));
|
||||
state.modal = { type: "user", data: user }; render(); return;
|
||||
}
|
||||
@@ -183,6 +193,30 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "node-expand") {
|
||||
var id = actionEl.getAttribute("data-id");
|
||||
if (state.expandedNodes.has(id)) state.expandedNodes.delete(id);
|
||||
else state.expandedNodes.add(id);
|
||||
render(); return;
|
||||
}
|
||||
if (action === "node-toggle-visible") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
var id = parseInt(actionEl.getAttribute("data-id"));
|
||||
var input = actionEl.querySelector("input");
|
||||
if (input) {
|
||||
input.checked = !input.checked;
|
||||
var show = input.checked ? 1 : 0;
|
||||
adminPost(api.adminBase + "/server/manage/update", { id: id, show: show }).then(hydrateRoute);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (action === "sidebar-toggle") {
|
||||
var id = actionEl.getAttribute("data-id");
|
||||
state.sidebarGroups[id] = !state.sidebarGroups[id];
|
||||
render(); return;
|
||||
}
|
||||
|
||||
if (action === "node-add") { state.modal = { type: "node", data: {} }; render(); return; }
|
||||
if (action === "node-edit") {
|
||||
var node = state.nodes.find(n => n.id == actionEl.getAttribute("data-id"));
|
||||
@@ -281,7 +315,7 @@
|
||||
root.innerHTML = state.user ? renderDashboard() : renderLogin();
|
||||
if (state.modal) {
|
||||
var modalShell = document.createElement("div");
|
||||
modalShell.className = "modal-overlay fade-in";
|
||||
modalShell.className = "modal-overlay";
|
||||
modalShell.innerHTML = renderModalContent();
|
||||
root.appendChild(modalShell);
|
||||
}
|
||||
@@ -294,7 +328,7 @@
|
||||
|
||||
function renderModalContent() {
|
||||
var m = state.modal;
|
||||
var html = '<div class="glass-card modal-content"><button class="btn-close" data-action="modal-close">×</button>';
|
||||
var html = '<div class="modal-card"><div class="card" style="margin:0; border:0; box-shadow:none;"><button class="btn btn-close" style="float:right; margin-top:-10px;" data-action="modal-close">×</button>';
|
||||
if (m.type === "plan") html += renderPlanForm(m.data);
|
||||
if (m.type === "user") html += renderUserForm(m.data);
|
||||
if (m.type === "coupon") html += renderCouponForm(m.data);
|
||||
@@ -351,20 +385,40 @@
|
||||
|
||||
function renderLogin() {
|
||||
return [
|
||||
'<div class="login-shell">',
|
||||
'<section class="glass-card card modal-card fade-in">',
|
||||
'<div style="text-align:center; margin-bottom:32px;">',
|
||||
'<span class="topbar-eyebrow">SingBox Gopanel</span>',
|
||||
'<h1 style="font-size:36px; margin:0;">Welcome Back</h1>',
|
||||
'<p style="color:var(--text-dim); margin-top:8px;">Sign in to manage your high-performance network.</p>',
|
||||
'<div class="login-container">',
|
||||
'<div class="login-card">',
|
||||
'<div class="login-header">',
|
||||
'<div class="login-logo">',
|
||||
'<div class="logo-icon">',
|
||||
'<svg width="32" height="32" viewBox="0 0 32 32" fill="none"><path d="M16 4L4 10L16 16L28 10L16 4Z" fill="currentColor"/><path d="M4 22L16 28L28 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16L16 22L28 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
'<h1>' + escapeHtml(cfg.title || "SingBox Console") + '</h1>',
|
||||
'<p>Managed Infrastructure & Performance</p>',
|
||||
'</div>',
|
||||
state.message ? renderNotice() : "",
|
||||
'<form data-form="login">',
|
||||
'<div class="field"><label>Email Address</label><input type="email" name="email" placeholder="admin@example.com" required autocomplete="email" /></div>',
|
||||
'<div class="field"><label>Secure Password</label><input type="password" name="password" placeholder="••••••••" required autocomplete="current-password" /></div>',
|
||||
'<button class="btn btn-primary" style="width:100%; height:52px; font-size:16px; margin-top:12px;" type="submit">Unlock Console</button>',
|
||||
'<form data-form="login" class="login-form">',
|
||||
'<div class="login-field">',
|
||||
'<label>Administrative Email</label>',
|
||||
'<div class="input-wrapper">',
|
||||
'<input type="email" name="email" placeholder="admin@network.local" required autocomplete="email" />',
|
||||
'</div>',
|
||||
'</div>',
|
||||
'<div class="login-field">',
|
||||
'<label>Access Key</label>',
|
||||
'<div class="input-wrapper">',
|
||||
'<input type="password" name="password" placeholder="••••••••••••" required autocomplete="current-password" />',
|
||||
'</div>',
|
||||
'</div>',
|
||||
'<button type="submit" class="btn btn-primary btn-login">',
|
||||
'<span>Establish Connection</span>',
|
||||
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>',
|
||||
'</button>',
|
||||
'</form>',
|
||||
'</section>',
|
||||
'<div class="login-footer">',
|
||||
'<p>© ' + new Date().getFullYear() + ' High-Performance Control Plane</p>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}
|
||||
@@ -387,59 +441,70 @@
|
||||
function renderSidebar() {
|
||||
return [
|
||||
'<aside class="admin-sidebar">',
|
||||
'<div class="sidebar-header">',
|
||||
'<div class="sidebar-brand">',
|
||||
'<span class="sidebar-brand-mark">Nebula</span>',
|
||||
'<strong>' + escapeHtml(cfg.title || "Admin") + '</strong>',
|
||||
'<span>/' + escapeHtml(getSecurePath()) + ' Console</span>',
|
||||
'<span>Control Plane v' + (cfg.version || "1.0") + '</span>',
|
||||
'</div>',
|
||||
'</div>',
|
||||
|
||||
'<div class="sidebar-nav-label">General</div>',
|
||||
'<nav class="sidebar-nav">',
|
||||
navItem("overview", "Overview", "Main statistics"),
|
||||
navItem("dashboard-node", "Nodes Status", "Real-time metrics"),
|
||||
'</nav>',
|
||||
'<div style="margin-top: 24px; display: flex; flex-direction: column; gap: 4px;">',
|
||||
renderSidebarGroup("general", "Dashboard", [
|
||||
navItem("overview", "Overview", "Main statistics"),
|
||||
navItem("dashboard-node", "Nodes Status", "Real-time metrics")
|
||||
]),
|
||||
|
||||
'<div class="sidebar-nav-label">Infrastructure</div>',
|
||||
'<nav class="sidebar-nav">',
|
||||
navItem("node-manage", "Server Nodes", "Core infrastructure"),
|
||||
navItem("node-group", "Permission Groups", "Access control"),
|
||||
navItem("node-route", "Traffic Routing", "Advanced rules"),
|
||||
'</nav>',
|
||||
renderSidebarGroup("infrastructure", "Infrastructure", [
|
||||
navItem("node-manage", "Server Nodes", "Core infrastructure"),
|
||||
navItem("node-group", "Permission Groups", "Access control"),
|
||||
navItem("node-route", "Traffic Routing", "Advanced rules")
|
||||
]),
|
||||
|
||||
'<div class="sidebar-nav-label">Financials</div>',
|
||||
'<nav class="sidebar-nav">',
|
||||
navItem("plan-manage", "Packages", "Sales and traffic"),
|
||||
navItem("order-manage", "Transactions", "Payment history"),
|
||||
navItem("coupon-manage", "Marketing", "Coupons & gifts"),
|
||||
'</nav>',
|
||||
renderSidebarGroup("financials", "Financials", [
|
||||
navItem("plan-manage", "Packages", "Sales and traffic"),
|
||||
navItem("order-manage", "Transactions", "Payment history"),
|
||||
navItem("coupon-manage", "Marketing", "Coupons & gifts")
|
||||
]),
|
||||
|
||||
'<div class="sidebar-nav-label">Operations</div>',
|
||||
'<nav class="sidebar-nav">',
|
||||
navItem("user-manage", "User Base", "Accounts & traffic"),
|
||||
navItem("user-online-devices", "Live Sessions", "Real-time monitoring"),
|
||||
navItem("ticket-manage", "Service Desk", "Support tickets"),
|
||||
navItem("realname", "KYC Workflow", "Identity verification"),
|
||||
'</nav>',
|
||||
|
||||
'<div class="sidebar-nav-label">Platform</div>',
|
||||
'<nav class="sidebar-nav">',
|
||||
navItem("system-config", "Settings", "System core"),
|
||||
'</nav>',
|
||||
renderSidebarGroup("operations", "Operations", [
|
||||
navItem("user-manage", "User Base", "Accounts & traffic"),
|
||||
navItem("user-online-devices", "Live Sessions", "Real-time monitoring"),
|
||||
navItem("ticket-manage", "Support Center", "Tickets & helpdesk")
|
||||
]),
|
||||
|
||||
'<div style="margin-top:auto;" class="sidebar-meta">',
|
||||
'<span class="sidebar-nav-label">Session</span>',
|
||||
'<div style="padding:0 12px; font-size:13px; color:var(--text-dim);">' + escapeHtml(state.user.email) + '</div>',
|
||||
'<div class="toolbar" style="padding:12px;"><button class="btn btn-ghost" style="width:100%; border:1px solid var(--border);" data-action="logout">Logout</button></div>',
|
||||
renderSidebarGroup("system", "System", [
|
||||
navItem("system-config", "Global Settings", "Platform parameters")
|
||||
]),
|
||||
'</div>',
|
||||
|
||||
'<div style="margin-top: auto; padding: 20px 0;">',
|
||||
'<button class="sidebar-nav-item-header" style="width:100%;" data-action="logout">',
|
||||
'<strong>Sign Out</strong>',
|
||||
'</button>',
|
||||
'</div>',
|
||||
'</aside>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderSidebarGroup(id, label, items) {
|
||||
var isExpanded = state.sidebarGroups[id] !== false;
|
||||
return [
|
||||
'<div class="sidebar-nav-group">',
|
||||
'<div class="sidebar-nav-item-header" data-action="sidebar-toggle" data-id="' + id + '">',
|
||||
'<span style="font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.1em; color:var(--text-faint);">' + escapeHtml(label) + '</span>',
|
||||
'<svg class="sidebar-arrow ' + (isExpanded ? 'expanded' : '') + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="width:12px;height:12px;"><path d="M9 18l6-6-6-6"/></svg>',
|
||||
'</div>',
|
||||
'<nav class="sidebar-submenu ' + (isExpanded ? '' : 'collapsed') + '">',
|
||||
items.join(""),
|
||||
'</nav>',
|
||||
'</div>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
function renderTopbar() {
|
||||
return [
|
||||
'<header class="topbar glass-card">',
|
||||
'<header class="topbar card">',
|
||||
'<div class="topbar-copy">',
|
||||
'<span class="topbar-eyebrow">Workspace / ' + escapeHtml(getRouteTitle(state.route)) + '</span>',
|
||||
'<h1>' + escapeHtml(getRouteTitle(state.route)) + '</h1>',
|
||||
'<p>' + escapeHtml(getRouteDescription(state.route)) + '</p>',
|
||||
'<div class="topbar-meta">',
|
||||
@@ -595,19 +660,65 @@
|
||||
}
|
||||
|
||||
function renderNodeManage() {
|
||||
var rows = state.nodes || [];
|
||||
var nodes = state.nodes || [];
|
||||
var roots = nodes.filter(n => !n.parent_id);
|
||||
var childrenByParent = nodes.reduce((acc, n) => {
|
||||
if (n.parent_id) {
|
||||
acc[n.parent_id] = acc[n.parent_id] || [];
|
||||
acc[n.parent_id].push(n);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
var tableRows = [];
|
||||
|
||||
roots.forEach(function(node) {
|
||||
var children = childrenByParent[node.id] || [];
|
||||
var isExpanded = state.expandedNodes.has(String(node.id));
|
||||
|
||||
// Render Root Item
|
||||
tableRows.push({
|
||||
isChild: false,
|
||||
data: node,
|
||||
hasChildren: children.length > 0,
|
||||
isExpanded: isExpanded
|
||||
});
|
||||
|
||||
// Render Children if expanded
|
||||
if (isExpanded) {
|
||||
children.forEach(function(child) {
|
||||
tableRows.push({
|
||||
isChild: true,
|
||||
data: child,
|
||||
hasChildren: false,
|
||||
isExpanded: false
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
'<div class="toolbar" style="margin-bottom:24px;"><button class="btn btn-primary" data-action="node-add">Add New Node</button></div>',
|
||||
'<div class="table-wrap glass-card">',
|
||||
renderTable(["ID", "Name/Host", "Type", "Rate", "Visibility", "Actions"], rows.map(function(row) {
|
||||
return [
|
||||
escapeHtml(String(row.id)),
|
||||
'<div><strong>' + escapeHtml(row.name) + '</strong><div style="font-size:12px; color:var(--text-faint);">' + escapeHtml(row.host) + '</div></div>',
|
||||
escapeHtml(row.type),
|
||||
'<strong>' + row.rate + 'x</strong>',
|
||||
renderStatus(row.show ? "visible" : "hidden"),
|
||||
'<div class="row-actions"><button class="btn btn-ghost" data-action="node-edit" data-id="' + row.id + '">Edit</button><button class="btn btn-ghost" data-action="node-copy" data-id="' + row.id + '">Copy</button><button class="btn btn-ghost" data-action="node-delete" data-id="' + row.id + '">Delete</button></div>'
|
||||
];
|
||||
'<div class="table-wrap glass-card" style="background:var(--panel-strong)">',
|
||||
'<style> .node-toggle { cursor:pointer; margin-right:8px; width:24px; height:24px; display:inline-flex; align-items:center; justify-content:center; border-radius:6px; background:rgba(0,0,0,0.05); } .node-toggle:hover { background:rgba(0,0,0,0.1); } </style>',
|
||||
renderTable(["ID", "Name/Host", "Type", "Rate", "Visibility", "Actions"], tableRows.map(function(row) {
|
||||
var n = row.data;
|
||||
var expandBtn = row.hasChildren ? '<span class="node-toggle" data-action="node-expand" data-id="' + n.id + '">' + (row.isExpanded ? "−" : "+") + '</span>' : (row.isChild ? '' : '<span style="display:inline-block;width:32px;"></span>');
|
||||
|
||||
var nameContent = '<div style="display:flex; align-items:center;">' + expandBtn + '<div><strong>' + escapeHtml(n.name) + '</strong><div style="font-size:12px; color:var(--text-faint);">' + escapeHtml(n.host) + '</div></div></div>';
|
||||
|
||||
return {
|
||||
className: row.isChild ? "node-child" : "",
|
||||
cells: [
|
||||
escapeHtml(String(n.id)),
|
||||
nameContent,
|
||||
escapeHtml(n.type),
|
||||
'<strong>' + n.rate + 'x</strong>',
|
||||
'<div style="display:flex; align-items:center; gap:8px;">' + renderStatus(n.show ? "visible" : "hidden") +
|
||||
'<label class="switch" data-action="node-toggle-visible" data-id="' + n.id + '"><input type="checkbox" ' + (n.show ? "checked" : "") + '><span class="slider"></span></label></div>',
|
||||
'<div class="row-actions"><button class="btn btn-ghost" data-action="node-edit" data-id="' + n.id + '">Edit</button><button class="btn btn-ghost" data-action="node-copy" data-id="' + n.id + '">Copy</button><button class="btn btn-ghost" data-action="node-delete" data-id="' + n.id + '">Delete</button></div>'
|
||||
]
|
||||
};
|
||||
})),
|
||||
'</div>'
|
||||
].join("");
|
||||
@@ -726,12 +837,17 @@
|
||||
}
|
||||
|
||||
function renderTable(headers, rows) {
|
||||
if (!rows.length) return '<div class="empty-state">No records found.</div>';
|
||||
if (!rows.length) return '<div class="empty-state" style="padding:48px; text-align:center; color:var(--text-faint);">No records found.</div>';
|
||||
return [
|
||||
'<table><thead><tr>',
|
||||
headers.map(function(h) { return '<th>' + escapeHtml(h) + '</th>'; }).join(""),
|
||||
'</tr></thead><tbody>',
|
||||
rows.map(function(row) { return '<tr>' + row.map(function(c) { return '<td>' + c + '</td>'; }).join("") + '</tr>'; }).join(""),
|
||||
rows.map(function(row) {
|
||||
var isObj = !Array.isArray(row);
|
||||
var cells = isObj ? row.cells : row;
|
||||
var cls = isObj ? (row.className || "") : "";
|
||||
return '<tr class="' + cls + '">' + cells.map(function(c) { return '<td>' + c + '</td>'; }).join("") + '</tr>';
|
||||
}).join(""),
|
||||
'</tbody></table>'
|
||||
].join("");
|
||||
}
|
||||
|
||||
BIN
frontend/admin/assets/codicon-ngg6Pgfi.ttf
Normal file
BIN
frontend/admin/assets/codicon-ngg6Pgfi.ttf
Normal file
Binary file not shown.
1
frontend/admin/assets/css.worker-Bx7Y3X-c.js
Normal file
1
frontend/admin/assets/css.worker-Bx7Y3X-c.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/admin/assets/html.worker-C4RoLUfz.js
Normal file
1
frontend/admin/assets/html.worker-C4RoLUfz.js
Normal file
File diff suppressed because one or more lines are too long
117
frontend/admin/assets/index-CO3BwsT2.js
Normal file
117
frontend/admin/assets/index-CO3BwsT2.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/admin/assets/index-DTPKq_WI.css
Normal file
1
frontend/admin/assets/index-DTPKq_WI.css
Normal file
File diff suppressed because one or more lines are too long
1
frontend/admin/assets/json.worker-qCj-lAKT.js
Normal file
1
frontend/admin/assets/json.worker-qCj-lAKT.js
Normal file
File diff suppressed because one or more lines are too long
20
frontend/admin/assets/ts.worker-sc26f_2L.js
Normal file
20
frontend/admin/assets/ts.worker-sc26f_2L.js
Normal file
File diff suppressed because one or more lines are too long
3219
frontend/admin/locales/en-US.js
Normal file
3219
frontend/admin/locales/en-US.js
Normal file
File diff suppressed because it is too large
Load Diff
3116
frontend/admin/locales/ru-RU.js
Normal file
3116
frontend/admin/locales/ru-RU.js
Normal file
File diff suppressed because it is too large
Load Diff
3235
frontend/admin/locales/zh-CN.js
Normal file
3235
frontend/admin/locales/zh-CN.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user