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

@@ -3,11 +3,15 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { clearAuthToken } from './api'
import { subscribeToAnnouncements } from './utils/liveRegion'
const route = useRoute()
const router = useRouter()
const clockLabel = ref('')
const liveMessage = ref('')
let clockTimer
let clearAnnouncementTimer
let unsubscribeAnnouncements = () => {}
const navItems = [
{ label: 'Dashboard', name: 'dashboard', icon: 'DataAnalysis' },
@@ -34,19 +38,34 @@ async function logout() {
onMounted(() => {
updateClock()
clockTimer = window.setInterval(updateClock, 60000)
unsubscribeAnnouncements = subscribeToAnnouncements((message) => {
liveMessage.value = message
if (clearAnnouncementTimer) {
window.clearTimeout(clearAnnouncementTimer)
}
clearAnnouncementTimer = window.setTimeout(() => {
liveMessage.value = ''
}, 3000)
})
})
onBeforeUnmount(() => {
if (clockTimer) {
window.clearInterval(clockTimer)
}
if (clearAnnouncementTimer) {
window.clearTimeout(clearAnnouncementTimer)
}
unsubscribeAnnouncements()
})
</script>
<template>
<p class="sr-only" aria-live="polite" role="status">{{ liveMessage }}</p>
<router-view v-if="hideShell" />
<div v-else class="shell">
<a class="skip-link" href="#main-content">Skip to main content</a>
<div class="shell-glow shell-glow--mint" />
<div class="shell-glow shell-glow--amber" />
@@ -60,7 +79,7 @@ onBeforeUnmount(() => {
</div>
</div>
<nav class="nav-list">
<nav class="nav-list" aria-label="Primary">
<router-link
v-for="item in navItems"
:key="item.name"
@@ -95,11 +114,11 @@ onBeforeUnmount(() => {
</div>
</aside>
<main class="shell-main">
<main class="shell-main" aria-labelledby="page-title">
<header class="shell-header panel">
<div class="header-copy">
<p class="eyebrow">{{ currentSection }}</p>
<h2 class="page-title">{{ route.meta.title || 'Sentinel' }}</h2>
<h2 id="page-title" class="page-title">{{ route.meta.title || 'Sentinel' }}</h2>
<p class="muted header-note">Edge policy, runtime settings, and operator visibility in one secure surface.</p>
</div>
<div class="header-actions">
@@ -108,7 +127,7 @@ onBeforeUnmount(() => {
<span class="header-chip-label">Mode</span>
<strong>Secure Proxy</strong>
</div>
<div class="header-chip">
<div class="header-chip" aria-live="polite">
<span class="header-chip-label">Updated</span>
<strong>{{ clockLabel }}</strong>
</div>
@@ -117,7 +136,7 @@ onBeforeUnmount(() => {
</div>
</header>
<section class="shell-content">
<section id="main-content" class="shell-content" tabindex="-1">
<router-view />
</section>
</main>
@@ -218,6 +237,7 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
gap: 24px;
min-width: 0;
}
.shell-header {
@@ -238,6 +258,7 @@ onBeforeUnmount(() => {
display: flex;
flex-direction: column;
gap: 24px;
min-width: 0;
}
.shell-glow {