m-app-bar.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
  2. /**
  3. * 页面顶栏 —— Demo 的「左右圆形浮动按钮 + 居中标题」(`.navbar` / `.fab`)
  4. *
  5. * 为什么不用 `ui-m-navbar`:底座版本是扁平常规导航条(白底 + 下边框),
  6. * 与 Demo 的浮钮式顶栏是两套视觉语言。此处按 Demo 自建,样式仍全部走令牌。
  7. *
  8. * 三种左侧形态(互斥,按 Demo `navbar()` 的参数决定):
  9. * · `back` → 返回箭头(二级页)
  10. * · `menu` → 抽屉入口(Tab 页)
  11. * · 都不传 → 占位,标题保持居中
  12. */
  13. @Component({
  14. selector: 'app-m-app-bar',
  15. changeDetection: ChangeDetectionStrategy.OnPush,
  16. template: `
  17. <header class="m-app-bar">
  18. @if (back()) {
  19. <button type="button" class="m-fab" aria-label="返回" (click)="backTap.emit()">
  20. <i class="fa-solid fa-chevron-left" aria-hidden="true"></i>
  21. </button>
  22. } @else if (menu()) {
  23. <button type="button" class="m-fab" aria-label="打开导航" (click)="menuTap.emit()">
  24. <i class="fa-solid fa-bars" aria-hidden="true"></i>
  25. </button>
  26. } @else {
  27. <span class="m-fab-spacer" aria-hidden="true"></span>
  28. }
  29. <div class="m-title">
  30. <span class="m-title-main">{{ title() }}</span>
  31. @if (subtitle()) {
  32. <small>{{ subtitle() }}</small>
  33. }
  34. </div>
  35. <div class="m-bar-right">
  36. <ng-content select="[barRight]" />
  37. @if (!hasRightContent()) {
  38. <span class="m-fab-spacer" aria-hidden="true"></span>
  39. }
  40. </div>
  41. </header>
  42. `,
  43. styles: [
  44. `
  45. :host {
  46. display: block;
  47. }
  48. /*
  49. * 🔴 标题必须相对**整个顶栏**居中(2026-11-04)。
  50. *
  51. * 原来三者等分 flex:左侧有时是返回键、有时是占位的 44px 圆圈,
  52. * 右侧按钮数量也不固定(0/1 个),两边宽度不等 → 标题偏左/偏右。
  53. *
  54. * 改为绝对定位铺满:标题左右各让出 44px,在剩余区域水平居中,
  55. * 左右按钮用相对定位压在两侧 —— 标题永远在整个顶栏的正中。
  56. */
  57. .m-app-bar {
  58. display: flex;
  59. align-items: center;
  60. justify-content: space-between;
  61. gap: var(--sp-3);
  62. height: calc(var(--mob-navbar-h) + var(--mob-safe-top));
  63. padding: var(--mob-safe-top) var(--sp-4) 0;
  64. position: relative;
  65. z-index: var(--z-affix);
  66. box-sizing: border-box;
  67. }
  68. .m-fab {
  69. width: var(--touch-min);
  70. height: var(--touch-min);
  71. border: 0;
  72. border-radius: var(--r-tag);
  73. display: grid;
  74. place-items: center;
  75. flex: none;
  76. background: var(--surface);
  77. box-shadow: var(--shadow-soft);
  78. color: var(--ink);
  79. font-size: var(--fs-17);
  80. cursor: pointer;
  81. transition: background var(--motion-fast) var(--ease-standard);
  82. }
  83. .m-fab:active {
  84. background: var(--surface-hover);
  85. }
  86. .m-fab-spacer {
  87. display: block;
  88. width: var(--touch-min);
  89. height: var(--touch-min);
  90. flex: none;
  91. }
  92. /*
  93. * 标题层铺满整条顶栏并居中;左右按钮盖在其上。
  94. *
  95. * 🔴 2026-11-07 修复重叠:原来 top 为安全区、bottom 为 0,直接铺满,
  96. * 但**没有约束子项高度** —— 标题 + 副标题两行同时存在时,
  97. * 文字会溢出这条绝对定位层、压到下方的消息区上(用户截图实测)。
  98. * 现在给标题层显式高度(与按钮同等的 44px 视觉行),并裁掉溢出。
  99. */
  100. .m-title {
  101. position: absolute;
  102. left: var(--touch-min);
  103. right: var(--touch-min);
  104. top: var(--mob-safe-top);
  105. height: calc(var(--mob-navbar-h) - var(--sp-2));
  106. display: flex;
  107. flex-direction: column;
  108. align-items: center;
  109. justify-content: center;
  110. gap: 1px;
  111. min-width: 0;
  112. overflow: hidden;
  113. pointer-events: none;
  114. text-align: center;
  115. font-size: var(--fs-17);
  116. font-weight: 600;
  117. line-height: 1.2;
  118. color: var(--ink);
  119. }
  120. .m-title-main {
  121. display: block;
  122. max-width: 100%;
  123. overflow: hidden;
  124. text-overflow: ellipsis;
  125. white-space: nowrap;
  126. }
  127. .m-title small {
  128. display: block;
  129. font-size: var(--fs-11);
  130. font-weight: 400;
  131. color: var(--muted);
  132. overflow: hidden;
  133. text-overflow: ellipsis;
  134. white-space: nowrap;
  135. }
  136. .m-bar-right {
  137. display: flex;
  138. align-items: center;
  139. gap: var(--sp-2);
  140. flex: none;
  141. }
  142. `,
  143. ],
  144. })
  145. export class MAppBarComponent {
  146. readonly title = input('');
  147. readonly subtitle = input('');
  148. /** 左侧显示返回箭头 */
  149. readonly back = input(false);
  150. /** 左侧显示抽屉入口(与 back 互斥,back 优先) */
  151. readonly menu = input(false);
  152. /** 右侧是否已有内容(父组件通过 `[barRight]` 投影);false 时渲染占位保持标题居中 */
  153. readonly hasRightContent = input(true);
  154. readonly backTap = output<void>();
  155. readonly menuTap = output<void>();
  156. }