2026-10-10 04:28:53 -04:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
|
|
|
|
|
|
import AppMenu from '@/components/layout/AppMenu.vue'
|
|
|
|
|
|
import { useUserStore } from '@/store/user'
|
|
|
|
|
|
|
|
|
|
|
|
/** 基础布局:左侧品牌导航(窄屏收为抽屉)+ 顶栏(面包屑/用户/退出)+ 内容区。 */
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
|
|
|
|
|
|
const drawerVisible = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
/** 面包屑:首页 + 分组(如有)+ 当前页标题(首页自身不重复) */
|
|
|
|
|
|
const breadcrumbs = computed<string[]>(() => {
|
|
|
|
|
|
const items: string[] = []
|
|
|
|
|
|
if (route.meta.group) items.push(route.meta.group)
|
|
|
|
|
|
if (route.meta.title && route.path !== '/dashboard') items.push(route.meta.title)
|
|
|
|
|
|
return items
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 移动端在抽屉里点完菜单即收起(路由变化意味着导航已发生)
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => route.path,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
drawerVisible.value = false
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async function handleLogout(): Promise<void> {
|
|
|
|
|
|
await userStore.logout()
|
|
|
|
|
|
ElMessage.success('已退出登录')
|
|
|
|
|
|
await router.replace('/login')
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<el-container class="app-layout">
|
|
|
|
|
|
<el-aside class="app-aside" width="220px">
|
|
|
|
|
|
<div class="app-brand">
|
|
|
|
|
|
<span class="app-brand__name">衍水集团</span>
|
|
|
|
|
|
<span class="app-brand__sub">营销人员工资管理系统</span>
|
2026-10-12 12:09:41 -04:00
|
|
|
|
<span class="app-brand__line"></span>
|
2026-10-10 04:28:53 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
<AppMenu class="app-aside__menu" />
|
|
|
|
|
|
</el-aside>
|
|
|
|
|
|
|
|
|
|
|
|
<el-container class="app-body">
|
|
|
|
|
|
<el-header class="app-header">
|
|
|
|
|
|
<div class="app-header__left">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="app-header__menu-toggle"
|
|
|
|
|
|
aria-label="打开导航菜单"
|
|
|
|
|
|
@click="drawerVisible = true"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true">
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M3 5h14M3 10h14M3 15h14"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
stroke-width="1.6"
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<el-breadcrumb separator="/">
|
|
|
|
|
|
<el-breadcrumb-item :to="{ path: '/dashboard' }">首页</el-breadcrumb-item>
|
|
|
|
|
|
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item">
|
|
|
|
|
|
{{ item }}
|
|
|
|
|
|
</el-breadcrumb-item>
|
|
|
|
|
|
</el-breadcrumb>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="app-header__user">
|
|
|
|
|
|
<el-tag v-for="role in userStore.roles" :key="role" size="small" type="info">
|
|
|
|
|
|
{{ role }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
<span class="app-header__username">{{ userStore.displayName }}</span>
|
|
|
|
|
|
<el-button link type="primary" @click="handleLogout">退出登录</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-header>
|
|
|
|
|
|
|
|
|
|
|
|
<el-main class="app-main">
|
|
|
|
|
|
<router-view />
|
|
|
|
|
|
</el-main>
|
|
|
|
|
|
</el-container>
|
|
|
|
|
|
|
|
|
|
|
|
<el-drawer
|
|
|
|
|
|
v-model="drawerVisible"
|
|
|
|
|
|
class="app-drawer"
|
|
|
|
|
|
direction="ltr"
|
|
|
|
|
|
size="240px"
|
|
|
|
|
|
:with-header="false"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="app-brand">
|
|
|
|
|
|
<span class="app-brand__name">衍水集团</span>
|
|
|
|
|
|
<span class="app-brand__sub">营销人员工资管理系统</span>
|
2026-10-12 12:09:41 -04:00
|
|
|
|
<span class="app-brand__line"></span>
|
2026-10-10 04:28:53 -04:00
|
|
|
|
</div>
|
|
|
|
|
|
<AppMenu />
|
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
</el-container>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.app-layout {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-aside {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-12 12:09:41 -04:00
|
|
|
|
/* 中性灰侧栏:比页面底沉一档,与纸白卡片拉开层次 */
|
|
|
|
|
|
background: linear-gradient(180deg, #f2f4f8 0%, #ecedf3 100%);
|
|
|
|
|
|
border-right: 1px solid var(--ys-line);
|
2026-10-10 04:28:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-brand {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-12 12:09:41 -04:00
|
|
|
|
gap: 3px;
|
|
|
|
|
|
padding: 18px 20px 12px;
|
2026-10-10 04:28:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-brand__name {
|
2026-10-12 12:09:41 -04:00
|
|
|
|
font-family: var(--ys-font-brand);
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
letter-spacing: 4px;
|
|
|
|
|
|
color: var(--ys-ink);
|
2026-10-10 04:28:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-brand__sub {
|
2026-10-12 12:09:41 -04:00
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
color: var(--ys-ink-3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 品牌签名:logo 下的渐隐水线(全站唯一装饰元素,页面标题处同款) */
|
|
|
|
|
|
.app-brand__line {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 2px;
|
|
|
|
|
|
border-radius: 1px;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
background: linear-gradient(90deg, var(--ys-indigo), rgba(46, 75, 168, 0));
|
2026-10-10 04:28:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-aside__menu {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
height: 56px;
|
2026-10-12 12:09:41 -04:00
|
|
|
|
background: linear-gradient(180deg, #ffffff, #fdfdfe);
|
|
|
|
|
|
border-bottom: 1px solid var(--ys-line);
|
2026-10-10 04:28:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-header__left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 窄屏才出现的汉堡按钮;不引入图标库,用内联 SVG */
|
|
|
|
|
|
.app-header__menu-toggle {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
color: var(--ys-text);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-header__menu-toggle:hover {
|
|
|
|
|
|
background-color: #f2f3f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-header__menu-toggle:focus-visible {
|
2026-10-12 12:09:41 -04:00
|
|
|
|
outline: 2px solid var(--ys-indigo);
|
2026-10-10 04:28:53 -04:00
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-header__user {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-header__username {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--ys-text);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-main {
|
|
|
|
|
|
padding: 20px;
|
2026-10-12 12:09:41 -04:00
|
|
|
|
/* 雾灰底 + 点阵网格:内容区的秩序与密度 */
|
|
|
|
|
|
background-color: var(--ys-mist);
|
|
|
|
|
|
background-image: radial-gradient(rgba(28, 35, 51, 0.05) 1px, transparent 1px);
|
|
|
|
|
|
background-size: 24px 24px;
|
2026-10-10 04:28:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.app-aside {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-header__menu-toggle {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
.app-header__menu-toggle {
|
|
|
|
|
|
transition: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-10-12 12:09:41 -04:00
|
|
|
|
/* 抽屉内容铺满侧栏同款灰(非 scoped:目标元素由 el-drawer 渲染在组件根之外) */
|
2026-10-10 04:28:53 -04:00
|
|
|
|
.app-drawer .el-drawer__body {
|
|
|
|
|
|
padding: 0;
|
2026-10-12 12:09:41 -04:00
|
|
|
|
background: linear-gradient(180deg, #f2f4f8 0%, #ecedf3 100%);
|
2026-10-10 04:28:53 -04:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|