CockpitPanel.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. <!--
  2. 二级面板外框:切角科技面板 + 可点击收放的标题栏。
  3. 视觉参考《驾驶舱原型图-美化版》的 .panel / .panel-head;
  4. 尺寸随外壳的 --u 设计像素缩放(大屏不留空,小屏不缩小)。
  5. 标题栏是 button,其文本即按钮的可访问名(标记符号 aria-hidden,不参与命名)。
  6. -->
  7. <template>
  8. <section class="cockpit-panel" :class="{ 'is-collapsed': !expanded }" :data-panel-id="panelId">
  9. <button
  10. type="button"
  11. class="cockpit-panel__head"
  12. :aria-expanded="expanded"
  13. @click="emit('toggle')"
  14. >
  15. <span class="cockpit-panel__title">
  16. <!-- 批次 1:面板图标按 panelId 取自蓝湖正式切图;纯装饰,aria-hidden 不参与命名。 -->
  17. <CockpitIcon v-if="iconName" :name="iconName" :size="panelIconSize" class="cockpit-panel__icon" />
  18. {{ title }}
  19. </span>
  20. <small class="cockpit-panel__meta">{{ meta }}</small>
  21. <span class="cockpit-panel__mark" aria-hidden="true">{{ expanded ? '−' : '+' }}</span>
  22. </button>
  23. <!-- v-show 而非 v-if:收起再展开不应重建面板内容或触发重新取数。 -->
  24. <div v-show="expanded" class="cockpit-panel__body">
  25. <slot />
  26. </div>
  27. </section>
  28. </template>
  29. <script setup>
  30. import { computed } from 'vue'
  31. import CockpitIcon from './CockpitIcon.vue'
  32. import { panelIcon } from '../domain/panelCatalog.js'
  33. const props = defineProps({
  34. title: { type: String, required: true },
  35. meta: { type: String, default: '' },
  36. expanded: { type: Boolean, default: true },
  37. /**
  38. * 面板的稳定英文键(`domain/panelCatalog.js` 的 panelId)。
  39. * 走查与断言读它来认面板 —— 不能从面板体内的文案反推:
  40. * 面板体可能是占位、也可能是真实内容,**断言不该依赖某一种**。
  41. */
  42. panelId: { type: String, default: '' },
  43. })
  44. const emit = defineEmits(['toggle'])
  45. /** 面板图标语义名;无对应图标时为空串 → 不渲染图标,不抛错。 */
  46. const iconName = computed(() => panelIcon(props.panelId))
  47. /** 与 15.5u 的标题字号配套的图标边长(走查反馈偏小,由 14 上调)。 */
  48. const panelIconSize = 'calc(17 * var(--u, 1px))'
  49. </script>
  50. <style scoped>
  51. /* 组件内自带的盒模型重置(不依赖全局 `*` 规则)。 */
  52. .cockpit-panel,
  53. .cockpit-panel *,
  54. .cockpit-panel *::before,
  55. .cockpit-panel *::after {
  56. box-sizing: border-box;
  57. }
  58. /* 切角描边:四角各一段青色 L 形,与原型图同一套 SVG(内联为自定义属性)。 */
  59. .cockpit-panel {
  60. --corner-tl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath d='M0.8 16 V3.3 A2.5 2.5 0 0 1 3.3 0.8 H16' fill='none' stroke='%234fd8ff' stroke-width='1.7'/%3E%3C/svg%3E");
  61. --corner-tr: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath d='M0 0.8 H12.7 A2.5 2.5 0 0 1 15.2 3.3 V16' fill='none' stroke='%234fd8ff' stroke-width='1.7'/%3E%3C/svg%3E");
  62. --corner-bl: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath d='M0.8 0 V12.7 A2.5 2.5 0 0 0 3.3 15.2 H16' fill='none' stroke='%234fd8ff' stroke-width='1.7'/%3E%3C/svg%3E");
  63. --corner-br: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath d='M0 15.2 H12.7 A2.5 2.5 0 0 0 15.2 12.7 V0' fill='none' stroke='%234fd8ff' stroke-width='1.7'/%3E%3C/svg%3E");
  64. position: relative;
  65. display: flex;
  66. /* 展开时参与分配所在栏的高度:两块等分,收起的那块缩回标题栏高度、其余吃掉全部空间。 */
  67. flex: 1 1 0;
  68. flex-direction: column;
  69. min-width: 0;
  70. min-height: 0;
  71. border: 1px solid var(--panel-line, rgba(58, 170, 226, 0.3));
  72. background: linear-gradient(
  73. 180deg,
  74. var(--panel-top, rgba(12, 48, 86, 0.58)) 0%,
  75. rgba(6, 24, 44, 0.82) 26%,
  76. var(--panel-bot, rgba(3, 14, 28, 0.9)) 100%
  77. );
  78. box-shadow: inset 0 1px 0 rgba(130, 225, 255, 0.14), var(--shadow, 0 18px 44px rgba(0, 0, 0, 0.55));
  79. }
  80. .cockpit-panel.is-collapsed {
  81. flex: 0 0 auto;
  82. }
  83. .cockpit-panel::before {
  84. content: '';
  85. position: absolute;
  86. inset: -1px;
  87. z-index: 3;
  88. pointer-events: none;
  89. background:
  90. var(--corner-tl) left top / 15px 15px no-repeat,
  91. var(--corner-tr) right top / 15px 15px no-repeat,
  92. var(--corner-bl) left bottom / 15px 15px no-repeat,
  93. var(--corner-br) right bottom / 15px 15px no-repeat;
  94. filter: drop-shadow(0 0 4px rgba(79, 216, 255, 0.5));
  95. }
  96. .cockpit-panel__head {
  97. position: relative;
  98. z-index: 2;
  99. display: flex;
  100. align-items: center;
  101. gap: calc(10 * var(--u, 1px));
  102. width: 100%;
  103. min-height: calc(40 * var(--u, 1px));
  104. padding: 0 calc(12 * var(--u, 1px)) 0 calc(8 * var(--u, 1px));
  105. border: 0;
  106. border-bottom: 1px solid rgba(58, 170, 226, 0.22);
  107. background:
  108. linear-gradient(90deg, rgba(24, 96, 152, 0.55) 0%, rgba(12, 52, 90, 0.3) 42%, transparent 78%),
  109. linear-gradient(180deg, rgba(90, 200, 255, 0.1), transparent);
  110. font: inherit;
  111. text-align: left;
  112. cursor: pointer;
  113. }
  114. .cockpit-panel__head::after {
  115. content: '';
  116. position: absolute;
  117. left: 12px;
  118. right: 12px;
  119. bottom: -1px;
  120. height: 1px;
  121. background: linear-gradient(90deg, rgba(79, 216, 255, 0.85), rgba(79, 216, 255, 0.1) 46%, transparent);
  122. }
  123. .cockpit-panel__head:hover {
  124. background:
  125. linear-gradient(90deg, rgba(34, 116, 176, 0.6) 0%, rgba(16, 66, 110, 0.34) 42%, transparent 78%),
  126. linear-gradient(180deg, rgba(90, 200, 255, 0.14), transparent);
  127. }
  128. .cockpit-panel__head:focus-visible {
  129. outline: 1px solid var(--cyan, #4fd8ff);
  130. outline-offset: -2px;
  131. }
  132. .cockpit-panel.is-collapsed .cockpit-panel__head {
  133. border-bottom-color: transparent;
  134. }
  135. .cockpit-panel.is-collapsed .cockpit-panel__head::after {
  136. display: none;
  137. }
  138. /* 标题:左侧一段青色竖条 + 辉光,与原型 .panel-head h2 一致 */
  139. .cockpit-panel__title {
  140. position: relative;
  141. flex: none;
  142. /* 批次 1:标题改为 inline-flex 以容纳面板图标(竖条仍是 ::before 绝对定位,不受影响)。 */
  143. display: inline-flex;
  144. align-items: center;
  145. gap: calc(5 * var(--u, 1px));
  146. padding-left: calc(10 * var(--u, 1px));
  147. color: #eaf9ff;
  148. /* 2026-10-12:走查反馈偏小,由 14.5 上调到 15.5,与 tokens 的 --fs-title 同步。 */
  149. font-size: calc(15.5 * var(--u, 1px));
  150. font-weight: 700;
  151. letter-spacing: 0.1em;
  152. white-space: nowrap;
  153. text-shadow: 0 0 12px rgba(79, 216, 255, 0.6);
  154. }
  155. /* 面板图标:单独一档弱辉光,避免继承标题的强发光把小图标糊成一团。 */
  156. .cockpit-panel__icon {
  157. flex: none;
  158. text-shadow: none;
  159. filter: drop-shadow(0 0 4px rgba(79, 216, 255, 0.45));
  160. }
  161. .cockpit-panel__title::before {
  162. content: '';
  163. position: absolute;
  164. left: 0;
  165. top: 50%;
  166. width: calc(4 * var(--u, 1px));
  167. height: calc(15 * var(--u, 1px));
  168. transform: translateY(-50%);
  169. background: linear-gradient(180deg, #b6f0ff, var(--cyan-d, #1b93cf));
  170. box-shadow: 0 0 10px var(--cyan, #4fd8ff);
  171. clip-path: polygon(0 0, 100% 3px, 100% 100%, 0 calc(100% - 3px));
  172. }
  173. .cockpit-panel__meta {
  174. flex: 1 1 auto;
  175. min-width: 0;
  176. overflow: hidden;
  177. color: var(--muted, #6d92ae);
  178. font-size: calc(10.5 * var(--u, 1px));
  179. letter-spacing: 0.04em;
  180. white-space: nowrap;
  181. text-overflow: ellipsis;
  182. }
  183. .cockpit-panel__meta::before {
  184. content: '▸ ';
  185. color: rgba(79, 216, 255, 0.7);
  186. }
  187. .cockpit-panel__mark {
  188. flex: none;
  189. width: calc(16 * var(--u, 1px));
  190. color: var(--cyan, #4fd8ff);
  191. font-size: calc(15 * var(--u, 1px));
  192. line-height: 1;
  193. text-align: center;
  194. text-shadow: 0 0 10px rgba(79, 216, 255, 0.7);
  195. }
  196. .cockpit-panel__body {
  197. position: relative;
  198. z-index: 1;
  199. flex: 1 1 auto;
  200. min-height: 0;
  201. padding: calc(10 * var(--u, 1px)) calc(11 * var(--u, 1px));
  202. overflow: auto;
  203. }
  204. </style>