feat(admin): add contextual sidebar account menu
This commit is contained in:
+106
-12
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user