admin后台 第一阶段目录优化

This commit is contained in:
yml2213
2026-05-15 14:12:38 +08:00
parent 34b71decf1
commit 11678a7f83
35 changed files with 41 additions and 46 deletions
@@ -0,0 +1,336 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
Box,
Monitor,
Connection,
DataAnalysis,
Document,
Expand,
Fold,
List,
Message,
Service,
Setting,
SwitchButton,
Tickets,
User,
} from '@element-plus/icons-vue'
import { showSuccess } from '@/lib/feedback'
import { logoutAdmin } from '@/services/admin'
import { clearAdminSession, getAdminRole, getAdminTokenExpiresAt, getAdminUsername } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
const route = useRoute()
const router = useRouter()
const sidebarStorageKey = 'admin-sidebar-collapsed'
const isSidebarCollapsed = ref(
typeof window !== 'undefined' && window.localStorage.getItem(sidebarStorageKey) === '1',
)
const currentRole = computed(() => getAdminRole())
const isAdmin = computed(() => currentRole.value === 'admin')
const isOperator = computed(() => currentRole.value === 'operator')
const roleLabel = computed(() => {
if (currentRole.value === 'admin') return '管理员'
if (currentRole.value === 'support') return '客服'
return '普通运营'
})
const navItems = computed(() => {
const items: { to: string; label: string; icon: typeof DataAnalysis }[] = [
{ to: '/admin/dashboard', label: '概览', icon: DataAnalysis },
]
if (isAdmin.value) {
items.push({ to: '/admin/users', label: '用户', icon: User })
}
items.push(
{ to: '/admin/orders', label: '订单', icon: Document },
{ to: '/admin/tasks', label: '任务', icon: List },
)
if (isAdmin.value || isOperator.value) {
items.push({ to: '/admin/inventory', label: '库存', icon: Box })
}
items.push(
{ to: '/admin/manual-redeem', label: '人工兑换', icon: Service },
{ to: '/admin/message-deliveries', label: '消息发送', icon: Message },
)
if (isAdmin.value || isOperator.value) {
items.push({ to: '/admin/webhook-events', label: 'Webhook', icon: Connection })
}
if (isAdmin.value) {
items.push(
{ to: '/admin/platform-shops', label: '平台配置', icon: Setting },
{ to: '/admin/platform-fulfillment', label: '履约配置', icon: Tickets },
{ to: '/admin/audit-logs', label: '审计', icon: Monitor },
)
}
return items
})
function toggleSidebarCollapse() {
isSidebarCollapsed.value = !isSidebarCollapsed.value
if (typeof window !== 'undefined') {
window.localStorage.setItem(sidebarStorageKey, isSidebarCollapsed.value ? '1' : '0')
}
}
async function submitLogout() {
try {
await logoutAdmin()
} catch {
// 后台退出是无状态的,本地清理优先
} finally {
clearAdminSession()
showSuccess('已退出后台')
await router.replace('/admin/login')
}
}
</script>
<template>
<div class="admin-page" :class="{ 'is-sidebar-collapsed': isSidebarCollapsed }">
<aside class="admin-sidebar">
<div class="sidebar-brand">
<div v-if="!isSidebarCollapsed" class="brand-copy">
<strong>运营后台</strong>
<p>订单与交付管理</p>
</div>
<el-tooltip :content="isSidebarCollapsed ? '展开导航' : '折叠导航'" placement="right">
<el-button
class="sidebar-toggle"
circle
:aria-label="isSidebarCollapsed ? '展开导航' : '折叠导航'"
@click="toggleSidebarCollapse"
>
<el-icon><component :is="isSidebarCollapsed ? Expand : Fold" /></el-icon>
</el-button>
</el-tooltip>
</div>
<div v-if="!isSidebarCollapsed" class="sidebar-user">
<div class="user-name">{{ getAdminUsername() || '未读取' }}</div>
<div class="user-role">{{ roleLabel }}</div>
<div class="user-expiry">有效期至 {{ formatAdminDateTime(getAdminTokenExpiresAt()) }}</div>
</div>
<el-menu
:default-active="route.path"
router
class="sidebar-menu"
:collapse="isSidebarCollapsed"
:collapse-transition="false"
:ellipsis="false"
>
<el-menu-item v-for="item in navItems" :key="item.to" :index="item.to" :title="item.label">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</el-menu>
<el-tooltip :disabled="!isSidebarCollapsed" content="退出登录" placement="right">
<el-button
class="logout-btn"
:circle="isSidebarCollapsed"
:round="!isSidebarCollapsed"
@click="submitLogout"
>
<el-icon v-if="isSidebarCollapsed"><SwitchButton /></el-icon>
<span v-else>退出登录</span>
</el-button>
</el-tooltip>
</aside>
<section class="admin-content">
<RouterView />
</section>
</div>
</template>
<style scoped>
.admin-page {
height: 100vh;
display: grid;
grid-template-columns: 232px minmax(0, 1fr);
overflow: hidden;
transition: grid-template-columns 0.18s ease;
background:
radial-gradient(circle at top left, rgba(73, 136, 255, 0.1), transparent 30%),
linear-gradient(180deg, var(--bg-page) 0%, #edf3fb 100%);
}
.admin-page.is-sidebar-collapsed {
grid-template-columns: 72px minmax(0, 1fr);
}
.admin-sidebar {
height: 100vh;
display: flex;
flex-direction: column;
padding: var(--space-6) 0;
overflow-y: auto;
border-right: 1px solid var(--border-default);
background: var(--bg-sidebar);
backdrop-filter: blur(18px);
}
.sidebar-brand {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
padding: 0 var(--space-5);
margin-bottom: var(--space-4);
}
.brand-copy { min-width: 0; }
.brand-copy strong {
display: block;
font-size: var(--text-xl);
color: var(--text-primary);
}
.brand-copy p {
margin: var(--space-2) 0 0;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.sidebar-toggle {
width: 34px;
height: 34px;
flex: 0 0 auto;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.72);
}
.sidebar-user {
margin: 0 var(--space-4) var(--space-4);
padding: var(--space-3);
border-radius: var(--radius-md);
background: #f5f8fc;
font-size: var(--text-xs);
}
.user-name {
color: var(--text-primary);
font-weight: 700;
}
.user-role {
color: var(--text-secondary);
margin-top: 2px;
}
.user-expiry {
color: var(--text-muted);
margin-top: 4px;
font-size: 11px;
}
.sidebar-menu {
border-right: none;
flex: 1;
overflow-y: auto;
width: 100%;
}
.sidebar-menu :deep(.el-menu-item) {
margin: 2px var(--space-2);
border-radius: var(--radius-md);
min-height: 44px;
line-height: 44px;
}
.sidebar-menu :deep(.el-menu-item.is-active) {
color: var(--color-primary);
background: var(--color-primary-light);
}
.logout-btn {
margin: var(--space-3) var(--space-4) 0;
flex-shrink: 0;
}
.is-sidebar-collapsed .admin-sidebar {
align-items: stretch;
padding-top: var(--space-5);
}
.is-sidebar-collapsed .sidebar-brand {
justify-content: center;
padding: 0;
margin-bottom: var(--space-5);
}
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item) {
justify-content: center;
margin: 4px 10px;
padding: 0;
}
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item .el-icon) {
margin-right: 0;
}
.is-sidebar-collapsed .logout-btn {
width: 40px;
margin: var(--space-3) auto 0;
}
.admin-content {
min-width: 0;
height: 100vh;
padding: var(--space-6);
overflow-y: auto;
}
@media (max-width: 900px) {
.admin-page {
height: auto;
min-height: 100vh;
grid-template-columns: 1fr;
overflow: visible;
}
.admin-page.is-sidebar-collapsed {
grid-template-columns: 1fr;
}
.admin-sidebar {
height: auto;
overflow: visible;
border-right: 0;
border-bottom: 1px solid var(--border-default);
padding-bottom: var(--space-4);
}
.is-sidebar-collapsed .admin-sidebar {
padding-top: var(--space-4);
}
.sidebar-menu { overflow: visible; }
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item) {
justify-content: flex-start;
margin: 2px var(--space-2);
padding: 0 var(--space-5);
}
.admin-content {
height: auto;
overflow: visible;
}
}
</style>