feat(frontend): 打磨管理台交互体验与可访问性

- 优化 Dashboard、Bindings、Logs、Settings 的布局、筛选区与信息层级
- 增加筛选状态同步、未保存提醒、运行时反馈和趋势表视图
- 补充跳转主内容、aria live、键盘导航与移动端触控细节
This commit is contained in:
2026-03-04 00:18:59 +08:00
parent 0a1eeb9ddf
commit 380a78283e
12 changed files with 675 additions and 110 deletions

View File

@@ -7,7 +7,7 @@ import PageHero from '../components/PageHero.vue'
import { useAsyncAction } from '../composables/useAsyncAction'
import { fetchDashboard } from '../api'
import { usePolling } from '../composables/usePolling'
import { formatCompactNumber, formatDateTime, formatPercent } from '../utils/formatters'
import { formatCompactNumber, formatDate, formatDateTime, formatPercent } from '../utils/formatters'
const dashboard = ref({
today: { total: 0, allowed: 0, intercepted: 0 },
@@ -111,14 +111,20 @@ async function loadDashboard() {
}, 'Failed to load dashboard.')
}
async function refreshDashboard() {
try {
await loadDashboard()
} catch {}
}
function resizeChart() {
chart?.resize()
}
const { start: startPolling, stop: stopPolling } = usePolling(loadDashboard, 30000)
const { start: startPolling, stop: stopPolling } = usePolling(refreshDashboard, 30000)
onMounted(async () => {
await loadDashboard()
await refreshDashboard()
startPolling()
window.addEventListener('resize', resizeChart)
})
@@ -151,7 +157,7 @@ onBeforeUnmount(() => {
</template>
<template #actions>
<el-button :loading="loading" type="primary" plain @click="loadDashboard">Refresh dashboard</el-button>
<el-button :loading="loading" type="primary" plain @click="refreshDashboard">Refresh Dashboard</el-button>
</template>
</PageHero>
@@ -195,6 +201,27 @@ onBeforeUnmount(() => {
</div>
</div>
<div ref="chartElement" class="chart-surface" />
<div class="trend-summary">
<p class="eyebrow">Trend table</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>
</tr>
</thead>
<tbody>
<tr v-for="item in dashboard.trend" :key="item.date">
<td>{{ formatDate(item.date) }}</td>
<td>{{ formatCompactNumber(item.allowed) }}</td>
<td>{{ formatCompactNumber(item.intercepted) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</article>
<article class="table-card panel">
@@ -206,12 +233,11 @@ onBeforeUnmount(() => {
<div v-if="!dashboard.recent_intercepts.length" class="empty-state">No intercepts recorded yet.</div>
<div v-else class="table-stack" style="margin-top: 18px;">
<div
<div v-else class="table-stack table-stack--spaced">
<article
v-for="item in dashboard.recent_intercepts"
:key="item.id"
class="insight-card"
style="padding: 16px; border-radius: 20px;"
class="insight-card insight-card--compact"
>
<div class="toolbar">
<strong>{{ item.token_display }}</strong>
@@ -222,7 +248,7 @@ onBeforeUnmount(() => {
<p class="insight-note">Bound CIDR: {{ item.bound_ip }}</p>
<p class="insight-note">Attempt IP: {{ item.attempt_ip }}</p>
<p class="insight-note">{{ formatDateTime(item.intercepted_at) }}</p>
</div>
</article>
</div>
</article>
</section>