From 4348ee799bcef631cf6189849a5c76413316c8e4 Mon Sep 17 00:00:00 2001 From: chy ky Date: Wed, 4 Mar 2026 15:00:52 +0800 Subject: [PATCH] Redesign admin UI with Chinese light theme --- frontend/src/App.vue | 116 +++++++++++----------- frontend/src/router/index.js | 18 ++-- frontend/src/styles.css | 164 +++++++++++++++---------------- frontend/src/views/Bindings.vue | 140 +++++++++++++------------- frontend/src/views/Dashboard.vue | 86 ++++++++-------- frontend/src/views/Login.vue | 72 +++++++------- frontend/src/views/Logs.vue | 112 +++++---------------- frontend/src/views/Settings.vue | 79 +++++++-------- 8 files changed, 363 insertions(+), 424 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index eb2443e..2c9f16b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -14,14 +14,14 @@ let clearAnnouncementTimer let unsubscribeAnnouncements = () => {} const navItems = [ - { label: 'Dashboard', name: 'dashboard', icon: 'DataAnalysis' }, - { label: 'Bindings', name: 'bindings', icon: 'Connection' }, - { label: 'Logs', name: 'logs', icon: 'WarningFilled' }, - { label: 'Settings', name: 'settings', icon: 'Setting' }, + { label: '总览看板', name: 'dashboard', icon: 'DataAnalysis' }, + { label: '绑定管理', name: 'bindings', icon: 'Connection' }, + { label: '拦截日志', name: 'logs', icon: 'WarningFilled' }, + { label: '运行设置', name: 'settings', icon: 'Setting' }, ] const hideShell = computed(() => Boolean(route.meta.public)) -const currentSection = computed(() => route.meta.kicker || 'Operations') +const currentSection = computed(() => route.meta.kicker || '控制台') function updateClock() { clockLabel.value = new Intl.DateTimeFormat(undefined, { @@ -65,7 +65,7 @@ onBeforeUnmount(() => {
- +
@@ -74,8 +74,8 @@ onBeforeUnmount(() => {
S

Key-IP Sentinel

-

Control Plane

-

First-use bind enforcement edge

+

安全控制台

+

API Key 首次使用 IP 绑定网关

@@ -93,23 +93,23 @@ onBeforeUnmount(() => {
- Surface - Admin UI - JWT protected + 入口 + 管理后台 + JWT 鉴权
- Proxy - Streaming - SSE passthrough + 网关 + 流式代理 + 支持 SSE 透传
@@ -119,20 +119,20 @@ onBeforeUnmount(() => {

{{ currentSection }}

{{ route.meta.title || 'Sentinel' }}

-

Edge policy, runtime settings, and operator visibility in one secure surface.

+

围绕绑定记录、拦截日志和运行设置的统一运维入口。

- Mode - Secure Proxy + 模式 + 安全代理
- Updated + 时间 {{ clockLabel }}
- Logout + 退出登录
@@ -147,10 +147,10 @@ onBeforeUnmount(() => { .shell { position: relative; display: grid; - grid-template-columns: 300px minmax(0, 1fr); - gap: 24px; + grid-template-columns: 276px minmax(0, 1fr); + gap: 18px; min-height: 100vh; - padding: 24px; + padding: 18px; } .shell-sidebar, @@ -162,8 +162,8 @@ onBeforeUnmount(() => { .shell-sidebar { display: flex; flex-direction: column; - gap: 28px; - padding: 28px; + gap: 20px; + padding: 22px; } .brand-block { @@ -175,19 +175,19 @@ onBeforeUnmount(() => { .brand-mark { display: grid; place-items: center; - width: 56px; - height: 56px; - border-radius: 18px; - background: linear-gradient(135deg, rgba(17, 231, 181, 0.95), rgba(21, 132, 214, 0.95)); - color: #071016; - font-size: 1.45rem; + width: 48px; + height: 48px; + border-radius: 16px; + background: linear-gradient(135deg, #6ea7ff, #86c8ff); + color: #ffffff; + font-size: 1.2rem; font-weight: 800; } .brand-title, .page-title { margin: 0; - font-size: clamp(1.5rem, 2vw, 2.1rem); + font-size: clamp(1.2rem, 1.6vw, 1.6rem); } .nav-list { @@ -199,19 +199,20 @@ onBeforeUnmount(() => { display: flex; align-items: center; gap: 12px; - padding: 14px 16px; - border-radius: 18px; + padding: 12px 14px; + border-radius: 16px; color: var(--sentinel-ink-soft); text-decoration: none; transition: transform 160ms ease, background 160ms ease, color 160ms ease, box-shadow 160ms ease; + font-size: 0.95rem; } .nav-link:hover, .nav-link.is-active { color: var(--sentinel-ink); - background: rgba(7, 176, 147, 0.14); - box-shadow: inset 0 0 0 1px rgba(7, 176, 147, 0.18); - transform: translateX(4px); + background: rgba(114, 163, 255, 0.14); + box-shadow: inset 0 0 0 1px rgba(114, 163, 255, 0.2); + transform: translateX(3px); } .nav-icon { @@ -220,15 +221,16 @@ onBeforeUnmount(() => { .sidebar-note { margin-top: auto; - padding: 18px; + padding: 16px; border-radius: 22px; - background: linear-gradient(180deg, rgba(8, 31, 45, 0.92), rgba(10, 26, 35, 0.8)); - color: #f3fffd; + background: linear-gradient(180deg, rgba(244, 248, 255, 0.98), rgba(235, 243, 255, 0.92)); + color: var(--sentinel-ink); + border: 1px solid rgba(122, 164, 255, 0.18); } .sidebar-note h3 { margin: 10px 0; - font-size: 1.15rem; + font-size: 1rem; } .shell-main { @@ -236,7 +238,7 @@ onBeforeUnmount(() => { z-index: 1; display: flex; flex-direction: column; - gap: 24px; + gap: 18px; min-width: 0; } @@ -244,8 +246,8 @@ onBeforeUnmount(() => { display: flex; align-items: center; justify-content: space-between; - gap: 20px; - padding: 22px 26px; + gap: 16px; + padding: 18px 20px; } .header-actions { @@ -257,7 +259,7 @@ onBeforeUnmount(() => { .shell-content { display: flex; flex-direction: column; - gap: 24px; + gap: 18px; min-width: 0; } @@ -272,19 +274,19 @@ onBeforeUnmount(() => { } .shell-glow--mint { - top: 80px; - right: 160px; - width: 240px; - height: 240px; - background: rgba(17, 231, 181, 0.22); + top: 60px; + right: 120px; + width: 220px; + height: 220px; + background: rgba(132, 196, 255, 0.2); } .shell-glow--amber { - bottom: 100px; - left: 420px; - width: 280px; - height: 280px; - background: rgba(255, 170, 76, 0.18); + bottom: 80px; + left: 360px; + width: 260px; + height: 260px; + background: rgba(177, 221, 255, 0.18); } @media (max-width: 1080px) { diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 73ef3d1..76d0eff 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -16,7 +16,7 @@ const router = createRouter({ component: Login, meta: { public: true, - title: 'Admin Login', + title: '管理员登录', }, }, { @@ -28,8 +28,8 @@ const router = createRouter({ name: 'dashboard', component: Dashboard, meta: { - title: 'Traffic Pulse', - kicker: 'Observability', + title: '总览看板', + kicker: '运行概览', }, }, { @@ -37,8 +37,8 @@ const router = createRouter({ name: 'bindings', component: Bindings, meta: { - title: 'Token Bindings', - kicker: 'Control', + title: '绑定管理', + kicker: '绑定控制', }, }, { @@ -46,8 +46,8 @@ const router = createRouter({ name: 'logs', component: Logs, meta: { - title: 'Intercept Logs', - kicker: 'Audit', + title: '拦截日志', + kicker: '审计追踪', }, }, { @@ -55,8 +55,8 @@ const router = createRouter({ name: 'settings', component: Settings, meta: { - title: 'Runtime Settings', - kicker: 'Operations', + title: '运行设置', + kicker: '运行配置', }, }, ], diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 704b46c..db5b7cd 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,22 +1,22 @@ :root { - --sentinel-bg: #08131c; - --sentinel-bg-soft: #102734; - --sentinel-panel: rgba(252, 255, 255, 0.82); - --sentinel-panel-strong: rgba(255, 255, 255, 0.9); - --sentinel-border: rgba(255, 255, 255, 0.24); - --sentinel-ink: #09161e; - --sentinel-ink-soft: #57717d; - --sentinel-accent: #07b093; - --sentinel-accent-deep: #0d7e8b; - --sentinel-warn: #ef7f41; - --sentinel-danger: #dc4f53; - --sentinel-shadow: 0 30px 80px rgba(2, 12, 18, 0.22); - --el-color-primary: #0b9e88; - --el-color-success: #1aa36f; - --el-color-warning: #ef7f41; - --el-color-danger: #dc4f53; + --sentinel-bg: #eef5ff; + --sentinel-bg-soft: #dfeefe; + --sentinel-panel: rgba(255, 255, 255, 0.9); + --sentinel-panel-strong: rgba(255, 255, 255, 0.96); + --sentinel-border: rgba(113, 157, 226, 0.18); + --sentinel-ink: #17324d; + --sentinel-ink-soft: #66809c; + --sentinel-accent: #4d8ff7; + --sentinel-accent-deep: #2d6fd5; + --sentinel-warn: #f29a44; + --sentinel-danger: #df5b67; + --sentinel-shadow: 0 20px 48px rgba(46, 92, 146, 0.12); + --el-color-primary: #4d8ff7; + --el-color-success: #36a980; + --el-color-warning: #f29a44; + --el-color-danger: #df5b67; color: var(--sentinel-ink); - font-family: "Avenir Next", "Segoe UI Variable", "Segoe UI", "PingFang SC", sans-serif; + font-family: "PingFang SC", "Microsoft YaHei UI", "Segoe UI Variable", "Noto Sans SC", sans-serif; line-height: 1.5; font-weight: 400; } @@ -32,11 +32,11 @@ html { min-height: 100%; - color-scheme: dark; + color-scheme: light; background: - radial-gradient(circle at top left, rgba(12, 193, 152, 0.22), transparent 34%), - radial-gradient(circle at top right, rgba(255, 170, 76, 0.18), transparent 30%), - linear-gradient(180deg, #09131d 0%, #0d1d29 35%, #112d3d 100%); + radial-gradient(circle at top left, rgba(146, 198, 255, 0.48), transparent 34%), + radial-gradient(circle at top right, rgba(215, 234, 255, 0.72), transparent 32%), + linear-gradient(180deg, #f4f8ff 0%, #edf5ff 40%, #e5f0fd 100%); } body { @@ -62,10 +62,10 @@ body::before { position: fixed; inset: 0; background: - linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px); + linear-gradient(rgba(77, 143, 247, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(77, 143, 247, 0.05) 1px, transparent 1px); background-size: 34px 34px; - mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent 95%); + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.32), transparent 95%); pointer-events: none; } @@ -105,22 +105,22 @@ body::before { .panel { background: var(--sentinel-panel); border: 1px solid var(--sentinel-border); - border-radius: 28px; - backdrop-filter: blur(18px); + border-radius: 24px; + backdrop-filter: blur(12px); box-shadow: var(--sentinel-shadow); min-width: 0; } .glass-panel { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(250, 255, 252, 0.74)); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(244, 249, 255, 0.84)); } .eyebrow { margin: 0; color: var(--sentinel-accent-deep); text-transform: uppercase; - letter-spacing: 0.16em; - font-size: 0.74rem; + letter-spacing: 0.14em; + font-size: 0.68rem; font-weight: 700; } @@ -130,19 +130,19 @@ body::before { .page-grid { display: grid; - gap: 24px; + gap: 18px; } .hero-panel { position: relative; - padding: 26px; + padding: 20px 22px; overflow: hidden; } .hero-layout { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr); - gap: 20px; + gap: 16px; align-items: stretch; } @@ -177,7 +177,7 @@ body::before { right: -40px; width: 220px; height: 220px; - background: radial-gradient(circle, rgba(7, 176, 147, 0.28), transparent 70%); + background: radial-gradient(circle, rgba(98, 168, 255, 0.22), transparent 70%); pointer-events: none; } @@ -187,24 +187,24 @@ body::before { .page-title, .login-stage h1 { margin: 10px 0 8px; - font-size: 1.4rem; + font-size: 1.16rem; text-wrap: balance; } .metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 16px; + gap: 12px; } .metric-card { position: relative; overflow: hidden; - padding: 20px; + padding: 16px; } .metric-card--enhanced { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(244, 252, 249, 0.78)); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(243, 248, 255, 0.88)); } .metric-card::before { @@ -214,20 +214,20 @@ body::before { width: 140px; height: 140px; border-radius: 999px; - background: radial-gradient(circle, rgba(7, 176, 147, 0.16), transparent 70%); + background: radial-gradient(circle, rgba(98, 168, 255, 0.14), transparent 70%); } .metric-card[data-accent="amber"]::before { - background: radial-gradient(circle, rgba(239, 127, 65, 0.16), transparent 70%); + background: radial-gradient(circle, rgba(242, 154, 68, 0.18), transparent 70%); } .metric-card[data-accent="slate"]::before { - background: radial-gradient(circle, rgba(54, 97, 135, 0.16), transparent 70%); + background: radial-gradient(circle, rgba(113, 157, 226, 0.16), transparent 70%); } .metric-value { margin: 10px 0 0; - font-size: clamp(1.8rem, 3vw, 2.5rem); + font-size: clamp(1.45rem, 2.3vw, 2rem); font-weight: 800; font-variant-numeric: tabular-nums; } @@ -240,7 +240,7 @@ body::before { .content-grid { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(320px, 0.9fr); - gap: 24px; + gap: 18px; } .content-grid--balanced { @@ -250,7 +250,7 @@ body::before { .chart-card, .table-card, .form-card { - padding: 24px; + padding: 18px; } .chart-surface { @@ -294,7 +294,7 @@ body::before { .toolbar { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 10px; align-items: center; justify-content: space-between; } @@ -310,21 +310,21 @@ body::before { .data-table .el-table { --el-table-border-color: rgba(9, 22, 30, 0.08); - --el-table-header-bg-color: rgba(7, 176, 147, 0.08); - --el-table-row-hover-bg-color: rgba(7, 176, 147, 0.05); - border-radius: 18px; + --el-table-header-bg-color: rgba(92, 151, 255, 0.1); + --el-table-row-hover-bg-color: rgba(92, 151, 255, 0.05); + border-radius: 16px; overflow: hidden; } .el-button { - min-height: 44px; + min-height: 38px; } .el-input__wrapper, .el-select__wrapper, .el-textarea__inner, .el-date-editor .el-input__wrapper { - min-height: 44px; + min-height: 38px; } .soft-grid { @@ -333,15 +333,15 @@ body::before { } .support-card { - padding: 20px; - border-radius: 24px; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(243, 251, 248, 0.72)); - border: 1px solid rgba(255, 255, 255, 0.32); + padding: 16px; + border-radius: 20px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(242, 247, 255, 0.82)); + border: 1px solid rgba(113, 157, 226, 0.14); } .support-card h4 { margin: 10px 0 8px; - font-size: 1.08rem; + font-size: 0.98rem; } .support-card p { @@ -384,10 +384,10 @@ body::before { } .insight-card { - padding: 18px 20px; - border-radius: 22px; - background: linear-gradient(180deg, rgba(8, 31, 45, 0.92), rgba(12, 24, 33, 0.8)); - color: #f2fffd; + padding: 16px 18px; + border-radius: 18px; + background: linear-gradient(180deg, rgba(80, 134, 236, 0.95), rgba(70, 123, 224, 0.9)); + color: #f7fbff; } .insight-value { @@ -425,8 +425,8 @@ body::before { min-height: 100vh; display: grid; grid-template-columns: 1.1fr 0.9fr; - gap: 24px; - padding: 24px; + gap: 18px; + padding: 18px; } .login-stage, @@ -436,20 +436,20 @@ body::before { } .login-stage { - padding: 42px; + padding: 30px; display: flex; flex-direction: column; - gap: 28px; + gap: 22px; justify-content: space-between; - color: #f7fffe; + color: #eef6ff; background: - radial-gradient(circle at top left, rgba(17, 231, 181, 0.24), transparent 28%), - linear-gradient(160deg, rgba(8, 24, 34, 0.95), rgba(15, 37, 50, 0.92)); + radial-gradient(circle at top left, rgba(255, 255, 255, 0.24), transparent 30%), + linear-gradient(160deg, rgba(92, 151, 255, 0.98), rgba(109, 176, 255, 0.92)); } .login-stage h1 { margin: 12px 0; - font-size: clamp(2.4rem, 4vw, 4rem); + font-size: clamp(2rem, 3vw, 3rem); line-height: 0.96; } @@ -462,14 +462,14 @@ body::before { .login-card { display: grid; place-items: center; - padding: 36px; + padding: 24px; } .login-card-inner { width: min(100%, 460px); - padding: 34px; + padding: 28px; background: var(--sentinel-panel-strong); - border-radius: 32px; + border-radius: 26px; border: 1px solid var(--sentinel-border); box-shadow: var(--sentinel-shadow); } @@ -480,10 +480,10 @@ body::before { gap: 8px; padding: 8px 12px; border-radius: 999px; - background: rgba(7, 176, 147, 0.12); + background: rgba(92, 151, 255, 0.12); color: var(--sentinel-accent-deep); font-weight: 700; - font-size: 0.82rem; + font-size: 0.78rem; font-variant-numeric: tabular-nums; } @@ -516,10 +516,10 @@ body::before { .rail-card { display: grid; gap: 4px; - padding: 14px 16px; + padding: 12px 14px; border-radius: 18px; - background: rgba(255, 255, 255, 0.44); - border: 1px solid rgba(255, 255, 255, 0.26); + background: rgba(255, 255, 255, 0.74); + border: 1px solid rgba(113, 157, 226, 0.14); } .rail-label, @@ -554,10 +554,10 @@ body::before { display: grid; gap: 2px; min-width: 140px; - padding: 10px 14px; + padding: 9px 12px; border-radius: 18px; - background: rgba(255, 255, 255, 0.56); - border: 1px solid rgba(255, 255, 255, 0.26); + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(113, 157, 226, 0.16); } .header-chip strong { @@ -571,16 +571,16 @@ body::before { } .hero-stat { - padding: 14px 16px; + padding: 12px 14px; border-radius: 20px; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(248, 255, 252, 0.64)); - border: 1px solid rgba(255, 255, 255, 0.36); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(243, 248, 255, 0.78)); + border: 1px solid rgba(113, 157, 226, 0.16); } .hero-stat strong { display: block; margin-top: 6px; - font-size: 1.35rem; + font-size: 1.15rem; font-variant-numeric: tabular-nums; } @@ -618,7 +618,7 @@ body::before { .filter-label { color: var(--sentinel-ink); - font-size: 0.82rem; + font-size: 0.78rem; font-weight: 700; } diff --git a/frontend/src/views/Bindings.vue b/frontend/src/views/Bindings.vue index 656fcc2..e8f2177 100644 --- a/frontend/src/views/Bindings.vue +++ b/frontend/src/views/Bindings.vue @@ -138,22 +138,22 @@ function isDormant(row) { function formatLastSeen(value) { if (!value) { - return 'No activity' + return '暂无记录' } const elapsedHours = Math.floor((Date.now() - new Date(value).getTime()) / 3600000) if (elapsedHours < 1) { - return 'Active within 1h' + return '1 小时内活跃' } if (elapsedHours < 24) { - return `Active ${elapsedHours}h ago` + return `${elapsedHours} 小时前活跃` } const days = Math.floor(elapsedHours / 24) if (days < staleWindowDays) { - return `Active ${days}d ago` + return `${days} 天前活跃` } - return `Dormant ${days}d` + return `沉寂 ${days} 天` } function statusTone(row) { @@ -165,19 +165,19 @@ function statusTone(row) { function statusText(row) { if (row.status === 2) { - return 'Banned' + return '已封禁' } - return isDormant(row) ? 'Dormant' : 'Healthy' + return isDormant(row) ? '沉寂' : '正常' } function ipTypeLabel(boundIp) { if (!boundIp) { - return 'Unknown' + return '未知' } if (!boundIp.includes('/')) { - return 'Single IP' + return '单个 IP' } - return boundIp.endsWith('/32') || boundIp.endsWith('/128') ? 'Single IP' : 'CIDR' + return boundIp.endsWith('/32') || boundIp.endsWith('/128') ? '单个 IP' : 'CIDR 网段' } function rowClassName({ row }) { @@ -193,9 +193,9 @@ function rowClassName({ row }) { async function copyValue(value, label) { try { await navigator.clipboard.writeText(String(value)) - ElMessage.success(`${label} copied.`) + ElMessage.success(`${label}已复制。`) } catch { - ElMessage.error(`Failed to copy ${label.toLowerCase()}.`) + ElMessage.error(`复制${label}失败。`) } } @@ -204,7 +204,7 @@ async function loadBindings() { const data = await fetchBindings(requestParams()) rows.value = data.items total.value = data.total - }, 'Failed to load bindings.') + }, '加载绑定列表失败。') } async function refreshBindings() { @@ -244,12 +244,12 @@ function openEdit(row) { async function submitEdit() { if (!form.bound_ip) { - ElMessage.warning('Provide a CIDR or single IP.') + ElMessage.warning('请输入 CIDR 或单个 IP。') return } try { - await run(() => updateBindingIp({ id: form.id, bound_ip: form.bound_ip }), 'Failed to update binding.') - ElMessage.success('Binding updated.') + await run(() => updateBindingIp({ id: form.id, bound_ip: form.bound_ip }), '更新绑定失败。') + ElMessage.success('绑定地址已更新。') dialogVisible.value = false await refreshBindings() } catch {} @@ -257,12 +257,12 @@ async function submitEdit() { async function confirmAction(title, action) { try { - await ElMessageBox.confirm(title, 'Confirm action', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', + await ElMessageBox.confirm(title, '确认操作', { + confirmButtonText: '确认', + cancelButtonText: '取消', type: 'warning', }) - await run(action, 'Operation failed.') + await run(action, '操作失败。') await refreshBindings() } catch (error) { if (error === 'cancel') { @@ -295,63 +295,61 @@ watch(