404 lines
9.8 KiB
Vue
404 lines
9.8 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import {
|
|
Bell,
|
|
Connection,
|
|
Monitor,
|
|
DataAnalysis,
|
|
Document,
|
|
Expand,
|
|
Fold,
|
|
List,
|
|
Memo,
|
|
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 roleLabel = computed(() => {
|
|
if (currentRole.value === 'admin') return '管理员'
|
|
if (currentRole.value === 'support') return '客服'
|
|
return '普通运营'
|
|
})
|
|
|
|
type AdminNavItem = {
|
|
to: string
|
|
label: string
|
|
icon: typeof DataAnalysis
|
|
children?: AdminNavItem[]
|
|
}
|
|
|
|
const platformConfigNavItems = computed<AdminNavItem[]>(() => {
|
|
return [
|
|
{ to: '/admin/platform-shops?tab=ninetyone', label: '91卡券', icon: Connection },
|
|
{ to: '/admin/platform-shops?tab=notifications', label: '内部通知', icon: Bell },
|
|
{ to: '/admin/platform-shops?tab=kuaishouEticket', label: '快手小店核销', icon: Tickets },
|
|
{
|
|
to: '/admin/platform-shops?tab=cloudtentacles',
|
|
label: 'cloudtentacles',
|
|
icon: Setting,
|
|
},
|
|
]
|
|
})
|
|
|
|
const navItems = computed(() => {
|
|
const items: AdminNavItem[] = [
|
|
{ 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 },
|
|
{ to: '/admin/cloudtentacles-records', label: '查询发货记录', icon: Memo },
|
|
)
|
|
|
|
if (isAdmin.value) {
|
|
items.push(
|
|
{
|
|
to: '/admin/platform-shops',
|
|
label: '平台配置',
|
|
icon: Setting,
|
|
children: platformConfigNavItems.value,
|
|
},
|
|
{ to: '/admin/platform-fulfillment', label: '履约配置', icon: Tickets },
|
|
{ to: '/admin/audit-logs', label: '审计', icon: Monitor },
|
|
)
|
|
}
|
|
|
|
return items
|
|
})
|
|
|
|
const activeNavIndex = computed(() => {
|
|
if (route.path === '/admin/platform-shops') {
|
|
const tab = typeof route.query.tab === 'string' ? route.query.tab : 'ninetyone'
|
|
return `/admin/platform-shops?tab=${tab}`
|
|
}
|
|
|
|
return route.path
|
|
})
|
|
|
|
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="activeNavIndex"
|
|
:default-openeds="['/admin/platform-shops']"
|
|
router
|
|
class="sidebar-menu"
|
|
:collapse="isSidebarCollapsed"
|
|
:collapse-transition="false"
|
|
:ellipsis="false"
|
|
>
|
|
<template v-for="item in navItems" :key="item.to">
|
|
<el-sub-menu v-if="item.children?.length" :index="item.to">
|
|
<template #title>
|
|
<el-icon><component :is="item.icon" /></el-icon>
|
|
<span>{{ item.label }}</span>
|
|
</template>
|
|
|
|
<el-menu-item
|
|
v-for="child in item.children"
|
|
:key="child.to"
|
|
:index="child.to"
|
|
:title="child.label"
|
|
>
|
|
<el-icon><component :is="child.icon" /></el-icon>
|
|
<span>{{ child.label }}</span>
|
|
</el-menu-item>
|
|
</el-sub-menu>
|
|
|
|
<el-menu-item v-else :index="item.to" :title="item.label">
|
|
<el-icon><component :is="item.icon" /></el-icon>
|
|
<span>{{ item.label }}</span>
|
|
</el-menu-item>
|
|
</template>
|
|
</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),
|
|
.sidebar-menu :deep(.el-sub-menu__title) {
|
|
margin: 2px var(--space-2);
|
|
border-radius: var(--radius-md);
|
|
min-height: 44px;
|
|
line-height: 44px;
|
|
}
|
|
|
|
.sidebar-menu :deep(.el-menu-item.is-active),
|
|
.sidebar-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
|
|
color: var(--color-primary);
|
|
background: var(--color-primary-light);
|
|
}
|
|
|
|
.sidebar-menu :deep(.el-sub-menu .el-menu-item) {
|
|
margin-left: var(--space-4);
|
|
min-height: 38px;
|
|
line-height: 38px;
|
|
color: var(--text-secondary);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.sidebar-menu :deep(.el-sub-menu .el-menu-item.is-active) {
|
|
color: var(--color-primary);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.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),
|
|
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title) {
|
|
justify-content: center;
|
|
margin: 4px 10px;
|
|
padding: 0;
|
|
}
|
|
|
|
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item .el-icon),
|
|
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title .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),
|
|
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title) {
|
|
justify-content: flex-start;
|
|
margin: 2px var(--space-2);
|
|
padding: 0 var(--space-5);
|
|
}
|
|
|
|
.admin-content {
|
|
height: auto;
|
|
overflow: visible;
|
|
}
|
|
}
|
|
</style>
|