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