Files
login_task_web/src/layouts/AdminLayout.vue

422 lines
9.9 KiB
Vue

<template>
<div class="admin-layout">
<!-- 移动端遮罩层 -->
<div
v-if="isMobile && !collapsed"
class="mobile-overlay"
@click="collapsed = true"
></div>
<aside :class="['sider', { 'sider-mobile': isMobile, 'sider-collapsed': isMobile && collapsed }]">
<div class="brand">
<img class="logo" src="https://vuejs.org/images/logo.png" alt="logo" />
<span v-show="!collapsed || !isMobile" class="name">管理后台</span>
</div>
<el-menu
class="menu"
router
:default-active="$route.name"
:collapse="collapsed && !isMobile"
background-color="#001529"
text-color="#bfcbd9"
active-text-color="#fff"
@select="onMenuSelect"
>
<el-menu-item v-if="canAccessUsers" index="Users" :route="{ name: 'Users' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 12c2.7 0 5-2.3 5-5s-2.3-5-5-5s-5 2.3-5 5s2.3 5 5 5m0 2c-3.3 0-10 1.7-10 5v3h20v-3c0-3.3-6.7-5-10-5Z"/></svg></i>
<span>用户管理</span>
</el-menu-item>
<el-menu-item v-if="canAccessLinks" index="Links" :route="{ name: 'Links' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M10.59 13.41c.41.39.41 1.03 0 1.42c-.39.39-1.03.39-1.42 0a5.003 5.003 0 0 1 0-7.07l3.54-3.54a5.003 5.003 0 0 1 7.07 0a5.003 5.003 0 0 1 0 7.07l-1.49 1.49c.01-.82-.12-1.64-.4-2.42l.47-.48a2.982 2.982 0 0 0 0-4.24a2.982 2.982 0 0 0-4.24 0l-3.53 3.53a2.982 2.982 0 0 0 0-4.24zm2.82-4.24c.39-.39 1.03-.39 1.42 0a5.003 5.003 0 0 1 0 7.07l-3.54 3.54a5.003 5.003 0 0 1-7.07 0a5.003 5.003 0 0 1 0-7.07l1.49-1.49c-.01.82.12 1.64.4 2.43l-.47.47a2.982 2.982 0 0 0 0 4.24a2.982 2.982 0 0 0 4.24 0l3.53-3.53a2.982 2.982 0 0 0 0-4.24a.973.973 0 0 1 0-1.42z"/></svg></i>
<span>链接管理</span>
</el-menu-item>
<el-menu-item v-if="canAccessRefund" index="Refund" :route="{ name: 'Refund' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg></i>
<span>退单管理</span>
</el-menu-item>
<el-menu-item v-if="canAccessAnnouncements" index="Announcements" :route="{ name: 'Announcements' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg></i>
<span>公告管理</span>
</el-menu-item>
<el-menu-item v-if="canAccessSettings" index="Settings" :route="{ name: 'Settings' }">
<i class="el-icon"><svg viewBox="0 0 24 24"><path fill="currentColor" d="m12 8l-2 4h4l-2 4"/></svg></i>
<span>系统设置</span>
</el-menu-item>
</el-menu>
</aside>
<section class="main">
<header class="header">
<el-button text @click="toggleSidebar" class="collapse-btn">
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M3 6h18v2H3V6m0 5h12v2H3v-2m0 5h18v2H3v-2Z"/></svg>
</el-button>
<div class="spacer" />
<!-- 移动端显示当前页面标题 -->
<span v-if="isMobile" class="mobile-page-title">{{ currentPageTitle }}</span>
<div v-if="!isMobile" class="spacer" />
<!-- 积分显示 -->
<span v-if="isAgent() && pointsBalance !== null" class="points-display">
积分: {{ formatPoints(pointsBalance) }}
</span>
<el-dropdown>
<span class="el-dropdown-link">
<span class="username">{{ currentUser?.username || '用户' }}</span>
<i class="el-icon el-icon--right"><svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="onProfile">个人中心</el-dropdown-item>
<el-dropdown-item divided @click="onLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</header>
<main class="content">
<router-view />
</main>
</section>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { clearTokens } from '@/utils/auth'
import { useRouter, useRoute } from 'vue-router'
import { canAccessRoute, getCurrentUser, isAgent } from '@/utils/permission'
import { getPointsBalance } from '@/api/points'
import { formatPoints } from '@/utils/points'
const collapsed = ref(false)
const isMobile = ref(false)
const pointsBalance = ref(null)
const router = useRouter()
const route = useRoute()
// 检测移动端
const checkMobile = () => {
isMobile.value = window.innerWidth <= 768
// 移动端默认收起侧边栏
if (isMobile.value) {
collapsed.value = true
}
}
// 页面标题映射
const pageTitleMap = {
'Users': '用户管理',
'Links': '链接管理',
'Refund': '退单管理',
'Announcements': '公告管理',
'Settings': '系统设置'
}
// 获取当前用户信息
const currentUser = computed(() => getCurrentUser())
// 获取当前页面标题
const currentPageTitle = computed(() => {
return pageTitleMap[route.name] || '管理后台'
})
// 权限检查
const canAccessUsers = computed(() => canAccessRoute('Users'))
const canAccessLinks = computed(() => canAccessRoute('Links'))
const canAccessRefund = computed(() => canAccessRoute('Refund'))
const canAccessAnnouncements = computed(() => canAccessRoute('Announcements'))
const canAccessSettings = computed(() => canAccessRoute('Settings'))
// 获取积分余额
const fetchPointsBalance = async () => {
if (!isAgent()) return // 只有代理商才需要显示积分
try {
const data = await getPointsBalance()
pointsBalance.value = data.pointsBalance
} catch (error) {
console.error('获取积分余额失败:', error)
pointsBalance.value = null
}
}
// 切换侧边栏
const toggleSidebar = () => {
collapsed.value = !collapsed.value
}
// 菜单选择事件(移动端选择后自动收起)
const onMenuSelect = () => {
if (isMobile.value) {
collapsed.value = true
}
}
function onProfile() {
// 可跳转到个人中心占位页
}
function onLogout() {
clearTokens()
router.replace({ name: 'Login' })
}
// 监听窗口大小变化
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
fetchPointsBalance() // 页面加载时获取积分余额
})
onUnmounted(() => {
window.removeEventListener('resize', checkMobile)
})
</script>
<style scoped>
.admin-layout {
display: flex;
height: 100vh;
position: relative;
}
/* 移动端遮罩层 */
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
display: block;
}
.sider {
width: 220px;
background: #001529;
color: #bfcbd9;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
z-index: 1000;
}
/* 移动端侧边栏样式 */
.sider-mobile {
position: fixed;
top: 0;
left: 0;
height: 100vh;
transform: translateX(0);
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
}
.sider-mobile.sider-collapsed {
transform: translateX(-100%);
}
.brand {
height: 56px;
display: flex;
align-items: center;
gap: 10px;
padding: 0 16px;
color: #fff;
border-bottom: 1px solid #334050;
}
.logo {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.name {
font-size: 16px;
font-weight: 600;
}
.menu {
border-right: none;
flex: 1;
overflow-y: auto;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
transition: all 0.3s ease;
}
.header {
height: 56px;
display: flex;
align-items: center;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
position: relative;
z-index: 100;
}
.collapse-btn {
margin-right: 8px;
min-width: 32px;
height: 32px;
border-radius: 4px;
}
.collapse-btn:hover {
background-color: #f5f5f5;
}
.spacer {
flex: 1;
}
.mobile-page-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.points-display {
font-size: 16px;
color: #409eff;
font-weight: 500;
margin-right: 16px;
}
.el-dropdown-link {
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
color: #606266;
font-size: 14px;
}
.el-dropdown-link:hover {
color: #409eff;
}
.username {
font-weight: 500;
}
.content {
padding: 16px;
overflow: auto;
background: #f5f7fa;
height: calc(100vh - 56px);
flex: 1;
}
/* 移动端响应式布局 */
@media (max-width: 768px) {
.main {
width: 100%;
}
.header {
padding: 0 12px;
}
.content {
padding: 12px;
}
.collapse-btn {
margin-right: 12px;
}
.mobile-page-title {
font-size: 15px;
}
.points-display {
font-size: 14px;
margin-right: 12px;
}
.el-dropdown-link {
font-size: 16px;
min-height: var(--mobile-touch-target);
padding: 8px;
}
.username {
font-size: 16px;
}
}
@media (max-width: 480px) {
.header {
padding: 0 8px;
}
.content {
padding: 8px;
}
.brand {
padding: 0 12px;
}
.name {
font-size: 15px;
}
.mobile-page-title {
font-size: 14px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.points-display {
font-size: 13px;
margin-right: 8px;
}
.el-dropdown-link {
font-size: 14px;
padding: 6px;
}
.el-dropdown-link span {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.username {
font-size: 14px;
}
}
/* 桌面端样式 */
@media (min-width: 769px) {
.sider-mobile {
position: relative;
transform: none !important;
box-shadow: none;
}
.mobile-overlay {
display: none;
}
}
</style>