You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular 19 + Material V3自定义主题切换及样式问题求助

Angular 19 + Material V3 主题实现问题

我正在构建基于Angular 19和Angular Material V3的示例应用,需要实现三个核心功能:自定义明暗主题、运行时切换主题、类旧版color指令的primary/warn等颜色变体。已通过ng add @angular/material完成集成,编写了styles.scss、主题服务及切换组件。


实现代码

1. 样式配置(styles.scss)

// styles.scss
@use "@angular/material" as mat;

@include mat.core();

// 创建明暗主题的颜色配置
$light-theme: mat.define-theme(
  (
    color: (
      theme-type: light,
      primary: mat.$blue-palette,
    ),
  )
);

$dark-theme: mat.define-theme(
  (
    color: (
      theme-type: dark,
      primary: mat.$blue-palette,
    ),
  )
);

// 基础样式
html {
  color-scheme: light dark;
}

// 默认(浅色)主题
:root,
html:not(.dark-theme) {
  @include mat.all-component-themes($light-theme);
  & {
    color-scheme: light;
  }
}

// 深色主题样式
html.dark-theme {
  // @include mat.all-component-themes($dark-theme);
  @include mat.all-component-colors($dark-theme);
  & {
    color-scheme: dark;
  }
}

body {
  background: var(--mat-sys-surface);
  color: var(--mat-sys-on-surface);
}

@include mat.color-variants-backwards-compatibility($light-theme);
@include mat.color-variants-backwards-compatibility($dark-theme);

2. 主题服务(theme.service.ts)

// theme.service.ts
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

const THEME_KEY = 'app-theme-preference';

@Injectable({
  providedIn: 'root',
})
export class ThemeService {
  private readonly _htmlElement?: HTMLHtmlElement;

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {
    // 确保仅在浏览器环境中访问document/window
    if (isPlatformBrowser(this.platformId)) {
      this._htmlElement = document.documentElement as HTMLHtmlElement;
    }
  }

  /**
   * 应用启动时初始化主题
   */
  public initializeTheme() {
    if (!isPlatformBrowser(this.platformId)) return;
    const savedTheme = this.getSavedTheme();
    const systemTheme = this.getSystemTheme();

    // 优先使用已保存的主题,否则回退到系统主题
    const initialTheme = savedTheme || systemTheme;
    this.setTheme(initialTheme);

    // 监听系统主题变化
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    mediaQuery.addEventListener('change', (e) => {
      if (!this.getSavedTheme()) {
        this.setTheme(e.matches ? 'dark' : 'light');
      }
    });
  }

  /**
   * 切换明暗主题
   */
  public toggleTheme(): 'light' | 'dark' | null {
    if (!isPlatformBrowser(this.platformId)) return null;

    const currentTheme = this.getCurrentTheme();
    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
    this.setTheme(newTheme);
    return newTheme;
  }

  /**
   * 设置指定主题
   * @param theme 'light' 或 'dark'
   */
  public setTheme(theme: 'light' | 'dark') {
    if (!isPlatformBrowser(this.platformId)) return;

    console.log('Setting theme:', theme);

    // 保存主题偏好到本地存储
    localStorage.setItem(THEME_KEY, theme);

    // 为html元素设置color-scheme属性
    this._htmlElement?.setAttribute('color-scheme', theme);

    // 切换主题专属类
    if (theme === 'dark') {
      console.log('Adding dark-theme class');
      this._htmlElement?.classList.add('dark-theme');
      this._htmlElement?.classList.remove('light-theme');
    } else {
      console.log('Adding light-theme class');
      this._htmlElement?.classList.add('light-theme');
      this._htmlElement?.classList.remove('dark-theme');
    }
  }
  /**
   * 获取当前激活的主题
   * @returns 'light' 或 'dark'
   */
  public getCurrentTheme(): 'light' | 'dark' {
    if (!isPlatformBrowser(this.platformId)) return 'light';

    return this._htmlElement?.getAttribute('color-scheme') as 'light' | 'dark';
  }

  private getSystemTheme(): 'light' | 'dark' {
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light';
  }

  /**
   * 从本地存储获取已保存的主题偏好
   * @returns 已保存的主题或null
   */
  private getSavedTheme(): 'light' | 'dark' | null {
    if (!isPlatformBrowser(this.platformId)) return null;

    const savedTheme = localStorage.getItem(THEME_KEY);
    return savedTheme === 'light' || savedTheme === 'dark' ? savedTheme : null;
  }

  /**
   * 根据系统偏好设置主题
   */
  public setThemeFromSystemPreference() {
    if (!isPlatformBrowser(this.platformId)) return;

    const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
    const initialTheme = prefersDarkScheme.matches ? 'dark' : 'light';
    this.setTheme(initialTheme);

    // 使用addEventListener替代已废弃的addListener
    prefersDarkScheme.addEventListener('change', (e) => {
      // 仅在无保存偏好时跟随系统变化
      if (!this.getSavedTheme()) {
        this.setTheme(e.matches ? 'dark' : 'light');
      }
    });
  }
}

3. 主题切换组件(theme-toggle.component.ts)

// theme-toggle.component.ts
import { Component, OnInit } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';

import { ThemeService } from '../../services/theme.service';

@Component({
  selector: 'app-theme-toggle',
  imports: [MatButtonModule, MatIconModule],
  templateUrl: './theme-toggle.component.html',
  styleUrl: './theme-toggle.component.scss',
})
export class ThemeToggleComponent implements OnInit {
  isDarkMode = false;

  constructor(private themeService: ThemeService) {}

  public ngOnInit() {
    this.themeService.initializeTheme();
    // 设置初始主题状态
    this.updateThemeMode();
  }

  public toggleTheme() {
    this.themeService.toggleTheme();
    this.updateThemeMode();
  }

  private updateThemeMode() {
    this.isDarkMode = this.themeService.getCurrentTheme() === 'dark';
  }
}

对应的HTML模板:

<!-- theme-toggle.component.html -->
<button
  type="button"
  mat-icon-button
  (click)="toggleTheme()"
  aria-label="切换主题"
>
  <mat-icon>{{ isDarkMode ? "light_mode" : "dark_mode" }}</mat-icon>
</button>

遇到的问题

1. 排版异常

仅Material组件内的文本应用了预期的Roboto排版,非组件内的文本(如mat-card中的<p>标签)显示默认字体(Times New Roman)。尝试添加typography: Roboto时因要求传入对象报错,且我仅需启用默认排版,无需自定义重定义。

2. 颜色变体失效

启用mat.color-variants-backwards-compatibility后,mat-primary等类仅改变按钮的文本前景色,背景色未生效。示例页面app.component.html代码如下:

<article>
  <div>
    <app-theme-toggle />
  </div>
  <hr />
  <mat-card>
    <mat-card-header>
      <!-- 这里的排版符合预期 -->
      <mat-card-title>自定义主题与切换</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <!-- 这里显示为Times New Roman -->
      <p>这是一个示例文本。</p>
      <div class="form-row">
        <!-- 仅文本颜色变化,背景色为默认 -->
        <button type="button" mat-raised-button class="mat-primary">
          primary
        </button>
        <!-- 同上 -->
        <button type="button" mat-raised-button class="mat-warn">
          warn
        </button>
      </div>
    </mat-card-content>
  </mat-card>
</article>

补充尝试及问题

尝试创建多主题(主主题、accent主题、error主题)替代兼容性混入,但效果不一致:比如复选框的普通色与accent色无差异,图标需要强制设置颜色才能生效。相关代码如下:

样式配置

@use "@angular/material" as mat;

@include mat.core();

$light-theme: mat.define-theme(
  (
    color: (
      theme-type: light,
      primary: mat.$blue-palette,
    ),
  )
);

$accent-theme: mat.define-theme(
  (
    color: (
      theme-type: light,
      primary: mat.$violet-palette,
    ),
  )
);

$error-theme: mat.define-theme(
  (
    color: (
      theme-type: light,
      primary: mat.$red-palette,
    ),
  )
);

html {
  @include mat.all-component-themes($light-theme);
  & {
    color-scheme: light;
  }
}

html,
body {
  height: 100%;
}
body {
  margin: 0;
  font-family: Roboto, "Helvetica Neue", sans-serif;
}

.mat-accent {
  @include mat.all-component-colors($accent-theme);
}

.mat-error, .mat-warn {
  @include mat.all-component-colors($error-theme);
}

mat-icon.mat-accent {
  color: var(--mdc-filled-text-field-focus-label-text-color) !important;
}
mat-icon.mat-error {
  color: var(--mat-form-field-error-focus-trailing-icon-color);
}

对应HTML

<!-- 图标按钮 -->
<div class="form-row">
  <button type="button" mat-icon-button>
    <mat-icon>info</mat-icon>
  </button>
  <button type="button" mat-icon-button>
    <mat-icon class="mat-accent">check_circle</mat-icon>
  </button>
  <button type="button" mat-icon-button>
    <mat-icon class="mat-error">delete</mat-icon>
  </button>
  <!-- 扁平按钮 -->
  <button type="button" mat-flat-button>normal</button>
  <button type="button" mat-flat-button class="mat-accent">accent</button>
  <button type="button" mat-flat-button class="mat-error">error</button>
</div>
<div class="form-row">
  <!-- 复选框 -->
  <mat-checkbox>normal</mat-checkbox>
  <mat-checkbox class="mat-accent">accent</mat-checkbox>
  <mat-checkbox class="mat-error">error</mat-checkbox>
</div>

由于Angular 19 + Material V3的官方文档较少,现寻求上述问题的解决方案。


内容的提问来源于stack exchange,提问作者Naftis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 23:40:54