优化系统设置页标签导航

This commit is contained in:
shaw
2026-05-11 16:10:40 +08:00
parent 8b0b507a95
commit 18cc4691e6
+168 -82
View File
@@ -1,6 +1,6 @@
<template> <template>
<AppLayout> <AppLayout>
<div class="mx-auto max-w-4xl space-y-6"> <div class="mx-auto max-w-6xl space-y-6">
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-12"> <div v-if="loading" class="flex items-center justify-center py-12">
<div <div
@@ -11,23 +11,36 @@
<!-- Settings Form --> <!-- Settings Form -->
<form v-else @submit.prevent="saveSettings" class="space-y-6" novalidate> <form v-else @submit.prevent="saveSettings" class="space-y-6" novalidate>
<!-- Tab Navigation --> <!-- Tab Navigation -->
<div class="sticky top-0 z-10 overflow-x-auto settings-tabs-scroll"> <div class="settings-tabs-shell">
<nav class="settings-tabs"> <nav
<button class="settings-tabs-scroll"
v-for="tab in settingsTabs" role="tablist"
:key="tab.key" :aria-label="t('admin.settings.title')"
type="button" >
:class="[ <div class="settings-tabs">
'settings-tab', <button
activeTab === tab.key && 'settings-tab-active', v-for="tab in settingsTabs"
]" :key="tab.key"
@click="activeTab = tab.key" :id="`settings-tab-${tab.key}`"
> type="button"
<span class="settings-tab-icon"> role="tab"
<Icon :name="tab.icon" size="sm" /> :aria-selected="activeTab === tab.key"
</span> :tabindex="activeTab === tab.key ? 0 : -1"
<span>{{ t(`admin.settings.tabs.${tab.key}`) }}</span> :class="[
</button> 'settings-tab',
activeTab === tab.key && 'settings-tab-active',
]"
@click="selectSettingsTab(tab.key)"
@keydown="handleSettingsTabKeydown($event, tab.key)"
>
<span class="settings-tab-icon">
<Icon :name="tab.icon" size="sm" />
</span>
<span class="settings-tab-label">{{
t(`admin.settings.tabs.${tab.key}`)
}}</span>
</button>
</div>
</nav> </nav>
</div> </div>
@@ -6152,6 +6165,57 @@ const settingsTabs = [
{ key: "email" as SettingsTab, icon: "mail" as const }, { key: "email" as SettingsTab, icon: "mail" as const },
{ key: "backup" as SettingsTab, icon: "database" as const }, { key: "backup" as SettingsTab, icon: "database" as const },
]; ];
const settingsTabKeyboardActions = {
ArrowLeft: -1,
ArrowUp: -1,
ArrowRight: 1,
ArrowDown: 1,
Home: "first",
End: "last",
} as const;
function selectSettingsTab(tab: SettingsTab): void {
activeTab.value = tab;
}
function focusSettingsTab(tab: SettingsTab): void {
window.requestAnimationFrame(() => {
document.getElementById(`settings-tab-${tab}`)?.focus();
});
}
function handleSettingsTabKeydown(event: KeyboardEvent, tab: SettingsTab): void {
const action =
settingsTabKeyboardActions[
event.key as keyof typeof settingsTabKeyboardActions
];
if (action === undefined) {
return;
}
event.preventDefault();
const currentIndex = settingsTabs.findIndex((item) => item.key === tab);
let nextIndex = currentIndex < 0 ? 0 : currentIndex;
if (action === "first") {
nextIndex = 0;
} else if (action === "last") {
nextIndex = settingsTabs.length - 1;
} else {
nextIndex =
(nextIndex + action + settingsTabs.length) % settingsTabs.length;
}
const nextTab = settingsTabs[nextIndex]?.key;
if (!nextTab) {
return;
}
selectSettingsTab(nextTab);
focusSettingsTab(nextTab);
}
const { copyToClipboard } = useClipboard(); const { copyToClipboard } = useClipboard();
const loading = ref(true); const loading = ref(true);
@@ -8881,94 +8945,116 @@ watch(
@apply h-[42px]; @apply h-[42px];
} }
/* ============ Settings Tab Navigation ============ */ /* ============ 系统设置 Tab 导航 ============ */
.settings-tabs-shell {
@apply sticky z-20 -mx-1 rounded-2xl border border-white/80 bg-white/90 p-1.5 backdrop-blur-xl;
top: 4.75rem;
box-shadow:
0 12px 28px rgb(15 23 42 / 0.07),
0 1px 0 rgb(255 255 255 / 0.9) inset;
}
:global(.dark) .settings-tabs-shell {
border-color: rgb(51 65 85 / 0.65);
background: rgb(15 23 42 / 0.86);
box-shadow:
0 16px 36px rgb(0 0 0 / 0.28),
0 1px 0 rgb(255 255 255 / 0.06) inset;
}
/* Scroll container: thin scrollbar on PC, auto-hide on mobile */
.settings-tabs-scroll { .settings-tabs-scroll {
scrollbar-width: thin; @apply overflow-x-auto;
scrollbar-color: transparent transparent; -ms-overflow-style: none;
} scrollbar-width: none;
.settings-tabs-scroll:hover {
scrollbar-color: rgb(0 0 0 / 0.15) transparent;
}
:root.dark .settings-tabs-scroll:hover {
scrollbar-color: rgb(255 255 255 / 0.2) transparent;
} }
.settings-tabs-scroll::-webkit-scrollbar { .settings-tabs-scroll::-webkit-scrollbar {
height: 3px; display: none;
}
.settings-tabs-scroll::-webkit-scrollbar-track {
background: transparent;
}
.settings-tabs-scroll::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
}
.settings-tabs-scroll:hover::-webkit-scrollbar-thumb {
background: rgb(0 0 0 / 0.15);
}
:root.dark .settings-tabs-scroll:hover::-webkit-scrollbar-thumb {
background: rgb(255 255 255 / 0.2);
} }
.settings-tabs { .settings-tabs {
@apply inline-flex min-w-full gap-0.5 rounded-2xl @apply flex min-w-max items-center gap-1;
border border-gray-100 bg-white/80 p-1 backdrop-blur-sm
dark:border-dark-700/50 dark:bg-dark-800/80;
box-shadow:
0 1px 3px rgb(0 0 0 / 0.04),
0 1px 2px rgb(0 0 0 / 0.02);
}
@media (min-width: 640px) {
.settings-tabs {
@apply flex;
}
} }
.settings-tab { .settings-tab {
@apply relative flex flex-1 items-center justify-center gap-1.5 @apply relative isolate flex h-10 min-w-[6.75rem] shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-xl border border-transparent px-3 text-sm font-medium text-gray-600 outline-none transition-colors duration-200 ease-out dark:text-gray-300;
whitespace-nowrap rounded-xl px-2.5 py-2
text-sm font-medium
text-gray-500 dark:text-dark-400
transition-all duration-200 ease-out;
} }
.settings-tab:hover:not(.settings-tab-active) { @media (min-width: 768px) {
@apply text-gray-700 dark:text-gray-300; .settings-tabs {
background: rgb(0 0 0 / 0.03); @apply min-w-full;
}
.settings-tab {
@apply min-w-0 flex-1 basis-0 overflow-hidden px-2 text-[13px];
}
.settings-tab-icon {
@apply h-6 w-6;
}
} }
:root.dark .settings-tab:hover:not(.settings-tab-active) { .settings-tab::before {
background: rgb(255 255 255 / 0.04); @apply absolute inset-0 -z-10 rounded-xl opacity-0 transition-opacity duration-200;
content: "";
background: linear-gradient(135deg, rgb(248 250 252 / 0.95), rgb(241 245 249 / 0.8));
}
.settings-tab:hover::before,
.settings-tab:focus-visible::before {
opacity: 1;
}
:global(.dark) .settings-tab::before {
background: linear-gradient(135deg, rgb(30 41 59 / 0.9), rgb(51 65 85 / 0.62));
}
.settings-tab:focus-visible {
@apply ring-2 ring-primary-500/40 ring-offset-2 ring-offset-white dark:ring-offset-dark-900;
} }
.settings-tab-active { .settings-tab-active {
@apply text-primary-600 dark:text-primary-400; @apply border-primary-200/80 bg-white text-primary-700 shadow-sm dark:border-primary-400/30 dark:bg-dark-700/95 dark:text-primary-200;
background: linear-gradient( box-shadow:
135deg, 0 8px 18px rgb(15 23 42 / 0.08),
rgba(20, 184, 166, 0.08), 0 1px 0 rgb(255 255 255 / 0.92) inset;
rgba(20, 184, 166, 0.03)
);
box-shadow: 0 1px 2px rgba(20, 184, 166, 0.1);
} }
:root.dark .settings-tab-active { :global(.dark) .settings-tab-active {
background: linear-gradient( box-shadow:
135deg, 0 12px 26px rgb(0 0 0 / 0.22),
rgba(45, 212, 191, 0.12), 0 1px 0 rgb(255 255 255 / 0.08) inset;
rgba(45, 212, 191, 0.05) }
);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25); .settings-tab-active::before {
opacity: 0;
}
.settings-tab-active::after {
position: absolute;
right: 0.75rem;
bottom: 0.25rem;
left: 0.75rem;
height: 2px;
border-radius: 9999px;
content: "";
background: linear-gradient(90deg, #14b8a6, #0ea5e9);
} }
.settings-tab-icon { .settings-tab-icon {
@apply flex h-6 w-6 items-center justify-center rounded-lg @apply flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-gray-500 transition-colors duration-200 dark:text-gray-400;
transition-all duration-200; }
.settings-tab:hover .settings-tab-icon,
.settings-tab:focus-visible .settings-tab-icon {
@apply text-gray-700 dark:text-gray-200;
} }
.settings-tab-active .settings-tab-icon { .settings-tab-active .settings-tab-icon {
@apply bg-primary-500/15 text-primary-600 @apply bg-primary-50 text-primary-600 dark:bg-primary-400/10 dark:text-primary-300;
dark:bg-primary-400/15 dark:text-primary-400; }
.settings-tab-label {
@apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap leading-none;
} }
</style> </style>