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">
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 {
Check,
ChevronUp,
ClipboardCheck,
Dashboard,
Eye,
@@ -10,14 +11,17 @@ import {
Home,
LayoutSidebarLeftCollapse,
LayoutSidebarLeftExpand,
Logout,
Photo,
Refresh,
Settings,
Shield,
Trash,
UserCircle,
Users,
X,
} from '@vicons/tabler'
import { useRouter } from 'vue-router'
import ImageDetailModal from '@/components/cloud/ImageDetailModal.vue'
import MiniLocationMap from '@/components/cloud/MiniLocationMap.vue'
import { apiRequest } from '@/lib/api'
@@ -61,9 +65,11 @@ interface DashboardStats {
const authStore = useAuthStore()
const message = useMessage()
const router = useRouter()
const activeTab = ref<AdminTab>('dashboard')
const sidebarCollapsed = ref(false)
const accountMenuOpen = ref(false)
const loading = ref(true)
const actionLoading = ref(false)
const loadError = ref('')
@@ -117,6 +123,8 @@ const filteredImages = computed(() => {
return images.value.filter(item => item.status === imageFilter.value)
})
const selectedManagedCount = computed(() => selectedImageIds.value.size)
const displayUsername = computed(() => authStore.profile?.username || authStore.user?.email || '管理员')
const userEmail = computed(() => authStore.user?.email || '未绑定邮箱')
watch(imageFilter, () => {
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)
</script>
@@ -498,7 +520,7 @@ onMounted(loadAdminData)
:aria-label="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
:aria-expanded="!sidebarCollapsed"
:title="sidebarCollapsed ? '展开侧边栏' : '折叠侧边栏'"
@click="sidebarCollapsed = !sidebarCollapsed"
@click="toggleSidebar"
>
<NIcon size="20">
<LayoutSidebarLeftExpand v-if="sidebarCollapsed" />
@@ -542,17 +564,89 @@ onMounted(loadAdminData)
</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="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"
:class="sidebarCollapsed ? 'lg:justify-center lg:px-0' : ''"
:title="sidebarCollapsed ? (authStore.profile?.username || '管理员') : undefined"
<NPopover
v-model:show="accountMenuOpen"
trigger="click"
placement="top-start"
:show-arrow="false"
raw
>
<NIcon size="22" class="shrink-0"><UserCircle /></NIcon>
<span class="min-w-0" :class="sidebarCollapsed ? 'lg:hidden' : ''">
<span class="block truncate text-sm font-semibold text-slate-900">{{ authStore.profile?.username || '管理员' }}</span>
<span class="mt-0.5 block text-xs text-slate-500">当前管理员</span>
</span>
</div>
<template #trigger>
<button
type="button"
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"
:class="[
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
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"