import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { AlertService } from '../../../core/alert.service'; /** * 全局异常提示容器(2026-11-10)。 * * 挂在应用根组件上,渲染 `AlertService` 里的提示。 * **不会自动消失**,每条都带「我知道了」按钮由用户手动关闭。 * * 层级用底座 `--z-toast`(通知层,永远最高),与设计令牌一致。 */ @Component({ selector: 'app-alert-host', changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (svc.alerts().length > 0) { } `, styles: ` @use '../../../core/styles/breakpoints' as bp; /* * ⚠️ CSS 顺序铁律:**基础样式在前,@media 覆盖在后**。 * 第一版把 @media 写在 .alert 之前,结果 .alert 的 display:flex * 反把媒体查询里的 display:grid 覆盖掉,grid-area 全部失效, * 文字被挤成一列竖排(用户截图实测)。改样式时务必保持这个顺序。 */ :host { position: fixed; top: var(--sp-4); left: 50%; transform: translateX(-50%); z-index: var(--z-toast); pointer-events: none; } .alert-stack { display: flex; flex-direction: column; gap: var(--sp-2); width: min(92vw, 520px); } .alert { display: flex; align-items: flex-start; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); border: 1px solid var(--danger); border-radius: var(--r-lg); background: var(--surface); box-shadow: var(--shadow-overlay); color: var(--ink); pointer-events: auto; animation: alert-in var(--motion-base) var(--ease-standard); } .alert.warn { border-color: var(--warning); } .alert > i { flex-shrink: 0; margin-top: 2px; color: var(--danger); font-size: var(--fs-lg); } .alert.warn > i { color: var(--warning); } .alert-body { flex: 1; min-width: 0; } .alert-title { margin: 0; font-size: var(--fs-md); font-weight: 600; line-height: 1.4; } .alert-detail { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--ink-2); line-height: 1.5; } /* 触达区 ≥44px(可访问性底线) */ .alert-close { flex-shrink: 0; min-height: 44px; padding: 0 var(--sp-3); border: 1px solid var(--edge-strong); border-radius: var(--r-md); background: var(--surface-2); color: var(--ink); font-size: var(--fs-sm); cursor: pointer; white-space: nowrap; transition: background-color var(--motion-fast) var(--ease-standard); } .alert-close:hover { background: var(--surface-hover); } @keyframes alert-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } /* 尊重系统「减少动态效果」设置 */ @media (prefers-reduced-motion: reduce) { .alert { animation: none; } } /* * ===== 移动端独立样式(2026-11-10 用户裁决)===== * 「不能和 PC 端一致,需要单独优化」。 * * 移动端特点: * · 屏窄 → 整宽贴顶,留刘海安全区 * · 触屏 → 按钮**整行铺满**、≥48px(手指比鼠标粗) * · 文字多 → 标题与正文分行,按钮独占一行,不跟文字抢宽度 */ @include bp.below-mobile { :host { top: 0; left: 0; right: 0; /* 用 inset 显式撑满,避免 :host 宽度塌成内容宽 */ bottom: auto; width: 100%; box-sizing: border-box; transform: none; padding-top: max(var(--sp-3), env(safe-area-inset-top)); padding-inline: var(--sp-2); } .alert-stack { width: 100%; max-width: none; gap: var(--sp-2); } .alert { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--sp-2) var(--sp-2); padding: var(--sp-3); box-sizing: border-box; } .alert-body { flex: 1 1 0; min-width: 0; } /* 按钮独占一整行,触屏好点 */ .alert-close { flex: 1 1 100%; width: 100%; min-height: 48px; margin-top: var(--sp-1); font-size: var(--fs-md); font-weight: 600; } .alert-title { font-size: var(--fs-lg); } } `, }) export class AlertHostComponent { protected readonly svc = inject(AlertService); }