Redesign admin UI with Chinese light theme

This commit is contained in:
2026-03-04 15:00:52 +08:00
parent f212b68c2c
commit 4348ee799b
8 changed files with 363 additions and 424 deletions

View File

@@ -45,8 +45,8 @@ async function renderChart() {
chart ||= echarts.init(chartElement.value)
chart.setOption({
animationDuration: 500,
color: ['#0b9e88', '#ef7f41'],
animationDuration: 400,
color: ['#4d8ff7', '#f29a44'],
grid: {
left: 24,
right: 24,
@@ -57,13 +57,13 @@ async function renderChart() {
legend: {
top: 0,
textStyle: {
color: '#516a75',
color: '#5f7893',
fontWeight: 600,
},
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(8, 24, 34, 0.9)',
backgroundColor: 'rgba(34, 67, 108, 0.92)',
borderWidth: 0,
textStyle: {
color: '#f7fffe',
@@ -73,30 +73,30 @@ async function renderChart() {
type: 'category',
boundaryGap: false,
data: dashboard.value.trend.map((item) => item.date.slice(5)),
axisLine: { lineStyle: { color: 'rgba(9, 22, 30, 0.18)' } },
axisLabel: { color: '#516a75', fontWeight: 600 },
axisLine: { lineStyle: { color: 'rgba(39, 74, 110, 0.16)' } },
axisLabel: { color: '#5f7893', fontWeight: 600 },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(9, 22, 30, 0.08)' } },
axisLabel: { color: '#516a75' },
splitLine: { lineStyle: { color: 'rgba(39, 74, 110, 0.08)' } },
axisLabel: { color: '#5f7893' },
},
series: [
{
name: 'Allowed',
name: '放行',
type: 'line',
smooth: true,
showSymbol: false,
areaStyle: { color: 'rgba(11, 158, 136, 0.14)' },
areaStyle: { color: 'rgba(77, 143, 247, 0.14)' },
lineStyle: { width: 3 },
data: dashboard.value.trend.map((item) => item.allowed),
},
{
name: 'Intercepted',
name: '拦截',
type: 'line',
smooth: true,
showSymbol: false,
areaStyle: { color: 'rgba(239, 127, 65, 0.12)' },
areaStyle: { color: 'rgba(242, 154, 68, 0.12)' },
lineStyle: { width: 3 },
data: dashboard.value.trend.map((item) => item.intercepted),
},
@@ -108,7 +108,7 @@ async function loadDashboard() {
await run(async () => {
dashboard.value = await fetchDashboard()
await renderChart()
}, 'Failed to load dashboard.')
}, '加载看板失败。')
}
async function refreshDashboard() {
@@ -139,51 +139,51 @@ onBeforeUnmount(() => {
<template>
<div class="page-grid">
<PageHero
eyebrow="Traffic pulse"
title="Edge decisions and security drift in one pass"
description="The dashboard combines live proxy metrics with persisted intercept records so security events remain visible even if Redis rolls over."
eyebrow="运行概览"
title="在一个页面里查看放行、拦截与绑定状态"
description="看板汇总今日代理结果、绑定规模和最近拦截记录,便于快速判断系统是否稳定运行。"
>
<template #aside>
<div class="hero-stat-pair">
<div class="hero-stat">
<span class="eyebrow">Intercept rate</span>
<span class="eyebrow">拦截率</span>
<strong>{{ formatPercent(interceptRate) }}</strong>
</div>
<div class="hero-stat">
<span class="eyebrow">Active share</span>
<span class="eyebrow">活跃占比</span>
<strong>{{ formatPercent(bindingCoverage) }}</strong>
</div>
</div>
</template>
<template #actions>
<el-button :loading="loading" type="primary" plain @click="refreshDashboard">Refresh Dashboard</el-button>
<el-button :loading="loading" type="primary" plain @click="refreshDashboard">刷新看板</el-button>
</template>
</PageHero>
<section class="metric-grid">
<MetricTile
eyebrow="Today"
eyebrow="今日总量"
:value="formatCompactNumber(dashboard.today.total)"
note="Total edge decisions recorded today."
note="今天经过网关处理的请求总数。"
accent="slate"
/>
<MetricTile
eyebrow="Allowed"
eyebrow="放行请求"
:value="formatCompactNumber(dashboard.today.allowed)"
note="Requests that passed binding enforcement."
accent="mint"
note="通过绑定校验并成功转发的请求。"
accent="slate"
/>
<MetricTile
eyebrow="Intercepted"
eyebrow="拦截请求"
:value="formatCompactNumber(dashboard.today.intercepted)"
note="Requests blocked for CIDR mismatch or banned keys."
note="因 IP 不匹配或 Token 被封禁而拦截。"
accent="amber"
/>
<MetricTile
eyebrow="Bindings"
eyebrow="当前绑定"
:value="formatCompactNumber(dashboard.bindings.active)"
:note="`Active bindings, with ${formatCompactNumber(dashboard.bindings.banned)} banned keys in reserve.`"
:note="`活跃绑定 ${formatCompactNumber(dashboard.bindings.active)} 条,封禁 ${formatCompactNumber(dashboard.bindings.banned)} 条。`"
accent="slate"
/>
</section>
@@ -192,24 +192,24 @@ onBeforeUnmount(() => {
<article class="chart-card panel">
<div class="toolbar">
<div>
<p class="eyebrow">7-day trend</p>
<h3 class="section-title">Allowed vs intercepted flow</h3>
<p class="eyebrow">7 日趋势</p>
<h3 class="section-title"> 7 天放行与拦截趋势</h3>
</div>
<div class="inline-meta">
<el-tag round effect="plain" type="success">30s auto refresh</el-tag>
<span class="muted">Redis metrics with PostgreSQL intercept backfill.</span>
<el-tag round effect="plain" type="primary">30 秒自动刷新</el-tag>
<span class="muted">结合 Redis 指标与 PostgreSQL 日志统计</span>
</div>
</div>
<div ref="chartElement" class="chart-surface" />
<div class="trend-summary">
<p class="eyebrow">Trend table</p>
<p class="eyebrow">趋势明细</p>
<div class="trend-table-wrap">
<table class="trend-table">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Allowed</th>
<th scope="col">Intercepted</th>
<th scope="col">日期</th>
<th scope="col">放行</th>
<th scope="col">拦截</th>
</tr>
</thead>
<tbody>
@@ -226,12 +226,12 @@ onBeforeUnmount(() => {
<article class="table-card panel">
<div class="table-toolbar-block">
<p class="eyebrow">Recent blocks</p>
<h3 class="section-title">Latest intercepted requests</h3>
<p class="muted">Operators can triage repeated misuse and verify whether alert escalation has already fired.</p>
<p class="eyebrow">最新事件</p>
<h3 class="section-title">最近拦截记录</h3>
<p class="muted">用于快速确认异常来源告警状态和是否需要进一步处置</p>
</div>
<div v-if="!dashboard.recent_intercepts.length" class="empty-state">No intercepts recorded yet.</div>
<div v-if="!dashboard.recent_intercepts.length" class="empty-state">当前还没有拦截记录</div>
<div v-else class="table-stack table-stack--spaced">
<article
@@ -242,11 +242,11 @@ onBeforeUnmount(() => {
<div class="toolbar">
<strong>{{ item.token_display }}</strong>
<el-tag :type="item.alerted ? 'danger' : 'warning'" round>
{{ item.alerted ? 'Alerted' : 'Pending' }}
{{ item.alerted ? '已告警' : '待观察' }}
</el-tag>
</div>
<p class="insight-note">Bound CIDR: {{ item.bound_ip }}</p>
<p class="insight-note">Attempt IP: {{ item.attempt_ip }}</p>
<p class="insight-note">绑定地址{{ item.bound_ip }}</p>
<p class="insight-note">尝试地址{{ item.attempt_ip }}</p>
<p class="insight-note">{{ formatDateTime(item.intercepted_at) }}</p>
</article>
</div>