feat(admin): add contextual sidebar account menu

This commit is contained in:
2026-07-24 00:23:36 +08:00
parent b8dc2a649e
commit 3aed4005f7
+106 -12
View File
@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch, type Component } from 'vue' import { computed, onMounted, ref, watch, type Component } from 'vue'
import { NAlert, NButton, NEmpty, NIcon, NSkeleton, NTag, useMessage } from 'naive-ui' import { NAlert, NButton, NEmpty, NIcon, NPopover, NSkeleton, NTag, useMessage } from 'naive-ui'
import { import {
Check, Check,
ChevronUp,
ClipboardCheck, ClipboardCheck,
Dashboard, Dashboard,
Eye, Eye,
@@ -10,14 +11,17 @@ import {
Home, Home,
LayoutSidebarLeftCollapse, LayoutSidebarLeftCollapse,
LayoutSidebarLeftExpand, LayoutSidebarLeftExpand,
Logout,
Photo, Photo,
Refresh, Refresh,
Settings,
Shield, Shield,
Trash, Trash,
UserCircle, UserCircle,
Users, Users,
X, X,
} from '@vicons/tabler' } from '@vicons/tabler'
import { useRouter } from 'vue-router'
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue' import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
import MiniLocationMap from '@/components/cloud/MiniLocationMap.vue' import MiniLocationMap from '@/components/cloud/MiniLocationMap.vue'
import { apiRequest } from '@/lib/api' import { apiRequest } from '@/lib/api'
@@ -61,9 +65,11 @@ interface DashboardStats {
const authStore = useAuthStore() const authStore = useAuthStore()
const message = useMessage() const message = useMessage()
const router = useRouter()
const activeTab = ref<AdminTab>('dashboard') const activeTab = ref<AdminTab>('dashboard')
const sidebarCollapsed = ref(false) const sidebarCollapsed = ref(false)
const accountMenuOpen = ref(false)
const loading = ref(true) const loading = ref(true)
const actionLoading = ref(false) const actionLoading = ref(false)
const loadError = ref('') const loadError = ref('')
@@ -117,6 +123,8 @@ const filteredImages = computed(() => {
return images.value.filter(item => item.status === imageFilter.value) return images.value.filter(item => item.status === imageFilter.value)
}) })
const selectedManagedCount = computed(() => selectedImageIds.value.size) const selectedManagedCount = computed(() => selectedImageIds.value.size)
const displayUsername = computed(() => authStore.profile?.username || authStore.user?.email || '管理员')
const userEmail = computed(() => authStore.user?.email || '未绑定邮箱')
watch(imageFilter, () => { watch(imageFilter, () => {
setSelectedImageIds([]) setSelectedImageIds([])
@@ -463,6 +471,20 @@ async function toggleUserDisabled(user: Profile) {
} }
} }
function toggleSidebar() {
accountMenuOpen.value = false
sidebarCollapsed.value = !sidebarCollapsed.value
}
async function handleLogout() {
accountMenuOpen.value = false
try {
await authStore.logout()
} finally {
await router.push('/')
}
}
onMounted(loadAdminData) onMounted(loadAdminData)
</script> </script>
@@ -498,7 +520,7 @@ onMounted(loadAdminData)
:aria-label="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'" :aria-label="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
:aria-expanded="!sidebarCollapsed" :aria-expanded="!sidebarCollapsed"
:title="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'" :title="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
@click="sidebarCollapsed = !sidebarCollapsed" @click="toggleSidebar"
> >
<NIcon size="20"> <NIcon size="20">
<LayoutSidebarLeftExpand v-if="sidebarCollapsed" /> <LayoutSidebarLeftExpand v-if="sidebarCollapsed" />
@@ -542,17 +564,89 @@ onMounted(loadAdminData)
</nav> </nav>
<div class="mt-4 flex gap-2 border-t border-slate-100 pt-3 lg:mt-auto lg:block lg:space-y-2 lg:border-t-0 lg:pt-0"> <div class="mt-4 flex gap-2 border-t border-slate-100 pt-3 lg:mt-auto lg:block lg:space-y-2 lg:border-t-0 lg:pt-0">
<div <NPopover
class="flex min-w-0 flex-1 items-center gap-3 border border-slate-200 bg-slate-50 px-3 py-2.5 text-slate-700" v-model:show="accountMenuOpen"
:class="sidebarCollapsed ? 'lg:justify-center lg:px-0' : ''" trigger="click"
:title="sidebarCollapsed ? (authStore.profile?.username || '管理员') : undefined" placement="top-start"
:show-arrow="false"
raw
> >
<NIcon size="22" class="shrink-0"><UserCircle /></NIcon> <template #trigger>
<span class="min-w-0" :class="sidebarCollapsed ? 'lg:hidden' : ''"> <button
<span class="block truncate text-sm font-semibold text-slate-900">{{ authStore.profile?.username || '管理员' }}</span> type="button"
<span class="mt-0.5 block text-xs text-slate-500">当前管理员</span> class="flex min-h-[46px] min-w-0 flex-1 items-center gap-3 border px-3 py-2 text-left transition-[background-color,border-color,color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-600 lg:w-full"
</span> :class="[
</div> accountMenuOpen
? 'border-teal-200 bg-teal-50 text-teal-800 shadow-[3px_3px_0_0_rgba(20,184,166,0.12)]'
: 'border-slate-200 bg-slate-50 text-slate-700 hover:border-teal-200 hover:bg-teal-50 hover:text-teal-800',
sidebarCollapsed ? 'lg:justify-center lg:px-0' : '',
]"
aria-label="打开管理员账户菜单"
aria-haspopup="menu"
:aria-expanded="accountMenuOpen"
:title="sidebarCollapsed ? displayUsername : undefined"
>
<NIcon size="22" class="shrink-0"><UserCircle /></NIcon>
<span class="min-w-0 flex-1" :class="sidebarCollapsed ? 'lg:hidden' : ''">
<span class="block truncate text-sm font-semibold text-slate-900">{{ displayUsername }}</span>
<span class="mt-0.5 block text-xs text-slate-500">管理员账户</span>
</span>
<NIcon
v-if="!sidebarCollapsed"
size="16"
class="shrink-0 transition-transform"
:class="accountMenuOpen ? 'rotate-180' : ''"
>
<ChevronUp />
</NIcon>
</button>
</template>
<div
class="w-64 border border-slate-200 bg-white shadow-[8px_8px_0_0_rgba(15,23,42,0.08)]"
role="menu"
aria-label="管理员账户"
>
<div class="border-b border-slate-200 bg-[linear-gradient(180deg,#f0fdfa_0%,#ffffff_100%)] px-4 py-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-teal-700">Administrator</p>
<p class="mt-2 truncate text-sm font-semibold text-slate-950">@{{ displayUsername }}</p>
<p class="mt-1 truncate text-xs text-slate-500">{{ userEmail }}</p>
</div>
<div class="py-2">
<RouterLink
to="/profile"
class="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-teal-50 hover:text-teal-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-teal-600"
role="menuitem"
@click="accountMenuOpen = false"
>
<NIcon size="18"><UserCircle /></NIcon>
<span>个人中心</span>
</RouterLink>
<RouterLink
to="/profile/settings"
class="flex items-center gap-3 px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-teal-50 hover:text-teal-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-teal-600"
role="menuitem"
@click="accountMenuOpen = false"
>
<NIcon size="18"><Settings /></NIcon>
<span>账号设置</span>
</RouterLink>
</div>
<div class="border-t border-slate-200 p-2">
<button
type="button"
class="flex w-full items-center gap-3 px-2 py-2.5 text-left text-sm font-medium text-rose-600 transition-colors hover:bg-rose-50 hover:text-rose-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-rose-500"
role="menuitem"
@click="handleLogout"
>
<NIcon size="18"><Logout /></NIcon>
<span>登出</span>
</button>
</div>
</div>
</NPopover>
<RouterLink <RouterLink
to="/" to="/"
class="flex h-[46px] shrink-0 items-center justify-center gap-3 border border-slate-200 bg-white px-3 text-sm font-semibold text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-colors hover:border-sky-200 hover:bg-sky-50 hover:text-sky-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-600" class="flex h-[46px] shrink-0 items-center justify-center gap-3 border border-slate-200 bg-white px-3 text-sm font-semibold text-slate-700 shadow-[3px_3px_0_0_rgba(15,23,42,0.06)] transition-colors hover:border-sky-200 hover:bg-sky-50 hover:text-sky-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-600"