为 Angular Material 应用添加完美深色模式支持

💡 为 Angular Material 应用添加完美深色模式支持

深色模式(Dark Mode)是现代应用不可或缺的功能。它不仅能提升用户在低光环境下的舒适度,还能让应用看起来更专业、更时尚。

如果你正在使用 Angular Material ,实现深色模式可以非常优雅和高效。本文将分享我如何通过一个独立的 ThemePickerComponent ,结合 Angular Signals系统偏好检测,为我的应用添加深色模式的完整过程。

🛠️ 核心思路概览

我的深色模式解决方案基于以下几个关键机制:

  1. CSS 变量与 color-scheme : 利用 Angular Material 基于 CSS 变量的主题机制,并通过在 <html> 标签上切换 color-scheme 属性来控制主题。
  2. Angular Signals : 使用 signal() 存储和管理当前的主题模式 ('light''dark')。
  3. 持久化与偏好 : 通过 localStorage 记住用户的选择,同时使用 window.matchMedia 监听用户操作系统的偏好设置。

💻 关键代码解析与实现步骤

1. 配置主题和 color-scheme (styles.scss)

在全局样式文件中,我们确保应用能响应 color-scheme 变化,并设置 Material 主题。

scss 复制代码
/* ui/src/styles.scss */

@use '@angular/material' as mat;

html, body {
  font-family: Roboto, "Helvetica Neue", sans-serif;
}

html {
  background-color: var(--mat-sys-surface);
  color: var(--mat-sys-on-surface);
  // ✨ 关键:允许浏览器知道页面支持两种颜色方案
  color-scheme: light dark;

  @include mat.theme((
        color: (
          // Material 会根据 color-scheme 自动应用 light/dark 调色板
          primary: mat.$rose-palette, 
          tertiary: mat.$red-palette,
      ),
      typography: Roboto,
      density: 0,
  ));
}

我们通过 background-color: var(--mat-sys-surface) 来使用 Material 系统级 CSS 变量,确保背景颜色随着主题切换而正确变化。

2. ThemePickerComponent:主题切换器

这是整个功能的核心。它负责初始化、监听和切换主题。

A. HTML 模板:根据模式切换图标

我们使用 mat-icon-button@if 语法根据 mode() 的值显示太阳或月亮图标。

html 复制代码
<button class="theme-button" mat-icon-button
    [matTooltip]="'Change Theme'"
    (click)="changeMode()"
  >
     @if (mode() === 'dark') {
       <mat-icon>dark_mode</mat-icon>
     } @else {
       <mat-icon>light_mode</mat-icon>
     }
</button>
B. TypeScript 逻辑:Signals, Effects, 和持久化
typescript 复制代码
// ui/src/app/components/theme-picker/theme-picker.component.ts

import { afterNextRender, Component, effect, inject, OnInit, Renderer2, signal } from '@angular/core';
// ... 导入 MatButtonModule, MatIconModule, MatTooltipModule

@Component({...})
export class ThemePickerComponent implements OnInit {
  static storageKey = 'batcher-ui-theme';
  mode = signal('light');
  private renderer = inject(Renderer2);

  constructor() {
    // 1. 响应式更新:当 mode() 变化时,更新 <html> 元素的 color-scheme
    effect(() => {
      const mode = this.mode();
      this.renderer.setStyle(document.documentElement, 'color-scheme', mode);
    });

    // 2. 初始化:客户端渲染后,加载存储或系统偏好
    afterNextRender(() => {
      // 优先加载 localStorage 存储的模式,否则检测系统偏好
      const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
      const storedMode = this.getStoredMode() || systemPreference;
      
      if (storedMode) {
        this.mode.set(storedMode);
      }
    });
  }

  ngOnInit(): void {
    // 3. 实时监听:如果用户未设置过偏好,跟随系统主题的实时变化
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
      const storedMode = this.getStoredMode();
      if (!storedMode) {
        this.mode.set(e.matches ? 'dark' : 'light');
      }
    });
  }

  changeMode(): void {
    const newMode = this.mode() === 'light' ? 'dark' : 'light';
    this.mode.set(newMode);
    this.storeMode(newMode);
  }

  // 4. 持久化方法 (storeMode, getStoredMode)
  storeMode(mode: string): void {
    try {
      localStorage.setItem(ThemePickerComponent.storageKey, mode);
    } catch { /* ignore */ }
  }

  getStoredMode(): string | null {
    try {
      return localStorage.getItem(ThemePickerComponent.storageKey);
    } catch { return null; }
  }
}
C. SCSS处理(可选)

修改icon的颜色去适配material 3的配色

css 复制代码
.theme-button {
    color: var(--mat-primary);
}

3. 集成到导航栏 (navigation.component.html)

我们将 app-theme-picker 组件放置在应用导航栏(mat-toolbar)的右侧。

html 复制代码
<mat-toolbar>
    <div class="flex-grow-1"></div>
    <app-theme-picker class="theme-picker"></app-theme-picker>
</mat-toolbar>

导航栏样式调整

我们还调整了导航栏的背景色,使其使用更适合作为容器背景的 Material 系统变量。

scss 复制代码
/* ui/src/app/components/navigation/navigation.component.scss */

:host {
  color: var(--mat-sys-primary);
  // 更改为使用 surface-container-low,这通常更适合作为页面/容器的背景
  background: var(--mat-sys-surface-container-low);
}

// ... 省略其他样式

.flex-grow-1 {
  flex-grow: 1;
}

✨ 总结

通过上述实现,我们的 Angular Material 应用获得了:

  1. 用户自定义: 用户可以随时通过点击按钮切换光亮/深色模式。
  2. 偏好记忆: 应用会记住用户的选择,即使重新打开浏览器也不会丢失。
  3. 尊重系统设置: 如果用户从未手动切换过主题,应用会默认跟随操作系统的偏好设置。

使用 Angular Signals 和 Effects 极大地简化了状态管理和响应式更新,让深色模式的实现既强大又简洁!

🔗 完整的代码变更

你可以查看这个 GitHub Commit 来了解所有相关的代码修改:

fix(ui): add dark mode

相关推荐
Ticnix2 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人2 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl2 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人2 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空3 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_3 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus3 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空3 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范