main-layout.component.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. import {
  2. Component,
  3. OnInit,
  4. OnDestroy,
  5. ViewChild,
  6. ElementRef,
  7. AfterViewInit,
  8. } from '@angular/core';
  9. import {
  10. Router,
  11. RouterOutlet,
  12. RouterModule,
  13. NavigationEnd,
  14. } from '@angular/router';
  15. import { CommonModule } from '@angular/common';
  16. import { filter, Subscription } from 'rxjs';
  17. // 导入所有需要的 NG-ZORRO 模块
  18. import { NzLayoutModule } from 'ng-zorro-antd/layout';
  19. import { NzMenuModule } from 'ng-zorro-antd/menu';
  20. import { NzIconModule } from 'ng-zorro-antd/icon';
  21. import { NzButtonModule } from 'ng-zorro-antd/button';
  22. import { NzTabsModule } from 'ng-zorro-antd/tabs';
  23. import { AuthService } from '../services/auth.service';
  24. interface TabItem {
  25. title: string;
  26. path: string;
  27. }
  28. @Component({
  29. selector: 'app-main-layout',
  30. template: `
  31. <nz-layout class="app-layout">
  32. <nz-sider
  33. class="menu-sidebar"
  34. nzCollapsible
  35. nzTheme="dark"
  36. [nzCollapsedWidth]="64"
  37. nzWidth="216px"
  38. [(nzCollapsed)]="isCollapsed"
  39. [nzTrigger]="null"
  40. >
  41. <div class="menu-sidebar-inner">
  42. <div class="sidebar-logo">
  43. <a href="https://ng.ant.design/" target="_blank">
  44. <img src="https://ng.ant.design/assets/img/logo.svg" alt="logo" />
  45. <h1>OMS</h1>
  46. </a>
  47. </div>
  48. <ul nz-menu nzTheme="dark" nzMode="inline" class="menu">
  49. <!-- 路由到控制台页面 -->
  50. <li
  51. nz-menu-item
  52. [routerLink]="['/dashboard']"
  53. [routerLinkActive]="['ant-menu-item-selected']"
  54. [nzSelected]="activePath === '/dashboard'"
  55. >
  56. <span nz-icon nzType="dashboard"></span>
  57. <span>Dashboard</span>
  58. </li>
  59. <li
  60. nz-menu-item
  61. [routerLink]="['/user']"
  62. [routerLinkActive]="['ant-menu-item-selected']"
  63. [nzSelected]="activePath === '/user'"
  64. >
  65. <span nz-icon nzType="team"></span>
  66. <span>用户管理</span>
  67. </li>
  68. <li nz-submenu nzTitle="消息系统" nzIcon="message">
  69. <ul>
  70. <li
  71. nz-menu-item
  72. [routerLink]="['/message-activity']"
  73. [routerLinkActive]="['ant-menu-item-selected']"
  74. [nzSelected]="activePath === '/message-activity'"
  75. >
  76. <span nz-icon nzType="notification"></span>
  77. <span>消息通知</span>
  78. </li>
  79. <li
  80. nz-menu-item
  81. [routerLink]="['/message-record']"
  82. [routerLinkActive]="['ant-menu-item-selected']"
  83. [nzSelected]="activePath === '/message-record'"
  84. >
  85. <span nz-icon nzType="send"></span>
  86. <span>推送记录</span>
  87. </li>
  88. <li
  89. nz-menu-item
  90. [routerLink]="['/message-template']"
  91. [routerLinkActive]="['ant-menu-item-selected']"
  92. [nzSelected]="activePath === '/message-template'"
  93. >
  94. <span nz-icon nzType="cluster"></span>
  95. <span>消息模板</span>
  96. </li>
  97. <li
  98. nz-menu-item
  99. [routerLink]="['/message-strategy']"
  100. [routerLinkActive]="['ant-menu-item-selected']"
  101. [nzSelected]="activePath === '/message-strategy'"
  102. >
  103. <span nz-icon nzType="ant-design"></span>
  104. <span>推送策略</span>
  105. </li>
  106. <li
  107. nz-menu-item
  108. [routerLink]="['/message-statistics']"
  109. [routerLinkActive]="['ant-menu-item-selected']"
  110. [nzSelected]="activePath === '/message-statistics'"
  111. >
  112. <span nz-icon nzType="line-chart"></span>
  113. <span>统计分析</span>
  114. </li>
  115. </ul>
  116. </li>
  117. </ul>
  118. </div>
  119. </nz-sider>
  120. <!-- 主内容区域 -->
  121. <nz-layout>
  122. <!-- 顶部导航栏:核心修复区域 -->
  123. <nz-header class="header">
  124. <!-- 顶部容器:使用flex确保子元素同行 -->
  125. <div class="header-container">
  126. <!-- 1. 折叠按钮:固定宽度,不压缩 -->
  127. <div class="header-trigger-wrapper">
  128. <span class="header-trigger" (click)="isCollapsed = !isCollapsed">
  129. <i
  130. class="trigger"
  131. nz-icon
  132. [nzType]="isCollapsed ? 'menu-unfold' : 'menu-fold'"
  133. ></i>
  134. </span>
  135. </div>
  136. <!-- 2. Tab标签页:占满中间剩余空间,溢出时可滚动 -->
  137. <div class="tab-container">
  138. <div class="tabs-wrapper" #tabsWrapper>
  139. <nz-tabs
  140. [nzSelectedIndex]="getTabIndex(activePath)"
  141. nzType="card"
  142. nzHideAll
  143. nzSize="small"
  144. (nzSelectedIndexChange)="selectTab(openTabs[$event]?.path)"
  145. >
  146. <nz-tab
  147. *ngFor="let tab of openTabs; let i = index"
  148. [nzTitle]="titleTemplate"
  149. >
  150. <ng-template #titleTemplate>
  151. <span class="tab-title" (click)="selectTab(tab.path)">{{
  152. tab.title
  153. }}</span>
  154. <i
  155. nz-icon
  156. nzType="close"
  157. class="close-icon"
  158. (click)="
  159. closeTab(tab.path, i); $event.stopPropagation()
  160. "
  161. ></i>
  162. </ng-template>
  163. </nz-tab>
  164. </nz-tabs>
  165. </div>
  166. <button
  167. nz-button
  168. nzType="text"
  169. nzSize="small"
  170. class="scroll-btn"
  171. (click)="scrollTabs('right')"
  172. >
  173. <span nz-icon nzType="right"></span>
  174. </button>
  175. </div>
  176. <!-- 3. Admin菜单:固定在右侧,不压缩 -->
  177. <div class="user-menu-wrapper">
  178. <ul nz-menu nzTheme="light" nzMode="horizontal" class="user-menu">
  179. <li nz-submenu nzTitle="Admin" nzIcon="user">
  180. <ul>
  181. <li nz-menu-item>个人设置</li>
  182. <li nz-menu-item (click)="logout()">退出登录</li>
  183. </ul>
  184. </li>
  185. </ul>
  186. </div>
  187. </div>
  188. </nz-header>
  189. <!-- 内容区域(保持不变) -->
  190. <nz-content>
  191. <div class="inner-content" #content>
  192. <router-outlet></router-outlet>
  193. </div>
  194. </nz-content>
  195. </nz-layout>
  196. </nz-layout>
  197. `,
  198. styles: [
  199. `
  200. :host {
  201. display: flex;
  202. text-rendering: optimizeLegibility;
  203. -webkit-font-smoothing: antialiased;
  204. -moz-osx-font-smoothing: grayscale;
  205. }
  206. .app-layout {
  207. min-height: 100vh;
  208. display: flex;
  209. }
  210. /* 侧边栏样式(保持不变) */
  211. .menu-sidebar {
  212. position: relative;
  213. z-index: 1001;
  214. min-height: 100vh;
  215. box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
  216. background: #001529;
  217. }
  218. .menu-sidebar-inner {
  219. position: sticky;
  220. height: 100vh;
  221. top: 0;
  222. left: 0;
  223. display: flex;
  224. flex-direction: column;
  225. background: #001529;
  226. }
  227. .menu {
  228. flex: 1;
  229. overflow-y: auto;
  230. padding-bottom: 20px;
  231. }
  232. /* 侧边栏Logo */
  233. .sidebar-logo {
  234. height: 64px;
  235. padding-left: 14px;
  236. overflow: hidden;
  237. line-height: 64px;
  238. }
  239. .sidebar-logo img {
  240. display: inline-block;
  241. height: 32px;
  242. width: 32px;
  243. vertical-align: middle;
  244. }
  245. .sidebar-logo h1 {
  246. display: inline-block;
  247. margin: 0 0 0 20px;
  248. color: #fff;
  249. font-weight: 600;
  250. font-size: 14px;
  251. vertical-align: middle;
  252. }
  253. /* 顶部导航栏核心修复样式 */
  254. .header {
  255. padding: 0;
  256. width: 100%;
  257. z-index: 1000;
  258. /* 固定顶部高度,避免元素挤压换行 */
  259. height: 64px;
  260. }
  261. /* 顶部容器:flex布局确保子元素同行 */
  262. .header-container {
  263. display: flex;
  264. align-items: center;
  265. justify-content: space-between;
  266. height: 100%;
  267. padding: 0 8px;
  268. background: #fff;
  269. box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  270. }
  271. /* 1. 折叠按钮容器:固定宽度,不压缩 */
  272. .header-trigger-wrapper {
  273. width: 64px; /* 与折叠按钮点击区域匹配 */
  274. display: flex;
  275. align-items: center;
  276. justify-content: center;
  277. }
  278. .header-trigger {
  279. height: 100%;
  280. width: 100%;
  281. display: flex;
  282. align-items: center;
  283. justify-content: center;
  284. cursor: pointer;
  285. transition: all 0.3s;
  286. }
  287. .trigger:hover {
  288. color: #1890ff;
  289. }
  290. /* 2. Tab标签页容器:占满中间空间,溢出时横向滚动 */
  291. .tab-container {
  292. flex: 1; /* 关键:占满剩余宽度 */
  293. margin: 0 8px;
  294. height: 100%;
  295. display: flex;
  296. align-items: center;
  297. overflow: hidden; /* 隐藏超出容器的内容 */
  298. }
  299. .tabs-wrapper {
  300. width: 100%;
  301. height: 100%;
  302. overflow-x: auto; /* 横向滚动 */
  303. white-space: nowrap; /* 禁止Tab换行 */
  304. /* 隐藏滚动条,保持美观 */
  305. scrollbar-width: none;
  306. -ms-overflow-style: none;
  307. }
  308. .tabs-wrapper::-webkit-scrollbar {
  309. display: none;
  310. }
  311. /* 优化Tab样式,确保垂直居中 */
  312. :host ::ng-deep .ant-tabs-card {
  313. height: 100%;
  314. display: flex;
  315. flex-direction: column;
  316. }
  317. :host ::ng-deep .ant-tabs-card .ant-tabs-card-bar {
  318. border-bottom: none;
  319. flex-shrink: 0; /* 禁止Tab栏被压缩 */
  320. }
  321. :host ::ng-deep .ant-tabs-card .ant-tabs-tab {
  322. height: 36px;
  323. line-height: 36px;
  324. margin-top: 8px; /* 与顶部保持间距 */
  325. }
  326. /* 3. Admin菜单容器:固定宽度,不压缩 */
  327. .user-menu-wrapper {
  328. width: 120px; /* 与Admin菜单宽度匹配 */
  329. display: flex;
  330. align-items: center;
  331. justify-content: center;
  332. }
  333. .user-menu {
  334. width: 100%;
  335. display: flex;
  336. align-items: center;
  337. justify-content: center;
  338. }
  339. /* 内容区域样式(保持不变) */
  340. nz-content {
  341. margin: 0;
  342. background-color: white;
  343. }
  344. .inner-content {
  345. padding: 24px;
  346. background: #fff;
  347. min-height: calc(100vh - 64px);
  348. }
  349. /* Tab标签页细节样式 */
  350. .tab-title {
  351. cursor: pointer;
  352. }
  353. .close-icon {
  354. margin-left: 8px;
  355. font-size: 12px;
  356. color: rgba(0, 0, 0, 0.45);
  357. cursor: pointer;
  358. }
  359. .close-icon:hover {
  360. color: #f5222d;
  361. }
  362. `,
  363. ],
  364. standalone: true,
  365. imports: [
  366. CommonModule,
  367. RouterOutlet,
  368. RouterModule,
  369. NzLayoutModule,
  370. NzMenuModule,
  371. NzIconModule,
  372. NzButtonModule,
  373. NzTabsModule,
  374. ],
  375. })
  376. export class MainLayoutComponent implements OnInit, OnDestroy, AfterViewInit {
  377. @ViewChild('tabsWrapper') tabsWrapper!: ElementRef<HTMLDivElement>;
  378. @ViewChild('content') content!: ElementRef;
  379. isCollapsed = true;
  380. openTabs: TabItem[] = [];
  381. activePath: string | null = null;
  382. private routerSubscription!: Subscription;
  383. private routeTitleMap: { [key: string]: string } = {
  384. '/dashboard': '数据看板',
  385. '/user': '用户管理',
  386. '/message-activity': '消息通知',
  387. '/message-record': '推送记录',
  388. '/message-template': '消息模板',
  389. '/message-strategy': '推送策略',
  390. '/message-statistics': '统计分析',
  391. };
  392. constructor(private router: Router, private authService: AuthService) {}
  393. ngOnInit(): void {
  394. // 监听路由变化,更新标签页
  395. this.routerSubscription = this.router.events
  396. .pipe(filter((event) => event instanceof NavigationEnd))
  397. .subscribe((event: NavigationEnd) => {
  398. const path = event.urlAfterRedirects.split('?')[0];
  399. this.activePath = path;
  400. // 新增未打开的标签页
  401. if (!this.openTabs.some((tab) => tab.path === path)) {
  402. const title = this.routeTitleMap[path] || '新页面';
  403. this.openTabs.push({ title, path });
  404. }
  405. });
  406. // 初始化当前路由标签页
  407. this.activePath = this.router.url.split('?')[0];
  408. if (
  409. this.activePath &&
  410. !this.openTabs.some((tab) => tab.path === this.activePath)
  411. ) {
  412. const title = this.routeTitleMap[this.activePath] || '新页面';
  413. this.openTabs.push({ title, path: this.activePath });
  414. }
  415. }
  416. ngAfterViewInit(): void {
  417. this.scrollToActiveTab();
  418. }
  419. ngOnDestroy(): void {
  420. if (this.routerSubscription) {
  421. this.routerSubscription.unsubscribe();
  422. }
  423. }
  424. // 获取当前标签页索引
  425. getTabIndex(path: string | null): number {
  426. return path ? this.openTabs.findIndex((tab) => tab.path === path) : -1;
  427. }
  428. // 切换标签页
  429. selectTab(path: string | undefined): void {
  430. if (path && this.activePath !== path) {
  431. this.router.navigateByUrl(path);
  432. }
  433. }
  434. // 关闭标签页
  435. closeTab(path: string, index: number): void {
  436. if (this.openTabs.length <= 1) return;
  437. this.openTabs = this.openTabs.filter((tab) => tab.path !== path);
  438. // 若关闭当前激活标签页,跳转至前一个标签页
  439. if (this.activePath === path) {
  440. const newIndex = Math.max(0, index - 1);
  441. this.router.navigateByUrl(this.openTabs[newIndex].path);
  442. }
  443. }
  444. // 退出登录
  445. logout(): void {
  446. this.authService.logout();
  447. this.router.navigate(['/login']);
  448. }
  449. // 滚动标签页(左右滚动)
  450. scrollTabs(direction: 'left' | 'right'): void {
  451. const wrapper = this.tabsWrapper.nativeElement;
  452. wrapper.scrollBy({
  453. left: direction === 'left' ? -150 : 150,
  454. behavior: 'smooth',
  455. });
  456. }
  457. // 滚动到当前激活标签页
  458. private scrollToActiveTab(): void {
  459. if (!this.tabsWrapper) return;
  460. const activeTab = this.tabsWrapper.nativeElement.querySelector(
  461. '.ant-tabs-tab-active'
  462. );
  463. if (activeTab) {
  464. activeTab.scrollIntoView({
  465. behavior: 'smooth',
  466. block: 'nearest',
  467. inline: 'center',
  468. });
  469. }
  470. }
  471. }