422 lines
9.9 KiB
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>
|
|
|