feat(frontend): 打磨管理台交互体验与可访问性
- 优化 Dashboard、Bindings、Logs、Settings 的布局、筛选区与信息层级 - 增加筛选状态同步、未保存提醒、运行时反馈和趋势表视图 - 补充跳转主内容、aria live、键盘导航与移动端触控细节
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user