diff --git a/panel/static/app.js b/panel/static/app.js index ca065d8..972c61c 100644 --- a/panel/static/app.js +++ b/panel/static/app.js @@ -210,6 +210,14 @@ function setStatusBadge(el, online) { el.classList.toggle("offline", !online); } +function setStatText(el, value) { + if (!el || el.textContent === value) return; + el.textContent = value; + el.classList.remove("stat-flash"); + void el.offsetWidth; + el.classList.add("stat-flash"); +} + function updateStats(data) { const summary = data.summary || {}; const onlineEl = document.getElementById("summaryOnline"); @@ -218,10 +226,10 @@ function updateStats(data) { const statusEl = document.getElementById("summaryStatus"); if (onlineEl) { - onlineEl.textContent = `${summary.online || 0} / ${summary.total_nodes || 0}`; + setStatText(onlineEl, `${summary.online || 0} / ${summary.total_nodes || 0}`); } - if (upEl) upEl.textContent = summary.upload_speed_human || "0 B/s"; - if (downEl) downEl.textContent = summary.download_speed_human || "0 B/s"; + if (upEl) setStatText(upEl, summary.upload_speed_human || "0 B/s"); + if (downEl) setStatText(downEl, summary.download_speed_human || "0 B/s"); if (statusEl) { if (data.singbox) { statusEl.textContent = "正常"; @@ -241,7 +249,7 @@ function updateStats(data) { const setText = (role, value) => { const el = card.querySelector(`[data-role="${role}"]`); - if (el) el.textContent = value; + if (el) setStatText(el, value); }; setText("connections", String(node.connections ?? 0)); diff --git a/panel/static/style.css b/panel/static/style.css index 446075d..3bf7e58 100644 --- a/panel/static/style.css +++ b/panel/static/style.css @@ -1,23 +1,101 @@ :root { - --bg: #0f1419; - --card: #1a2332; - --border: #2a3544; - --text: #e7ecf3; - --muted: #8b98a8; - --primary: #3b82f6; - --danger: #ef4444; - --radius: 12px; + --bg: #050508; + --bg-elevated: #0a0a12; + --card: rgba(10, 12, 22, 0.82); + --card-solid: #0c0e18; + --border: rgba(0, 240, 255, 0.18); + --border-bright: rgba(0, 240, 255, 0.45); + --text: #e8f4ff; + --muted: #6b8299; + --cyan: #00f0ff; + --magenta: #ff00aa; + --yellow: #fcee0a; + --green: #39ff14; + --danger: #ff2a6d; + --primary: #00f0ff; + --radius: 4px; + --glow-cyan: 0 0 20px rgba(0, 240, 255, 0.35); + --glow-magenta: 0 0 20px rgba(255, 0, 170, 0.35); + --font-display: "Orbitron", sans-serif; + --font-body: "Rajdhani", "Segoe UI", system-ui, sans-serif; + --font-mono: "JetBrains Mono", "Consolas", monospace; } * { box-sizing: border-box; } + body { margin: 0; - font-family: "Segoe UI", system-ui, sans-serif; + font-family: var(--font-body); + font-size: 16px; background: var(--bg); color: var(--text); min-height: 100vh; + position: relative; } +/* ── Cyber background layers ── */ +.cyber-bg { + position: fixed; + inset: 0; + z-index: -1; + overflow: hidden; + pointer-events: none; +} + +.cyber-grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(0, 240, 255, 0.04) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 240, 255, 0.04) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(ellipse 80% 70% at 50% 30%, black 20%, transparent 75%); +} + +.cyber-glow { + position: absolute; + inset: 0; + background: + radial-gradient(ellipse 60% 40% at 15% 0%, rgba(0, 240, 255, 0.12) 0%, transparent 55%), + radial-gradient(ellipse 50% 35% at 85% 10%, rgba(255, 0, 170, 0.1) 0%, transparent 50%), + radial-gradient(ellipse 70% 50% at 50% 100%, rgba(0, 240, 255, 0.06) 0%, transparent 60%); +} + +.cyber-scanlines { + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.12) 2px, + rgba(0, 0, 0, 0.12) 4px + ); + opacity: 0.35; + animation: scanline-drift 8s linear infinite; +} + +@keyframes scanline-drift { + 0% { transform: translateY(0); } + 100% { transform: translateY(4px); } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 8px rgba(57, 255, 20, 0.4), inset 0 0 8px rgba(57, 255, 20, 0.08); } + 50% { box-shadow: 0 0 16px rgba(57, 255, 20, 0.7), inset 0 0 12px rgba(57, 255, 20, 0.15); } +} + +@keyframes flicker-border { + 0%, 100% { border-color: rgba(0, 240, 255, 0.35); } + 50% { border-color: rgba(0, 240, 255, 0.55); } +} + +@keyframes data-flow { + 0% { background-position: 0% 50%; } + 100% { background-position: 200% 50%; } +} + +/* ── Auth / Login ── */ .auth-wrap { min-height: 100vh; display: grid; @@ -25,47 +103,167 @@ body { padding: 24px; } +.auth-card { + width: min(440px, 100%); + position: relative; +} + +.auth-card::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: calc(var(--radius) + 2px); + background: linear-gradient(135deg, var(--cyan), var(--magenta), var(--cyan)); + background-size: 200% 200%; + animation: data-flow 6s ease infinite; + z-index: -1; + opacity: 0.7; +} + +.auth-logo { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 900; + letter-spacing: 0.12em; + text-transform: uppercase; + margin: 0 0 4px; + background: linear-gradient(90deg, var(--cyan), #fff, var(--magenta)); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: none; + filter: drop-shadow(0 0 12px rgba(0, 240, 255, 0.4)); +} + +.auth-subtitle { + font-family: var(--font-mono); + font-size: 0.75rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--cyan); + opacity: 0.7; + margin: 0 0 20px; +} + +.auth-card h1.auth-logo + .muted, +.auth-card .auth-subtitle + .muted { + margin-bottom: 24px; +} + +/* ── Shared card styles ── */ .auth-card, .modal-card, .node-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; + backdrop-filter: blur(12px); + position: relative; } -.auth-card { width: min(420px, 100%); } -.auth-card h1 { margin: 0 0 8px; font-size: 1.5rem; } +.node-card { + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} +.node-card:hover { + transform: translateY(-2px); + border-color: var(--border-bright); + box-shadow: var(--glow-cyan); +} + +.node-card::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--cyan), var(--magenta), transparent); + opacity: 0.6; +} + +/* ── Topbar ── */ .topbar { display: flex; justify-content: space-between; align-items: center; - padding: 16px 24px; + padding: 14px 24px; border-bottom: 1px solid var(--border); - background: rgba(26, 35, 50, 0.8); - backdrop-filter: blur(8px); + background: rgba(5, 5, 8, 0.85); + backdrop-filter: blur(16px); position: sticky; top: 0; + z-index: 100; } -.container { max-width: 960px; margin: 0 auto; padding: 24px; } +.brand { + display: flex; + align-items: center; + gap: 12px; +} +.brand-icon { + width: 32px; + height: 32px; + color: var(--cyan); + filter: drop-shadow(0 0 6px rgba(0, 240, 255, 0.6)); +} + +.brand-name { + font-family: var(--font-display); + font-weight: 700; + font-size: 1rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--cyan); +} + +.brand-domain { + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--muted); + letter-spacing: 0.05em; +} + +.container { max-width: 1024px; margin: 0 auto; padding: 24px; } + +/* ── Hero ── */ .hero { display: flex; justify-content: space-between; align-items: center; gap: 16px; - margin-bottom: 24px; + margin-bottom: 28px; } -.hero h1 { margin: 0 0 8px; } +.hero h1 { + margin: 0 0 6px; + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.hero-tag { + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--cyan); + opacity: 0.75; + letter-spacing: 0.06em; +} .muted { color: var(--muted); } +/* ── Forms ── */ .form label, .field label { display: block; - margin: 12px 0 6px; - color: var(--muted); - font-size: 0.9rem; + margin: 14px 0 6px; + color: var(--cyan); + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; + opacity: 0.85; } input[type="text"], @@ -73,132 +271,290 @@ input[type="password"], input[readonly], input.copy-input { width: 100%; - padding: 10px 12px; - border-radius: 8px; + padding: 10px 14px; + border-radius: var(--radius); border: 1px solid var(--border); - background: #111827; + background: rgba(0, 0, 0, 0.5); color: var(--text); + font-family: var(--font-mono); + font-size: 0.85rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +input:focus { + outline: none; + border-color: var(--cyan); + box-shadow: 0 0 0 2px rgba(0, 240, 255, 0.15), var(--glow-cyan); } input.copy-input { cursor: pointer; - font-size: 0.82rem; + font-size: 0.78rem; + color: var(--cyan); + opacity: 0.9; } .link-hint { - margin: 6px 0 0; - font-size: 0.78rem; - line-height: 1.45; + margin: 8px 0 0; + font-size: 0.75rem; + line-height: 1.5; word-break: break-all; + font-family: var(--font-mono); } +/* ── Buttons ── */ .btn { border: 1px solid var(--border); - background: #111827; + background: rgba(0, 0, 0, 0.4); color: var(--text); - padding: 8px 14px; - border-radius: 8px; + padding: 9px 16px; + border-radius: var(--radius); cursor: pointer; text-decoration: none; - display: inline-block; + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--font-body); + font-weight: 600; + font-size: 0.9rem; + letter-spacing: 0.04em; + text-transform: uppercase; + transition: all 0.2s ease; } -.btn:hover { border-color: var(--primary); } -.btn.primary { - background: var(--primary); - border-color: var(--primary); - color: white; +.btn:hover { + border-color: var(--cyan); + color: var(--cyan); + box-shadow: var(--glow-cyan); } -.btn.ghost { background: transparent; } + +.btn.primary { + background: linear-gradient(135deg, rgba(0, 240, 255, 0.2), rgba(255, 0, 170, 0.15)); + border-color: var(--cyan); + color: var(--cyan); + font-family: var(--font-display); + font-size: 0.78rem; + letter-spacing: 0.1em; +} + +.btn.primary:hover { + background: linear-gradient(135deg, rgba(0, 240, 255, 0.35), rgba(255, 0, 170, 0.25)); + color: #fff; + text-shadow: 0 0 8px var(--cyan); +} + +.btn.ghost { + background: transparent; + font-size: 0.78rem; + letter-spacing: 0.08em; +} + .btn.danger { color: var(--danger); - border-color: rgba(239, 68, 68, 0.4); + border-color: rgba(255, 42, 109, 0.45); + font-size: 0.78rem; } -.node-list { display: grid; gap: 16px; } +.btn.danger:hover { + border-color: var(--danger); + box-shadow: 0 0 16px rgba(255, 42, 109, 0.4); + color: #fff; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ── Node list ── */ +.node-list { display: grid; gap: 20px; } + .node-head { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 12px; + margin-bottom: 16px; } -.node-head h2 { margin: 0; font-size: 1.1rem; } + +.node-head h2 { + margin: 0; + font-family: var(--font-display); + font-size: 1rem; + font-weight: 700; + letter-spacing: 0.06em; +} + .node-title { display: flex; align-items: center; - gap: 10px; + gap: 12px; flex-wrap: wrap; } .status-badge { - font-size: 0.75rem; - padding: 3px 10px; - border-radius: 999px; + font-family: var(--font-mono); + font-size: 0.65rem; + padding: 4px 10px; + border-radius: 2px; border: 1px solid var(--border); + letter-spacing: 0.1em; + text-transform: uppercase; } + .status-badge.online { - color: #34d399; - border-color: rgba(52, 211, 153, 0.45); - background: rgba(52, 211, 153, 0.12); + color: var(--green); + border-color: rgba(57, 255, 20, 0.5); + background: rgba(57, 255, 20, 0.08); + animation: pulse-glow 2.5s ease-in-out infinite; } + .status-badge.offline { color: var(--muted); - background: #111827; + background: rgba(0, 0, 0, 0.3); + border-color: rgba(107, 130, 153, 0.3); } -.status-text.ok { color: #34d399; } -.status-text.err { color: #f87171; } +.status-text.ok { + color: var(--green); + text-shadow: 0 0 10px rgba(57, 255, 20, 0.5); + font-family: var(--font-mono); +} + +.status-text.err { + color: var(--danger); + text-shadow: 0 0 10px rgba(255, 42, 109, 0.5); + font-family: var(--font-mono); +} + +/* ── Summary HUD bar ── */ .summary-bar { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; - margin-bottom: 24px; + margin-bottom: 28px; } + .summary-item { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); - padding: 14px 16px; + padding: 16px; + position: relative; + overflow: hidden; + animation: flicker-border 4s ease-in-out infinite; } + +.summary-item::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: linear-gradient(180deg, var(--cyan), var(--magenta)); +} + +.summary-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.summary-icon { + width: 16px; + height: 16px; + color: var(--cyan); + opacity: 0.8; +} + .summary-label { display: block; color: var(--muted); - font-size: 0.85rem; - margin-bottom: 6px; + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; } -.node-stats { - margin-bottom: 16px; - padding: 12px; - border-radius: 10px; - background: rgba(17, 24, 39, 0.55); - border: 1px solid var(--border); +.summary-item strong, +.summary-value { + font-family: var(--font-mono); + font-size: 1.1rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--cyan); + text-shadow: 0 0 12px rgba(0, 240, 255, 0.3); } + +.summary-item.speed-up strong { color: var(--green); text-shadow: 0 0 12px rgba(57, 255, 20, 0.3); } +.summary-item.speed-down strong { color: var(--magenta); text-shadow: 0 0 12px rgba(255, 0, 170, 0.3); } + +/* ── Node stats grid ── */ +.node-stats { + margin-bottom: 18px; + padding: 14px; + border-radius: var(--radius); + background: rgba(0, 0, 0, 0.35); + border: 1px solid rgba(0, 240, 255, 0.1); +} + .stat-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 10px; + gap: 12px; } + .stat-box { display: flex; flex-direction: column; - gap: 4px; + gap: 6px; + padding: 8px; + border-left: 2px solid rgba(0, 240, 255, 0.15); } + +.stat-box.speed { border-left-color: rgba(57, 255, 20, 0.3); } +.stat-box.total { border-left-color: rgba(255, 0, 170, 0.3); } + .stat-label { color: var(--muted); - font-size: 0.78rem; + font-family: var(--font-mono); + font-size: 0.65rem; + letter-spacing: 0.1em; + text-transform: uppercase; } + .stat-value { - font-size: 0.95rem; + font-family: var(--font-mono); + font-size: 0.92rem; + font-weight: 600; font-variant-numeric: tabular-nums; + color: var(--text); + transition: color 0.3s ease; +} + +.stat-box.speed .stat-value { color: var(--green); } +.stat-box.total .stat-value { color: var(--magenta); } + +.stat-value.stat-flash { + animation: stat-flash 0.4s ease; +} + +@keyframes stat-flash { + 0% { opacity: 0.5; transform: scale(0.98); } + 50% { opacity: 1; transform: scale(1.02); color: var(--cyan); } + 100% { transform: scale(1); } } .tag { - font-size: 0.8rem; + font-family: var(--font-mono); + font-size: 0.68rem; color: var(--muted); - background: #111827; - padding: 4px 8px; - border-radius: 999px; + background: rgba(0, 0, 0, 0.4); + padding: 4px 10px; + border-radius: 2px; + border: 1px solid rgba(107, 130, 153, 0.25); + letter-spacing: 0.06em; } .copy-row { @@ -207,45 +563,87 @@ input.copy-input { gap: 8px; } -.node-actions { margin-top: 16px; text-align: right; } +.node-actions { margin-top: 18px; text-align: right; } .alert { - background: rgba(239, 68, 68, 0.15); - border: 1px solid rgba(239, 68, 68, 0.35); - padding: 10px 12px; - border-radius: 8px; - margin: 12px 0; + background: rgba(255, 42, 109, 0.12); + border: 1px solid rgba(255, 42, 109, 0.4); + padding: 10px 14px; + border-radius: var(--radius); + margin: 14px 0; + font-family: var(--font-mono); + font-size: 0.82rem; + color: var(--danger); } +/* ── Toast ── */ .toast { position: fixed; right: 24px; bottom: 24px; - background: var(--card); - border: 1px solid var(--border); - padding: 12px 16px; - border-radius: 8px; - z-index: 20; + background: var(--card-solid); + border: 1px solid var(--cyan); + padding: 12px 18px; + border-radius: var(--radius); + z-index: 200; + font-family: var(--font-mono); + font-size: 0.82rem; + color: var(--cyan); + box-shadow: var(--glow-cyan); + animation: toast-in 0.3s ease; +} + +@keyframes toast-in { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } } .hidden { display: none !important; } +/* ── Modal ── */ .modal { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.55); + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(4px); display: grid; place-items: center; padding: 24px; - z-index: 10; + z-index: 150; +} + +.modal-card { + width: min(440px, 100%); + border-color: var(--border-bright); + box-shadow: var(--glow-cyan), var(--glow-magenta); +} + +.modal-card h3 { + margin: 0 0 16px; + font-family: var(--font-display); + font-size: 1rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--cyan); } -.modal-card { width: min(420px, 100%); } .modal-actions { display: flex; justify-content: flex-end; - gap: 8px; - margin-top: 20px; + gap: 10px; + margin-top: 24px; +} + +/* ── Empty state ── */ +.empty-nodes { + text-align: center; + padding: 48px 24px; + border: 1px dashed var(--border); + border-radius: var(--radius); + color: var(--muted); + font-family: var(--font-mono); + font-size: 0.85rem; + letter-spacing: 0.06em; } @media (max-width: 640px) { @@ -253,4 +651,5 @@ input.copy-input { .copy-row { grid-template-columns: 1fr; } .summary-bar { grid-template-columns: repeat(2, minmax(0, 1fr)); } .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .brand-domain { display: none; } } diff --git a/panel/templates/base.html b/panel/templates/base.html index 7e96bc2..413db2e 100644 --- a/panel/templates/base.html +++ b/panel/templates/base.html @@ -4,9 +4,17 @@ {% block title %}jiedian 面板{% endblock %} + + + + {% block body %}{% endblock %} {% block scripts %}{% endblock %} diff --git a/panel/templates/dashboard.html b/panel/templates/dashboard.html index 4b9d785..cfe1c4f 100644 --- a/panel/templates/dashboard.html +++ b/panel/templates/dashboard.html @@ -2,9 +2,16 @@ {% block title %}节点管理 · jiedian{% endblock %} {% block body %}
-
- jiedian 面板 - · {{ domain }} +
+ + + + + +
+
jiedian
+
{{ domain }}
+
退出
@@ -12,27 +19,47 @@
-

节点列表

-

VPS {{ vps_ip }} · Hysteria2 8443+

+

节点控制台

+

VPS {{ vps_ip }} · HYSTERIA2 UDP 8443+

- 在线节点 +
+ + + + 在线节点 +
-
-
- 实时上行 +
+
+ + + + 实时上行 +
-
-
- 实时下行 +
+
+ + + + 实时下行 +
-
- 统计服务 +
+ + + + 统计服务 +
检测中
@@ -54,31 +81,31 @@ 连接数 - -
+
实时 ↑ -
-
+
实时 ↓ -
-
+
累计 ↑ -
-
+
累计 ↓ -
- +
- +
diff --git a/panel/templates/login.html b/panel/templates/login.html index 10cd924..35e6851 100644 --- a/panel/templates/login.html +++ b/panel/templates/login.html @@ -3,7 +3,8 @@ {% block body %}
-

jiedian 管理面板

+

jiedian

+

// secure access terminal

登录后管理节点与分享链接

{% if error %}
{{ error }}
@@ -13,7 +14,7 @@ - +