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
相关产品推荐
相关产品推荐

