PanelBlocks.vue 127 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793
  1. <!--
  2. 面板块的**共享渲染层**(票 17 建立,票 18 改名并解除模式耦合)。
  3. 为什么所有面板共用一个组件(而不是每个模式一套):它们的形状是同一件事 ——
  4. 几组 KPI + 若干分布/清单/趋势,差异只在字段与文案,而那些**都已经在各模式的
  5. `domain/*Panels.js` 里翻好了**。组件因此只按 `block.kind` 分派画法,不认识任何后端字段。
  6. 票 18 的两处改动(都是为了让票 19/20 能接进来,而不是再长一套):
  7. 1. **改名 `InfrastructurePanel` → `PanelBlocks`**:旧名字带着「基础设施模式」,
  8. 而它从建立起就是通用块渲染(当时只有那一个模式在用)。名字与职责错位会让下一个模式
  9. 误以为要另写一个组件 —— 那正是票 17 明确反对的「下游三票再长出十二份实现」。
  10. 2. **吃算好的 `view` prop,不再按 `panelId` 自己找视图**:分发收进
  11. `domain/panelViews.js` 的注册表。这样组件与任何模式解耦(它甚至不知道共有几个模式),
  12. 而「哪个面板有哪些块」各自归本模式的 domain 模块。
  13. 三条纪律在这里的落点(票 17 立,本票一字不改):
  14. · 缺值显示占位符(`—`,来自 domain,组件只判断"是不是占位符"来降亮度);
  15. · 面板自己的成败由 `SectionStateTag` 说(色键与文案只来自 domain/sectionState.js);
  16. · 接口词汇不上大屏 —— 组件里不出现任何后端字段名或原因码(票 05 的静态守卫盯着这条)。
  17. `data-*` 是走查断言面:`data-panel-blocks`/`data-panel-id` 定位面板,
  18. `data-locate-point` 是可定位对象,`data-ledger-card` 是台账卡片,
  19. `data-pie-*` 是状态分布饼图(票 14:扇区/图例行/选中态;面板不再自带筛选入口),
  20. `data-panel-block` 是块类型,
  21. 票 56(二/三轮):顶部口径说明统一折叠进 `data-panel-summary-toggle` /
  22. `data-panel-summary-details`,每条**保留自己原有的断言面**(折叠只是 `display:none`,
  23. 文本仍在 DOM 里):`data-panel-gap` 契约缺口、`data-panel-note` 面板口径说明、
  24. `data-panel-link-note` 联动出口、`data-panel-excluded-note` 口径剔除、
  25. `data-panel-industry` / `data-panel-type-note` / `data-panel-event-note` /
  26. `data-panel-range-note` / `data-panel-offline-note` / `data-panel-page-note` /
  27. `data-locate-note`,另带 `data-summary="<键>"` 供按条目定位。
  28. -->
  29. <template>
  30. <div ref="rootRef" class="panel-blocks" :data-panel-blocks="view.name" :data-panel-id="view.name">
  31. <!-- 面板级状态(色键与文案只来自 domain/sectionState.js)。票 56 二轮:整条收成折叠入口。 -->
  32. <header v-if="summaryItems.length" class="panel-blocks__head">
  33. <!--
  34. 票 56(第二轮,维护者 2026-10-17):**所有面板**的顶部摘要说明统一收成
  35. 一个紧凑入口,且**只有点击**才展开/收起(撤销第一轮的 hover 自动展开——
  36. 大屏上鼠标掠过就弹开,等于没有折叠)。键盘与触屏走原生 button:
  37. Enter / Space / click 都会触发同一个 toggle,不必另写 keydown。
  38. 每条摘要**保留自己原有的 `data-*` 断言面**(折叠时 `v-show` 只是隐藏,
  39. 文本仍在 DOM 里),走查脚本读到的是同一句 domain 文案。
  40. -->
  41. <span class="panel-summary-disclosure">
  42. <button
  43. type="button"
  44. class="panel-summary-disclosure__toggle"
  45. :aria-expanded="summaryOpen"
  46. :aria-controls="summaryDetailsId"
  47. :aria-label="`${view.label || '面板'}摘要,${summaryOpen ? '收起' : '展开'}完整说明`"
  48. data-panel-summary-toggle
  49. @click="summaryOpen = !summaryOpen"
  50. >
  51. <span>{{ view.label || '面板' }}摘要</span><b>{{ summaryStatus }}</b><i aria-hidden="true">{{ summaryOpen ? '收起' : '展开' }}</i>
  52. </button>
  53. <span v-show="summaryOpen" :id="summaryDetailsId" class="panel-summary-disclosure__details" data-panel-summary-details>
  54. <span
  55. v-for="item in summaryItems"
  56. :key="item.key"
  57. :class="item.isGap ? 'panel-blocks__gap' : 'panel-blocks__note'"
  58. v-bind="item.attrs"
  59. >{{ item.text }}</span>
  60. </span>
  61. </span>
  62. </header>
  63. <!-- 还没取过数:说"正在取数",不说"没有数据"(那是在报告一件还没问过的事) -->
  64. <p v-if="pending" class="panel-blocks__placeholder" data-panel-pending>{{ view.placeholder }}</p>
  65. <!-- 有内容:按块分派 -->
  66. <template v-else-if="view.hasContent">
  67. <section
  68. v-for="(block, index) in view.blocks"
  69. :key="`${block.kind}-${index}`"
  70. class="panel-block"
  71. :data-panel-block="block.kind"
  72. >
  73. <!--
  74. 块标题行(票 11 视觉对齐):左侧竖条 + 标题,右侧可选小字(`block.aside`)。
  75. 小字是**块级口径提要**(如「点击卡片可按行业筛选地图」「片区在网设备」),
  76. 与面板右上角小字(PANEL_SUBTITLES,面板身份)分开:一个说块、一个说面板。
  77. 文案全部由 domain 给,渲染层不合成、不改写。
  78. -->
  79. <div v-if="block.label || block.aside" class="panel-block__head">
  80. <h4 v-if="block.label" class="panel-block__title" :data-block-title="block.label">
  81. <i class="panel-block__title-bar" aria-hidden="true"></i>
  82. <EllipsisText tag="span" :text="block.label" />
  83. </h4>
  84. <EllipsisText v-if="block.aside" tag="small" class="panel-block__aside" :text="block.aside" data-block-aside />
  85. </div>
  86. <!-- KPI:等宽数字 + 单位,缺值降亮度显示占位符。
  87. 2026-10-13 维护者要求:每项读数左侧带语义图标(domain 的 KPI_ICONS
  88. 按 key 映射,未命中不画);图标 aria-hidden 不参与命名。 -->
  89. <div
  90. v-if="block.kind === 'kpi'"
  91. class="panel-kpis"
  92. :class="{
  93. 'panel-kpis--two-by-two': block.layout === 'two-by-two',
  94. 'panel-kpis--value-first': block.layout === 'value-first',
  95. 'panel-kpis--two': block.layout === 'two',
  96. 'panel-kpis--three': block.layout === 'three',
  97. }"
  98. data-kpi-row
  99. >
  100. <!-- 票 38(第 4 条):基础档案的「设施总数 + 管线里程」两项落回同一张
  101. auto-fit 网格自然同行。
  102. 票 38 第二轮(反馈第 1 条):整行独占改由 domain 的通用 `wide` 标志声明
  103. (概览基础设施的管线里程独占一行排版更好看),渲染层不认识任何 key。 -->
  104. <div
  105. v-for="item in block.items"
  106. :key="item.key"
  107. class="panel-kpi"
  108. :class="[
  109. { 'panel-kpi--wide': item.wide === true },
  110. block.layout === 'value-first' ? 'panel-kpi--value-first' : '',
  111. item.accent ? `panel-kpi--accent-${item.accent}` : '',
  112. ]"
  113. :data-kpi="item.key"
  114. :data-kpi-accent="item.accent || null"
  115. :data-kpi-value-tone="item.valueTone || null"
  116. >
  117. <!-- 票 37 走查扩展:读数图标走 mask 染语义色(KPI_ICON_COLORS,未命中主蓝)。
  118. 色画在容器上(currentColor),图标本体是白剪影。 -->
  119. <span
  120. v-if="item.icon"
  121. class="panel-kpi__icon"
  122. :style="{ color: item.iconColor || 'var(--cyan, #01abea)' }"
  123. >
  124. <CockpitIcon :name="`kpi${item.icon[0].toUpperCase()}${item.icon.slice(1)}`" size="calc(14 * var(--u, 1px))" />
  125. </span>
  126. <EllipsisText class="panel-kpi__label" :text="item.label" />
  127. <!--
  128. 票 20:带 `link` 的读数可点(联动出口)。组件只回答"这一项被点了",
  129. `link` 是什么、点了去哪,全由 domain 给、外壳翻 —— 组件仍不认识任何 panelId。
  130. 票 37:读数与标签一样走**共用省略方法**(越界省略 + hover 读全);
  131. 动作提示用 `hint` 附在完整原文之后,原文因此不会因为要写提示而丢。
  132. -->
  133. <EllipsisText
  134. v-if="item.link"
  135. tag="button"
  136. type="button"
  137. class="panel-kpi__value panel-kpi__value--link"
  138. :class="{ 'is-muted': isPlaceholder(item.value) }"
  139. :data-kpi-link="item.link"
  140. :text="kpiReading(item)"
  141. :hint="`${item.label}:${item.linkHint || '查看明细'}`"
  142. @click="emit('panel-action', { kind: 'link', key: item.link })"
  143. >
  144. {{ item.value }}<em v-if="item.unit && !isPlaceholder(item.value)"> {{ item.unit }}</em>
  145. </EllipsisText>
  146. <EllipsisText
  147. v-else
  148. tag="b"
  149. class="panel-kpi__value"
  150. :class="[{ 'is-muted': isPlaceholder(item.value) }, item.valueTone ? `is-${item.valueTone}` : '']"
  151. :text="kpiReading(item)"
  152. :hint="item.hint"
  153. >
  154. {{ item.value }}<em v-if="item.unit && !isPlaceholder(item.value)"> {{ item.unit }}</em>
  155. </EllipsisText>
  156. <!--
  157. 卡内小字(`caption`,设计稿的第三行,如「已派发处置」「全域管网总长」):
  158. **屏上常显**,超框时按共用省略方法收成「…」,完整原文进 title。
  159. `hint`(口径细节,如在线率的「分子 3 / 分母 10」)**不再占屏上位置**:
  160. 维护者 2026-10-23 明确「不需要在外展示分子分母,鼠标 hover 展示即可」。
  161. 它现在挂在读数自身的 title 上 —— 读数与说明同属一个可悬停对象,
  162. 不用再在卡里多占一行(这正是卡片被撑高的原因之一)。
  163. -->
  164. <EllipsisText v-if="item.caption" tag="small" class="panel-kpi__caption" :text="item.caption" data-kpi-caption />
  165. <!--
  166. 差值(规格 §6.6「较昨日」):文案与色调**都由 domain 给**
  167. (`deltaText` / `deltaTone`),组件只画 —— 正红负绿、0 中性
  168. 这条规则不落在渲染层(否则第二个面板就会各写一套)。
  169. -->
  170. <small
  171. v-if="item.deltaText"
  172. class="panel-kpi__delta"
  173. :class="`is-${item.deltaTone}`"
  174. :data-kpi-delta="item.deltaTone"
  175. >{{ item.deltaText }}</small>
  176. </div>
  177. </div>
  178. <!--
  179. 卡片组(票 02):一排「标签 + 计数」的小卡 —— 设备类型卡(卡内小字=监测内容)、
  180. 片区卡、固定等级卡共用这一块画法。`fixed` 声明的是语义(固定项、无数据 0),
  181. 不是另画一种卡;`note` 是卡内小字,没有就不画那一行。
  182. -->
  183. <!--
  184. 卡片组(票 02 建立,票 11 视觉对齐):三种卡共用这一块画法 ——
  185. 类型卡(徽标 + 计数 + 名称 + 监测内容小字)、片区卡(计数 + 名称)、
  186. 固定等级卡(按等级着色的计数 + 名称)。
  187. 版式由 domain 的 `layout` 声明(`two-by-two` / `three` / `four`),
  188. 语义色由行的 `tone`(边框+徽标)与 `valueTone`(只染数字)给;
  189. 可点行(`action` 且行有 `actionKey`)渲染成 `<button>`,与条形图同一条接缝。
  190. -->
  191. <div
  192. v-else-if="block.kind === 'cards'"
  193. class="panel-cards"
  194. :class="block.layout ? `panel-cards--${block.layout}` : ''"
  195. :data-cards-fixed="block.fixed ? 'yes' : 'no'"
  196. :data-cards-layout="block.layout || null"
  197. :data-cards-action="block.action || null"
  198. data-card-list
  199. >
  200. <component
  201. :is="block.action && row.actionKey ? 'button' : 'div'"
  202. v-for="row in block.rows"
  203. :key="row.key"
  204. class="panel-card"
  205. :class="[
  206. block.action && row.actionKey ? 'panel-card--button' : '',
  207. row.icon ? 'panel-card--with-icon' : '',
  208. row.tone ? `panel-card--tone-${row.tone.replace(':', '-')}` : '',
  209. row.valueTone ? `panel-card--value-${row.valueTone}` : '',
  210. row.selected ? 'is-active' : '',
  211. ]"
  212. :type="block.action && row.actionKey ? 'button' : null"
  213. :data-card="row.key"
  214. :data-card-tone="row.tone || null"
  215. :data-card-value-tone="row.valueTone || null"
  216. :data-card-action="block.action || null"
  217. :aria-pressed="block.action && row.actionKey ? (row.selected ? 'true' : 'false') : null"
  218. :title="block.action && row.actionKey ? `按「${row.label}」筛选地图` : null"
  219. @click="block.action && row.actionKey ? emit('panel-action', { kind: block.action, key: row.actionKey }) : null"
  220. >
  221. <!-- 徽标:卡内小图标(行业图标走既有切图,语义色染在徽章容器上)。 -->
  222. <span v-if="row.icon" class="panel-card__badge" aria-hidden="true">
  223. <CockpitIcon :name="row.icon" size="calc(15 * var(--u, 1px))" />
  224. </span>
  225. <b class="panel-card__count" data-card-count>{{ row.count }}<em v-if="block.unit"> {{ block.unit }}</em></b>
  226. <EllipsisText class="panel-card__label" :text="row.label" />
  227. <!-- 小字要能吃两行,所以不套共用省略组件(它固定 nowrap);
  228. 完整原文仍走 title,与共用省略方法是同一条纪律的另一形态。 -->
  229. <small v-if="row.note" class="panel-card__note" :title="row.noteTitle || row.note">{{ row.note }}</small>
  230. </component>
  231. <p v-if="!block.rows.length" class="panel-block__empty" data-card-empty>该维度当前没有记录</p>
  232. </div>
  233. <!--
  234. 分布:条长按最大项归一(相对长度,不冒充占比)。
  235. 票 20:带 `action` 的块整行可点(如行业构成 → 写全局行业筛选);
  236. 组件只转发"哪一行被点了",不解释这个动作的语义。
  237. 票 37:**只有带得出 `actionKey` 的行**才是按钮 —— 展示名不是后端筛选码,
  238. 拿它兜底去发参数就是"拿脏值换后端 400";`row.selected` 画选中态(再点即取消)。
  239. 2026-10-13 维护者要求:条形按行语义着色(行业行=行业色、状态行=语义色、
  240. 其余=图表色序),一眼对应类别;色值仍只在 tokens.css 声明一份。
  241. -->
  242. <div
  243. v-else-if="block.kind === 'bars'"
  244. class="panel-bars"
  245. :class="{ 'panel-bars--clickable': block.action, 'panel-bars--rank': block.variant === 'rank' }"
  246. :data-bar-variant="block.variant || null"
  247. data-bar-list
  248. >
  249. <component
  250. :is="block.action && row.actionKey ? 'button' : 'div'"
  251. v-for="(row, rowIndex) in block.rows"
  252. :key="row.key"
  253. class="panel-bar"
  254. :class="{ 'panel-bar--button': block.action && row.actionKey, 'is-active': row.selected }"
  255. :type="block.action && row.actionKey ? 'button' : null"
  256. :data-bar="row.key"
  257. :data-bar-action="block.action || null"
  258. :data-bar-tone="row.tone || null"
  259. :data-bar-selected="row.selected ? 'yes' : 'no'"
  260. :title="block.action && row.actionKey ? `把「${row.label}」设为全局筛选条件` : null"
  261. @click="block.action && row.actionKey ? emit('panel-action', { kind: block.action, key: row.actionKey }) : null"
  262. @mouseenter="showChartTip($event, row.label, row.hint || row.count, barColor(row.tone))"
  263. @mouseleave="hideChartTip"
  264. >
  265. <!-- rank 版式:名次徽章(序号由渲染层按行序给,1 起) -->
  266. <i v-if="block.variant === 'rank'" class="panel-bar__rank" aria-hidden="true">{{ rowIndex + 1 }}</i>
  267. <EllipsisText class="panel-bar__label" :text="row.label" />
  268. <i class="panel-bar__track"><i class="panel-bar__fill" :style="{ width: `${Math.round(row.ratio * 100)}%`, background: barColor(row.tone) }"></i></i>
  269. <span class="panel-bar__count" data-bar-count>{{ row.count }}<em v-if="block.unit">{{ block.unit }}</em></span>
  270. </component>
  271. <p v-if="!block.rows.length" class="panel-block__empty" data-bar-empty>该维度当前没有记录</p>
  272. </div>
  273. <!--
  274. 可切换维度的分类柱状图(票 38 · 管理信息「片区 × 行业」)。
  275. 维度切换是**纯展示状态**(组件本地):两个维度的行都由 domain 预先聚合好,
  276. 点标签只换 rows,不回后端、不进全局筛选——它回答「同一份计数换哪个维度看」,
  277. 不是筛选条件(与条形图可点行的 `panel-action` 接缝刻意分开,互不冒充)。
  278. 柱/条/读数与 `.panel-bar` 同一套画法与色键(barColor + 语义 tone),
  279. `data-dimbar-*` 与 `data-dim-tab-*` 是走查断言面。
  280. -->
  281. <div v-else-if="block.kind === 'dimbars'" class="panel-dimbars" data-dimbars-list>
  282. <div class="panel-dimbars__tabs" role="group" :aria-label="`${block.label} · 维度切换`">
  283. <button
  284. v-for="dim in block.dimensions"
  285. :key="dim.key"
  286. type="button"
  287. class="panel-dimbars__tab"
  288. :class="{ 'is-active': dim.key === activeDimension(block) }"
  289. :aria-pressed="dim.key === activeDimension(block)"
  290. :data-dim-tab="dim.key"
  291. @click="setDimension(block, dim.key)"
  292. >
  293. {{ dim.label }}
  294. </button>
  295. </div>
  296. <div v-if="currentDim(block)" class="panel-bars panel-dimbars__bars">
  297. <div
  298. v-for="row in currentDim(block).rows"
  299. :key="row.key"
  300. class="panel-bar"
  301. :data-dimbar="row.key"
  302. :data-dim-tone="row.tone || null"
  303. @mouseenter="showChartTip($event, row.label, row.hint || `${row.count}${block.unit}`, barColor(row.tone))"
  304. @mouseleave="hideChartTip"
  305. >
  306. <EllipsisText class="panel-bar__label" :text="row.label" />
  307. <i class="panel-bar__track"><i class="panel-bar__fill" :style="{ width: `${Math.round(row.ratio * 100)}%`, background: barColor(row.tone) }"></i></i>
  308. <span class="panel-bar__count" data-dimbar-count>{{ row.count }}</span>
  309. </div>
  310. <p v-if="!currentDim(block).rows.length" class="panel-block__empty" data-dimbar-empty>{{ block.emptyText }}</p>
  311. </div>
  312. <p v-else class="panel-block__empty" data-dimbar-empty>{{ block.emptyText }}</p>
  313. </div>
  314. <!--
  315. 趋势:分桶序列(桶粒度与顺序由后端给,本层不重采样)。
  316. 2026-10-13 维护者图表选型:时间趋势优先折线;<2 点或柱形态仍画柱列。
  317. 折线版式(维护者第三轮:「太丑、线与数值对不上」):
  318. · Y 轴三档刻度(0/半程/峰值)+ 水平网格线 —— 数值与高度有坐标对应;
  319. · 面积渐变(主色向下渐隐)+ 平滑折线 + 正圆数据点(HTML 叠层,不被拉伸);
  320. · 每点顶标注数值(≤8 点全标;多时标峰值),悬停 title 兜底;
  321. · X 轴保留桶标签行。读数接缝(data-trend-*)不变。
  322. -->
  323. <div
  324. v-else-if="block.kind === 'trend'"
  325. class="panel-trend"
  326. :class="{ 'panel-trend--line': block.chart === 'line' && block.linePoints.length >= 2 }"
  327. :style="block.tone ? { '--trend-tone': barColor(block.tone) } : null"
  328. :data-trend-tone="block.tone || null"
  329. data-trend-list
  330. >
  331. <!--
  332. 多序列折线(维护者 2026-10-23 反馈第 2 条):一条图里对比多条线 + 图例。
  333. 与单线版式的差别:**不画面积**(多条面积会糊成一片)、**不标点顶数值**
  334. (会互相压),改在图例里给每条线的名字与色块,悬停仍读具体日期与数值。
  335. Y 轴按所有线的联合最大值归一(domain 已算好),所以两条线的高低可比。
  336. -->
  337. <template v-if="block.multiSeries">
  338. <div class="panel-trend__chart" data-trend-chart data-trend-multi="yes">
  339. <div
  340. v-for="tick in block.yTicks"
  341. :key="`mgrid-${tick.pct}`"
  342. class="panel-trend__grid"
  343. :class="{ 'is-top': tick.pct === 100 }"
  344. :style="{ bottom: `${tick.pct}%` }"
  345. >
  346. <b class="panel-trend__ytick" data-trend-ytick>{{ tick.value }}</b>
  347. </div>
  348. <div class="panel-trend__plot">
  349. <svg class="panel-trend__line" viewBox="0 0 100 100" preserveAspectRatio="none" role="img" :aria-label="block.label">
  350. <polyline
  351. v-for="series in block.series"
  352. :key="series.key"
  353. class="panel-trend__line-path"
  354. :points="series.points.map((p) => `${p.x},${p.y}`).join(' ')"
  355. :style="{ stroke: barColor(series.tone) }"
  356. fill="none"
  357. :data-trend-series="series.key"
  358. />
  359. </svg>
  360. <template v-for="series in block.series" :key="`dots-${series.key}`">
  361. <div
  362. v-for="p in series.points"
  363. :key="`${series.key}-${p.key}`"
  364. class="panel-trend__node"
  365. :style="{ left: `${p.xPct}%`, bottom: `${p.yPct}%` }"
  366. :data-trend="p.key"
  367. :data-trend-series="series.key"
  368. :aria-label="`${series.label} ${p.hint || p.label}:${p.count}`"
  369. @mouseenter="showChartTip($event, `${series.label} · ${p.hint || p.label}`, p.count, barColor(series.tone))"
  370. @mouseleave="hideChartTip"
  371. >
  372. <i class="panel-trend__dot" :style="{ background: barColor(series.tone) }" aria-hidden="true"></i>
  373. </div>
  374. </template>
  375. </div>
  376. </div>
  377. <div class="panel-trend__axis" data-trend-axis>
  378. <EllipsisText
  379. v-for="(point, i) in block.points"
  380. :key="point.key"
  381. tag="span"
  382. class="panel-trend__label"
  383. :style="{ left: `${series_xPct(block, i)}%` }"
  384. :text="block.axisIndices.includes(i) ? point.label : ''"
  385. />
  386. </div>
  387. <!-- 图例:每条线一个色块 + 名称(维护者要求「不同颜色区分并增加图例」)。 -->
  388. <ul class="panel-trend__legend" data-trend-legend>
  389. <li
  390. v-for="series in block.series"
  391. :key="`lg-${series.key}`"
  392. class="panel-trend__legend-item"
  393. :data-trend-legend-item="series.key"
  394. @mouseenter="showChartTip($event, series.label, series.points.reduce((s, p) => s + p.count, 0), barColor(series.tone))"
  395. @mouseleave="hideChartTip"
  396. >
  397. <i class="panel-trend__legend-swatch" :style="{ background: barColor(series.tone) }" aria-hidden="true"></i>
  398. <span>{{ series.label }}</span>
  399. </li>
  400. </ul>
  401. </template>
  402. <template v-else-if="block.chart === 'line' && block.linePoints.length >= 2">
  403. <div class="panel-trend__chart" data-trend-chart>
  404. <!-- Y 轴刻度 + 网格线(刻度标签落在左侧留出的槽里) -->
  405. <div
  406. v-for="tick in block.yTicks"
  407. :key="`grid-${tick.pct}`"
  408. class="panel-trend__grid"
  409. :class="{ 'is-top': tick.pct === 100 }"
  410. :style="{ bottom: `${tick.pct}%` }"
  411. >
  412. <b class="panel-trend__ytick" data-trend-ytick>{{ tick.value }}</b>
  413. </div>
  414. <!-- 绘图区:SVG 与数据点/标签共用同一坐标基准(百分比都相对它) -->
  415. <div class="panel-trend__plot">
  416. <svg class="panel-trend__line" viewBox="0 0 100 100" preserveAspectRatio="none" role="img" :aria-label="block.label">
  417. <polygon class="panel-trend__area" :points="block.areaPoints" />
  418. <polyline
  419. class="panel-trend__line-path"
  420. :points="block.linePoints.map((p) => `${p.x},${p.y}`).join(' ')"
  421. fill="none"
  422. />
  423. </svg>
  424. <div
  425. v-for="p in block.linePoints"
  426. :key="p.key"
  427. class="panel-trend__node"
  428. :class="{ 'is-peak': !block.showInlineLabels && p.annotate }"
  429. :style="{ left: `${p.xPct}%`, bottom: `${p.yPct}%` }"
  430. :data-trend="p.key"
  431. :aria-label="`${p.label}:${p.displayText || p.count}`"
  432. @mouseenter="showChartTip($event, p.hint || p.label, p.displayText || p.count, 'var(--cyan-hi, #00b8ff)')"
  433. @mouseleave="hideChartTip"
  434. >
  435. <b v-if="p.annotate" class="panel-trend__vlabel" data-trend-count>{{ p.displayText || p.count }}</b>
  436. <i class="panel-trend__dot" aria-hidden="true"></i>
  437. </div>
  438. </div>
  439. </div>
  440. <div class="panel-trend__axis" data-trend-axis>
  441. <EllipsisText
  442. v-for="(point, i) in block.points"
  443. :key="point.key"
  444. tag="span"
  445. class="panel-trend__label"
  446. :style="{ left: `${block.linePoints[i] ? block.linePoints[i].xPct : 0}%` }"
  447. :text="block.axisIndices.includes(i) ? point.label : ''"
  448. />
  449. </div>
  450. </template>
  451. <template v-else>
  452. <div
  453. v-for="point in block.points"
  454. :key="point.key"
  455. class="panel-trend__point"
  456. :data-trend="point.key"
  457. @mouseenter="showChartTip($event, point.hint || point.label, point.displayText || point.count, 'var(--cyan, #01abea)')"
  458. @mouseleave="hideChartTip"
  459. >
  460. <i class="panel-trend__bar" :style="{ height: `${Math.max(2, Math.round(point.ratio * 100))}%` }"></i>
  461. <span class="panel-trend__count" data-trend-count>{{ point.displayText || point.count }}</span>
  462. <EllipsisText class="panel-trend__label" :text="point.label" />
  463. </div>
  464. </template>
  465. <p v-if="!block.points.length" class="panel-block__empty" data-trend-empty>该窗口没有事件记录</p>
  466. </div>
  467. <!--
  468. 百分比堆叠条(2026-10-13 维护者图表选型:多类别占比)。
  469. 一根条按占比分段(domain 算好 start/宽度),下方图例逐行列出计数——
  470. 0 计数的行保留图例(含义可查),不占条宽。
  471. 2026-10-13 维护者反馈:分段按语义 tone 着色(工单状态=语义色);
  472. 同语义多行(两个待办态都是 lv3)用 shade 深浅区分。
  473. 票 37 走查扩展:分段悬停**放大 + 详情浮层**(如「蓝色:待派单:1」);
  474. 分段是绝对定位的窄条,放大用 scaleY+Y 向拉伸、宽度保持(不挤邻居)。
  475. 票 43:段间小间隔=`left`/`width` 各自独立取整露出的底色缝(绘制缺陷,
  476. 非语义边界)。边界改由 domain 的接力取整(`startPct`/`widthPct`,
  477. 每段起点=上一段终点)给出整数百分点,连续由构造保证。
  478. -->
  479. <div v-else-if="block.kind === 'stacked'" class="panel-stacked" data-stacked-list>
  480. <div class="panel-stacked__bar" role="img" :aria-label="`${block.label}(合计 ${block.total})`">
  481. <i
  482. v-for="row in block.rows"
  483. :key="row.key"
  484. v-show="row.ratio > 0"
  485. class="panel-stacked__seg"
  486. :data-stacked-seg="row.key"
  487. :style="{
  488. left: `${row.startPct}%`,
  489. width: `${row.widthPct}%`,
  490. '--seg-shade': row.shade,
  491. '--seg-color': stackedColor(row),
  492. }"
  493. :title="`${row.label} ${row.count}`"
  494. @mouseenter="showChartTip($event, row.label, row.count, stackedColor(row))"
  495. @mouseleave="hideChartTip"
  496. ></i>
  497. </div>
  498. <ul class="panel-stacked__legend" data-stacked-legend>
  499. <li
  500. v-for="row in block.rows"
  501. :key="row.key"
  502. class="panel-stacked__row"
  503. :data-stacked="row.key"
  504. @mouseenter="showChartTip($event, row.label, row.count, stackedColor(row))"
  505. @mouseleave="hideChartTip"
  506. >
  507. <i class="panel-stacked__swatch" :style="{ '--seg-color': stackedColor(row) }" aria-hidden="true"></i>
  508. <EllipsisText class="panel-stacked__label" :text="row.label" />
  509. <b class="panel-stacked__count" data-stacked-count>{{ row.count }}</b>
  510. </li>
  511. </ul>
  512. </div>
  513. <!--
  514. 环形图(2026-10-13 维护者反馈:条形图太多易视觉疲劳——「类别占比、类别少」
  515. 场景改用环形图,中心读数合计)。扇区与图例按语义 tone 着色(行业行=行业色)。
  516. 带 `action` 时图例行可点(联动写全局筛选,与条形图同一条接缝)。
  517. 与上方饼图的分工:饼图是点位状态分布专用(选中高亮 + 地图图例同源)。
  518. 票 43(重开)交互修复:可见环带太窄(42/26),真实指针落在中心孔或
  519. 环带外都不触发——「只有悬停图例才出详情」的根因。改画**透明整楔命中区**
  520. (`hitPath`,内半径 0)承接悬停/聚焦,热区与视觉扇区同角度同边界;
  521. 扇区 ⇄ 图例**双向联动**(任一侧悬停/聚焦,两侧同时亮 + 同一个共享浮层),
  522. 手感对齐已验收横条统计(整行即热区)。键盘焦点在命中楔形上,
  523. 可访问名称=「类别:数量」;中心合计 `pointer-events:none` 让位给命中区。
  524. -->
  525. <div
  526. v-else-if="block.kind === 'donut'"
  527. class="panel-donut"
  528. :class="block.layout ? `panel-donut--${block.layout}` : ''"
  529. :data-donut-layout="block.layout || null"
  530. data-donut-list
  531. >
  532. <template v-if="block.total > 0">
  533. <div class="panel-donut__wrap">
  534. <svg class="panel-donut__chart" viewBox="0 0 100 100" role="group" :aria-label="block.label">
  535. <path
  536. v-for="slice in block.slices"
  537. :key="slice.key"
  538. class="panel-donut__slice"
  539. :class="{ 'is-hovered': isChartHovered(chartScope(index, block), slice.key) }"
  540. :d="slice.path"
  541. :fill="barColor(slice.tone)"
  542. :data-donut-slice="slice.key"
  543. aria-hidden="true"
  544. ></path>
  545. <path
  546. v-for="slice in block.slices"
  547. :key="`hit-${slice.key}`"
  548. class="panel-donut__hit"
  549. :d="slice.hitPath"
  550. :data-donut-hit="slice.key"
  551. tabindex="0"
  552. role="img"
  553. :aria-label="`${slice.label}:${slice.count}`"
  554. @mouseenter="showChartTip($event, slice.label, slice.count, barColor(slice.tone)); setChartHover(chartScope(index, block), slice.key)"
  555. @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
  556. @focus="showChartTip($event, slice.label, slice.count, barColor(slice.tone)); setChartHover(chartScope(index, block), slice.key)"
  557. @blur="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
  558. ></path>
  559. </svg>
  560. <b class="panel-donut__total" data-donut-total>{{ block.total }}</b>
  561. </div>
  562. <ul class="panel-donut__legend" data-donut-legend>
  563. <component
  564. :is="block.action && row.actionKey ? 'button' : 'li'"
  565. v-for="row in block.rows"
  566. :key="row.key"
  567. class="panel-donut__row"
  568. :class="{ 'panel-donut__row--button': block.action && row.actionKey, 'is-hovered': isChartHovered(chartScope(index, block), row.key) }"
  569. :type="block.action && row.actionKey ? 'button' : null"
  570. :data-donut-row="row.key"
  571. :data-donut-action="block.action || null"
  572. :title="block.action && row.actionKey ? `把「${row.label}」设为全局筛选条件` : null"
  573. @click="block.action && row.actionKey ? emit('panel-action', { kind: block.action, key: row.actionKey }) : null"
  574. @mouseenter="showChartTip($event, row.label, row.count, barColor(row.tone)); setChartHover(chartScope(index, block), row.key)"
  575. @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), row.key)"
  576. >
  577. <i class="panel-donut__swatch" :style="{ background: barColor(row.tone) }" aria-hidden="true"></i>
  578. <EllipsisText class="panel-donut__label" :text="row.label" />
  579. <b class="panel-donut__count" data-donut-count>{{ row.count }}</b>
  580. </component>
  581. </ul>
  582. </template>
  583. <p v-else class="panel-block__empty" data-donut-empty>该维度当前没有记录</p>
  584. </div>
  585. <!--
  586. 票 14:状态分布**饼图**(决策 02 的 Q12/Q19)。消费全局未筛分布(与顶部
  587. 状态栏共用同一份),只**高亮当前选中**——它不是筛选入口,不可点;
  588. 全局条件唯一可操作面在地图顶部状态栏。
  589. 票 43(重开):与环形图同一修复——透明整楔命中区(`hitPath`)承接
  590. 悬停/聚焦(可见环带太窄是「只有图例触发」的根因),扇区 ⇄ 图例双向
  591. 联动,浮层与放大两侧同步;外层 svg `role="group"` 让名称可达。
  592. -->
  593. <div v-else-if="block.kind === 'pie'" class="panel-pie" data-pie>
  594. <template v-if="block.ready && block.total > 0">
  595. <svg class="panel-pie__chart" viewBox="0 0 100 100" role="group" :aria-label="block.label">
  596. <path
  597. v-for="slice in block.slices"
  598. :key="slice.key"
  599. class="panel-pie__slice"
  600. :class="{ 'is-dim': !slice.selected && block.hasSelection, 'is-hovered': isChartHovered(chartScope(index, block), slice.key) }"
  601. :d="slice.path"
  602. :fill="pieToneColor(slice.tone, slice.key)"
  603. :data-pie-slice="slice.key"
  604. :data-pie-selected="slice.selected ? 'yes' : 'no'"
  605. aria-hidden="true"
  606. ></path>
  607. <path
  608. v-for="slice in block.slices"
  609. :key="`hit-${slice.key}`"
  610. class="panel-pie__hit"
  611. :d="slice.hitPath"
  612. :data-pie-hit="slice.key"
  613. tabindex="0"
  614. role="img"
  615. :aria-label="`${slice.label}:${slice.count}`"
  616. @mouseenter="showChartTip($event, slice.label, slice.count, pieToneColor(slice.tone, slice.key)); setChartHover(chartScope(index, block), slice.key)"
  617. @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
  618. @focus="showChartTip($event, slice.label, slice.count, pieToneColor(slice.tone, slice.key)); setChartHover(chartScope(index, block), slice.key)"
  619. @blur="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
  620. ></path>
  621. </svg>
  622. <ul class="panel-pie__legend" data-pie-legend>
  623. <li
  624. v-for="row in block.rows"
  625. :key="row.key"
  626. class="panel-pie__row"
  627. :class="{ 'is-selected': row.selected, 'is-hovered': isChartHovered(chartScope(index, block), row.key) }"
  628. :data-pie-row="row.key"
  629. :data-pie-selected="row.selected ? 'yes' : 'no'"
  630. @mouseenter="showChartTip($event, row.label, row.count, pieToneColor(row.tone, row.key)); setChartHover(chartScope(index, block), row.key)"
  631. @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), row.key)"
  632. >
  633. <!--
  634. 票 39 二轮(维护者走查反馈):图例行与一张图顶部状态栏**同一形态**——
  635. `legendIconFor` 单源图标 + 文字(StatusTag 传 icon 就只画图标、不画形状,
  636. 票 46 返工定稿的同一条接缝)。颜色不再走 pieToneColor 这份第二映射:
  637. StatusTag 的 tone 类解析出完全相同的 token,顶部图例也是这么取色的。
  638. -->
  639. <StatusTag :tone="row.tone" :label="row.label" :icon="row.icon || ''" />
  640. <b class="panel-pie__count" data-pie-count>{{ row.count }}</b>
  641. </li>
  642. </ul>
  643. </template>
  644. <!-- 全局分布未取到:如实说明(不伪造全零饼图,票 05 的纪律)。 -->
  645. <p v-else-if="block.note" class="panel-block__empty" data-pie-note>{{ block.note }}</p>
  646. <!-- 取到了但全为 0:这是真实值,不是缺失。 -->
  647. <p v-else class="panel-block__empty" data-pie-empty>当前范围内没有监测点与未关联设备</p>
  648. </div>
  649. <!-- 键值行 -->
  650. <dl v-else-if="block.kind === 'rows'" class="panel-rows" data-row-list>
  651. <div v-for="row in block.rows" :key="row.key" class="panel-row" :data-row="row.key">
  652. <dt>{{ row.label }}</dt>
  653. <dd :class="{ 'is-muted': isPlaceholder(row.value) }">{{ row.value }}</dd>
  654. </div>
  655. </dl>
  656. <!--
  657. 条目清单:两种形态共用这一块 —— 普通条目(带可定位点位)与**台账卡片**
  658. (整张卡可点,走与地图相同的两级详情)。判据是行上有没有 `code`。
  659. 票 40:清单卡升级为「事件卡片」形态(样式参考当前预警面板的事件卡,
  660. 维护者 2026-10-16:卡片高度不需要那么高,主要参考设计及交互效果)。
  661. 整卡可点:有 `code` 的卡(台账监测点)打开两级详情;没有 `code` 的卡
  662. (运行概况异常设备)走既有 `locate` 接缝 —— 诚实的三种「点不动」反馈
  663. + 设备级详情,不新造第二条路径。卡内按钮自己 `.stop`,不二次触发。
  664. 票 64:行数由 `windowOf(block)` 给 —— domain 声明 `paginate` 的块
  665. 每页 ≤10(统一分页规范),其余块交出原数组,画法与行为一字不改。
  666. -->
  667. <div v-else-if="block.kind === 'list'" class="panel-list" data-item-list>
  668. <article
  669. v-for="row in windowOf(block).rows"
  670. :key="row.key"
  671. class="panel-item"
  672. :class="{ 'panel-item--clickable': row.code || (row.points && row.points.length) }"
  673. :data-item="row.key"
  674. :data-ledger-card="row.code || null"
  675. @click="onCardClick(row, $event)"
  676. >
  677. <div class="panel-item__head">
  678. <!-- 有编码的行整张卡可点(台账);没有编码的只有点位按钮可点(票 17 的形态)。
  679. 票 37:标题与下面三行说明一样走**共用省略方法**(越界省略 + hover 读全)。 -->
  680. <EllipsisText
  681. v-if="row.code"
  682. tag="button"
  683. type="button"
  684. class="panel-item__title panel-item__title--button"
  685. :text="row.name"
  686. hint="点击打开两级详情"
  687. @click.stop="emit('open-card', row.code)"
  688. />
  689. <EllipsisText v-else class="panel-item__title" :text="row.title" />
  690. <!-- 色键必带文字(票 06:标签不能只靠颜色);文案由 domain 给 -->
  691. <StatusTag v-if="row.tone && row.toneLabel" :tone="row.tone" :label="row.toneLabel" />
  692. </div>
  693. <EllipsisText v-if="row.meta && row.meta.length" tag="p" class="panel-item__meta" :text="row.meta.join(' · ')" />
  694. <!-- data-data-time 是走查断言面(票 15 的 t15 闸门按源码钉住 `data-data-time>数据时间` 这个形状,
  695. 属性与正文必须同行);文本走插槽,title 仍拿完整原文(票 37 的共用省略方法)。 -->
  696. <EllipsisText v-if="row.dataTime" tag="p" class="panel-item__meta" :text="`数据时间 ${row.dataTime}`" data-data-time>数据时间 {{ row.dataTime }}</EllipsisText>
  697. <EllipsisText v-if="row.reason" tag="p" class="panel-item__meta panel-item__reason" data-ledger-reason :text="row.reason" />
  698. <!-- 徽标(票 18 的台账 N+1):取到才画,没取到显示"取数中/未取到",**不画 0** -->
  699. <p v-if="row.badge" class="panel-item__badge" data-ledger-badge>
  700. 设备 {{ row.badge.total }} · 报警 {{ row.badge.alarm }} · 预警 {{ row.badge.warning }} · 离线 {{ row.badge.offline }}
  701. </p>
  702. <p v-else-if="row.badgePending" class="panel-item__badge is-muted" data-ledger-badge-pending>设备统计取数中…</p>
  703. <div v-if="row.points && row.points.length" class="panel-item__points">
  704. <button
  705. v-for="point in row.points"
  706. :key="point.key"
  707. type="button"
  708. class="panel-locate"
  709. :data-locate-point="point.pointCode"
  710. :title="`在地图上定位 ${point.label || point.pointCode}`"
  711. @click.stop="emit('locate', point.pointCode)"
  712. >
  713. 定位 {{ point.pointCode }}
  714. </button>
  715. </div>
  716. </article>
  717. <!--
  718. 该段自己的成败:后端 `UNAVAILABLE`(没取到)与 `EMPTY`(确实没有)分得开。
  719. 它**优先于**下面的空列表提示,且不挂在 `v-else-if` 上 ——
  720. 原来挂在空列表之后时,两个分支的条件在本契约里互斥
  721. (有 stateText 就必然没有 rows),那个分支永远不执行,是条死代码。
  722. -->
  723. <p v-if="block.stateText" class="panel-block__empty" data-item-state>{{ block.stateText }}</p>
  724. <p v-else-if="!windowOf(block).total" class="panel-block__empty" data-item-empty>
  725. {{ block.emptyText }}
  726. </p>
  727. <!--
  728. 票 64:统一分页控件(共享 PagerNav,规范条文 2/3/4/9 + 跳页节)。
  729. 只有 domain 声明 `paginate`(前端集合分页)或 `serverPage`
  730. (服务端页码分页,行已是后端当前页)的块才画;一页不画(条文 4,
  731. 由控件自己按 pageCount>1 收口)。总数取**过滤前**集合长度或后端
  732. `pagination.total`,不由当前页反推(条文 7)。
  733. -->
  734. <PagerNav
  735. v-if="block.paginate === true || block.serverPage"
  736. :page="windowOf(block).page"
  737. :page-count="windowOf(block).pageCount"
  738. :total="windowOf(block).total"
  739. :list-name="block.label"
  740. @page="(target) => gotoListPage(block, target)"
  741. />
  742. <!--
  743. 票 61:翻页区的**如实口径说明**(多窗合并范围「已合并 N 个时间窗 ·
  744. 已加载 M 条」)。它是口径不是控件——一页不到、控件不画时也要出现,
  745. 所以不挂在 PagerNav 的 v-if 里。
  746. -->
  747. <p v-if="block.pageNote" class="panel-block__page-note" data-list-page-note>{{ block.pageNote }}</p>
  748. </div>
  749. </section>
  750. <!--
  751. 票 62:「加载下一页监测点」按钮与 `ledger-more` 接缝已退役 ——
  752. 台账装载改**自动渐进链**(与一张图点位数据同款纪律),界面只对已装载集合
  753. 做页码分页;「还有没有下一批」由页脚 `pageNote` 如实说明,不再靠一个按钮。
  754. -->
  755. <!-- 字段缺失:只说条数,路径进 tooltip(票 11 的立场) -->
  756. <p
  757. v-if="view.missing.count"
  758. class="panel-blocks__missing"
  759. data-panel-missing
  760. :data-missing-count="view.missing.count"
  761. :title="view.missing.tooltip"
  762. >
  763. {{ view.missing.text }}
  764. </p>
  765. </template>
  766. <!-- 没内容:如实说(失败/空/不可用各有措辞,来自 domain) -->
  767. <p v-else class="panel-blocks__placeholder" data-panel-placeholder>{{ view.placeholder }}</p>
  768. <!-- 图表 hover 详情浮层(票 37 走查扩展):锚在悬停元素上方,pointer-events:none 不挡交互 -->
  769. <div
  770. v-if="chartTip"
  771. class="chart-tip"
  772. :style="{ left: `${chartTip.x}px`, top: `${chartTip.y}px`, '--tip-color': chartTip.color }"
  773. data-chart-tip
  774. >
  775. <i class="chart-tip__swatch" aria-hidden="true"></i>
  776. <span class="chart-tip__label">{{ chartTip.label }}</span>
  777. <b class="chart-tip__value">{{ chartTip.value }}</b>
  778. </div>
  779. </div>
  780. </template>
  781. <script setup>
  782. import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
  783. import { DETAIL_PLACEHOLDER } from '../domain/detailFlow.js'
  784. import { sectionPending } from '../domain/panelBlocks.js'
  785. import { pagedRows, readListPage, resetListPages, writeListPage } from '../domain/pagination.js'
  786. import CockpitIcon from './CockpitIcon.vue'
  787. import EllipsisText from './EllipsisText.vue'
  788. import PagerNav from './PagerNav.vue'
  789. import StatusTag from './StatusTag.vue'
  790. const props = defineProps({
  791. /**
  792. * 算好的视图计划(来自 `domain/panelViews.js` 的 `panelView()`)。
  793. * 组件**不认识 panelId**:分发归注册表,画法归 `block.kind`。
  794. */
  795. view: { type: Object, required: true },
  796. })
  797. // 票 62:`ledger-more` 已退役(台账装载是自动链,界面不再有「加载下一页」入口)。
  798. const emit = defineEmits(['locate', 'open-card', 'badge-needed', 'panel-action', 'list-page'])
  799. /**
  800. * 票 40:整卡可点(交互参考「当前预警」的事件卡片)。
  801. * · 有 `code` 的卡(台账监测点)→ 打开两级详情(与标题按钮同一出口,不新造路径);
  802. * · 没有 `code` 的卡(运行概况异常设备)→ 走既有 `locate` 接缝:诚实的三种
  803. * 「点不动」反馈 + 设备级详情(`monitoringStore.locateCode` 的既有分流)。
  804. * 卡内按钮已 `.stop`,走到这里的就是卡片空白处的点击;再兜一层 closest('button')
  805. * 防将来有人忘了加 stop —— 双保险便宜,误触发的代价(飞镜头)不便宜。
  806. */
  807. function onCardClick(row, event) {
  808. if (event && event.target && typeof event.target.closest === 'function' && event.target.closest('button')) return
  809. if (row.code) {
  810. emit('open-card', row.code)
  811. return
  812. }
  813. const point = (row.points || [])[0]
  814. if (point) emit('locate', point.pointCode)
  815. }
  816. /** 还没取过数(界面说"正在取数",不说"没有数据")。 */
  817. const pending = computed(() => sectionPending(props.view.section))
  818. // 票 56(第二轮):所有面板顶部的摘要说明统一折叠,**只有点击**才展开/收起。
  819. // 只有一份状态(`summaryOpen`):hover / focus 都不碰它,所以鼠标掠过不会弹开、
  820. // 移开也不会把点击展开的内容收掉。展开态是纯展示状态,不进 store(面板私有、即挂即弃)。
  821. const summaryOpen = ref(false)
  822. const summaryDetailsId = computed(() => `panel-summary-details-${props.view.name || 'default'}`)
  823. /**
  824. * 折叠进去的摘要条目。三条纪律:
  825. * · **措辞与有无都由 domain 决定**,渲染层只按字段是否存在决定画不画,不重写、不合成;
  826. * · 每条**保留原有的 `data-*` 断言面**(走查脚本 t17/t18/t20 读的还是同一个选择器与同一句话)。
  827. * `v-show` 折叠时元素仍在 DOM 里,文本读得到 —— 折叠只改呈现,不改信息;
  828. * · 本表是顶部口径说明的**唯一**消费点([t56 闸门](../../.scratch/cockpit-real-data-wayfinding/verify/t56-panel-summary-disclosure.mjs)
  829. * E 组按 domain 声明双向锁定:domain 新增 `*Note`/`gap`/`note` 而不进这张表 → 判红;表内字段在 domain 已无声明 → 判红)。
  830. *
  831. * 后三条(`excludedNote` / `linkNote` / `note`)是第三轮补的:它们和前七条是同一件事,
  832. * 但渲染层**从来没有消费点**——「故障口径」「两族口径」「联动出口」「口径剔除」这些必须
  833. * 让值班人员看见的说明实际是隐形的。补法与票 49/50 的裁定一致:与其逐条发现,
  834. * 不如把完备性做成机制。
  835. */
  836. const SUMMARY_SOURCES = [
  837. { key: 'industry', field: 'industryNote', hook: 'data-panel-industry' },
  838. { key: 'type', field: 'typeNote', hook: 'data-panel-type-note' },
  839. { key: 'event', field: 'eventNote', hook: 'data-panel-event-note' },
  840. { key: 'range', field: 'rangeNote', hook: 'data-panel-range-note' },
  841. { key: 'gap', field: 'gap', hook: 'data-panel-gap', isGap: true },
  842. { key: 'offline', field: 'offlineNote', hook: 'data-panel-offline-note' },
  843. { key: 'page', field: 'pageNote', hook: 'data-panel-page-note' },
  844. { key: 'locate', field: 'locateNote', hook: 'data-locate-note' },
  845. { key: 'excluded', field: 'excludedNote', hook: 'data-panel-excluded-note' },
  846. { key: 'link', field: 'linkNote', hook: 'data-panel-link-note' },
  847. { key: 'note', field: 'note', hook: 'data-panel-note' },
  848. // 票 64:运行信息的数据来源与口径说明(设施实体适配器逐条生成、一条指标对应一个设施、
  849. // 不跨行业合并)。表是顶部说明的唯一消费点——domain 新增说明字段而不进这张表即判红
  850. // (t56 C 组的双向普查)。
  851. { key: 'source', field: 'sourceNote', hook: 'data-panel-source-note' },
  852. ]
  853. const summaryItems = computed(() =>
  854. SUMMARY_SOURCES.flatMap(({ key, field, hook, isGap }) => {
  855. const text = props.view[field]
  856. if (!text) return []
  857. return [{ key, text, isGap: Boolean(isGap), attrs: { [hook]: '', 'data-summary': key } }]
  858. }),
  859. )
  860. /**
  861. * 折叠态也要说得出「有没有要关注的东西」——只留标题会把口径缺口读成「一切正常」。
  862. * 判据是 domain 的措辞本身(`'当前窗口没有离线设备'` 是 `monitoringPanels.js` 给的无离线句),
  863. * 渲染层不自己算任何数。`note` / `gap` / `excludedNote` 同属口径说明一档。
  864. */
  865. const summaryStatus = computed(() => {
  866. const view = props.view
  867. if (view.offlineNote && view.offlineNote !== '当前窗口没有离线设备') return '有离线设备'
  868. if (view.gap || view.note || view.excludedNote) return '有口径提示'
  869. return `${summaryItems.value.length} 条说明`
  870. })
  871. /**
  872. * 票 38:维度切换的**组件本地状态**(键=面板名 + 块标题,值=当前维度键)。
  873. * 为什么放组件而不是 store:「换哪个维度看」不是全局条件(与 industry/district/deviceType
  874. * 不同层,切模式/刷新都不该动它);两个维度的行 domain 已经算完,这里只决定显示哪一份。
  875. * 键不用块对象:视图计划每次重算都生成新块对象,按对象记会被刷新顶掉(表现为「切了又跳回」)。
  876. */
  877. const dimensionOverrides = ref({})
  878. function blockKey(block) {
  879. return `${props.view.name}|${block.label}`
  880. }
  881. // ---- 票 64:list 块的统一分页(运行指标列表实测 6772 条,全量直渲=一屏几千张卡)----
  882. //
  883. // 四条纪律:
  884. // · **只有 domain 声明 `paginate` 的块切窗口**:同一个 list 画法服务全仓多处清单,
  885. // 管理信息明细(实测 9 行)与预警分析重点识别是规范排除项,应急保障另有专票(63),
  886. // 渲染层不猜、不顺手全翻;未声明的块 `windowOf` 原样交出 `block.rows`(同一引用、零 slice)。
  887. // · **翻页算术只有一份**(条文 11):窗口、越界夹回、页码寻址与复位全在
  888. // `domain/pagination.js`(`listPageKey`/`readListPage`/`writeListPage`/`resetListPages`
  889. // 共用同一个键函数——读写键不一致就会造出「该复位的没复位」这类最难查的漂移),
  890. // 控件只有 `PagerNav.vue` 一份;本组件不出现 ceil/trunc/rows.slice/自画 pager。
  891. // · **复位=显式清槽**(条文 5/6):domain 给每块一个 `pageScope`(选中行业 + 片区),
  892. // 它变了就清掉那一块的页码槽;定时刷新时它不变 ⇒ 页码保持。
  893. // 第一版把条件编进页码键,被条文 6 判住:切走再切回会读到旧槽位停在第 3 页,
  894. // 而「切回来」同样是一次条件变化。清槽与票 46/59「换设备清页码」同一条成例。
  895. // · **数据变少越界由 `pagedRows` 夹回**(条文 6),界面上的页码即夹回后的值。
  896. const listPages = ref({})
  897. /**
  898. * 分页块的条件快照(`{ [块归属键]: pageScope }`)。只收分页块——非分页块没有页码,
  899. * 也就没有可复位的槽;把它们放进来会让这张表每次刷新都变,白清一次别的块。
  900. */
  901. const listScopeSnapshot = computed(() => {
  902. const snapshot = {}
  903. for (const block of props.view.blocks || []) {
  904. if (block.paginate === true) snapshot[blockKey(block)] = block.pageScope || ''
  905. }
  906. return snapshot
  907. })
  908. // 哪一块的条件变了,就只清那一块的页码(别的面板/别的块不受牵连)。
  909. // 只按**值**逐块比较:computed 每次重算都是新对象,按对象身份比会把每次刷新都当成换筛选。
  910. watch(listScopeSnapshot, (next, prev) => {
  911. const before = prev || {}
  912. const changed = Object.keys(next).filter((key) => before[key] !== next[key])
  913. if (changed.length) listPages.value = resetListPages(listPages.value, changed)
  914. })
  915. /** 这一块当前生效的页码(没记过=第 1 页)。 */
  916. function pageOf(block) {
  917. return readListPage(listPages.value, blockKey(block))
  918. }
  919. /**
  920. * 这一块要画几条:分页块交窗口行,服务端分页块交**后端当前页**,其余块交全部行
  921. * (引用不变,既有渲染零影响)。两种分页形态共用同一个 PagerNav,只是页码来源不同:
  922. * · `paginate`(前端集合):渲染层自己的 `listPages` 切窗口;
  923. * · `serverPage`(服务端页码,票 61):行已是后端当前页,页码/总数取后端回显,
  924. * 翻页 emit `list-page` 交给 store 发请求。
  925. */
  926. function windowOf(block) {
  927. const rows = block.rows || []
  928. if (block.serverPage) {
  929. const page = Number(block.serverPage.page) || 1
  930. const pageCount = Number(block.serverPage.pageCount) || 1
  931. const total = Number.isFinite(Number(block.serverPage.total)) ? Number(block.serverPage.total) : rows.length
  932. return { rows, page, pageCount: Math.max(1, pageCount), total }
  933. }
  934. if (block.paginate !== true) return { rows, page: 1, pageCount: 1, total: rows.length }
  935. return pagedRows(rows, pageOf(block))
  936. }
  937. /**
  938. * 翻页唯一出口(与票 59 同一条):前后页/页码直达/跳页都带**目标页码(1 起)**回来。
  939. * 前端集合分页只存状态(越界由 domain `pagedRows` 在下一帧夹回);服务端分页把目标页
  940. * 交给父组件/store 发请求(页码状态在 store,块里不存第二份)。
  941. */
  942. function gotoListPage(block, target) {
  943. if (block.serverPage) {
  944. emit('list-page', { label: block.label, page: Number(target) || 1 })
  945. return
  946. }
  947. listPages.value = writeListPage(listPages.value, blockKey(block), target)
  948. }
  949. function activeDimension(block) {
  950. const override = dimensionOverrides.value[blockKey(block)]
  951. if (override && (block.dimensions || []).some((dim) => dim.key === override)) return override
  952. return block.active || (block.dimensions && block.dimensions[0] ? block.dimensions[0].key : '')
  953. }
  954. function currentDim(block) {
  955. const key = activeDimension(block)
  956. return (block.dimensions || []).find((dim) => dim.key === key) || null
  957. }
  958. function setDimension(block, key) {
  959. dimensionOverrides.value = { ...dimensionOverrides.value, [blockKey(block)]: key }
  960. }
  961. function isPlaceholder(text) {
  962. return text === DETAIL_PLACEHOLDER
  963. }
  964. /**
  965. * 一条 KPI 读数的**完整原文**(值 + 单位),供共用省略方法当 `title` 用(票 37)。
  966. * 显示仍走模板里的子元素(单位是独立的 `<em>`,有自己的弱化样式),
  967. * 但 title 取的是**整条读数**——所以窄列里被省略时,hover 读到的是"12 台"而不是"12"。
  968. *
  969. * 票 54:domain 可给 `full` 声明「屏上短读、原文另有其值」(数据时间的短读法 + 完整 ISO)。
  970. * 渲染层只认这个通用字段,不再按 `item.key` 特判哪个读数是时间(票 38 立的同一条纪律)。
  971. */
  972. function kpiReading(item) {
  973. const unit = item.unit && !isPlaceholder(item.value) ? ` ${item.unit}` : ''
  974. return `${item.full || item.value}${unit}`
  975. }
  976. /**
  977. * 票 14:饼图扇区的填充色。色键 → token CSS 变量(色值只在 tokens.css 声明一份,
  978. * 票 06 的单一口径;`var()` 在 SVG 属性里同样生效)。
  979. */
  980. function pieToneColor(tone, key) {
  981. const stateColors = {
  982. OFFLINE: 'var(--point-offline)',
  983. EMPTY: 'var(--point-empty)',
  984. UNKNOWN: 'var(--point-unknown)',
  985. UNASSOCIATED: 'var(--point-unassociated)',
  986. }
  987. if (stateColors[key]) return stateColors[key]
  988. const map = {
  989. lv1: 'var(--lv1, #ff3b52)',
  990. lv2: 'var(--lv2, #ffb020)',
  991. lv3: 'var(--lv3, #01abea)',
  992. lv4: 'var(--lv4, #27d17a)',
  993. }
  994. return map[tone] || 'var(--neutral, #5680b0)'
  995. }
  996. // ---- 图表 hover 交互(2026-10-14 维护者走查要求:悬停放大 + 数据详情浮层)----
  997. //
  998. // 三种图(堆叠条分段 / 折线数据点 / 饼·环形扇区)共用**一个**浮层状态与同一个
  999. // `.chart-tip` 浮层——「悬停放大」由各图自己的 CSS `:hover` / `.is-hovered` 完成,
  1000. // 这里只负责「详情显示什么、浮在哪」。为什么不用原生 `title`:原生提示有 ~1s 延迟、
  1001. // 样式不可控且与「放大」反馈不同步;面板内自绘浮层与 hover 是同一帧的。
  1002. // 纯展示层状态:不进 store(面板私有、即挂即弃,不构成全局条件)。
  1003. /** 当前悬停的图表详情(`{ label, value, color, x, y }`;null = 无悬停)。 */
  1004. const chartTip = ref(null)
  1005. /**
  1006. * 票 43(第三轮根因):联动状态必须用 **shallowRef**,不能用 `ref`。
  1007. * `ref(null)` 存入 `{owner: block}` 后会被 Vue **深层响应式代理**——读回的
  1008. * `chartHover.value.owner` 是 block 的 reactive 代理,而模板 v-for 拿到的 `block`
  1009. * 是原始对象,`===` 恒 false → `.is-hovered` 永不生效(扇区不放大、图例不亮),
  1010. * 而浮层不依赖这个比较所以照常出现——这正是维护者看到的「只有详情、没有联动」。
  1011. * `shallowRef` 不做深层包装(整体替换即触发渲染)。
  1012. * (第四轮进一步发现:光保持身份还不够,见下方 `chartScope`——键根本不该是对象。)
  1013. */
  1014. const chartHover = shallowRef(null)
  1015. /**
  1016. * 票 43(第四轮根因):**不能拿对象身份当键**。外壳时钟每秒 `clockText` 变化会重渲染,
  1017. * 而 `:view="panelViewOf(panelId)"` 是模板里的**方法调用**——每次重渲染都重算出一批全新的
  1018. * block 对象。于是鼠标没动、hover 还在,下一次 tick 的 `block` 已经不是存进去的那个,
  1019. * `owner === block` 静默变 false → 放大与联动高亮"自己消失"(浮层存的是标量所以不受影响,
  1020. * 症状正是「详情还在、高亮掉了」)。
  1021. * 修法=键换成**跨重算稳定的标量 scope**:视图名 + 块序号 + 块类型 + 块标题。
  1022. * 这四样在同一面板同一块的数据刷新前后不变,既不会因重渲染漂移,
  1023. * 也仍区分并排的同类图(视图名/序号不同)与同块内不同行(key 不同)。
  1024. */
  1025. function chartScope(index, block) {
  1026. return `${props.view.name}|${index}|${block.kind}|${block.label || ''}`
  1027. }
  1028. function setChartHover(scope, key) {
  1029. chartHover.value = { scope, key }
  1030. }
  1031. function clearChartHover(scope, key) {
  1032. const current = chartHover.value
  1033. if (current && current.scope === scope && current.key === key) {
  1034. chartHover.value = null
  1035. }
  1036. }
  1037. function isChartHovered(scope, key) {
  1038. const current = chartHover.value
  1039. return !!current && current.scope === scope && current.key === key
  1040. }
  1041. /** 图表元素悬停 → 记录详情与浮层锚点(相对 `.panel-blocks` 根,避免随滚动漂移)。 */
  1042. function showChartTip(event, label, value, color) {
  1043. const root = rootRef.value
  1044. if (!root || !event.currentTarget) return
  1045. const rect = root.getBoundingClientRect()
  1046. const host = event.currentTarget.getBoundingClientRect()
  1047. const x = host.left + host.width / 2 - rect.left
  1048. const y = host.top - rect.top
  1049. // 面板底部/顶部被滚动裁切时,把浮层移到元素下方,避免悬停详情跑出可视区域。
  1050. const top = Math.max(8, y)
  1051. chartTip.value = {
  1052. label,
  1053. value,
  1054. color,
  1055. x,
  1056. y: top,
  1057. }
  1058. }
  1059. /** 离开图表元素:收起浮层(除非已移入浮层本身——浮层 pointer-events:none,不需要)。 */
  1060. function hideChartTip() {
  1061. chartTip.value = null
  1062. }
  1063. /**
  1064. * 分类色键 → CSS 颜色(2026-10-13 维护者要求:图表用行业语义色区分类别)。
  1065. * `industry:*` → 行业 token(与行业切图/地图标记同源);`lv*`/`neutral` → 语义 token;
  1066. * `chart:N` → 图表色序 token。空键回主色(无语义的行仍可辨)。
  1067. */
  1068. const BAR_TONE_COLORS = {
  1069. 'industry:gas': 'var(--industry-gas, #fe3b53)',
  1070. 'industry:water': 'var(--industry-water, #01abea)',
  1071. 'industry:drainage': 'var(--industry-drainage, #27d17a)',
  1072. 'industry:manhole': 'var(--industry-manhole, #a970ff)',
  1073. lv1: 'var(--lv1, #ff3b52)',
  1074. lv2: 'var(--lv2, #ffb020)',
  1075. lv3: 'var(--lv3, #01abea)',
  1076. lv4: 'var(--lv4, #27d17a)',
  1077. neutral: 'var(--neutral, #5680b0)',
  1078. 'point-offline': 'var(--point-offline)',
  1079. 'point-empty': 'var(--point-empty)',
  1080. 'point-unknown': 'var(--point-unknown)',
  1081. 'point-unassociated': 'var(--point-unassociated)',
  1082. 'chart:1': 'var(--chart-c1, #01abea)',
  1083. 'chart:2': 'var(--chart-c2, #00b8ff)',
  1084. 'chart:3': 'var(--chart-c3, #33b3ed)',
  1085. 'chart:4': 'var(--chart-c4, #14c2ff)',
  1086. 'chart:5': 'var(--chart-c5, #27d17a)',
  1087. 'chart:6': 'var(--chart-c6, #ffb020)',
  1088. 'chart:7': 'var(--chart-c7, #a970ff)',
  1089. 'chart:8': 'var(--chart-c8, #5680b0)',
  1090. }
  1091. /**
  1092. * 多序列折线里 X 轴标签的横向位置(%)。
  1093. *
  1094. * 单序列时几何在 `block.linePoints[i].xPct`;多序列时每条线各有一份点列,
  1095. * 但它们共用同一组日期 —— 取第一条序列的第 i 个点即可(不重复计算、也不假设两条线等长)。
  1096. */
  1097. function series_xPct(block, index) {
  1098. const series = (block.series || [])[0]
  1099. if (!series || !series.points || !series.points[index]) return 0
  1100. return series.points[index].xPct
  1101. }
  1102. function barColor(tone) {
  1103. return BAR_TONE_COLORS[tone] || 'var(--cyan, #01abea)'
  1104. }
  1105. /**
  1106. * 堆叠条分段色:语义 tone 优先;该语义在块内**重复出现**时(如工单四个待办态都是 lv3)
  1107. * 用 shade 深浅区分——语义色与图表色按 shade 插值,行间可辨。纯函数(toneShared 由 domain 判定)。
  1108. */
  1109. function stackedColor(row) {
  1110. const base = barColor(row.tone)
  1111. if (!row.toneShared) return base
  1112. return `color-mix(in srgb, ${base} ${Math.round(100 - (row.shade ?? 0.5) * 55)}%, var(--chart-c2, #00b8ff))`
  1113. }
  1114. // ---- 台账卡片的设备徽标:**卡片进视口才要**(票 18 的 Q14 裁定;票 62 收窄到当前窗口)----
  1115. //
  1116. // 为什么必须是"进视口才拉"而不是"一页出来就全拉":台账的分页窗口横跨「监测点 + 未关联设备」,
  1117. // 一页可能几十张卡,而用户常常只看最上面几张;更关键的是**逐点位一次请求**(本端点不给设备编码,
  1118. // 见 `domain/monitoringPanels.js` 的说明),无脑全拉会在一屏之内打出一串并发请求。
  1119. //
  1120. // 票 62:台账接上统一页码分页后,**DOM 里只有当前页窗口的卡片**(≤10 张),
  1121. // 于是"进视口才拉"自然只覆盖当前窗口 —— 翻到第 2 页才会为第 11–20 张要徽标。
  1122. // 这比旧形态(一页 500 张卡都进观察集)负载低一个量级,是票面 Acceptance 第 3 条要的形态。
  1123. //
  1124. // 组件不自己取数(那一层在 store)——它只回答「这张卡露出来了」,
  1125. // 由外壳转给 `monitoringStore.loadLedgerBadge()`。请求过的点位由 store 记账,不会重复要。
  1126. //
  1127. // 注意 ref 挂在**根元素**上而不是那个 `v-for` 里的 `div`:`v-for` 上的 ref 在 Vue 3 里
  1128. // 收回来的是**数组**(每个面板实例一个元素),`listRef.value.querySelectorAll` 会直接抛
  1129. // "not a function" —— 而且抛在 `onMounted` 里会把整页的后续挂载打断
  1130. // (真机走查第一轮就是这样:一个组件报错,A8b/D1/D2/D3 一起红)。
  1131. const rootRef = ref(null)
  1132. let observer = null
  1133. /** 已经交给外面要过徽标的点位(组件这侧也记一份,避免 observer 反复触发同一张卡)。 */
  1134. const requested = new Set()
  1135. function observeCards() {
  1136. if (typeof IntersectionObserver === 'undefined' || !rootRef.value) return
  1137. for (const card of rootRef.value.querySelectorAll('[data-ledger-card]')) {
  1138. const code = card.dataset.ledgerCard || ''
  1139. if (!code || requested.has(code)) continue
  1140. requested.add(code)
  1141. observer.observe(card)
  1142. }
  1143. }
  1144. onMounted(() => {
  1145. if (typeof IntersectionObserver === 'undefined') {
  1146. // 没有 IntersectionObserver 时**退化为全要**:徽标是可选增强,不该因此整块不显示。
  1147. // 票 62:只覆盖**当前页窗口**的行(DOM 窗口与视图窗口同一份 `windowOf`)。
  1148. for (const block of props.view.blocks || []) {
  1149. if (block.kind !== 'list') continue
  1150. for (const row of windowOf(block).rows) {
  1151. if (row.code && !requested.has(row.code)) {
  1152. requested.add(row.code)
  1153. emit('badge-needed', row.code)
  1154. }
  1155. }
  1156. }
  1157. return
  1158. }
  1159. observer = new IntersectionObserver(
  1160. (entries) => {
  1161. for (const entry of entries) {
  1162. if (!entry.isIntersecting) continue
  1163. const code = (entry.target.dataset && entry.target.dataset.ledgerCard) || ''
  1164. // 要过就不再要(store 也会挡一道,这里是省一次事件转发)。
  1165. if (code) emit('badge-needed', code)
  1166. observer.unobserve(entry.target)
  1167. }
  1168. },
  1169. // 提前 80px 触发:等卡片真正露出来再拉,用户会先看到"取数中"再看到数字。
  1170. { rootMargin: '80px 0px' },
  1171. )
  1172. observeCards()
  1173. })
  1174. // 分页追加、或换筛选后重渲染:新卡片要补进观察集。
  1175. watch(
  1176. () => props.view.blocks,
  1177. () => observeCards(),
  1178. { deep: false },
  1179. )
  1180. onBeforeUnmount(() => {
  1181. if (observer) {
  1182. observer.disconnect()
  1183. observer = null
  1184. }
  1185. requested.clear()
  1186. })
  1187. </script>
  1188. <style scoped>
  1189. .panel-summary-disclosure {
  1190. position: relative;
  1191. /* 头部是 flex-wrap 容器:折叠入口独占一行(`flex-basis: 100%`),长句才能在栏宽内换行,
  1192. 不被 max-content 顶出横向溢出(票 13 的「右侧被裁」是同一类缺陷)。 */
  1193. flex: 1 1 100%;
  1194. min-width: 0;
  1195. }
  1196. .panel-summary-disclosure__toggle {
  1197. display: inline-flex;
  1198. align-items: center;
  1199. gap: calc(5 * var(--u, 1px));
  1200. min-height: calc(24 * var(--u, 1px));
  1201. padding: calc(2 * var(--u, 1px)) calc(7 * var(--u, 1px));
  1202. border: 1px solid rgba(1, 171, 234, 0.38);
  1203. border-radius: 3px;
  1204. background: rgba(0, 20, 72, 0.58);
  1205. color: var(--text-2, #88b7e8);
  1206. font: inherit;
  1207. font-size: var(--fs-caption, 10.5px);
  1208. cursor: pointer;
  1209. }
  1210. .panel-summary-disclosure__toggle:hover,
  1211. .panel-summary-disclosure__toggle:focus-visible {
  1212. border-color: var(--cyan-hi, #00b8ff);
  1213. color: var(--text-strong, #fff);
  1214. outline: none;
  1215. }
  1216. .panel-summary-disclosure__toggle b { color: var(--text, #d7ecff); font-weight: 700; }
  1217. .panel-summary-disclosure__toggle i { color: var(--muted, #5680b0); font-style: normal; }
  1218. .panel-summary-disclosure__details {
  1219. display: grid;
  1220. gap: calc(3 * var(--u, 1px));
  1221. margin-top: calc(4 * var(--u, 1px));
  1222. padding: calc(4 * var(--u, 1px)) calc(7 * var(--u, 1px));
  1223. border-left: 2px solid var(--cyan, #01abea);
  1224. background: rgba(0, 20, 72, 0.42);
  1225. }
  1226. /* 展开区内每条摘要自己收口:等宽长串(统计窗口的 ISO 时间)按字符断行,不溢出面板。 */
  1227. .panel-summary-disclosure__details > span {
  1228. min-width: 0;
  1229. overflow-wrap: anywhere;
  1230. }
  1231. .panel-summary-disclosure__details > .panel-blocks__gap {
  1232. margin: 0;
  1233. }
  1234. .panel-blocks,
  1235. .panel-blocks *,
  1236. .panel-blocks *::before,
  1237. .panel-blocks *::after {
  1238. box-sizing: border-box;
  1239. }
  1240. .panel-blocks {
  1241. /* 图表 hover 详情浮层(.chart-tip)的定位基准:坐标按本根元素算。 */
  1242. position: relative;
  1243. display: grid;
  1244. gap: calc(6 * var(--u, 1px));
  1245. align-content: start;
  1246. min-height: 0;
  1247. }
  1248. .panel-blocks__head {
  1249. display: flex;
  1250. flex-wrap: wrap;
  1251. align-items: center;
  1252. gap: var(--space-2, 8px);
  1253. }
  1254. .panel-blocks__note {
  1255. margin: 0;
  1256. color: var(--muted, #5680b0);
  1257. font-size: var(--fs-note, 9.2px);
  1258. letter-spacing: 0.04em;
  1259. }
  1260. /* 契约缺口:用「关注」色而不是错误色 —— 它不是故障,是后端还没有这个字段。 */
  1261. .panel-blocks__gap {
  1262. margin: 0;
  1263. padding: calc(3 * var(--u, 1px)) var(--space-2, 8px);
  1264. border-left: 2px solid var(--lv2, #ffb020);
  1265. background: rgba(255, 176, 32, 0.08);
  1266. color: var(--text-2, #88b7e8);
  1267. font-size: var(--fs-caption, 10.5px);
  1268. line-height: 1.5;
  1269. }
  1270. /* 块间距(2026-10-23 压矮):3u→2u,配合 body 内边距收紧,
  1271. 目标是四块面板内容在一屏内看完、不靠滚动。 */
  1272. .panel-block {
  1273. display: grid;
  1274. gap: calc(2 * var(--u, 1px));
  1275. }
  1276. /* 块标题行(票 11 视觉对齐):左侧竖条 + 标题,右侧块级小字。
  1277. 竖条与标题共用一行,行内两端对齐;窄列时小字收缩省略(走共用省略方法)。 */
  1278. .panel-block__head {
  1279. display: flex;
  1280. align-items: center;
  1281. justify-content: space-between;
  1282. gap: var(--space-2, 8px);
  1283. min-width: 0;
  1284. /* 2026-10-23:块头不额外吃间距(间距由 .panel-block 的 gap 统一给)。 */
  1285. line-height: 1.2;
  1286. }
  1287. .panel-block__title {
  1288. display: flex;
  1289. align-items: center;
  1290. gap: calc(6 * var(--u, 1px));
  1291. min-width: 0;
  1292. margin: 0;
  1293. color: var(--text, #d7ecff);
  1294. font-size: var(--fs-body, 14px);
  1295. font-weight: 700;
  1296. letter-spacing: 0.04em;
  1297. }
  1298. /* 标题左侧竖条:块的身份标记(设计稿每条分区标题都带)。 */
  1299. .panel-block__title-bar {
  1300. flex: none;
  1301. width: calc(3 * var(--u, 1px));
  1302. height: calc(13 * var(--u, 1px));
  1303. background: linear-gradient(180deg, var(--cyan-hi, #00b8ff), var(--cyan, #01abea));
  1304. box-shadow: 0 0 6px rgba(1, 171, 234, 0.65);
  1305. }
  1306. .panel-block__aside {
  1307. flex: 0 1 auto;
  1308. min-width: 0;
  1309. color: var(--muted, #5680b0);
  1310. font-size: var(--fs-note, 9.2px);
  1311. letter-spacing: 0.02em;
  1312. }
  1313. .panel-kpis {
  1314. display: grid;
  1315. grid-template-columns: repeat(auto-fit, minmax(calc(78 * var(--u, 1px)), 1fr));
  1316. gap: calc(5 * var(--u, 1px));
  1317. }
  1318. /* value-first 版式固定一排四张(感知设备顶部四卡,设计稿的形态)。
  1319. 栏宽收起时(容器 < 300u)退 2×2 —— 判的是**容器**不是视口,理由同下方 two-by-two:
  1320. 大屏驾驶舱里视口 media query 永远命中不到。
  1321. `overflow: visible` 是必需的:accent 横条贴在卡顶,裁掉就看不见了。 */
  1322. .panel-kpis--value-first {
  1323. grid-template-columns: repeat(4, minmax(0, 1fr));
  1324. gap: calc(6 * var(--u, 1px));
  1325. container-type: inline-size;
  1326. container-name: panel-kpi-value-first;
  1327. }
  1328. /* 固定列数版式(票 11):设计稿里「两张一行」「三张一行」是明确版式,
  1329. 不靠 auto-fit 按容器宽度去撞 —— 撞出来的列数会随栏宽变,走查与设计稿对不上。 */
  1330. .panel-kpis--two {
  1331. grid-template-columns: repeat(2, minmax(0, 1fr));
  1332. }
  1333. .panel-kpis--three {
  1334. grid-template-columns: repeat(3, minmax(0, 1fr));
  1335. }
  1336. @container panel-kpi-value-first (max-width: 300px) {
  1337. .panel-kpis--value-first {
  1338. grid-template-columns: repeat(2, minmax(0, 1fr));
  1339. }
  1340. }
  1341. /* 票 54:监测概览四卡在常规宽度固定 2×2,避免 auto-fit 展开成一行。
  1342. **降级按容器判、不按视口判**:面板栏宽是 `--column-w = clamp(280px, 330u, 460px)`
  1343. (styles/tokens.css:135),`--u = clamp(1px, 0.0694vw, 2px)`。栏内容宽实测:1920 视口
  1344. ≈410px、1440 视口(u 收到 1)≈308px —— 两档都稳在 2×2。写视口 media query 等于写一条
  1345. 在大屏驾驶舱里永不触发的规则,所以这里查的是**容器**宽度。
  1346. **260px 这个阈值的来源**(不是拍的):单格宽 =(容器 − gap 8u)/2;读数行 = 单格 − 卡片
  1347. 左右内边距 16u;`--fs-kpi = 20u` 下「10-12 15:15」11 个等宽字符 ≈ 110u。
  1348. 读数不省略要求 单格 ≥ 126u → 容器 ≥ 2×126 + 8 = 260u。低于它退单列。
  1349. 两档都不横向溢出:列宽 `minmax(0, 1fr)`(不是 auto),卡片内文本走共用省略方法。
  1350. 形状说明:**容器查不了自己的尺寸**(CSS containment 的规则,`container-type` 加在谁身上,
  1351. `@container` 就只对谁的后代生效),所以降级写在子卡上 `grid-column: 1 / -1`
  1352. 横贯两列,而不是改父元素的 `grid-template-columns`。
  1353. `container-type` 只加在本变体上:本元素内没有绝对定位后代(图表 hover 浮层的定位基准
  1354. 仍是 `.panel-blocks` 根),因此不影响其它块的布局。 */
  1355. .panel-kpis--two-by-two {
  1356. grid-template-columns: repeat(2, minmax(0, 1fr));
  1357. container-type: inline-size;
  1358. container-name: panel-kpi-grid;
  1359. }
  1360. @container panel-kpi-grid (max-width: 260px) {
  1361. .panel-kpis--two-by-two .panel-kpi {
  1362. grid-column: 1 / -1;
  1363. }
  1364. }
  1365. /* ---- value-first 版式(票 11 视觉对齐:读数在上、标签在下)----
  1366. 设计稿顶部四卡:卡顶一条语义色横条、大号读数居中、标签在下。
  1367. 与缺省版的差别只在**顺序与对齐**;图标在 value-first 里不画(设计稿这类卡只用数字说话),
  1368. 所以网格退成单列、行内容居中,其余(占位符/单位/差值)一字不改。 */
  1369. .panel-kpis--value-first .panel-kpi,
  1370. .panel-kpi--value-first {
  1371. /* 让格内数值能用 cqw 按格宽自适应(见 .panel-kpi--value-first .panel-kpi__value)。
  1372. inline-size 隔离只影响尺寸计算、不动外观。 */
  1373. container-type: inline-size;
  1374. grid-template-columns: minmax(0, 1fr);
  1375. justify-items: center;
  1376. row-gap: 1px;
  1377. /* 2026-10-23 第二轮压矮:上下 7/6u → 5/4u。 */
  1378. padding: calc(5 * var(--u, 1px)) var(--space-2, 8px) calc(4 * var(--u, 1px));
  1379. overflow: hidden;
  1380. }
  1381. /* 卡顶语义色横条:一条 2u 亮线贴在卡顶(accent 由 domain 给色键)。 */
  1382. .panel-kpi--value-first::before {
  1383. content: '';
  1384. position: absolute;
  1385. top: 0;
  1386. left: 0;
  1387. right: 0;
  1388. height: calc(2 * var(--u, 1px));
  1389. background: var(--kpi-accent, var(--cyan, #01abea));
  1390. box-shadow: 0 0 8px var(--kpi-accent, var(--cyan, #01abea));
  1391. }
  1392. .panel-kpi--value-first {
  1393. position: relative;
  1394. }
  1395. /* accent 色键 → token(色值仍只在 tokens.css 声明一份)。 */
  1396. .panel-kpi--accent-lv1 { --kpi-accent: var(--lv1, #ff3b52); }
  1397. .panel-kpi--accent-lv2 { --kpi-accent: var(--lv2, #ffb020); }
  1398. .panel-kpi--accent-lv3 { --kpi-accent: var(--lv3, #01abea); }
  1399. .panel-kpi--accent-lv4 { --kpi-accent: var(--lv4, #27d17a); }
  1400. .panel-kpi--accent-info { --kpi-accent: var(--cyan-hi, #00b8ff); }
  1401. /* value-first 的排布:读数在上、标签在下。
  1402. DOM 顺序仍是「读数与标签成对出现」,视觉顺序靠 `order` —— 两者分离后,
  1403. 屏幕阅读器读到的语义顺序不变,而画面上符合设计稿的「10 / 设备总数」。 */
  1404. .panel-kpi--value-first .panel-kpi__label {
  1405. order: 2;
  1406. color: var(--text-2, #88b7e8);
  1407. font-size: var(--fs-caption, 10.5px);
  1408. }
  1409. .panel-kpi--value-first .panel-kpi__value,
  1410. .panel-kpi--value-first .panel-kpi__value--link {
  1411. order: 1;
  1412. /* 20u → 22u(维护者 2026-10-26 第三次反馈:「还是小了,参考运行监测-报警处置卡片里的字号」)。
  1413. 报警处置的同级读数走 --fs-kpi(22u),这里**统一成同一个值** —— 四块面板的指标数值
  1414. 从此一样大,不再各自一档。装不下怎么办?由下面的 cqw 上限兜底,而不是靠把字号调小。 */
  1415. /* 兜底约束:字号再受**所在格子宽度**限制(34cqw)。
  1416. 34cqw 在正常格宽下**不生效**(实测格宽约 83px → 28px > 22u≈25px),
  1417. 只在格子被压得很窄时才降字号,保证任何长度都画不到格子外。
  1418. 数字字体是 Rajdhani/Bahnschrift 这类窄体(约 0.5em),「27.3%」在 22u 下约 63px,
  1419. 83px 的格子放得下 —— 所以这条是安全网,不是日常生效的规则。
  1420. (早先写 27cqw 会真的压住 22u:格宽 83px 时只给到 22.4px,等于白改。)
  1421. cqw 需要格子上有 container-type,见 .panel-kpi--value-first 那条;
  1422. 规则作用在**子元素**上,不是容器自身,不踩「容器不能被自身查询命中」那个坑。 */
  1423. font-size: min(calc(22 * var(--u, 1px)), 34cqw);
  1424. line-height: 1.1;
  1425. text-align: center;
  1426. font-weight: 700;
  1427. max-width: 100%;
  1428. overflow: hidden;
  1429. }
  1430. /* value-first 卡在画面上不显示单位(设计稿顶部四卡只有数字):
  1431. 数据层仍带单位(悬停 title 由 kpiReading 给「10 台」),只是不占视觉位。 */
  1432. .panel-kpi--value-first .panel-kpi__value em {
  1433. display: none;
  1434. }
  1435. /* 差值 / 口径小字在 value-first 里落在标签之下(同列居中)。 */
  1436. .panel-kpi--value-first .panel-kpi__delta,
  1437. .panel-kpi--value-first .panel-kpi__caption {
  1438. order: 3;
  1439. text-align: center;
  1440. }
  1441. /* 读数语义色(`valueTone`:在线绿、离线红、等级色…)。只染数字,不动卡片其余部分。 */
  1442. .panel-kpi__value.is-lv1 { color: var(--lv1, #ff3b52); }
  1443. .panel-kpi__value.is-lv2 { color: var(--lv2, #ffb020); }
  1444. .panel-kpi__value.is-lv3 { color: var(--lv3, #01abea); }
  1445. .panel-kpi__value.is-lv4 { color: var(--lv4, #27d17a); }
  1446. .panel-kpi__value.is-neutral { color: var(--text-strong, #fff); }
  1447. /* 卡内常显口径小字(`caption`):比 hint 更亮一档 —— 它是屏上要读的字,不是悬停补充。 */
  1448. .panel-kpi__caption {
  1449. grid-column: 1 / -1;
  1450. color: var(--text-2, #88b7e8);
  1451. font-size: var(--fs-note, 9.2px);
  1452. letter-spacing: 0.02em;
  1453. }
  1454. .panel-kpi {
  1455. display: grid;
  1456. grid-template-columns: auto minmax(0, 1fr);
  1457. align-items: center;
  1458. column-gap: calc(5 * var(--u, 1px));
  1459. row-gap: 0;
  1460. padding: calc(3 * var(--u, 1px)) var(--space-2, 6px);
  1461. border: 1px solid rgba(1, 171, 234, 0.35);
  1462. /* 2026-10-23 维护者要求:面板背景取消颜色渐变后,颜色渐变转移到数据统计指标卡片 ——
  1463. .panel-kpi 用 tokens 的 --stat-card-bg(亮科技蓝 → 深蓝,色相可感知)。 */
  1464. background: var(--stat-card-bg, linear-gradient(180deg, rgba(0, 82, 155, 0.45), rgba(0, 28, 70, 0.6)));
  1465. /* 2026-10-23 维护者要求:指标卡加圆角(8u,与 tab/notice 的既有圆角档一致)。 */
  1466. border-radius: calc(8 * var(--u, 1px));
  1467. }
  1468. /* KPI 图标:与标签同一行,弱发光(同面板标题图标的弱辉光档)。
  1469. 颜色由容器 currentColor 给(KPI_ICON_COLORS 语义色),mask 图标本体跟随。 */
  1470. .panel-kpi__icon {
  1471. display: inline-flex;
  1472. align-items: center;
  1473. color: var(--cyan, #01abea);
  1474. filter: drop-shadow(0 0 4px rgba(1, 171, 234, 0.45));
  1475. }
  1476. /* 票 37:越界省略与 hover 完整原文都归 `EllipsisText.vue` 的 `.ellipsis-text` 一份声明;
  1477. 本文件里的类只管字体、颜色与排布。 */
  1478. .panel-kpi__label {
  1479. color: var(--muted, #5680b0);
  1480. font-size: var(--fs-caption, 10.5px);
  1481. letter-spacing: 0.04em;
  1482. }
  1483. /* 数值行跨两列(图标 + 标签在上一行,数值独占下一行全宽);
  1484. 口径说明(hint)**同样独占一行**:它若落进第一列(auto 列),
  1485. 会被说明文字把该列撑宽、把 minmax(0,1fr) 的标签列挤到 0 —— 标签被省略成"消失"
  1486. (真机走查 2026-10-14:在线率 / 预警已解除率两张卡的标题整行不见,根因即此)。 */
  1487. .panel-kpi__value,
  1488. .panel-kpi__value--link,
  1489. .panel-kpi__caption {
  1490. grid-column: 1 / -1;
  1491. }
  1492. /* 票 38 第二轮(反馈第 1 条):整行独占**回归**,但改由 domain 的通用 `wide` 标志声明
  1493. (概览基础设施的管线里程数值最长,独占一行排版更好看)。
  1494. 与第一轮删掉的那版区别:那时是渲染层按 `item.key === 'lengthKm'` 特判(第二份布局语义),
  1495. 现在组件只认 `item.wide` 这个通用呈现意图,「谁独占」的决定权在 domain。
  1496. 列布局沿用 HEAD 原样(图标 / 标签 / 右对齐数值三列)——「占一整行」由 `grid-column`
  1497. 表达,不需要顺带改写卡片内部排布。 */
  1498. .panel-kpi--wide {
  1499. grid-column: 1 / -1;
  1500. grid-template-columns: auto minmax(0, 1fr) auto;
  1501. padding: var(--space-2);
  1502. }
  1503. .panel-kpi--wide .panel-kpi__value {
  1504. grid-column: 3;
  1505. text-align: right;
  1506. font-size: var(--fs-kpi);
  1507. }
  1508. .panel-kpi__value {
  1509. min-width: 0;
  1510. color: var(--text-strong, #fff);
  1511. font-family: var(--num, monospace);
  1512. /* 批次 2(票 15):KPI 数值是设计稿最显眼的档位(20px Rajdhani),单列 --fs-kpi。 */
  1513. font-size: var(--fs-kpi, 20px);
  1514. line-height: 1.2;
  1515. }
  1516. /* 票 54:原先这里有一条**时间读数专用降档**(把数据时间压到 caption 字号),
  1517. 它正是「数据时间与上一行四卡不在同一视觉层级」的根因。所有 KPI 数值共用
  1518. `.panel-kpi__value` 一档之后它失去全部消费点,按规格 03「无消费点不提交」整条删除;
  1519. 短读法归 domain 声明(`createdAtKpi`),渲染层不再按 `item.key` 认哪个读数是时间。 */
  1520. .panel-kpi__value em {
  1521. color: var(--muted, #5680b0);
  1522. font-style: normal;
  1523. font-size: var(--fs-caption, 10.5px);
  1524. }
  1525. /* 票 20:可点的 KPI 读数(联动出口)。抹掉按钮默认外观,但保留可聚焦与 hover 反馈 ——
  1526. 与 `.panel-bar--button` 同一立场:可点的东西要在键盘上也能到达。 */
  1527. .panel-kpi__value--link {
  1528. padding: 0;
  1529. border: 0;
  1530. background: transparent;
  1531. font: inherit;
  1532. font-family: var(--num, monospace);
  1533. font-size: var(--fs-kpi, 20px);
  1534. text-align: left;
  1535. cursor: pointer;
  1536. border-bottom: 1px dashed rgba(1, 171, 234, 0.45);
  1537. }
  1538. .panel-kpi__value--link:hover,
  1539. .panel-kpi__value--link:focus-visible {
  1540. color: var(--text-strong);
  1541. border-bottom-color: var(--cyan, #01abea);
  1542. }
  1543. .panel-kpi__value--link:focus-visible {
  1544. outline: 1px solid var(--cyan, #01abea);
  1545. outline-offset: 1px;
  1546. }
  1547. /* 可点分布的 hover 反馈与状态分布行同款(`block.action` 版)。 */
  1548. .panel-bars--clickable .panel-bar--button:hover .panel-bar__count,
  1549. .panel-bars--clickable .panel-bar--button:focus-visible .panel-bar__count {
  1550. color: var(--text);
  1551. }
  1552. /* 票 11 二轮(维护者 2026-10-23):KPI 的口径细节(如在线率的「分子 3 / 分母 10」)
  1553. **不再上屏占位**,改由读数自身的 title 承载;这里不再有 .panel-kpi__hint 的样式
  1554. (无消费点不保留 —— 留着它会诱导出第二个"常显说明")。 */
  1555. /* 差值(规格 §6.6):正红、负绿、0 中性。色键来自 domain 的 deltaTone(lv1/lv4/neutral),
  1556. 与其它语义读数共用 tokens.css 的同一批变量,不在这里写第二份色值。 */
  1557. .panel-kpi__delta {
  1558. grid-column: 1 / -1;
  1559. font-family: var(--num, monospace);
  1560. font-size: var(--fs-caption, 10.5px);
  1561. letter-spacing: 0.02em;
  1562. }
  1563. .panel-kpi__delta.is-lv1 {
  1564. color: var(--lv1, #ff3b52);
  1565. }
  1566. .panel-kpi__delta.is-lv4 {
  1567. color: var(--lv4, #27d17a);
  1568. }
  1569. .panel-kpi__delta.is-neutral {
  1570. color: var(--muted, #5680b0);
  1571. }
  1572. /* 卡片组(票 02 建立,票 11 视觉对齐):一排「计数 + 名称(+ 小字)」的卡。
  1573. 版式由 domain 的 `layout` 选列数(设计稿:类型卡 2×2、片区卡一排三张、等级卡一排四张);
  1574. 缺省仍是 auto-fit 自适应。卡内自上而下:徽标 → 计数 → 名称 → 小字。 */
  1575. .panel-cards {
  1576. display: grid;
  1577. grid-template-columns: repeat(auto-fit, minmax(calc(72 * var(--u, 1px)), 1fr));
  1578. gap: calc(4 * var(--u, 1px));
  1579. }
  1580. .panel-cards--two-by-two {
  1581. grid-template-columns: repeat(2, minmax(0, 1fr));
  1582. }
  1583. .panel-cards--three {
  1584. grid-template-columns: repeat(3, minmax(0, 1fr));
  1585. }
  1586. .panel-cards--four {
  1587. grid-template-columns: repeat(4, minmax(0, 1fr));
  1588. }
  1589. .panel-card {
  1590. display: grid;
  1591. justify-items: center;
  1592. gap: 0;
  1593. min-width: 0;
  1594. /* 2026-10-23 第二轮压矮:上下 5u→2u、左右 8u→5u,行距归零。
  1595. 卡片是纵向堆叠里最占高度的一块,压它是"一屏看完"的主要杠杆。 */
  1596. padding: calc(2 * var(--u, 1px)) calc(5 * var(--u, 1px));
  1597. border: 1px solid rgba(1, 171, 234, 0.24);
  1598. border-radius: calc(8 * var(--u, 1px));
  1599. /* 同上:计数卡也是数据统计指标卡片,共用 --stat-card-bg 颜色渐变。 */
  1600. background: var(--stat-card-bg, linear-gradient(180deg, rgba(0, 82, 155, 0.45), rgba(0, 28, 70, 0.6)));
  1601. text-align: center;
  1602. }
  1603. /* 可点卡片(类型卡 → 行业筛选):抹按钮默认外观,保留 hover/focus(与条形图同一立场)。 */
  1604. .panel-card--button {
  1605. width: 100%;
  1606. border: 1px solid rgba(1, 171, 234, 0.24);
  1607. border-radius: calc(8 * var(--u, 1px));
  1608. font: inherit;
  1609. color: inherit;
  1610. cursor: pointer;
  1611. }
  1612. .panel-card--button:hover,
  1613. .panel-card--button:focus-visible {
  1614. border-color: rgba(1, 171, 234, 0.7);
  1615. background: rgba(1, 171, 234, 0.12);
  1616. }
  1617. .panel-card--button:focus-visible {
  1618. outline: 1px solid var(--cyan, #01abea);
  1619. outline-offset: 1px;
  1620. }
  1621. /* 选中态(当前生效的行业筛选):与条形图选中行同一视觉语言。 */
  1622. .panel-card--button.is-active {
  1623. border-color: var(--cyan, #01abea);
  1624. background: rgba(1, 171, 234, 0.16);
  1625. box-shadow: inset 0 0 0 1px var(--cyan, #01abea);
  1626. }
  1627. /* 卡内徽标:圆形淡底 + 语义色图标(类型卡的行业图标)。 */
  1628. /* 卡内徽标:2026-10-23 由 26u 收至 22u(压矮卡片,图标仍看得清)。 */
  1629. .panel-card__badge {
  1630. display: inline-flex;
  1631. align-items: center;
  1632. justify-content: center;
  1633. width: calc(19 * var(--u, 1px));
  1634. height: calc(19 * var(--u, 1px));
  1635. border-radius: 50%;
  1636. background: rgba(1, 171, 234, 0.12);
  1637. color: var(--cyan, #01abea);
  1638. }
  1639. /* 行语义色(`tone`):染边框与徽标底(类型卡 = 行业色)。值仍只在 tokens.css 一份。 */
  1640. .panel-card--tone-industry-gas { border-color: rgba(254, 59, 83, 0.45); }
  1641. .panel-card--tone-industry-water { border-color: rgba(1, 171, 234, 0.45); }
  1642. .panel-card--tone-industry-drainage { border-color: rgba(39, 209, 122, 0.45); }
  1643. .panel-card--tone-industry-manhole { border-color: rgba(169, 112, 255, 0.45); }
  1644. .panel-card--tone-industry-gas .panel-card__badge { background: rgba(254, 59, 83, 0.16); color: var(--industry-gas, #fe3b53); }
  1645. .panel-card--tone-industry-water .panel-card__badge { background: rgba(1, 171, 234, 0.16); color: var(--industry-water, #01abea); }
  1646. .panel-card--tone-industry-drainage .panel-card__badge { background: rgba(39, 209, 122, 0.16); color: var(--industry-drainage, #27d17a); }
  1647. .panel-card--tone-industry-manhole .panel-card__badge { background: rgba(169, 112, 255, 0.16); color: var(--industry-manhole, #a970ff); }
  1648. /* 卡名:**单行省略**(超框收成「…」,完整原文在 title)。
  1649. 不能用多行:卡名长短不一时多行会把同排卡片撑成不同高度,一排卡参差不齐。 */
  1650. .panel-card__label {
  1651. max-width: 100%;
  1652. color: var(--text-2, #88b7e8);
  1653. font-size: var(--fs-caption, 10.5px);
  1654. letter-spacing: 0.04em;
  1655. }
  1656. /* 卡内小字(监测内容):多段短词,**最多两行**,超出按行数截断并显示省略标记。
  1657. 为什么要限行而不是自由换行:自由换行时「排水设备」那张卡会因为 8 项监测内容
  1658. 把整排拉高,其余三张卡被同高拉伸成大片空白(维护者 2026-10-23:卡片高度要压矮、
  1659. 不要靠滚动看全)。两行是够读的量,细节仍在 title。
  1660. 兼容性:-webkit-line-clamp 是这套写法里被广泛实现的那个;不支持时退回单行省略,
  1661. 信息量降级但不破版。 */
  1662. .panel-card__note {
  1663. display: -webkit-box;
  1664. -webkit-box-orient: vertical;
  1665. -webkit-line-clamp: 2;
  1666. line-clamp: 2;
  1667. max-width: 100%;
  1668. overflow: hidden;
  1669. color: var(--muted, #5680b0);
  1670. font-size: var(--fs-note, 9.2px);
  1671. /* 行高 1.35→1.2:两行小字本来就密,压行高比压字号更安全(字号可读性结论不动)。 */
  1672. line-height: 1.2;
  1673. letter-spacing: 0.02em;
  1674. overflow-wrap: anywhere;
  1675. text-overflow: ellipsis;
  1676. }
  1677. .panel-card__count {
  1678. min-width: 0;
  1679. color: var(--text-strong, #fff);
  1680. font-family: var(--num, monospace);
  1681. /* 与 KPI 读数**完全同档**(22u,维护者 2026-10-26 第 1 条:以报警处置卡片字号为准)。
  1682. 卡片是 2/3 列布局、格宽 100~160px,22u 的数字放得下;4 分格的等级卡也够宽。 */
  1683. font-size: calc(22 * var(--u, 1px));
  1684. line-height: 1;
  1685. }
  1686. .panel-card__count em {
  1687. color: var(--muted, #5680b0);
  1688. font-style: normal;
  1689. font-size: calc(10 * var(--u, 1px));
  1690. }
  1691. /* 读数语义色(等级卡):数字与边框同色(设计稿的一级红/二级黄/三级蓝/四级蓝)。 */
  1692. .panel-card--value-lv1 { border-color: rgba(255, 59, 82, 0.55); }
  1693. .panel-card--value-lv1 .panel-card__count { color: var(--lv1, #ff3b52); }
  1694. .panel-card--value-lv2 { border-color: rgba(255, 176, 32, 0.55); }
  1695. .panel-card--value-lv2 .panel-card__count { color: var(--lv2, #ffb020); }
  1696. .panel-card--value-lv3 { border-color: rgba(1, 171, 234, 0.55); }
  1697. .panel-card--value-lv3 .panel-card__count { color: var(--lv3, #01abea); }
  1698. .panel-card--value-lv4 { border-color: rgba(39, 209, 122, 0.55); }
  1699. .panel-card--value-lv4 .panel-card__count { color: var(--lv4, #27d17a); }
  1700. .panel-bars {
  1701. display: grid;
  1702. gap: calc(2 * var(--u, 1px));
  1703. }
  1704. /* 票 38:dimbars 的维度切换标签 + 柱列。柱/条复用 .panel-bar 的同一套画法,
  1705. 这里只管切换器本身——小胶囊按钮,选中态与顶栏筛选 chips 同一视觉语言(is-active)。 */
  1706. .panel-dimbars {
  1707. display: grid;
  1708. gap: var(--space-2, 8px);
  1709. }
  1710. .panel-dimbars__tabs {
  1711. display: flex;
  1712. gap: calc(4 * var(--u, 1px));
  1713. }
  1714. .panel-dimbars__tab {
  1715. padding: calc(2 * var(--u, 1px)) calc(8 * var(--u, 1px));
  1716. border: 1px solid rgba(1, 171, 234, 0.24);
  1717. border-radius: calc(8 * var(--u, 1px));
  1718. background: var(--inset, rgba(2, 10, 20, 0.55));
  1719. color: var(--text-2, #88b7e8);
  1720. font-family: var(--font-topbar);
  1721. font-size: var(--fs-caption, 10.5px);
  1722. cursor: pointer;
  1723. }
  1724. .panel-dimbars__tab.is-active {
  1725. color: var(--text-strong, #fff);
  1726. border-color: rgba(0, 184, 255, 0.85);
  1727. background: rgba(1, 171, 234, 0.16);
  1728. }
  1729. .panel-dimbars__tab:focus-visible {
  1730. outline: 1px solid var(--cyan, #01abea);
  1731. outline-offset: 2px;
  1732. }
  1733. /* 可点的状态分布行:按钮要抹掉浏览器默认外观,但仍保留可聚焦与 hover 反馈。 */
  1734. .panel-bars--clickable .panel-bar--button {
  1735. width: 100%;
  1736. padding: 1px 0;
  1737. border: 0;
  1738. background: transparent;
  1739. font: inherit;
  1740. text-align: left;
  1741. cursor: pointer;
  1742. }
  1743. .panel-bar--button:hover .panel-bar__label,
  1744. .panel-bar--button:focus-visible .panel-bar__label {
  1745. color: var(--text);
  1746. }
  1747. .panel-bar--button:focus-visible {
  1748. outline: 1px solid var(--cyan, #01abea);
  1749. outline-offset: 1px;
  1750. }
  1751. /* 选中态:整行加一条左边线 + 底色,不改变条长口径(选中不该被读成"计数变了")。
  1752. 票 37 起由 `row.selected` 驱动(此前样式在、值没人传,选中行与未选中行画面上分不出)。 */
  1753. .panel-bar--button.is-active {
  1754. background: rgba(1, 171, 234, 0.12);
  1755. box-shadow: inset 2px 0 0 var(--cyan, #01abea);
  1756. }
  1757. /* 票 44(视觉密度):文字与统计条收紧(8u→4u),读数列同档;行距不变,密度靠"贴紧"而不是"堆挤"。 */
  1758. .panel-bar {
  1759. display: grid;
  1760. grid-template-columns: calc(64 * var(--u, 1px)) minmax(0, 1fr) calc(26 * var(--u, 1px));
  1761. align-items: center;
  1762. gap: calc(4 * var(--u, 1px));
  1763. }
  1764. /* rank 版式(票 11 · 预警类型排行):名次徽章 + 名称 + 计数一行,条形在**下一行通栏**。
  1765. 与缺省三列格子不同,这里换成两行网格:第一行三段(徽章/名称/计数),第二行条轨道跨两列。 */
  1766. .panel-bars--rank .panel-bar {
  1767. grid-template-columns: calc(18 * var(--u, 1px)) minmax(0, 1fr) auto;
  1768. row-gap: calc(4 * var(--u, 1px));
  1769. align-items: center;
  1770. padding: calc(3 * var(--u, 1px)) 0;
  1771. }
  1772. .panel-bars--rank .panel-bar__rank {
  1773. display: inline-flex;
  1774. align-items: center;
  1775. justify-content: center;
  1776. width: calc(16 * var(--u, 1px));
  1777. height: calc(16 * var(--u, 1px));
  1778. border-radius: 50%;
  1779. background: var(--chart-c1, #1780ff);
  1780. color: var(--text-strong, #fff);
  1781. font-family: var(--num, monospace);
  1782. font-size: var(--fs-caption, 10.5px);
  1783. font-style: normal;
  1784. line-height: 1;
  1785. }
  1786. /* 前三名徽章按名次换色(第 1 名橙、第 2 名黄、第 3 名蓝;其余沿用主蓝)。 */
  1787. .panel-bars--rank .panel-bar:nth-child(1) .panel-bar__rank {
  1788. background: var(--lv2, #ffb020);
  1789. box-shadow: 0 0 6px rgba(255, 176, 32, 0.6);
  1790. }
  1791. .panel-bars--rank .panel-bar:nth-child(2) .panel-bar__rank {
  1792. background: var(--chart-c3, #00b8ff);
  1793. }
  1794. .panel-bars--rank .panel-bar:nth-child(3) .panel-bar__rank {
  1795. background: var(--chart-c1, #1780ff);
  1796. }
  1797. /* 第一行=徽章 / 名称 / 计数;条轨道独占第二行、跨满三列。
  1798. 计数必须**显式**定位回第 1 行第 3 列:轨道带 `grid-column: 1 / -1` 会占掉下一整行,
  1799. 排在它后面的计数若走自动放置就会掉到第 3 行(名次、名称、条、计数各占一行)。 */
  1800. .panel-bars--rank .panel-bar__count {
  1801. grid-column: 3;
  1802. grid-row: 1;
  1803. }
  1804. .panel-bars--rank .panel-bar__track {
  1805. grid-column: 1 / -1;
  1806. grid-row: 2;
  1807. height: calc(5 * var(--u, 1px));
  1808. }
  1809. .panel-bar__count em {
  1810. color: var(--muted, #5680b0);
  1811. font-style: normal;
  1812. font-size: var(--fs-caption, 10.5px);
  1813. }
  1814. .panel-bar__label {
  1815. color: var(--text-2, #88b7e8);
  1816. font-size: var(--fs-caption, 10.5px);
  1817. }
  1818. /* 轨道:5u→6u 并加内衬与 1u 发丝边 —— 条段有明确的容器边界,不再与相邻行的辉光糊在一起。 */
  1819. .panel-bar__track {
  1820. position: relative;
  1821. display: block;
  1822. height: calc(6 * var(--u, 1px));
  1823. background: linear-gradient(180deg, rgba(1, 171, 234, 0.1), rgba(1, 171, 234, 0.22));
  1824. box-shadow: inset 0 0 0 1px rgba(1, 171, 234, 0.12);
  1825. }
  1826. /* 票 44(条段分割):删掉旧的外辉光 `0 0 8px` —— 辉光越过 2u 行距渗进相邻条,
  1827. 正是"条段之间没有明确分割"的直接原因。层次改由填充自身的顶部高光线 + 右端头帽给
  1828. (::after 装饰层,pointer-events:none 不挡悬停;行 hover 的增亮反馈仍归 .panel-bar__fill 本体)。 */
  1829. .panel-bar__fill {
  1830. position: relative;
  1831. display: block;
  1832. height: 100%;
  1833. background: linear-gradient(90deg, var(--cyan-d, #009de9), var(--cyan, #01abea));
  1834. }
  1835. .panel-bar__fill::after {
  1836. content: '';
  1837. position: absolute;
  1838. inset: 0;
  1839. background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0) 55%);
  1840. box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.85);
  1841. pointer-events: none;
  1842. }
  1843. .panel-bar__count {
  1844. color: var(--text);
  1845. font-family: var(--num, monospace);
  1846. font-size: var(--fs-caption, 10.5px);
  1847. text-align: right;
  1848. }
  1849. /* 趋势:横向分桶的柱列。高度按最大桶归一;0 也留 2% 高度,好让"这个桶存在且为 0"看得出来。 */
  1850. .panel-trend {
  1851. display: flex;
  1852. align-items: flex-end;
  1853. gap: calc(2 * var(--u, 1px));
  1854. height: calc(52 * var(--u, 1px));
  1855. padding-top: calc(2 * var(--u, 1px));
  1856. }
  1857. /* 折线形态(2026-10-13 维护者第三轮美化:带 Y 轴刻度、网格线、面积、点顶数值)。
  1858. 容器改列布局:绘图区 + X 轴标签行;绘图区左侧留刻度槽。 */
  1859. .panel-trend--line {
  1860. display: grid;
  1861. grid-template-rows: minmax(0, 1fr) auto;
  1862. gap: calc(2 * var(--u, 1px));
  1863. /* 2026-10-23 压矮:78u→64u(报警处置面板底部折线;点数与数值标注不变)。 */
  1864. height: calc(64 * var(--u, 1px));
  1865. padding: calc(6 * var(--u, 1px)) 0 calc(1 * var(--u, 1px));
  1866. }
  1867. .panel-trend__chart {
  1868. position: relative;
  1869. height: 100%;
  1870. min-height: 0;
  1871. }
  1872. /* 绘图区:左侧让出刻度槽,SVG 与数据点/标签都以它为一个坐标基准。 */
  1873. .panel-trend__plot {
  1874. position: absolute;
  1875. inset: 0 0 0 calc(20 * var(--u, 1px));
  1876. }
  1877. /* 网格线:0/半程/峰值三档;刻度标签落在左侧槽里。 */
  1878. .panel-trend__grid {
  1879. position: absolute;
  1880. left: calc(20 * var(--u, 1px));
  1881. right: 0;
  1882. height: 0;
  1883. border-top: 1px dashed rgba(1, 171, 234, 0.14);
  1884. }
  1885. .panel-trend__grid.is-top {
  1886. border-top-color: rgba(1, 171, 234, 0.3);
  1887. }
  1888. .panel-trend__ytick {
  1889. position: absolute;
  1890. left: calc(-20 * var(--u, 1px));
  1891. top: -0.55em;
  1892. width: calc(17 * var(--u, 1px));
  1893. color: var(--muted, #5680b0);
  1894. font-family: var(--num, monospace);
  1895. font-size: var(--fs-caption, 10.5px);
  1896. font-weight: 400;
  1897. line-height: 1;
  1898. text-align: right;
  1899. }
  1900. /* 折线 + 面积:SVG 铺满绘图区,viewBox 0–100 与百分比坐标同基准。 */
  1901. .panel-trend__line {
  1902. position: absolute;
  1903. inset: 0;
  1904. width: 100%;
  1905. height: 100%;
  1906. overflow: visible;
  1907. }
  1908. .panel-trend__area {
  1909. fill: var(--trend-tone, var(--cyan, #01abea));
  1910. fill-opacity: 0.14;
  1911. }
  1912. .panel-trend__line-path {
  1913. /* 折线色(票 11):由 domain 的 `tone` 注入 --trend-tone(如报警折线=lv2 橙),
  1914. 缺省仍是主蓝 —— 色值只经 barColor 查 tokens.css 的同一张表,组件不写死色。 */
  1915. stroke: var(--trend-tone, var(--cyan, #01abea));
  1916. stroke-width: 1.6;
  1917. stroke-linejoin: round;
  1918. stroke-linecap: round;
  1919. vector-effect: non-scaling-stroke;
  1920. filter: drop-shadow(0 0 3px rgba(1, 171, 234, 0.55));
  1921. }
  1922. /* 数据点(HTML 叠层,保持正圆):以点自身中心对齐坐标。 */
  1923. .panel-trend__node {
  1924. position: absolute;
  1925. width: calc(5 * var(--u, 1px));
  1926. height: calc(5 * var(--u, 1px));
  1927. transform: translate(-50%, 50%);
  1928. }
  1929. .panel-trend__dot {
  1930. display: block;
  1931. width: 100%;
  1932. height: 100%;
  1933. border-radius: 50%;
  1934. background: var(--trend-tone, var(--cyan-hi, #00b8ff));
  1935. box-shadow: 0 0 4px var(--trend-tone, rgba(1, 171, 234, 0.8));
  1936. }
  1937. .panel-trend__node.is-peak .panel-trend__dot {
  1938. background: var(--lv2, #ffb020);
  1939. box-shadow: 0 0 5px rgba(255, 176, 32, 0.9);
  1940. }
  1941. /* 点顶数值标签:悬在点上方居中,等宽数字。 */
  1942. .panel-trend__vlabel {
  1943. position: absolute;
  1944. bottom: calc(100% + 2px);
  1945. left: 50%;
  1946. transform: translateX(-50%);
  1947. color: var(--text, #d7ecff);
  1948. font-family: var(--num, monospace);
  1949. font-size: var(--fs-caption, 10.5px);
  1950. font-weight: 700;
  1951. line-height: 1;
  1952. white-space: nowrap;
  1953. }
  1954. .panel-trend__node.is-peak .panel-trend__vlabel {
  1955. color: var(--lv2, #ffb020);
  1956. }
  1957. /* X 轴标签行:绝对定位到各点 x(与绘图区同基准),按 axisEvery 抽样,首尾必显。 */
  1958. .panel-trend__axis {
  1959. position: relative;
  1960. height: calc(12 * var(--u, 1px));
  1961. margin-left: calc(20 * var(--u, 1px));
  1962. }
  1963. /* 票 37:`white-space: nowrap` 与省略标记一并归 `.ellipsis-text`(唯一一份)。 */
  1964. .panel-trend__axis .panel-trend__label {
  1965. position: absolute;
  1966. top: 0;
  1967. transform: translateX(-50%);
  1968. color: var(--muted, #5680b0);
  1969. font-size: var(--fs-caption, 10.5px);
  1970. line-height: 1.1;
  1971. }
  1972. .panel-trend__axis .panel-trend__label:first-child {
  1973. transform: none;
  1974. }
  1975. .panel-trend__axis .panel-trend__label:last-child {
  1976. transform: translateX(-100%);
  1977. }
  1978. /* 多序列折线的图例(维护者 2026-10-23 反馈第 2 条):横排、色块 + 名称,压在 X 轴下方一行。
  1979. 不放右上角:那是面板级小字的位置,两者挤一起会互相顶。 */
  1980. .panel-trend__legend {
  1981. display: flex;
  1982. flex-wrap: wrap;
  1983. justify-content: center;
  1984. gap: calc(4 * var(--u, 1px)) calc(12 * var(--u, 1px));
  1985. margin: calc(2 * var(--u, 1px)) 0 0;
  1986. padding: 0;
  1987. list-style: none;
  1988. }
  1989. .panel-trend__legend-item {
  1990. display: inline-flex;
  1991. align-items: center;
  1992. gap: calc(4 * var(--u, 1px));
  1993. color: var(--text-2, #88b7e8);
  1994. font-size: var(--fs-caption, 10.5px);
  1995. line-height: 1.2;
  1996. cursor: default;
  1997. }
  1998. .panel-trend__legend-swatch {
  1999. flex: none;
  2000. width: calc(10 * var(--u, 1px));
  2001. height: calc(3 * var(--u, 1px));
  2002. border-radius: 1px;
  2003. box-shadow: 0 0 4px currentcolor;
  2004. }
  2005. /* 多序列的线路径色由行内 style 给(每条线自己的 tone);这里只保证描边属性不被覆盖成默认色。 */
  2006. .panel-trend__line-path[data-trend-series] {
  2007. stroke-width: 1.8;
  2008. }
  2009. /* 百分比堆叠条(2026-10-13 图表选型:多类别占比)。 */
  2010. .panel-stacked {
  2011. display: grid;
  2012. gap: calc(4 * var(--u, 1px));
  2013. }
  2014. .panel-stacked__bar {
  2015. position: relative;
  2016. display: flex;
  2017. height: calc(8 * var(--u, 1px));
  2018. background: rgba(1, 171, 234, 0.12);
  2019. overflow: visible;
  2020. }
  2021. .panel-stacked__seg {
  2022. position: absolute;
  2023. top: 0;
  2024. bottom: 0;
  2025. /* 2026-10-13:分段色由语义 tone 给(--seg-color,组件注入);shade 兜底删色。 */
  2026. background: var(--seg-color, var(--cyan, #01abea));
  2027. }
  2028. .panel-stacked__legend {
  2029. display: grid;
  2030. grid-template-columns: repeat(auto-fill, minmax(calc(96 * var(--u, 1px)), 1fr));
  2031. gap: calc(2 * var(--u, 1px)) var(--space-2, 8px);
  2032. margin: 0;
  2033. padding: 0;
  2034. list-style: none;
  2035. }
  2036. .panel-stacked__row {
  2037. display: flex;
  2038. align-items: center;
  2039. gap: calc(4 * var(--u, 1px));
  2040. min-width: 0;
  2041. }
  2042. .panel-stacked__swatch {
  2043. flex: none;
  2044. width: calc(8 * var(--u, 1px));
  2045. height: calc(8 * var(--u, 1px));
  2046. background: var(--seg-color, var(--cyan, #01abea));
  2047. }
  2048. /* 环形图(2026-10-13:条形图疲劳后的占比形态;左图右例、中心合计)。 */
  2049. .panel-donut {
  2050. display: flex;
  2051. align-items: center;
  2052. gap: var(--space-3, 12px);
  2053. min-width: 0;
  2054. }
  2055. /* stack-below 版式(票 11 · 管道分类占比):环形居中、图例在下横排 —— 设计稿的形态。 */
  2056. .panel-donut--stack-below {
  2057. flex-direction: column;
  2058. align-items: center;
  2059. gap: calc(6 * var(--u, 1px));
  2060. }
  2061. .panel-donut--stack-below .panel-donut__wrap {
  2062. width: calc(96 * var(--u, 1px));
  2063. height: calc(96 * var(--u, 1px));
  2064. }
  2065. .panel-donut--stack-below .panel-donut__legend {
  2066. display: flex;
  2067. flex: none;
  2068. flex-wrap: wrap;
  2069. justify-content: center;
  2070. gap: var(--space-3, 12px);
  2071. width: 100%;
  2072. }
  2073. .panel-donut--stack-below .panel-donut__row {
  2074. flex: none;
  2075. }
  2076. .panel-donut__wrap {
  2077. position: relative;
  2078. flex: none;
  2079. width: calc(72 * var(--u, 1px));
  2080. height: calc(72 * var(--u, 1px));
  2081. }
  2082. .panel-donut__chart {
  2083. display: block;
  2084. width: 100%;
  2085. height: 100%;
  2086. }
  2087. .panel-donut__slice {
  2088. stroke: rgba(1, 5, 20, 0.6);
  2089. stroke-width: 1;
  2090. }
  2091. .panel-donut__total {
  2092. position: absolute;
  2093. inset: 0;
  2094. display: grid;
  2095. place-items: center;
  2096. /* 票 43(重开):中心读数让位给整楔命中区——它盖在圆心上方,
  2097. 不关掉指针事件的话「悬停饼图中心」永远触发不了最近的扇区。 */
  2098. pointer-events: none;
  2099. color: var(--text-strong, #fff);
  2100. font-family: var(--num, monospace);
  2101. font-size: var(--fs-body, 14px);
  2102. }
  2103. .panel-donut__legend {
  2104. display: grid;
  2105. gap: calc(2 * var(--u, 1px));
  2106. margin: 0;
  2107. padding: 0;
  2108. list-style: none;
  2109. min-width: 0;
  2110. flex: 1 1 auto;
  2111. }
  2112. .panel-donut__row {
  2113. display: flex;
  2114. align-items: center;
  2115. gap: calc(4 * var(--u, 1px));
  2116. min-width: 0;
  2117. }
  2118. /* 可点图例行(行业构成联动):抹按钮默认外观,保留 hover/focus(同 .panel-bar--button 立场)。 */
  2119. .panel-donut__row--button {
  2120. width: 100%;
  2121. padding: 0;
  2122. border: 0;
  2123. background: transparent;
  2124. font: inherit;
  2125. text-align: left;
  2126. cursor: pointer;
  2127. }
  2128. .panel-donut__row--button:hover .panel-donut__label,
  2129. .panel-donut__row--button:focus-visible .panel-donut__label {
  2130. color: var(--text-strong, #fff);
  2131. }
  2132. .panel-donut__row--button:focus-visible {
  2133. outline: 1px solid var(--cyan, #01abea);
  2134. outline-offset: 1px;
  2135. }
  2136. .panel-donut__swatch {
  2137. flex: none;
  2138. width: calc(8 * var(--u, 1px));
  2139. height: calc(8 * var(--u, 1px));
  2140. }
  2141. .panel-donut__label {
  2142. /* 票 43(重开):数值紧跟标签成对。`flex: 0 1 auto` 而不是 `1 1 auto`——
  2143. 会生长的标签把自己撑满整行、把数值又顶回行尾(正是维护者说的"离得太远");
  2144. 只收缩才能在"标签+数值"之间零间隙成对,同时窄面板里仍可省略。 */
  2145. flex: 0 1 auto;
  2146. min-width: 0;
  2147. color: var(--text-2, #88b7e8);
  2148. font-size: var(--fs-caption, 10.5px);
  2149. }
  2150. .panel-donut__count {
  2151. flex: none;
  2152. color: var(--text, #d7ecff);
  2153. font-family: var(--num, monospace);
  2154. font-size: var(--fs-caption, 10.5px);
  2155. }
  2156. .panel-stacked__label {
  2157. color: var(--text-2, #88b7e8);
  2158. font-size: var(--fs-caption, 10.5px);
  2159. }
  2160. .panel-stacked__count {
  2161. flex: none;
  2162. color: var(--text, #d7ecff);
  2163. font-family: var(--num, monospace);
  2164. font-size: var(--fs-caption, 10.5px);
  2165. }
  2166. .panel-trend__point {
  2167. display: flex;
  2168. flex: 1 1 0;
  2169. flex-direction: column;
  2170. align-items: center;
  2171. justify-content: flex-end;
  2172. gap: 1px;
  2173. min-width: 0;
  2174. height: 100%;
  2175. }
  2176. .panel-trend__bar {
  2177. display: block;
  2178. width: 100%;
  2179. background: linear-gradient(180deg, var(--cyan, #01abea), var(--cyan-d, #009de9));
  2180. box-shadow: 0 0 6px rgba(1, 171, 234, 0.35);
  2181. }
  2182. .panel-trend__count {
  2183. color: var(--text);
  2184. font-family: var(--num, monospace);
  2185. font-size: var(--fs-caption, 10.5px);
  2186. line-height: 1;
  2187. }
  2188. .panel-trend__label {
  2189. max-width: 100%;
  2190. color: var(--muted, #5680b0);
  2191. font-size: var(--fs-caption, 11.5px);
  2192. line-height: 1.1;
  2193. }
  2194. /* 票 14:状态分布饼图。左图右例;未选中的扇区在"有选中"时降透明度(高亮当前选中)。 */
  2195. .panel-pie {
  2196. display: flex;
  2197. align-items: center;
  2198. gap: var(--space-3, 12px);
  2199. }
  2200. .panel-pie__chart {
  2201. flex: none;
  2202. width: calc(64 * var(--u, 1px));
  2203. height: calc(64 * var(--u, 1px));
  2204. }
  2205. .panel-pie__slice {
  2206. stroke: rgba(2, 10, 20, 0.6);
  2207. stroke-width: 1;
  2208. transition: opacity 0.16s;
  2209. }
  2210. .panel-pie__slice.is-dim {
  2211. opacity: 0.3;
  2212. }
  2213. .panel-pie__legend {
  2214. display: grid;
  2215. gap: 1px;
  2216. margin: 0;
  2217. padding: 0;
  2218. list-style: none;
  2219. min-width: 0;
  2220. }
  2221. .panel-pie__row {
  2222. display: flex;
  2223. align-items: center;
  2224. /* 票 43(重开):数值紧跟标签成对(原 space-between 把数字顶到最右,
  2225. 296px 面板里标签与数值隔整行宽,没有关联感)。与堆叠图例同一贴法。 */
  2226. gap: var(--space-2, 8px);
  2227. min-width: 0;
  2228. border-radius: 2px;
  2229. }
  2230. .panel-pie__row.is-selected .panel-pie__count {
  2231. color: var(--text);
  2232. text-shadow: 0 0 8px rgba(1, 171, 234, 0.6);
  2233. }
  2234. .panel-pie__count {
  2235. flex: none;
  2236. color: var(--text);
  2237. font-family: var(--num, monospace);
  2238. font-size: var(--fs-caption, 10.5px);
  2239. }
  2240. .panel-rows {
  2241. display: grid;
  2242. gap: calc(2 * var(--u, 1px));
  2243. margin: 0;
  2244. }
  2245. .panel-row {
  2246. display: grid;
  2247. grid-template-columns: calc(64 * var(--u, 1px)) minmax(0, 1fr);
  2248. align-items: baseline;
  2249. gap: var(--space-2, 8px);
  2250. border-bottom: 1px dashed rgba(1, 171, 234, 0.14);
  2251. }
  2252. /* 票 55:rows 口径块与共享 bars/donut 图例统一字号、行高和间距。
  2253. 只作用于 rows renderer,避免把面板正文或其它块的字号一起放大/缩小。 */
  2254. .panel-row dt,
  2255. .panel-row dd {
  2256. font-size: var(--fs-caption, 10.5px);
  2257. line-height: 1.35;
  2258. }
  2259. .panel-row dt {
  2260. color: var(--muted, #5680b0);
  2261. }
  2262. .panel-row dd {
  2263. margin: 0;
  2264. color: var(--text, #d7ecff);
  2265. word-break: break-word;
  2266. }
  2267. .panel-list {
  2268. display: grid;
  2269. gap: calc(4 * var(--u, 1px));
  2270. }
  2271. /* 票 40:清单卡升级为「事件卡片」形态(样式参考当前预警面板的事件卡;维护者:
  2272. 卡片高度不需要那么高,主要参考设计及交互效果)。独立卡框 + 内衬底 + 紧凑两列网格,
  2273. 比事件卡更矮(行距与内边距再收一档)。渲染形态只有一份 —— 应急保障/管理单位等
  2274. 同一 list 块的卡片一并升级,不为单个面板长第二套样式。 */
  2275. .panel-item {
  2276. display: grid;
  2277. grid-template-columns: repeat(2, minmax(0, 1fr));
  2278. gap: calc(2 * var(--u, 1px)) calc(8 * var(--u, 1px));
  2279. align-items: start;
  2280. padding: calc(4 * var(--u, 1px)) calc(6 * var(--u, 1px));
  2281. border: 1px solid var(--panel-line, rgba(1, 171, 234, 0.35));
  2282. border-radius: 4px;
  2283. background: var(--inset, rgba(0, 31, 92, 0.5));
  2284. min-width: 0;
  2285. }
  2286. /* 可点卡(台账有 code、异常设备有定位点):交互语言与事件卡同一套 ——
  2287. hover/focus 高亮边框、底色抬亮、左缘压 3px 竖条。 */
  2288. .panel-item--clickable {
  2289. cursor: pointer;
  2290. transition: border-color 0.15s, background-color 0.15s, box-shadow 0.15s;
  2291. }
  2292. .panel-item--clickable:hover,
  2293. .panel-item--clickable:focus-within {
  2294. border-color: var(--cyan-hi, #00b8ff);
  2295. background: rgba(0, 82, 168, 0.5);
  2296. box-shadow: inset 3px 0 0 var(--cyan-hi, #00b8ff);
  2297. }
  2298. .panel-item__head {
  2299. grid-column: 1 / -1;
  2300. display: flex;
  2301. align-items: center;
  2302. justify-content: space-between;
  2303. gap: var(--space-2, 8px);
  2304. }
  2305. /* 原因/徽标/定位按钮行横贯整卡;两条 meta(说明与数据时间)并排一行收矮卡高。 */
  2306. .panel-item__reason,
  2307. .panel-item__badge,
  2308. .panel-item__points {
  2309. grid-column: 1 / -1;
  2310. }
  2311. .panel-item__title {
  2312. min-width: 0;
  2313. color: var(--text, #d7ecff);
  2314. font-size: var(--fs-caption, 10.5px);
  2315. }
  2316. /* 台账卡片标题是可点按钮:外观与文字标题一致,但可聚焦、有 hover。 */
  2317. .panel-item__title--button {
  2318. padding: 0;
  2319. border: 0;
  2320. background: transparent;
  2321. font: inherit;
  2322. font-size: var(--fs-caption, 10.5px);
  2323. text-align: left;
  2324. cursor: pointer;
  2325. }
  2326. .panel-item__title--button:hover,
  2327. .panel-item__title--button:focus-visible {
  2328. color: var(--cyan, #01abea);
  2329. }
  2330. .panel-item__title--button:focus-visible {
  2331. outline: 1px solid var(--cyan, #01abea);
  2332. outline-offset: 1px;
  2333. }
  2334. .panel-item__meta {
  2335. margin: 0;
  2336. color: var(--text-2, #88b7e8);
  2337. font-size: var(--fs-caption, 10.5px);
  2338. }
  2339. /* 状态原因与徽标:低一档亮度,不跟标题抢注意力。 */
  2340. .panel-item__reason,
  2341. .panel-item__badge {
  2342. color: var(--muted, #5680b0);
  2343. font-size: var(--fs-caption, 10.5px);
  2344. }
  2345. .panel-item__points {
  2346. display: flex;
  2347. flex-wrap: wrap;
  2348. gap: var(--space-1, 4px);
  2349. }
  2350. /* 「加载下一页」:与面板内其它按钮同一套视觉。 */
  2351. .panel-more {
  2352. justify-self: start;
  2353. padding: 1px var(--space-2, 8px);
  2354. border: 1px solid rgba(1, 171, 234, 0.4);
  2355. border-radius: calc(8 * var(--u, 1px));
  2356. background: rgba(2, 9, 27, 0.6);
  2357. color: var(--text-2, #88b7e8);
  2358. font: inherit;
  2359. font-size: var(--fs-caption, 10.5px);
  2360. cursor: pointer;
  2361. }
  2362. .panel-more:hover,
  2363. .panel-more:focus-visible {
  2364. border-color: var(--cyan, #01abea);
  2365. color: var(--text);
  2366. }
  2367. /* 可定位按钮:小、可点、有 hover 反馈(它是面板与地图之间唯一的交互面)。 */
  2368. .panel-locate {
  2369. padding: 1px var(--space-2, 8px);
  2370. border: 1px solid rgba(1, 171, 234, 0.45);
  2371. border-radius: calc(8 * var(--u, 1px));
  2372. background: rgba(2, 9, 27, 0.6);
  2373. color: var(--cyan, #01abea);
  2374. font: inherit;
  2375. font-size: var(--fs-caption, 10.5px);
  2376. letter-spacing: 0.04em;
  2377. white-space: nowrap;
  2378. cursor: pointer;
  2379. }
  2380. .panel-locate:hover,
  2381. .panel-locate:focus-visible {
  2382. border-color: var(--cyan, #01abea);
  2383. background: rgba(1, 171, 234, 0.14);
  2384. color: var(--text);
  2385. }
  2386. .panel-block__empty,
  2387. .panel-block__page-note,
  2388. .panel-blocks__placeholder,
  2389. .panel-blocks__missing,
  2390. .is-muted {
  2391. margin: 0;
  2392. color: var(--muted, #5680b0);
  2393. font-size: var(--fs-caption, 10.5px);
  2394. letter-spacing: 0.04em;
  2395. }
  2396. .panel-blocks__missing {
  2397. color: var(--lv2, #ffb020);
  2398. }
  2399. /* ================================================================
  2400. 图表 hover 交互(2026-10-14 维护者走查要求:悬停放大 + 数据详情浮层)。
  2401. 放大 = 各图自己的一档 transform(过渡 0.15s,出离即还原);
  2402. 详情 = 面板内一个共享浮层(.chart-tip),锚在悬停元素上方居中。
  2403. ================================================================ */
  2404. /* 折线数据点:点放大 + 光晕增强(数据点是 5u 的小圆,放大 1.8 倍才可感)。 */
  2405. .panel-trend__node {
  2406. cursor: default;
  2407. transition: transform 0.15s;
  2408. }
  2409. .panel-trend__node:hover {
  2410. transform: translate(-50%, 50%) scale(1.8);
  2411. }
  2412. .panel-trend__node:hover .panel-trend__dot {
  2413. box-shadow: 0 0 10px rgba(1, 171, 234, 1);
  2414. }
  2415. /* 柱列形态:柱体增亮 + 顶部上浮一档(柱是 flex 子项,scale 会糊相邻柱,改用亮度与位移)。 */
  2416. .panel-trend__point {
  2417. cursor: default;
  2418. }
  2419. .panel-trend__point:hover .panel-trend__bar {
  2420. background: linear-gradient(180deg, var(--cyan-hi, #00b8ff), var(--cyan, #01abea));
  2421. box-shadow: 0 0 10px rgba(1, 171, 234, 0.8);
  2422. transform: translateY(-1px);
  2423. }
  2424. /* 堆叠条分段:悬停时实际放大(不挤邻居),并以亮度/描边强化反馈。
  2425. 父条不裁切,避免 scaleY 被吞;分段自身 z-index 覆盖相邻段。 */
  2426. .panel-stacked__seg {
  2427. cursor: default;
  2428. transform-origin: center;
  2429. transition: transform 0.15s, filter 0.15s, box-shadow 0.15s;
  2430. }
  2431. .panel-stacked__seg:hover {
  2432. transform: scaleY(1.8) scaleX(1.04);
  2433. filter: brightness(1.35) saturate(1.2);
  2434. box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7), 0 0 8px var(--seg-color, var(--cyan, #01abea));
  2435. z-index: 1;
  2436. }
  2437. /* 环形图扇区:沿圆心向外放大(transform-origin 在 SVG path 上按各自中点近似取中心即可)。
  2438. 票 43(重开):放大/增亮改由 `.is-hovered` 驱动(联动状态),不再是扇区自己的 `:hover` ——
  2439. 指针事件现在落在透明命中楔形上,可见扇区收不到 hover;命中区与图例行都写同一个联动状态,
  2440. 所以「悬停扇区 ⇄ 悬停图例」两侧同时亮(对齐横条统计的整行热区手感)。
  2441. 票 43(第三轮):放大档从 1.06 加到 **1.1 + 提亮 1.35 + 白描边**——维护者反馈
  2442. 「只出详情、没有放大」;除联动状态本身失效(shallowRef 根因)外,1.06 在 72px 图上
  2443. 只有 ~2px 位移也不够"明显"。命中楔形:透明填充、只接收指针;键盘焦点环画在它身上。 */
  2444. .panel-donut__slice {
  2445. transition: transform 0.15s, filter 0.15s;
  2446. transform-origin: 50px 50px;
  2447. transform-box: view-box;
  2448. }
  2449. .panel-donut__slice.is-hovered {
  2450. transform: scale(1.1);
  2451. filter: brightness(1.35);
  2452. stroke: rgba(255, 255, 255, 0.9);
  2453. stroke-width: 1.5;
  2454. }
  2455. .panel-donut__hit {
  2456. fill: transparent;
  2457. cursor: default;
  2458. stroke: none;
  2459. outline: none;
  2460. transform-box: view-box;
  2461. }
  2462. .panel-donut__hit:focus-visible {
  2463. stroke: rgba(255, 255, 255, 0.85);
  2464. stroke-width: 1.5;
  2465. }
  2466. /* 饼图扇区:同环形(票 14 的 is-dim 选中降透明保留;opacity 过渡不能丢)。
  2467. 票 43(第三轮):放大档同环形加到 1.1 + 提亮 + 白描边(64px 图上 1.05 只有 ~1.5px,不够明显)。 */
  2468. .panel-pie__slice {
  2469. transition: transform 0.15s, filter 0.15s, opacity 0.16s;
  2470. transform-origin: 50px 50px;
  2471. transform-box: view-box;
  2472. }
  2473. .panel-pie__slice.is-hovered {
  2474. transform: scale(1.1);
  2475. filter: brightness(1.35);
  2476. stroke: rgba(255, 255, 255, 0.9);
  2477. stroke-width: 1.5;
  2478. }
  2479. .panel-pie__hit {
  2480. fill: transparent;
  2481. cursor: default;
  2482. stroke: none;
  2483. outline: none;
  2484. transform-box: view-box;
  2485. }
  2486. .panel-pie__hit:focus-visible {
  2487. stroke: rgba(255, 255, 255, 0.85);
  2488. stroke-width: 1.5;
  2489. }
  2490. /* 票 43(重开):图例行联动高亮(与扇区双向)——底色一档 + 文字提亮,
  2491. 与横条统计 `.panel-bar:hover .panel-bar__fill` 同一档可感度。 */
  2492. .panel-donut__row.is-hovered,
  2493. .panel-pie__row.is-hovered {
  2494. background: rgba(1, 171, 234, 0.12);
  2495. }
  2496. .panel-donut__row.is-hovered .panel-donut__label,
  2497. .panel-pie__row.is-hovered .panel-pie__count {
  2498. color: var(--text-strong, #fff);
  2499. }
  2500. /* 条形图行:填充条增亮(行高太小,不位移)。 */
  2501. .panel-bar__fill {
  2502. transition: filter 0.15s;
  2503. }
  2504. .panel-bar:hover .panel-bar__fill {
  2505. filter: brightness(1.3);
  2506. }
  2507. /* 共享详情浮层:锚点在悬停元素上缘中点,自身再向上偏移并水平居中。
  2508. pointer-events:none —— 浮层不拦截鼠标,移开即消失;z-index 盖过图表但不盖面板头。 */
  2509. .chart-tip {
  2510. position: absolute;
  2511. z-index: 5;
  2512. display: flex;
  2513. align-items: center;
  2514. gap: calc(5 * var(--u, 1px));
  2515. padding: calc(3 * var(--u, 1px)) var(--space-2, 8px);
  2516. border: 1px solid var(--tip-color, var(--cyan, #01abea));
  2517. border-radius: calc(8 * var(--u, 1px));
  2518. background: rgba(2, 10, 24, 0.92);
  2519. box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5), 0 0 8px color-mix(in srgb, var(--tip-color, #01abea) 55%, transparent);
  2520. transform: translate(-50%, calc(-100% - 6px));
  2521. pointer-events: none;
  2522. white-space: nowrap;
  2523. }
  2524. .chart-tip__swatch {
  2525. flex: none;
  2526. width: calc(7 * var(--u, 1px));
  2527. height: calc(7 * var(--u, 1px));
  2528. background: var(--tip-color, var(--cyan, #01abea));
  2529. }
  2530. .chart-tip__label {
  2531. color: var(--text-2, #88b7e8);
  2532. font-size: var(--fs-caption, 10.5px);
  2533. }
  2534. .chart-tip__value {
  2535. color: var(--text-strong, #fff);
  2536. font-family: var(--num, monospace);
  2537. font-size: var(--fs-caption, 10.5px);
  2538. }
  2539. </style>