MainLayout.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. <template>
  2. <el-container class="layout-container">
  3. <!-- 侧边栏 -->
  4. <el-aside :width="isCollapse ? '64px' : '240px'" class="layout-aside">
  5. <div class="logo-container">
  6. <img src="@/assets/qingluan.png" alt="Logo" class="logo" />
  7. <transition name="fade">
  8. <span v-if="!isCollapse" class="logo-text">青鸾之恋</span>
  9. </transition>
  10. </div>
  11. <el-menu
  12. :default-active="activeMenu"
  13. :collapse="isCollapse"
  14. :unique-opened="true"
  15. router
  16. class="aside-menu"
  17. background-color="transparent"
  18. text-color="#cbd5e1"
  19. active-text-color="#ffffff"
  20. >
  21. <!-- 数据面板 - 仅超级管理员 -->
  22. <el-menu-item v-if="isSuperAdmin" index="/dashboard">
  23. <el-icon><DataBoard /></el-icon>
  24. <template #title>数据面板</template>
  25. </el-menu-item>
  26. <el-menu-item index="/banner">
  27. <el-icon><Picture /></el-icon>
  28. <template #title>轮播图管理</template>
  29. </el-menu-item>
  30. <el-menu-item index="/announcement">
  31. <el-icon><Microphone /></el-icon>
  32. <template #title>小喇叭公告</template>
  33. </el-menu-item>
  34. <!-- 管理员管理 - 仅超级管理员 -->
  35. <el-menu-item v-if="isSuperAdmin" index="/admin-user">
  36. <el-icon><User /></el-icon>
  37. <template #title>管理员管理</template>
  38. </el-menu-item>
  39. <el-sub-menu index="/activity">
  40. <template #title>
  41. <el-icon><Calendar /></el-icon>
  42. <span>活动管理</span>
  43. </template>
  44. <el-menu-item index="/activity/list">活动列表</el-menu-item>
  45. <el-menu-item index="/activity/create">创建活动</el-menu-item>
  46. </el-sub-menu>
  47. <el-sub-menu index="/matchmaker">
  48. <template #title>
  49. <el-icon><User /></el-icon>
  50. <span>红娘管理</span>
  51. </template>
  52. <el-menu-item index="/matchmaker/list">红娘列表</el-menu-item>
  53. <el-menu-item index="/matchmaker/audit">红娘审核</el-menu-item>
  54. <el-menu-item index="/matchmaker/create">添加红娘</el-menu-item>
  55. <el-menu-item index="/matchmaker/points-product">积分商品</el-menu-item>
  56. <el-menu-item index="/matchmaker/points-order">积分商城订单</el-menu-item>
  57. <el-menu-item index="/matchmaker/resource">线索管理</el-menu-item>
  58. <el-menu-item index="/matchmaker/case-audit">案例审核</el-menu-item>
  59. </el-sub-menu>
  60. <el-sub-menu index="/course">
  61. <template #title>
  62. <el-icon><Reading /></el-icon>
  63. <span>课程管理</span>
  64. </template>
  65. <el-menu-item index="/course/list">课程列表</el-menu-item>
  66. <el-menu-item index="/course/create">创建课程</el-menu-item>
  67. </el-sub-menu>
  68. <el-sub-menu index="/success-case">
  69. <template #title>
  70. <el-icon><TrophyBase /></el-icon>
  71. <span>成功案例</span>
  72. </template>
  73. <el-menu-item index="/success-case/list">案例列表</el-menu-item>
  74. <el-menu-item index="/success-case/create">创建案例</el-menu-item>
  75. </el-sub-menu>
  76. <el-sub-menu index="/user">
  77. <template #title>
  78. <el-icon><UserFilled /></el-icon>
  79. <span>用户管理</span>
  80. </template>
  81. <el-menu-item index="/user/list">用户列表</el-menu-item>
  82. <el-menu-item index="/user/vip">VIP用户</el-menu-item>
  83. </el-sub-menu>
  84. <!-- VIP套餐 - 仅超级管理员 -->
  85. <el-menu-item v-if="isSuperAdmin" index="/vip-package">
  86. <el-icon><Medal /></el-icon>
  87. <template #title>VIP套餐</template>
  88. </el-menu-item>
  89. <el-menu-item index="/dynamic">
  90. <el-icon><ChatDotSquare /></el-icon>
  91. <template #title>动态管理</template>
  92. </el-menu-item>
  93. <el-menu-item index="/report">
  94. <el-icon><Warning /></el-icon>
  95. <template #title>举报管理</template>
  96. </el-menu-item>
  97. </el-menu>
  98. </el-aside>
  99. <!-- 主内容区 -->
  100. <el-container>
  101. <!-- 顶部导航 -->
  102. <el-header class="layout-header">
  103. <div class="header-left">
  104. <el-icon class="collapse-icon" @click="toggleCollapse">
  105. <Fold v-if="!isCollapse" />
  106. <Expand v-else />
  107. </el-icon>
  108. <el-breadcrumb separator="/">
  109. <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
  110. <el-breadcrumb-item v-if="currentRoute.meta.title">
  111. {{ currentRoute.meta.title }}
  112. </el-breadcrumb-item>
  113. </el-breadcrumb>
  114. </div>
  115. <div class="header-right">
  116. <span class="username">{{ userInfo?.username || '管理员' }}</span>
  117. <el-dropdown @command="handleCommand">
  118. <el-avatar :size="35" :src="userInfo?.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
  119. <template #dropdown>
  120. <el-dropdown-menu>
  121. <el-dropdown-item command="profile">个人资料</el-dropdown-item>
  122. <el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
  123. </el-dropdown-menu>
  124. </template>
  125. </el-dropdown>
  126. </div>
  127. </el-header>
  128. <!-- 主内容 -->
  129. <el-main class="layout-main">
  130. <router-view v-slot="{ Component }">
  131. <transition name="fade" mode="out-in">
  132. <component :is="Component" />
  133. </transition>
  134. </router-view>
  135. </el-main>
  136. </el-container>
  137. </el-container>
  138. </template>
  139. <script setup>
  140. import { ref, computed } from 'vue'
  141. import { useRoute, useRouter } from 'vue-router'
  142. import { useUserStore } from '@/stores/user'
  143. import { ElMessage, ElMessageBox } from 'element-plus'
  144. import {
  145. DataBoard,
  146. Picture,
  147. Microphone,
  148. Calendar,
  149. User,
  150. Reading,
  151. TrophyBase,
  152. UserFilled,
  153. ChatDotSquare,
  154. Warning,
  155. Fold,
  156. Expand,
  157. Medal
  158. } from '@element-plus/icons-vue'
  159. const route = useRoute()
  160. const router = useRouter()
  161. const userStore = useUserStore()
  162. const isCollapse = ref(false)
  163. const userInfo = computed(() => userStore.userInfo)
  164. const currentRoute = computed(() => route)
  165. const activeMenu = computed(() => route.path)
  166. const isSuperAdmin = computed(() => userStore.isSuperAdmin)
  167. // 切换侧边栏折叠状态
  168. const toggleCollapse = () => {
  169. isCollapse.value = !isCollapse.value
  170. }
  171. // 处理用户菜单命令
  172. const handleCommand = async (command) => {
  173. switch (command) {
  174. case 'profile':
  175. router.push('/profile')
  176. break
  177. case 'logout':
  178. try {
  179. await ElMessageBox.confirm('确定要退出登录吗?', '提示', {
  180. confirmButtonText: '确定',
  181. cancelButtonText: '取消',
  182. type: 'warning'
  183. })
  184. await userStore.logout()
  185. ElMessage.success('退出成功')
  186. router.push('/login')
  187. } catch (error) {
  188. // 取消退出
  189. }
  190. break
  191. }
  192. }
  193. </script>
  194. <style scoped>
  195. /* ==================== 布局容器 ==================== */
  196. .layout-container {
  197. height: 100vh;
  198. overflow: hidden;
  199. }
  200. /* ==================== 侧边栏样式 ==================== */
  201. .layout-aside {
  202. background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
  203. box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
  204. transition: width var(--transition-slow);
  205. position: relative;
  206. z-index: 100;
  207. }
  208. /* Logo 容器 */
  209. .logo-container {
  210. height: var(--header-height);
  211. display: flex;
  212. align-items: center;
  213. justify-content: center;
  214. padding: 0 var(--spacing-lg);
  215. border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  216. background: rgba(255, 255, 255, 0.02);
  217. gap: var(--spacing-base);
  218. }
  219. .logo {
  220. width: 36px;
  221. height: 36px;
  222. flex-shrink: 0;
  223. filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
  224. transition: transform var(--transition-base);
  225. }
  226. .logo:hover {
  227. transform: scale(1.05);
  228. }
  229. .logo-text {
  230. font-size: var(--font-lg);
  231. font-weight: var(--font-bold);
  232. color: #ffffff;
  233. white-space: nowrap;
  234. letter-spacing: 0.5px;
  235. text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  236. }
  237. /* 侧边栏菜单 */
  238. .aside-menu {
  239. border: none;
  240. height: calc(100vh - var(--header-height));
  241. overflow-y: auto;
  242. overflow-x: hidden;
  243. background-color: transparent;
  244. }
  245. /* 菜单项样式覆盖 */
  246. .aside-menu :deep(.el-menu-item) {
  247. margin: 4px 12px;
  248. border-radius: var(--radius-base);
  249. transition: all var(--transition-base);
  250. height: 48px;
  251. line-height: 48px;
  252. position: relative;
  253. overflow: hidden;
  254. }
  255. .aside-menu :deep(.el-menu-item:hover) {
  256. background-color: var(--sidebar-hover-bg) !important;
  257. color: #ffffff !important;
  258. }
  259. .aside-menu :deep(.el-menu-item.is-active) {
  260. background: linear-gradient(90deg, var(--primary-color) 0%, var(--primary-light) 100%) !important;
  261. color: #ffffff !important;
  262. font-weight: var(--font-semibold);
  263. box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
  264. }
  265. .aside-menu :deep(.el-menu-item.is-active::before) {
  266. content: '';
  267. position: absolute;
  268. left: 0;
  269. top: 50%;
  270. transform: translateY(-50%);
  271. width: 4px;
  272. height: 24px;
  273. background-color: #ffffff;
  274. border-radius: 0 4px 4px 0;
  275. opacity: 0.8;
  276. }
  277. /* 子菜单样式 */
  278. .aside-menu :deep(.el-sub-menu__title) {
  279. margin: 4px 12px;
  280. border-radius: var(--radius-base);
  281. transition: all var(--transition-base);
  282. height: 48px;
  283. line-height: 48px;
  284. }
  285. .aside-menu :deep(.el-sub-menu__title:hover) {
  286. background-color: var(--sidebar-hover-bg) !important;
  287. color: #ffffff !important;
  288. }
  289. .aside-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
  290. color: var(--primary-light) !important;
  291. font-weight: var(--font-medium);
  292. }
  293. .aside-menu :deep(.el-menu--inline) {
  294. background-color: rgba(0, 0, 0, 0.15);
  295. }
  296. .aside-menu :deep(.el-menu--inline .el-menu-item) {
  297. padding-left: 52px !important;
  298. margin: 2px 8px;
  299. height: 42px;
  300. line-height: 42px;
  301. }
  302. /* 菜单图标 */
  303. .aside-menu :deep(.el-icon) {
  304. font-size: 18px;
  305. margin-right: var(--spacing-base);
  306. transition: transform var(--transition-base);
  307. }
  308. .aside-menu :deep(.el-menu-item:hover .el-icon),
  309. .aside-menu :deep(.el-sub-menu__title:hover .el-icon) {
  310. transform: scale(1.1);
  311. }
  312. /* 折叠状态 */
  313. .aside-menu.el-menu--collapse :deep(.el-menu-item),
  314. .aside-menu.el-menu--collapse :deep(.el-sub-menu__title) {
  315. justify-content: center;
  316. }
  317. /* 滚动条美化 */
  318. .aside-menu::-webkit-scrollbar {
  319. width: 6px;
  320. }
  321. .aside-menu::-webkit-scrollbar-thumb {
  322. background-color: rgba(255, 255, 255, 0.2);
  323. border-radius: 3px;
  324. transition: background-color var(--transition-base);
  325. }
  326. .aside-menu::-webkit-scrollbar-thumb:hover {
  327. background-color: rgba(255, 255, 255, 0.3);
  328. }
  329. .aside-menu::-webkit-scrollbar-track {
  330. background-color: transparent;
  331. }
  332. /* ==================== 顶部导航样式 ==================== */
  333. .layout-header {
  334. display: flex;
  335. justify-content: space-between;
  336. align-items: center;
  337. background-color: var(--bg-primary);
  338. border-bottom: 1px solid var(--border-color);
  339. padding: 0 var(--spacing-xl);
  340. height: var(--header-height);
  341. box-shadow: var(--shadow-sm);
  342. position: relative;
  343. z-index: 90;
  344. }
  345. .header-left {
  346. display: flex;
  347. align-items: center;
  348. gap: var(--spacing-xl);
  349. }
  350. .collapse-icon {
  351. font-size: 20px;
  352. cursor: pointer;
  353. color: var(--text-secondary);
  354. transition: all var(--transition-base);
  355. padding: var(--spacing-sm);
  356. border-radius: var(--radius-base);
  357. }
  358. .collapse-icon:hover {
  359. color: var(--primary-color);
  360. background-color: var(--bg-hover);
  361. transform: scale(1.05);
  362. }
  363. .header-right {
  364. display: flex;
  365. align-items: center;
  366. gap: var(--spacing-lg);
  367. }
  368. .username {
  369. font-size: var(--font-sm);
  370. color: var(--text-secondary);
  371. font-weight: var(--font-medium);
  372. margin-right: var(--spacing-sm);
  373. }
  374. /* 头像样式 */
  375. .header-right :deep(.el-avatar) {
  376. cursor: pointer;
  377. border: 2px solid var(--border-color);
  378. transition: all var(--transition-base);
  379. }
  380. .header-right :deep(.el-avatar:hover) {
  381. border-color: var(--primary-color);
  382. transform: scale(1.05);
  383. box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
  384. }
  385. /* 面包屑增强 */
  386. .header-left :deep(.el-breadcrumb) {
  387. font-size: var(--font-sm);
  388. }
  389. .header-left :deep(.el-breadcrumb__inner) {
  390. color: var(--text-secondary);
  391. transition: color var(--transition-base);
  392. }
  393. .header-left :deep(.el-breadcrumb__inner:hover) {
  394. color: var(--primary-color);
  395. }
  396. /* ==================== 主内容区样式 ==================== */
  397. .layout-main {
  398. background-color: var(--bg-secondary);
  399. padding: var(--spacing-xl);
  400. overflow-y: auto;
  401. height: calc(100vh - var(--header-height));
  402. position: relative;
  403. }
  404. /* ==================== 动画效果 ==================== */
  405. /* 路由过渡动画 */
  406. .fade-enter-active {
  407. animation: fadeInUp var(--transition-slow);
  408. }
  409. .fade-leave-active {
  410. animation: fadeOut var(--transition-fast);
  411. }
  412. @keyframes fadeOut {
  413. from {
  414. opacity: 1;
  415. }
  416. to {
  417. opacity: 0;
  418. }
  419. }
  420. /* Logo 文字淡入淡出 */
  421. .fade-enter-active,
  422. .fade-leave-active {
  423. transition: opacity var(--transition-base);
  424. }
  425. .fade-enter-from,
  426. .fade-leave-to {
  427. opacity: 0;
  428. }
  429. /* ==================== 响应式设计 ==================== */
  430. /* 平板设备 */
  431. @media (max-width: 1024px) {
  432. .layout-header {
  433. padding: 0 var(--spacing-lg);
  434. }
  435. .layout-main {
  436. padding: var(--spacing-lg);
  437. }
  438. .username {
  439. display: none;
  440. }
  441. }
  442. /* 移动设备 */
  443. @media (max-width: 768px) {
  444. .layout-aside {
  445. position: fixed;
  446. left: 0;
  447. top: 0;
  448. height: 100vh;
  449. z-index: 1000;
  450. }
  451. .layout-header {
  452. padding: 0 var(--spacing-md);
  453. }
  454. .header-left {
  455. gap: var(--spacing-md);
  456. }
  457. .layout-main {
  458. padding: var(--spacing-md);
  459. width: 100%;
  460. }
  461. .collapse-icon {
  462. font-size: 18px;
  463. }
  464. .header-right :deep(.el-avatar) {
  465. width: 32px;
  466. height: 32px;
  467. }
  468. }
  469. /* 超大屏幕优化 */
  470. @media (min-width: 1920px) {
  471. .layout-main {
  472. padding: var(--spacing-2xl) var(--spacing-3xl);
  473. }
  474. }
  475. </style>