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) {
@for (a of svc.alerts(); track a.id) {
{{ a.title }}
@if (a.detail) {
{{ a.detail }}
}
}
}
`,
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);
}