| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- import {
- Component,
- OnInit,
- OnDestroy,
- ViewChild,
- ElementRef,
- AfterViewInit,
- } from '@angular/core';
- import {
- Router,
- RouterOutlet,
- RouterModule,
- NavigationEnd,
- } from '@angular/router';
- import { CommonModule } from '@angular/common';
- import { filter, Subscription } from 'rxjs';
- // 导入所有需要的 NG-ZORRO 模块
- import { NzLayoutModule } from 'ng-zorro-antd/layout';
- import { NzMenuModule } from 'ng-zorro-antd/menu';
- import { NzIconModule } from 'ng-zorro-antd/icon';
- import { NzButtonModule } from 'ng-zorro-antd/button';
- import { NzTabsModule } from 'ng-zorro-antd/tabs';
- import { AuthService } from '../services/auth.service';
- interface TabItem {
- title: string;
- path: string;
- }
- @Component({
- selector: 'app-main-layout',
- template: `
- <nz-layout class="app-layout">
- <nz-sider
- class="menu-sidebar"
- nzCollapsible
- nzTheme="dark"
- [nzCollapsedWidth]="64"
- nzWidth="216px"
- [(nzCollapsed)]="isCollapsed"
- [nzTrigger]="null"
- >
- <div class="menu-sidebar-inner">
- <div class="sidebar-logo">
- <a href="https://ng.ant.design/" target="_blank">
- <img src="https://ng.ant.design/assets/img/logo.svg" alt="logo" />
- <h1>OMS</h1>
- </a>
- </div>
- <ul nz-menu nzTheme="dark" nzMode="inline" class="menu">
- <!-- 路由到控制台页面 -->
- <li
- nz-menu-item
- [routerLink]="['/dashboard']"
- [routerLinkActive]="['ant-menu-item-selected']"
- [nzSelected]="activePath === '/dashboard'"
- >
- <span nz-icon nzType="dashboard"></span>
- <span>Dashboard</span>
- </li>
- <li
- nz-menu-item
- [routerLink]="['/user']"
- [routerLinkActive]="['ant-menu-item-selected']"
- [nzSelected]="activePath === '/user'"
- >
- <span nz-icon nzType="team"></span>
- <span>用户管理</span>
- </li>
- <li nz-submenu nzTitle="消息系统" nzIcon="message">
- <ul>
- <li
- nz-menu-item
- [routerLink]="['/message-activity']"
- [routerLinkActive]="['ant-menu-item-selected']"
- [nzSelected]="activePath === '/message-activity'"
- >
- <span nz-icon nzType="notification"></span>
- <span>消息通知</span>
- </li>
- <li
- nz-menu-item
- [routerLink]="['/message-record']"
- [routerLinkActive]="['ant-menu-item-selected']"
- [nzSelected]="activePath === '/message-record'"
- >
- <span nz-icon nzType="send"></span>
- <span>推送记录</span>
- </li>
- <li
- nz-menu-item
- [routerLink]="['/message-template']"
- [routerLinkActive]="['ant-menu-item-selected']"
- [nzSelected]="activePath === '/message-template'"
- >
- <span nz-icon nzType="cluster"></span>
- <span>消息模板</span>
- </li>
- <li
- nz-menu-item
- [routerLink]="['/message-strategy']"
- [routerLinkActive]="['ant-menu-item-selected']"
- [nzSelected]="activePath === '/message-strategy'"
- >
- <span nz-icon nzType="ant-design"></span>
- <span>推送策略</span>
- </li>
- <li
- nz-menu-item
- [routerLink]="['/message-statistics']"
- [routerLinkActive]="['ant-menu-item-selected']"
- [nzSelected]="activePath === '/message-statistics'"
- >
- <span nz-icon nzType="line-chart"></span>
- <span>统计分析</span>
- </li>
- </ul>
- </li>
- </ul>
- </div>
- </nz-sider>
- <!-- 主内容区域 -->
- <nz-layout>
- <!-- 顶部导航栏:核心修复区域 -->
- <nz-header class="header">
- <!-- 顶部容器:使用flex确保子元素同行 -->
- <div class="header-container">
- <!-- 1. 折叠按钮:固定宽度,不压缩 -->
- <div class="header-trigger-wrapper">
- <span class="header-trigger" (click)="isCollapsed = !isCollapsed">
- <i
- class="trigger"
- nz-icon
- [nzType]="isCollapsed ? 'menu-unfold' : 'menu-fold'"
- ></i>
- </span>
- </div>
- <!-- 2. Tab标签页:占满中间剩余空间,溢出时可滚动 -->
- <div class="tab-container">
- <div class="tabs-wrapper" #tabsWrapper>
- <nz-tabs
- [nzSelectedIndex]="getTabIndex(activePath)"
- nzType="card"
- nzHideAll
- nzSize="small"
- (nzSelectedIndexChange)="selectTab(openTabs[$event]?.path)"
- >
- <nz-tab
- *ngFor="let tab of openTabs; let i = index"
- [nzTitle]="titleTemplate"
- >
- <ng-template #titleTemplate>
- <span class="tab-title" (click)="selectTab(tab.path)">{{
- tab.title
- }}</span>
- <i
- nz-icon
- nzType="close"
- class="close-icon"
- (click)="
- closeTab(tab.path, i); $event.stopPropagation()
- "
- ></i>
- </ng-template>
- </nz-tab>
- </nz-tabs>
- </div>
- <button
- nz-button
- nzType="text"
- nzSize="small"
- class="scroll-btn"
- (click)="scrollTabs('right')"
- >
- <span nz-icon nzType="right"></span>
- </button>
- </div>
- <!-- 3. Admin菜单:固定在右侧,不压缩 -->
- <div class="user-menu-wrapper">
- <ul nz-menu nzTheme="light" nzMode="horizontal" class="user-menu">
- <li nz-submenu nzTitle="Admin" nzIcon="user">
- <ul>
- <li nz-menu-item>个人设置</li>
- <li nz-menu-item (click)="logout()">退出登录</li>
- </ul>
- </li>
- </ul>
- </div>
- </div>
- </nz-header>
- <!-- 内容区域(保持不变) -->
- <nz-content>
- <div class="inner-content" #content>
- <router-outlet></router-outlet>
- </div>
- </nz-content>
- </nz-layout>
- </nz-layout>
- `,
- styles: [
- `
- :host {
- display: flex;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- .app-layout {
- min-height: 100vh;
- display: flex;
- }
- /* 侧边栏样式(保持不变) */
- .menu-sidebar {
- position: relative;
- z-index: 1001;
- min-height: 100vh;
- box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
- background: #001529;
- }
- .menu-sidebar-inner {
- position: sticky;
- height: 100vh;
- top: 0;
- left: 0;
- display: flex;
- flex-direction: column;
- background: #001529;
- }
- .menu {
- flex: 1;
- overflow-y: auto;
- padding-bottom: 20px;
- }
- /* 侧边栏Logo */
- .sidebar-logo {
- height: 64px;
- padding-left: 14px;
- overflow: hidden;
- line-height: 64px;
- }
- .sidebar-logo img {
- display: inline-block;
- height: 32px;
- width: 32px;
- vertical-align: middle;
- }
- .sidebar-logo h1 {
- display: inline-block;
- margin: 0 0 0 20px;
- color: #fff;
- font-weight: 600;
- font-size: 14px;
- vertical-align: middle;
- }
- /* 顶部导航栏核心修复样式 */
- .header {
- padding: 0;
- width: 100%;
- z-index: 1000;
- /* 固定顶部高度,避免元素挤压换行 */
- height: 64px;
- }
- /* 顶部容器:flex布局确保子元素同行 */
- .header-container {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 100%;
- padding: 0 8px;
- background: #fff;
- box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
- }
- /* 1. 折叠按钮容器:固定宽度,不压缩 */
- .header-trigger-wrapper {
- width: 64px; /* 与折叠按钮点击区域匹配 */
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .header-trigger {
- height: 100%;
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.3s;
- }
- .trigger:hover {
- color: #1890ff;
- }
- /* 2. Tab标签页容器:占满中间空间,溢出时横向滚动 */
- .tab-container {
- flex: 1; /* 关键:占满剩余宽度 */
- margin: 0 8px;
- height: 100%;
- display: flex;
- align-items: center;
- overflow: hidden; /* 隐藏超出容器的内容 */
- }
- .tabs-wrapper {
- width: 100%;
- height: 100%;
- overflow-x: auto; /* 横向滚动 */
- white-space: nowrap; /* 禁止Tab换行 */
- /* 隐藏滚动条,保持美观 */
- scrollbar-width: none;
- -ms-overflow-style: none;
- }
- .tabs-wrapper::-webkit-scrollbar {
- display: none;
- }
- /* 优化Tab样式,确保垂直居中 */
- :host ::ng-deep .ant-tabs-card {
- height: 100%;
- display: flex;
- flex-direction: column;
- }
- :host ::ng-deep .ant-tabs-card .ant-tabs-card-bar {
- border-bottom: none;
- flex-shrink: 0; /* 禁止Tab栏被压缩 */
- }
- :host ::ng-deep .ant-tabs-card .ant-tabs-tab {
- height: 36px;
- line-height: 36px;
- margin-top: 8px; /* 与顶部保持间距 */
- }
- /* 3. Admin菜单容器:固定宽度,不压缩 */
- .user-menu-wrapper {
- width: 120px; /* 与Admin菜单宽度匹配 */
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .user-menu {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- /* 内容区域样式(保持不变) */
- nz-content {
- margin: 0;
- background-color: white;
- }
- .inner-content {
- padding: 24px;
- background: #fff;
- min-height: calc(100vh - 64px);
- }
- /* Tab标签页细节样式 */
- .tab-title {
- cursor: pointer;
- }
- .close-icon {
- margin-left: 8px;
- font-size: 12px;
- color: rgba(0, 0, 0, 0.45);
- cursor: pointer;
- }
- .close-icon:hover {
- color: #f5222d;
- }
- `,
- ],
- standalone: true,
- imports: [
- CommonModule,
- RouterOutlet,
- RouterModule,
- NzLayoutModule,
- NzMenuModule,
- NzIconModule,
- NzButtonModule,
- NzTabsModule,
- ],
- })
- export class MainLayoutComponent implements OnInit, OnDestroy, AfterViewInit {
- @ViewChild('tabsWrapper') tabsWrapper!: ElementRef<HTMLDivElement>;
- @ViewChild('content') content!: ElementRef;
- isCollapsed = true;
- openTabs: TabItem[] = [];
- activePath: string | null = null;
- private routerSubscription!: Subscription;
- private routeTitleMap: { [key: string]: string } = {
- '/dashboard': '数据看板',
- '/user': '用户管理',
- '/message-activity': '消息通知',
- '/message-record': '推送记录',
- '/message-template': '消息模板',
- '/message-strategy': '推送策略',
- '/message-statistics': '统计分析',
- };
- constructor(private router: Router, private authService: AuthService) {}
- ngOnInit(): void {
- // 监听路由变化,更新标签页
- this.routerSubscription = this.router.events
- .pipe(filter((event) => event instanceof NavigationEnd))
- .subscribe((event: NavigationEnd) => {
- const path = event.urlAfterRedirects.split('?')[0];
- this.activePath = path;
- // 新增未打开的标签页
- if (!this.openTabs.some((tab) => tab.path === path)) {
- const title = this.routeTitleMap[path] || '新页面';
- this.openTabs.push({ title, path });
- }
- });
- // 初始化当前路由标签页
- this.activePath = this.router.url.split('?')[0];
- if (
- this.activePath &&
- !this.openTabs.some((tab) => tab.path === this.activePath)
- ) {
- const title = this.routeTitleMap[this.activePath] || '新页面';
- this.openTabs.push({ title, path: this.activePath });
- }
- }
- ngAfterViewInit(): void {
- this.scrollToActiveTab();
- }
- ngOnDestroy(): void {
- if (this.routerSubscription) {
- this.routerSubscription.unsubscribe();
- }
- }
- // 获取当前标签页索引
- getTabIndex(path: string | null): number {
- return path ? this.openTabs.findIndex((tab) => tab.path === path) : -1;
- }
- // 切换标签页
- selectTab(path: string | undefined): void {
- if (path && this.activePath !== path) {
- this.router.navigateByUrl(path);
- }
- }
- // 关闭标签页
- closeTab(path: string, index: number): void {
- if (this.openTabs.length <= 1) return;
- this.openTabs = this.openTabs.filter((tab) => tab.path !== path);
- // 若关闭当前激活标签页,跳转至前一个标签页
- if (this.activePath === path) {
- const newIndex = Math.max(0, index - 1);
- this.router.navigateByUrl(this.openTabs[newIndex].path);
- }
- }
- // 退出登录
- logout(): void {
- this.authService.logout();
- this.router.navigate(['/login']);
- }
- // 滚动标签页(左右滚动)
- scrollTabs(direction: 'left' | 'right'): void {
- const wrapper = this.tabsWrapper.nativeElement;
- wrapper.scrollBy({
- left: direction === 'left' ? -150 : 150,
- behavior: 'smooth',
- });
- }
- // 滚动到当前激活标签页
- private scrollToActiveTab(): void {
- if (!this.tabsWrapper) return;
- const activeTab = this.tabsWrapper.nativeElement.querySelector(
- '.ant-tabs-tab-active'
- );
- if (activeTab) {
- activeTab.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- inline: 'center',
- });
- }
- }
- }
|