Files
danyang-match/admin-web/src/layouts/MainLayout.vue
T

497 lines
13 KiB
Vue
Raw Normal View History

2026-09-09 22:05:00 +08:00
<template>
<el-container class="main-layout">
<el-aside :width="isCollapse ? '72px' : '220px'" class="sidebar">
<div class="brand" :class="{ compact: isCollapse }">
2026-09-11 17:02:42 +08:00
<img class="brand-mark" src="/brand/logo-mark.png" alt="" />
<div v-if="!isCollapse" class="brand-copy">
<strong>善缘丹阳</strong>
<span>公益婚恋运营平台</span>
</div>
2026-09-09 22:05:00 +08:00
</div>
2026-09-11 17:02:42 +08:00
<el-scrollbar class="menu-scroll">
2026-09-11 17:02:42 +08:00
<nav aria-label="管理后台主导航">
<el-menu :default-active="activeMenu" :collapse="isCollapse" :router="true" class="sidebar-menu">
<el-menu-item index="/dashboard">
<el-icon><DataBoard /></el-icon>
<template #title>工作台</template>
</el-menu-item>
<el-menu-item-group v-if="showGroup(['review.view', 'user.view'])" title="用户与资料审核">
<el-menu-item v-if="hasPerm('review.view')" index="/review">
<el-icon><DocumentChecked /></el-icon>
<template #title>资料审核</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('user.view')" index="/users">
<el-icon><User /></el-icon>
<template #title>用户管理</template>
</el-menu-item>
</el-menu-item-group>
<el-menu-item-group v-if="hasPerm('activity.review_registration')" title="匹配与活动服务">
<el-menu-item index="/activities/interests">
<el-icon><Calendar /></el-icon>
<template #title>活动与互选</template>
</el-menu-item>
</el-menu-item-group>
<el-menu-item-group
v-if="showGroup(['counseling.service.view', 'referral.manage'])"
title="公益咨询与转介"
>
<el-menu-item v-if="hasPerm('counseling.service.view')" index="/counseling">
<el-icon><ChatDotRound /></el-icon>
<template #title>婚恋咨询</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('referral.manage')" index="/referral">
<el-icon><Connection /></el-icon>
<template #title>服务转介</template>
</el-menu-item>
</el-menu-item-group>
<el-menu-item-group
v-if="showGroup(['service_location.view', 'service_appointment.view', 'service_program.manage'])"
title="服务站、预约与专项服务"
>
<el-menu-item v-if="hasPerm('service_location.view')" index="/service-station">
<el-icon><Location /></el-icon>
<template #title>善缘服务站</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_appointment.view')" index="/service-appointment">
<el-icon><Memo /></el-icon>
<template #title>服务预约</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('service_program.manage')" index="/service-program">
<el-icon><Service /></el-icon>
<template #title>专项公益服务</template>
</el-menu-item>
</el-menu-item-group>
<el-menu-item-group v-if="showGroup(['banner.view', 'content.view'])" title="Banner 与内容运营">
<el-menu-item v-if="hasPerm('banner.view')" index="/banner">
<el-icon><Promotion /></el-icon>
<template #title>首页 Banner</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('content.view')" index="/content">
<el-icon><Reading /></el-icon>
<template #title>课堂与反诈内容</template>
</el-menu-item>
</el-menu-item-group>
<el-menu-item-group
v-if="showGroup(['risk.report.view', 'risk.report.process', 'user.view'])"
title="举报、申诉与审计"
>
<el-menu-item v-if="hasPerm('risk.report.view')" index="/risk/reports">
<el-icon><Warning /></el-icon>
<template #title>举报中心</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('risk.report.process')" index="/appeals">
<el-icon><CircleCheck /></el-icon>
<template #title>申诉管理</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('user.view')" index="/audit">
<el-icon><Document /></el-icon>
<template #title>操作日志</template>
</el-menu-item>
</el-menu-item-group>
<el-menu-item-group
v-if="showGroup(['organization.manage', 'permission.manage'])"
title="组织、人员与权限"
>
<el-menu-item v-if="hasPerm('organization.manage')" index="/organizations">
<el-icon><OfficeBuilding /></el-icon>
<template #title>组织管理</template>
</el-menu-item>
<el-menu-item v-if="hasPerm('permission.manage')" index="/operators">
<el-icon><Setting /></el-icon>
<template #title>工作人员</template>
</el-menu-item>
</el-menu-item-group>
<el-menu-item v-if="hasPerm('analytics.view')" index="/analytics">
<el-icon><TrendCharts /></el-icon>
<template #title>数据统计</template>
</el-menu-item>
</el-menu>
</nav>
</el-scrollbar>
2026-09-11 17:02:42 +08:00
<button
class="collapse-control"
type="button"
:aria-label="isCollapse ? '展开导航' : '收起导航'"
@click="isCollapse = !isCollapse"
>
<el-icon><Fold v-if="!isCollapse" /><Expand v-else /></el-icon>
<span v-if="!isCollapse">收起导航</span>
</button>
2026-09-09 22:05:00 +08:00
</el-aside>
2026-09-11 17:02:42 +08:00
<el-container class="workspace">
2026-09-09 22:05:00 +08:00
<el-header class="header">
2026-09-11 17:02:42 +08:00
<div class="page-context">
<div class="context-kicker">善缘丹阳 · 公益服务</div>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/dashboard' }">工作台</el-breadcrumb-item>
<el-breadcrumb-item v-if="currentTitle !== '工作台'">{{ currentTitle }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-tools">
<el-input
v-model="globalSearch"
class="global-search"
clearable
aria-label="搜索后台页面"
placeholder="搜索用户、活动、内容…"
@keyup.enter="handleSearch"
>
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<button class="notification-button" type="button" aria-label="查看系统通知" @click="showNotifications">
<el-icon><Bell /></el-icon>
<span class="notification-dot" aria-hidden="true"></span>
</button>
<el-dropdown>
<span class="user-info">
<el-avatar :size="36">{{ (operator?.name || '工')[0] }}</el-avatar>
<span class="user-copy">
<strong>{{ operator?.name || '工作人员' }}</strong>
<small>运营人员</small>
</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
2026-09-09 22:05:00 +08:00
</el-header>
2026-09-11 17:02:42 +08:00
<el-main class="main-content">
<router-view v-slot="{ Component }">
<keep-alive><component :is="Component" /></keep-alive>
</router-view>
</el-main>
2026-09-09 22:05:00 +08:00
</el-container>
</el-container>
</template>
2026-09-11 17:02:42 +08:00
2026-09-09 22:05:00 +08:00
<script setup lang="ts">
2026-09-11 17:02:42 +08:00
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ArrowDown,
Bell,
Calendar,
ChatDotRound,
CircleCheck,
Connection,
DataBoard,
Document,
DocumentChecked,
Expand,
Fold,
Location,
Memo,
OfficeBuilding,
Promotion,
Reading,
Search,
Service,
Setting,
TrendCharts,
User,
Warning,
} from '@element-plus/icons-vue'
import { useAuthStore } from '@/stores/auth'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const isCollapse = ref(false)
const globalSearch = ref('')
const activeMenu = computed(() => route.path === '/users/detail' ? '/users' : route.path)
const currentTitle = computed(() => (route.meta?.title as string) || '页面')
const operator = computed(() => authStore.operator)
function hasPerm(permission: string) {
return (authStore.permissions || []).includes(permission)
}
function showGroup(permissions: string[]) {
return permissions.some(hasPerm)
}
function handleSearch() {
const query = globalSearch.value.trim()
if (!query) return
const destination = router.getRoutes().find((item) => {
const title = String(item.meta?.title || '')
const permissions = (item.meta?.permissions as string[] | undefined) || []
return item.path !== '/login' && title.includes(query) && permissions.every(hasPerm)
})
if (!destination) {
ElMessage.info('没有找到可访问的相关页面')
return
}
router.push(destination.path)
globalSearch.value = ''
}
function showNotifications() {
ElMessage.info('当前没有新的系统通知')
}
function handleLogout() {
authStore.logout()
router.push('/login')
}
2026-09-09 22:05:00 +08:00
</script>
2026-09-11 17:02:42 +08:00
2026-09-09 22:05:00 +08:00
<style scoped>
2026-09-11 17:02:42 +08:00
.main-layout {
width: 100%;
height: 100vh;
}
.sidebar {
display: flex;
flex-direction: column;
overflow: hidden;
color: var(--admin-sidebar-text);
background: var(--admin-sidebar-bg);
border-right: 1px solid rgba(255, 255, 255, 0.06);
transition: width 0.2s ease;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
height: 92px;
padding: 20px 18px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.brand.compact {
justify-content: center;
padding: 18px 10px;
}
.brand-mark {
width: 44px;
height: 44px;
flex: none;
object-fit: contain;
}
.brand-copy {
display: flex;
flex-direction: column;
white-space: nowrap;
}
.brand-copy strong {
color: #FFFFFF;
font-size: 18px;
letter-spacing: 1px;
}
.brand-copy span {
margin-top: 4px;
color: var(--admin-sidebar-muted);
font-size: 11px;
}
.menu-scroll {
flex: 1;
}
.sidebar-menu {
padding: 12px 10px;
border: 0;
background: transparent;
}
.sidebar-menu :deep(.el-menu-item) {
height: 42px;
margin: 3px 0;
color: var(--admin-sidebar-text);
border-radius: 9px;
}
.sidebar-menu :deep(.el-menu-item:hover) {
color: #FFFFFF;
background: var(--admin-sidebar-surface);
}
.sidebar-menu :deep(.el-menu-item.is-active) {
color: #FFFFFF;
font-weight: 600;
background: var(--admin-primary);
}
.sidebar-menu :deep(.el-menu-item-group__title) {
padding: 18px 12px 5px !important;
overflow: hidden;
color: var(--admin-sidebar-muted);
font-size: 11px;
letter-spacing: 0.5px;
text-overflow: ellipsis;
white-space: nowrap;
}
.collapse-control {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
height: 52px;
margin: 10px;
color: var(--admin-sidebar-muted);
cursor: pointer;
background: transparent;
border: 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.collapse-control:hover {
color: #FFFFFF;
}
.workspace {
min-width: 0;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--header-height);
padding: 0 24px;
background: var(--admin-header-bg);
border-bottom: 1px solid var(--color-border-light);
}
.page-context {
min-width: 180px;
}
.context-kicker {
margin-bottom: 4px;
color: var(--admin-primary);
font-size: 10px;
letter-spacing: 1px;
}
.header-tools,
.user-info {
display: flex;
align-items: center;
}
.header-tools {
gap: 16px;
}
.global-search {
width: 260px;
}
.global-search :deep(.el-input__wrapper) {
background: #F5F6F7;
border-radius: 10px;
box-shadow: inset 0 0 0 1px #E8EAED;
}
.notification-button {
position: relative;
display: grid;
width: 38px;
height: 38px;
color: var(--color-text-secondary);
cursor: pointer;
background: #FFFFFF;
border: 1px solid var(--color-border-light);
border-radius: 10px;
place-items: center;
}
.notification-dot {
position: absolute;
top: 8px;
right: 8px;
width: 7px;
height: 7px;
background: var(--admin-primary);
border: 2px solid #FFFFFF;
border-radius: 50%;
}
.user-info {
gap: 9px;
cursor: pointer;
}
.user-info .el-avatar {
color: #FFFFFF;
background: var(--admin-primary);
}
.user-copy {
display: flex;
flex-direction: column;
}
.user-copy strong {
font-size: 13px;
}
.user-copy small {
margin-top: 2px;
color: var(--color-text-tertiary);
font-size: 11px;
}
.main-content {
padding: 0;
overflow: auto;
background: var(--admin-page-bg);
}
@media (max-width: 1000px) {
.sidebar {
width: 72px !important;
}
.brand-copy,
.collapse-control span,
.sidebar-menu :deep(.el-menu-item-group__title) {
display: none;
}
.brand {
justify-content: center;
}
}
@media (max-width: 760px) {
.header {
padding: 0 14px;
}
.page-context,
.global-search,
.user-copy {
display: none;
}
}
</style>