第42课:Angular Material 集成——模块导入、主题、CDK、响应式布局 Angular Material 是 Angular 团队官方维护的 UI 组件库,严格遵循 Material Design 3 规范。与 MUI(React)和 Element Plus(Vue)不同,Angular Material 深度集成 Angular 的模块系统 、依赖注入 和响应式编程模型 。它的独特之处在于将核心交互逻辑抽象为独立的 CDK(Component Dev Kit) 层,允许开发者在不使用 Material 样式的情况下复用复杂交互(拖拽、虚拟滚动、覆盖层等)。本节课将系统讲解 Angular Material 的模块导入策略、主题定制体系、CDK 的核心工具,以及基于 BreakpointObserver 的响应式布局实现。
1. 安装与项目初始化 1.1 通过 Angular CLI 添加 Angular Material 在已有的 Angular 项目(v17+ 推荐使用 Standalone Components)中,通过 CLI 一键安装:
1 ng add @angular/material
该命令会执行以下操作:
安装 @angular/material 和 @angular/cdk。
询问主题选择(预置主题或自定义)。
询问是否设置全局排版样式(推荐 y)。
询问是否包含浏览器动画模块(@angular/platform-browser/animations,必须选择 y,否则大部分组件无法正常工作)。
1.2 手动安装(了解底层依赖) 1 pnpm add @angular/material @angular/cdk @angular/animations
在 app.config.ts(Standalone 应用)或 app.module.ts(NgModule 应用)中启用动画:
1 2 3 4 5 6 7 8 9 10 import { ApplicationConfig , provideZoneChangeDetection } from '@angular/core' ;import { provideAnimationsAsync } from '@angular/platform-browser/animations/async' ;export const appConfig : ApplicationConfig = { providers : [ provideZoneChangeDetection ({ eventCoalescing : true }), provideAnimationsAsync (), ], };
1 2 3 4 5 6 7 8 import { NgModule } from '@angular/core' ;import { BrowserAnimationsModule } from '@angular/platform-browser/animations' ;@NgModule ({ imports : [BrowserAnimationsModule ], }) export class AppModule {}
2. 模块导入策略 Angular Material 的每个组件都位于独立的模块中,允许精确的 Tree Shaking。
2.1 Standalone Components 导入(Angular v17+ 推荐) 在 Standalone 组件中直接导入所需的组件模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { Component } from '@angular/core' ;import { MatCardModule } from '@angular/material/card' ;import { MatButtonModule } from '@angular/material/button' ;import { MatInputModule } from '@angular/material/input' ;import { MatFormFieldModule } from '@angular/material/form-field' ;import { MatIconModule } from '@angular/material/icon' ;@Component ({ selector : 'app-user-profile' , standalone : true , imports : [ MatCardModule , MatButtonModule , MatInputModule , MatFormFieldModule , MatIconModule , ], template : ` <mat-card> <mat-card-header> <mat-card-title>用户资料</mat-card-title> </mat-card-header> <mat-card-content> <mat-form-field appearance="outline" class="w-full"> <mat-label>姓名</mat-label> <input matInput placeholder="请输入姓名" /> </mat-form-field> </mat-card-content> <mat-card-actions align="end"> <button mat-button>取消</button> <button mat-raised-button color="primary">保存</button> </mat-card-actions> </mat-card> ` ,}) export class UserProfileComponent {}
关键点 :
每个 Material 组件对应一个 MatXxxModule(如 MatCardModule、MatButtonModule)。
仅在需要的 Standalone 组件的 imports 中引入,即可在该组件的模板中使用对应的选择器。
MatFormFieldModule 必须与 MatInputModule 配合使用才能正确渲染输入框的标签和错误提示。
2.2 共享模块模式(NgModule 传统方式) 对于仍使用 NgModule 的项目,推荐创建一个 MaterialModule 统一导入和导出所有需要的 Material 模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { NgModule } from '@angular/core' ;import { MatCardModule } from '@angular/material/card' ;import { MatButtonModule } from '@angular/material/button' ;import { MatInputModule } from '@angular/material/input' ;import { MatFormFieldModule } from '@angular/material/form-field' ;import { MatToolbarModule } from '@angular/material/toolbar' ;import { MatSidenavModule } from '@angular/material/sidenav' ;import { MatIconModule } from '@angular/material/icon' ;import { MatListModule } from '@angular/material/list' ;import { MatTableModule } from '@angular/material/table' ;import { MatPaginatorModule } from '@angular/material/paginator' ;import { MatDialogModule } from '@angular/material/dialog' ;import { MatSnackBarModule } from '@angular/material/snack-bar' ;const MATERIAL_MODULES = [ MatCardModule , MatButtonModule , MatInputModule , MatFormFieldModule , MatToolbarModule , MatSidenavModule , MatIconModule , MatListModule , MatTableModule , MatPaginatorModule , MatDialogModule , MatSnackBarModule , ]; @NgModule ({ exports : MATERIAL_MODULES , }) export class MaterialModule {}
然后在其他功能模块中导入 MaterialModule 即可使用所有组件。
3. 主题定制体系 Angular Material 的主题系统基于 SCSS,从 v19 开始也支持 CSS 变量。它通过调色板(define-palette)、主题定义(define-light-theme / define-dark-theme)和组件密度(define-density)三层构建主题。
3.1 预置主题 安装时可选择 4 套预置主题之一:indigo-pink、deeppurple-amber、pink-bluegrey、purple-green。预置主题的样式文件已在 angular.json 或 styles.scss 中引入。
3.2 自定义主题(SCSS 方式) 在 styles.scss 中使用 @angular/material/theming API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @use '@angular/material' as mat;$my-primary : mat.define-palette (mat.$indigo-palette , 500 );$my-accent : mat.define-palette (mat.$pink-palette , A200, A100, A400);$my-warn : mat.define-palette (mat.$red-palette );$my-light-theme : mat.define-light-theme (( color: ( primary: $my-primary , accent: $my-accent , warn: $my-warn , ), typography: mat.define-typography-config ( $font-family : "'Inter', 'Roboto', sans-serif" , ), density: 0 , )); @include mat.all-component-themes($my-light-theme );$my-dark-theme : mat.define-dark-theme (( color: ( primary: $my-primary , accent: $my-accent , warn: $my-warn , ), )); .dark-theme { @include mat.all-component-colors($my-dark-theme ); }
调色板 API 解析 :
mat.define-palette(base-palette, default-hue, lighter-hue, darker-hue) 返回一个调色板对象,Material 组件会自动从中提取不同状态的颜色。
参数
说明
默认值
base-palette
基础调色板(如 $indigo-palette),Material 内置 20+ 种。
必填
default-hue
默认色调(50~900,或 A100/A200/A400/A700)。
500
lighter-hue
较浅色调(用于 hover/focus 背景等)。
100
darker-hue
较深色调(用于 active 状态等)。
700
3.3 主题切换(运行时) 在 index.html 的 <body> 上动态添加/移除 dark-theme 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { Injectable , Renderer2 , RendererFactory2 } from '@angular/core' ;@Injectable ({ providedIn : 'root' })export class ThemeService { private renderer : Renderer2 ; private isDark = false ; constructor (rendererFactory: RendererFactory2 ) { this .renderer = rendererFactory.createRenderer (null , null ); } toggleTheme ( ) { this .isDark = !this .isDark ; if (this .isDark ) { this .renderer .addClass (document .body , 'dark-theme' ); } else { this .renderer .removeClass (document .body , 'dark-theme' ); } } }
注意 :由于 Angular 的视图封装机制,使用 Renderer2 操作 DOM 是最规范的方式,可确保在服务端渲染和 Web Worker 环境中安全运行。
3.4 组件密度 Material 组件支持三种密度模式:0(默认)、-1(中等密度)、-2(最大密度)。在主题定义中设置 density 参数即可全局生效,也可在特定组件上通过 density CSS 类覆盖。
4. CDK(Component Dev Kit)核心工具 CDK 是 Angular Material 的底层抽象库,提供了一组无样式、纯交互逻辑 的工具。它的设计目标是:即使你不使用 Material Design 视觉风格,也能复用这些经过充分测试的复杂交互行为。
4.1 常用 CDK 模块一览
CDK 模块
功能描述
典型场景
@angular/cdk/overlay
创建浮层(对话框、下拉菜单、工具提示等),自动管理定位、滚动和焦点。
自定义 Select、DatePicker、上下文菜单。
@angular/cdk/drag-drop
拖拽排序功能,支持自由拖拽、列表排序、容器间转移。
看板、任务排序、文件管理器。
@angular/cdk/virtual-scroll
虚拟滚动,仅渲染可视区域内的 DOM 节点。
万级数据列表、聊天消息、日志查看器。
@angular/cdk/scrolling
滚动监听、ScrollDispatcher 全局滚动事件总线。
滚动联动、无限滚动加载。
@angular/cdk/layout
BreakpointObserver 和 MediaMatcher,响应式断点监听。
响应式布局、根据屏幕尺寸动态调整 UI。
@angular/cdk/a11y
焦点管理、FocusTrap、LiveAnnouncer、FocusMonitor。
模态框焦点锁定、动态内容朗读。
@angular/cdk/portal
Portal 系统,允许在 DOM 树的任意位置动态渲染组件。
Toast 通知挂载、动态 Tab 内容。
4.2 cdkDrag 拖拽示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { Component } from '@angular/core' ;import { CdkDragDrop , DragDropModule , moveItemInArray } from '@angular/cdk/drag-drop' ;import { CommonModule } from '@angular/common' ;@Component ({ selector : 'app-task-board' , standalone : true , imports : [CommonModule , DragDropModule ], template : ` <h3>任务列表(可拖拽排序)</h3> <div cdkDropList (cdkDropListDropped)="drop($event)"> <div *ngFor="let task of tasks; let i = index" cdkDrag class="task-item"> {{ task }} <span class="drag-handle" cdkDragHandle>⠿</span> </div> </div> ` , styles : [` .task-item { padding: 12px 16px; margin-bottom: 8px; background: #f5f5f5; border-radius: 4px; cursor: grab; display: flex; justify-content: space-between; align-items: center; } .cdk-drag-preview { box-shadow: 0 5px 15px rgba(0,0,0,0.2); } .cdk-drag-placeholder { opacity: 0.3; } ` ],}) export class TaskBoardComponent { tasks = ['完成需求文档' , '开发登录模块' , '修复 Bug #1024' , '代码审查' ]; drop (event: CdkDragDrop<string []> ) { moveItemInArray (this .tasks , event.previousIndex , event.currentIndex ); } }
关键 CDK 指令 :
cdkDropList:声明一个可接受拖放项的容器。
cdkDrag:声明一个可拖拽的元素。
cdkDragHandle:指定拖拽的“把手”(只有拖拽把手区域才能拖动)。
(cdkDropListDropped):拖放完成事件,携带 previousIndex 和 currentIndex。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Component } from '@angular/core' ;import { ScrollingModule } from '@angular/cdk/scrolling' ;@Component ({ selector : 'app-virtual-list' , standalone : true , imports : [ScrollingModule ], template : ` <cdk-virtual-scroll-viewport itemSize="50" class="viewport"> <div *cdkVirtualFor="let item of items" class="item"> {{ item }} </div> </cdk-virtual-scroll-viewport> ` , styles : [` .viewport { height: 400px; border: 1px solid #ddd; } .item { height: 50px; display: flex; align-items: center; padding: 0 16px; border-bottom: 1px solid #eee; } ` ],}) export class VirtualListComponent { items = Array .from ({ length : 100000 }, (_, i ) => `第 ${i + 1 } 条数据` ); }
itemSize 指定每个条目的固定高度(像素),这是虚拟滚动正确计算滚动位置的关键。若条目高度不固定,可使用 autosize 属性配合 cdk-virtual-scroll-viewport 的自动测量。
Angular CDK 的 @angular/cdk/layout 提供了响应式断点监听工具,使组件能根据视口尺寸动态调整行为。
5.1 BreakpointObserver:响应式断点观察者 BreakpointObserver 是一个服务,基于标准 CSS 媒体查询来观察视口变化,并返回一个 Observable<BreakpointState>。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import { Component , OnInit , OnDestroy } from '@angular/core' ;import { BreakpointObserver , Breakpoints } from '@angular/cdk/layout' ;import { Subscription } from 'rxjs' ;@Component ({ selector : 'app-responsive-layout' , template : ` <div [ngClass]="isSmallScreen ? 'mobile-layout' : 'desktop-layout'"> <p>当前屏幕:{{ isSmallScreen ? '移动端' : '桌面端' }}</p> </div> ` ,}) export class ResponsiveLayoutComponent implements OnInit , OnDestroy { isSmallScreen = false ; private subscription : Subscription ; constructor (private breakpointObserver: BreakpointObserver ) {} ngOnInit ( ) { this .subscription = this .breakpointObserver .observe ([Breakpoints .Handset , Breakpoints .TabletPortrait ]) .subscribe ((result ) => { this .isSmallScreen = result.matches ; }); } ngOnDestroy ( ) { this .subscription .unsubscribe (); } }
Breakpoints 内置常量 :
常量
媒体查询
对应设备
Breakpoints.Handset
(max-width: 599px)
手机竖屏
Breakpoints.TabletPortrait
(min-width: 600px) and (max-width: 839px)
平板竖屏
Breakpoints.TabletLandscape
(min-width: 840px) and (max-width: 1279px)
平板横屏
Breakpoints.Web
(min-width: 840px)
平板横屏 + 桌面
Breakpoints.WebPortrait
(min-width: 840px) and (orientation: portrait)
桌面竖屏
Breakpoints.WebLandscape
(min-width: 840px) and (orientation: landscape)
桌面横屏
可以组合多个断点:observe([Breakpoints.Handset, Breakpoints.TabletPortrait]),只要任一匹配,result.matches 即为 true。
如果不需要持续监听(仅需在初始化时判断一次),可使用 MediaMatcher:
1 2 3 4 5 6 7 8 9 10 11 import { MediaMatcher } from '@angular/cdk/layout' ;@Component ({ ... })export class MyComponent { isMobile : boolean ; constructor (mediaMatcher: MediaMatcher ) { const mediaQueryList = mediaMatcher.matchMedia ('(max-width: 599px)' ); this .isMobile = mediaQueryList.matches ; } }
5.3 响应式侧边导航实战 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import { Component } from '@angular/core' ;import { BreakpointObserver , Breakpoints } from '@angular/cdk/layout' ;import { map, Observable , shareReplay } from 'rxjs' ;import { MatSidenavModule } from '@angular/material/sidenav' ;import { MatToolbarModule } from '@angular/material/toolbar' ;import { MatIconModule } from '@angular/material/icon' ;import { MatListModule } from '@angular/material/list' ;import { CommonModule } from '@angular/common' ;import { RouterModule } from '@angular/router' ;@Component ({ selector : 'app-nav' , standalone : true , imports : [ CommonModule , RouterModule , MatSidenavModule , MatToolbarModule , MatIconModule , MatListModule , ], template : ` <mat-sidenav-container class="sidenav-container"> <!-- 侧边栏:移动端覆盖模式,桌面端侧边模式 --> <mat-sidenav #drawer [mode]="(isHandset$ | async) ? 'over' : 'side'" [opened]="(isHandset$ | async) === false" > <mat-nav-list> <a mat-list-item routerLink="/dashboard">仪表板</a> <a mat-list-item routerLink="/users">用户管理</a> <a mat-list-item routerLink="/settings">设置</a> </mat-nav-list> </mat-sidenav> <mat-sidenav-content> <mat-toolbar color="primary"> <button *ngIf="isHandset$ | async" mat-icon-button (click)="drawer.toggle()" > <mat-icon>menu</mat-icon> </button> <span>管理后台</span> </mat-toolbar> <div class="content"> <ng-content></ng-content> </div> </mat-sidenav-content> </mat-sidenav-container> ` , styles : [` .sidenav-container { height: 100vh; } mat-sidenav { width: 240px; } .content { padding: 24px; } ` ],}) export class NavComponent { isHandset$ : Observable <boolean > = this .breakpointObserver .observe (Breakpoints .Handset ) .pipe ( map ((result ) => result.matches ), shareReplay (1 ) ); constructor (private breakpointObserver: BreakpointObserver ) {} }
布局逻辑 :
移动端(Handset):侧边栏为 mode="over"(浮层覆盖),默认关闭,点击汉堡图标打开。
桌面端:侧边栏为 mode="side"(侧边推开),默认打开。
shareReplay(1) 确保多个 async 管道共享同一个 Observable 订阅,避免重复发出 HTTP 请求或重复计算。
6. 综合实战:Angular Material 仪表板框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 import { Component } from '@angular/core' ;import { MatToolbarModule } from '@angular/material/toolbar' ;import { MatSidenavModule } from '@angular/material/sidenav' ;import { MatIconModule } from '@angular/material/icon' ;import { MatListModule } from '@angular/material/list' ;import { MatButtonModule } from '@angular/material/button' ;import { MatCardModule } from '@angular/material/card' ;import { CommonModule } from '@angular/common' ;import { BreakpointObserver , Breakpoints } from '@angular/cdk/layout' ;import { map, Observable , shareReplay } from 'rxjs' ;@Component ({ selector : 'app-root' , standalone : true , imports : [ CommonModule , MatToolbarModule , MatSidenavModule , MatIconModule , MatListModule , MatButtonModule , MatCardModule , ], template : ` <mat-sidenav-container class="sidenav-container"> <mat-sidenav #drawer [mode]="(isHandset$ | async) ? 'over' : 'side'" [opened]="(isHandset$ | async) === false" > <div class="logo">📊 DashPro</div> <mat-nav-list> <a mat-list-item> <mat-icon matListItemIcon>dashboard</mat-icon> 仪表板 </a> <a mat-list-item> <mat-icon matListItemIcon>people</mat-icon> 用户 </a> <a mat-list-item> <mat-icon matListItemIcon>settings</mat-icon> 设置 </a> </mat-nav-list> </mat-sidenav> <mat-sidenav-content> <mat-toolbar color="primary"> <button *ngIf="isHandset$ | async" mat-icon-button (click)="drawer.toggle()" > <mat-icon>menu</mat-icon> </button> <span>仪表板</span> <span class="spacer"></span> <button mat-icon-button> <mat-icon>notifications</mat-icon> </button> </mat-toolbar> <div class="content"> <div class="card-grid"> <mat-card *ngFor="let stat of stats"> <mat-card-content> <h3>{{ stat.value }}</h3> <p>{{ stat.label }}</p> </mat-card-content> </mat-card> </div> </div> </mat-sidenav-content> </mat-sidenav-container> ` , styles : [` .sidenav-container { height: 100vh; } mat-sidenav { width: 240px; background: #fafafa; } .logo { padding: 16px; font-size: 1.2rem; font-weight: bold; text-align: center; } .spacer { flex: 1 1 auto; } .content { padding: 24px; } .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } mat-card { text-align: center; } mat-card h3 { font-size: 2rem; margin: 0; color: #4f46e5; } mat-card p { color: #666; margin-top: 8px; } ` ],}) export class AppComponent { isHandset$ : Observable <boolean > = this .breakpointObserver .observe (Breakpoints .Handset ) .pipe ( map ((result ) => result.matches ), shareReplay (1 ) ); stats = [ { label : '总收入' , value : '¥45,230' }, { label : '订单数' , value : '1,240' }, { label : '用户数' , value : '3,580' }, { label : '转化率' , value : '24.5%' }, ]; constructor (private breakpointObserver: BreakpointObserver ) {} }
关键架构决策 :
使用 <mat-sidenav-container> 作为页面根布局,提供内置的侧边栏和内容区域协调。
BreakpointObserver 驱动侧边栏的模式切换(over vs side),实现移动端/桌面端不同的导航体验。
统计卡片使用原生 CSS Grid 而非 Material Grid List,展示 Angular Material 与自定义 CSS 的无缝混合。
matListItemIcon 指令自动为列表项中的图标添加标准间距。
课后练习 一、概念自测(选择题 / 填空题)
(单选) Angular Material 的底层交互抽象库的名称是? A. Angular Flex Layout B. Angular CDK C. Angular Elements D. Angular Universal
(单选) 在 Angular Material 主题中,define-palette 的第二个参数 default-hue 如果不指定,默认值是? A. 100 B. 300 C. 500 D. A200
(填空) 要在 Angular 组件中监听视口是否为手机屏幕,应注入 ______ 服务,并调用 observe([Breakpoints.Handset]) 方法。
(多选) 以下哪些属于 Angular CDK 提供的功能模块? A. @angular/cdk/drag-drop B. @angular/cdk/table C. @angular/cdk/virtual-scroll D. @angular/cdk/layout
二、AI 编程任务:编写面向 AI 的提示词 场景 :你需要使用 Angular Material + CDK 实现一个可拖拽排序的仪表板卡片 页面。要求如下:
页面使用 Angular Material 的 mat-toolbar 作为顶部导航,mat-sidenav 作为侧边栏(含 3 个菜单项)。
主内容区使用 CSS Grid 布局展示 6 张卡片,每张卡片使用 mat-card。
使用 CDK 的 cdkDrag 和 cdkDropList 实现卡片在网格内自由拖拽排序。
使用 BreakpointObserver 实现响应式:移动端侧边栏为覆盖模式,桌面端为侧边模式。
主题自定义:主色为 #0ea5e9(天蓝色),字体为 Inter。
任务要求 :请写出一段完整的中文提示词 ,发送给 AI,使其生成完整的 Angular Standalone 组件文件(包含 TypeScript 逻辑、HTML 模板和 SCSS 样式)。提示词中需明确指定 CDK 拖拽指令的使用方式、响应式断点监听、以及自定义主题的 SCSS 配置。
课后练习答案 一、概念自测答案
B
解析:Angular CDK(Component Dev Kit)是 Angular Material 的底层抽象,提供无样式的交互工具。Angular Flex Layout 是独立的响应式布局库(现已进入维护模式)。
C
解析:mat.define-palette() 的第二个参数 default-hue 默认值为 500,这是 Material Design 调色板的标准主色值。
BreakpointObserver
解析:BreakpointObserver 来自 @angular/cdk/layout,用于观察 CSS 媒体查询的变化并返回 Observable。
A、C、D
解析:drag-drop、virtual-scroll、layout 均为 CDK 模块。@angular/cdk/table 存在(提供数据表格的基础行为抽象),但 @angular/material/table 是 Material 样式的表格——题目问的是 CDK 级别,B 也是 CDK 模块之一。但在实际使用中 @angular/cdk/table 通常不直接导入,而是通过 MatTableModule 间接使用。严格来说 B 是存在的 CDK 包,但最常用的是 A、C、D。
二、AI 编程任务参考答案(提示词示例)
示例提示词 : “请生成一个 Angular Standalone 组件,实现可拖拽排序的仪表板卡片页面。要求:
组件使用 standalone: true,导入 MatToolbarModule, MatSidenavModule, MatIconModule, MatListModule, MatCardModule, MatButtonModule 和 CDK 的 DragDropModule、LayoutModule。
注入 BreakpointObserver,创建 isHandset$: Observable<boolean>,使用 Breakpoints.Handset 判断是否为手机屏幕,用 shareReplay(1) 共享订阅。
模板使用 <mat-sidenav-container>,侧边栏 mode 根据 isHandset$ 切换 'over' 或 'side',opened 属性桌面端为 true。
工具栏包含汉堡按钮(仅手机端显示,通过 *ngIf + async 管道)和标题。
主内容区使用 <div class="card-grid" cdkDropList (cdkDropListDropped)="drop($event)"> 包裹 6 个 <mat-card cdkDrag>。每个卡片内有标题和描述文字。拖拽手柄使用 cdkDragHandle 指令放在卡片右上角。
drop 方法使用 CDK 的 moveItemInArray 更新卡片数组顺序。
全局样式文件 styles.scss 使用 @use '@angular/material' as mat; 定义主题:主色 $custom-primary: mat.define-palette(mat.$light-blue-palette, 500);,字体 font-family: 'Inter', sans-serif。应用 mat.all-component-themes()。
组件内 SCSS:.card-grid 使用 CSS Grid(grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))),拖拽预览添加阴影,占位符半透明。
代码完整可运行。输出组件文件和全局样式文件。”