| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140 |
- <!--
- 面板块的**共享渲染层**(票 17 建立,票 18 改名并解除模式耦合)。
- 为什么所有面板共用一个组件(而不是每个模式一套):它们的形状是同一件事 ——
- 几组 KPI + 若干分布/清单/趋势,差异只在字段与文案,而那些**都已经在各模式的
- `domain/*Panels.js` 里翻好了**。组件因此只按 `block.kind` 分派画法,不认识任何后端字段。
- 票 18 的两处改动(都是为了让票 19/20 能接进来,而不是再长一套):
- 1. **改名 `InfrastructurePanel` → `PanelBlocks`**:旧名字带着「基础设施模式」,
- 而它从建立起就是通用块渲染(当时只有那一个模式在用)。名字与职责错位会让下一个模式
- 误以为要另写一个组件 —— 那正是票 17 明确反对的「下游三票再长出十二份实现」。
- 2. **吃算好的 `view` prop,不再按 `panelId` 自己找视图**:分发收进
- `domain/panelViews.js` 的注册表。这样组件与任何模式解耦(它甚至不知道共有几个模式),
- 而「哪个面板有哪些块」各自归本模式的 domain 模块。
- 三条纪律在这里的落点(票 17 立,本票一字不改):
- · 缺值显示占位符(`—`,来自 domain,组件只判断"是不是占位符"来降亮度);
- · 面板自己的成败由 `SectionStateTag` 说(色键与文案只来自 domain/sectionState.js);
- · 接口词汇不上大屏 —— 组件里不出现任何后端字段名或原因码(票 05 的静态守卫盯着这条)。
- `data-*` 是走查断言面:`data-panel-blocks`/`data-panel-id` 定位面板,
- `data-locate-point` 是可定位对象,`data-ledger-card` 是台账卡片,
- `data-pie-*` 是状态分布饼图(票 14:扇区/图例行/选中态;面板不再自带筛选入口),
- `data-panel-block` 是块类型,
- 票 56(二/三轮):顶部口径说明统一折叠进 `data-panel-summary-toggle` /
- `data-panel-summary-details`,每条**保留自己原有的断言面**(折叠只是 `display:none`,
- 文本仍在 DOM 里):`data-panel-gap` 契约缺口、`data-panel-note` 面板口径说明、
- `data-panel-link-note` 联动出口、`data-panel-excluded-note` 口径剔除、
- `data-panel-industry` / `data-panel-type-note` / `data-panel-event-note` /
- `data-panel-range-note` / `data-panel-offline-note` / `data-panel-page-note` /
- `data-locate-note`,另带 `data-summary="<键>"` 供按条目定位。
- -->
- <template>
- <div ref="rootRef" class="panel-blocks" :data-panel-blocks="view.name" :data-panel-id="view.name">
- <!-- 面板级状态(色键与文案只来自 domain/sectionState.js)。票 56 二轮:整条收成折叠入口。 -->
- <header v-if="summaryItems.length" class="panel-blocks__head">
- <!--
- 票 56(第二轮,维护者 2026-10-17):**所有面板**的顶部摘要说明统一收成
- 一个紧凑入口,且**只有点击**才展开/收起(撤销第一轮的 hover 自动展开——
- 大屏上鼠标掠过就弹开,等于没有折叠)。键盘与触屏走原生 button:
- Enter / Space / click 都会触发同一个 toggle,不必另写 keydown。
- 每条摘要**保留自己原有的 `data-*` 断言面**(折叠时 `v-show` 只是隐藏,
- 文本仍在 DOM 里),走查脚本读到的是同一句 domain 文案。
- -->
- <span class="panel-summary-disclosure">
- <button
- type="button"
- class="panel-summary-disclosure__toggle"
- :aria-expanded="summaryOpen"
- :aria-controls="summaryDetailsId"
- :aria-label="`${view.label || '面板'}摘要,${summaryOpen ? '收起' : '展开'}完整说明`"
- data-panel-summary-toggle
- @click="summaryOpen = !summaryOpen"
- >
- <span>{{ view.label || '面板' }}摘要</span><b>{{ summaryStatus }}</b><i aria-hidden="true">{{ summaryOpen ? '收起' : '展开' }}</i>
- </button>
- <span v-show="summaryOpen" :id="summaryDetailsId" class="panel-summary-disclosure__details" data-panel-summary-details>
- <span
- v-for="item in summaryItems"
- :key="item.key"
- :class="item.isGap ? 'panel-blocks__gap' : 'panel-blocks__note'"
- v-bind="item.attrs"
- >{{ item.text }}</span>
- </span>
- </span>
- </header>
- <!-- 还没取过数:说"正在取数",不说"没有数据"(那是在报告一件还没问过的事) -->
- <p v-if="pending" class="panel-blocks__placeholder" data-panel-pending>{{ view.placeholder }}</p>
- <!-- 有内容:按块分派 -->
- <template v-else-if="view.hasContent">
- <section
- v-for="(block, index) in view.blocks"
- :key="`${block.kind}-${index}`"
- class="panel-block"
- :data-panel-block="block.kind"
- >
- <EllipsisText v-if="block.label" tag="h4" class="panel-block__title" :text="block.label" />
- <!-- KPI:等宽数字 + 单位,缺值降亮度显示占位符。
- 2026-10-13 维护者要求:每项读数左侧带语义图标(domain 的 KPI_ICONS
- 按 key 映射,未命中不画);图标 aria-hidden 不参与命名。 -->
- <div
- v-if="block.kind === 'kpi'"
- class="panel-kpis"
- :class="{ 'panel-kpis--two-by-two': block.layout === 'two-by-two' }"
- data-kpi-row
- >
- <!-- 票 38(第 4 条):基础档案的「设施总数 + 管线里程」两项落回同一张
- auto-fit 网格自然同行。
- 票 38 第二轮(反馈第 1 条):整行独占改由 domain 的通用 `wide` 标志声明
- (概览基础设施的管线里程独占一行排版更好看),渲染层不认识任何 key。 -->
- <div
- v-for="item in block.items"
- :key="item.key"
- class="panel-kpi"
- :class="{ 'panel-kpi--wide': item.wide === true }"
- :data-kpi="item.key"
- >
- <!-- 票 37 走查扩展:读数图标走 mask 染语义色(KPI_ICON_COLORS,未命中主蓝)。
- 色画在容器上(currentColor),图标本体是白剪影。 -->
- <span
- v-if="item.icon"
- class="panel-kpi__icon"
- :style="{ color: item.iconColor || 'var(--cyan, #01abea)' }"
- >
- <CockpitIcon :name="`kpi${item.icon[0].toUpperCase()}${item.icon.slice(1)}`" size="calc(14 * var(--u, 1px))" />
- </span>
- <EllipsisText class="panel-kpi__label" :text="item.label" />
- <!--
- 票 20:带 `link` 的读数可点(联动出口)。组件只回答"这一项被点了",
- `link` 是什么、点了去哪,全由 domain 给、外壳翻 —— 组件仍不认识任何 panelId。
- 票 37:读数与标签一样走**共用省略方法**(越界省略 + hover 读全);
- 动作提示用 `hint` 附在完整原文之后,原文因此不会因为要写提示而丢。
- -->
- <EllipsisText
- v-if="item.link"
- tag="button"
- type="button"
- class="panel-kpi__value panel-kpi__value--link"
- :class="{ 'is-muted': isPlaceholder(item.value) }"
- :data-kpi-link="item.link"
- :text="kpiReading(item)"
- :hint="`${item.label}:${item.linkHint || '查看明细'}`"
- @click="emit('panel-action', { kind: 'link', key: item.link })"
- >
- {{ item.value }}<em v-if="item.unit && !isPlaceholder(item.value)"> {{ item.unit }}</em>
- </EllipsisText>
- <EllipsisText
- v-else
- tag="b"
- class="panel-kpi__value"
- :class="{ 'is-muted': isPlaceholder(item.value) }"
- :text="kpiReading(item)"
- >
- {{ item.value }}<em v-if="item.unit && !isPlaceholder(item.value)"> {{ item.unit }}</em>
- </EllipsisText>
- <EllipsisText v-if="item.hint" tag="small" class="panel-kpi__hint" :text="item.hint" />
- </div>
- </div>
- <!--
- 分布:条长按最大项归一(相对长度,不冒充占比)。
- 票 20:带 `action` 的块整行可点(如行业构成 → 写全局行业筛选);
- 组件只转发"哪一行被点了",不解释这个动作的语义。
- 票 37:**只有带得出 `actionKey` 的行**才是按钮 —— 展示名不是后端筛选码,
- 拿它兜底去发参数就是"拿脏值换后端 400";`row.selected` 画选中态(再点即取消)。
- 2026-10-13 维护者要求:条形按行语义着色(行业行=行业色、状态行=语义色、
- 其余=图表色序),一眼对应类别;色值仍只在 tokens.css 声明一份。
- -->
- <div v-else-if="block.kind === 'bars'" class="panel-bars" :class="{ 'panel-bars--clickable': block.action }" data-bar-list>
- <component
- :is="block.action && row.actionKey ? 'button' : 'div'"
- v-for="row in block.rows"
- :key="row.key"
- class="panel-bar"
- :class="{ 'panel-bar--button': block.action && row.actionKey, 'is-active': row.selected }"
- :type="block.action && row.actionKey ? 'button' : null"
- :data-bar="row.key"
- :data-bar-action="block.action || null"
- :data-bar-tone="row.tone || null"
- :data-bar-selected="row.selected ? 'yes' : 'no'"
- :title="block.action && row.actionKey ? `把「${row.label}」设为全局筛选条件` : null"
- @click="block.action && row.actionKey ? emit('panel-action', { kind: block.action, key: row.actionKey }) : null"
- @mouseenter="showChartTip($event, row.label, row.hint || row.count, barColor(row.tone))"
- @mouseleave="hideChartTip"
- >
- <EllipsisText class="panel-bar__label" :text="row.label" />
- <i class="panel-bar__track"><i class="panel-bar__fill" :style="{ width: `${Math.round(row.ratio * 100)}%`, background: barColor(row.tone) }"></i></i>
- <span class="panel-bar__count" data-bar-count>{{ row.count }}</span>
- </component>
- <p v-if="!block.rows.length" class="panel-block__empty" data-bar-empty>该维度当前没有记录</p>
- </div>
- <!--
- 可切换维度的分类柱状图(票 38 · 管理信息「片区 × 行业」)。
- 维度切换是**纯展示状态**(组件本地):两个维度的行都由 domain 预先聚合好,
- 点标签只换 rows,不回后端、不进全局筛选——它回答「同一份计数换哪个维度看」,
- 不是筛选条件(与条形图可点行的 `panel-action` 接缝刻意分开,互不冒充)。
- 柱/条/读数与 `.panel-bar` 同一套画法与色键(barColor + 语义 tone),
- `data-dimbar-*` 与 `data-dim-tab-*` 是走查断言面。
- -->
- <div v-else-if="block.kind === 'dimbars'" class="panel-dimbars" data-dimbars-list>
- <div class="panel-dimbars__tabs" role="group" :aria-label="`${block.label} · 维度切换`">
- <button
- v-for="dim in block.dimensions"
- :key="dim.key"
- type="button"
- class="panel-dimbars__tab"
- :class="{ 'is-active': dim.key === activeDimension(block) }"
- :aria-pressed="dim.key === activeDimension(block)"
- :data-dim-tab="dim.key"
- @click="setDimension(block, dim.key)"
- >
- {{ dim.label }}
- </button>
- </div>
- <div v-if="currentDim(block)" class="panel-bars panel-dimbars__bars">
- <div
- v-for="row in currentDim(block).rows"
- :key="row.key"
- class="panel-bar"
- :data-dimbar="row.key"
- :data-dim-tone="row.tone || null"
- @mouseenter="showChartTip($event, row.label, row.hint || `${row.count}${block.unit}`, barColor(row.tone))"
- @mouseleave="hideChartTip"
- >
- <EllipsisText class="panel-bar__label" :text="row.label" />
- <i class="panel-bar__track"><i class="panel-bar__fill" :style="{ width: `${Math.round(row.ratio * 100)}%`, background: barColor(row.tone) }"></i></i>
- <span class="panel-bar__count" data-dimbar-count>{{ row.count }}</span>
- </div>
- <p v-if="!currentDim(block).rows.length" class="panel-block__empty" data-dimbar-empty>{{ block.emptyText }}</p>
- </div>
- <p v-else class="panel-block__empty" data-dimbar-empty>{{ block.emptyText }}</p>
- </div>
- <!--
- 趋势:分桶序列(桶粒度与顺序由后端给,本层不重采样)。
- 2026-10-13 维护者图表选型:时间趋势优先折线;<2 点或柱形态仍画柱列。
- 折线版式(维护者第三轮:「太丑、线与数值对不上」):
- · Y 轴三档刻度(0/半程/峰值)+ 水平网格线 —— 数值与高度有坐标对应;
- · 面积渐变(主色向下渐隐)+ 平滑折线 + 正圆数据点(HTML 叠层,不被拉伸);
- · 每点顶标注数值(≤8 点全标;多时标峰值),悬停 title 兜底;
- · X 轴保留桶标签行。读数接缝(data-trend-*)不变。
- -->
- <div v-else-if="block.kind === 'trend'" class="panel-trend" :class="{ 'panel-trend--line': block.chart === 'line' && block.linePoints.length >= 2 }" data-trend-list>
- <template v-if="block.chart === 'line' && block.linePoints.length >= 2">
- <div class="panel-trend__chart" data-trend-chart>
- <!-- Y 轴刻度 + 网格线(刻度标签落在左侧留出的槽里) -->
- <div
- v-for="tick in block.yTicks"
- :key="`grid-${tick.pct}`"
- class="panel-trend__grid"
- :class="{ 'is-top': tick.pct === 100 }"
- :style="{ bottom: `${tick.pct}%` }"
- >
- <b class="panel-trend__ytick" data-trend-ytick>{{ tick.value }}</b>
- </div>
- <!-- 绘图区:SVG 与数据点/标签共用同一坐标基准(百分比都相对它) -->
- <div class="panel-trend__plot">
- <svg class="panel-trend__line" viewBox="0 0 100 100" preserveAspectRatio="none" role="img" :aria-label="block.label">
- <polygon class="panel-trend__area" :points="block.areaPoints" />
- <polyline
- class="panel-trend__line-path"
- :points="block.linePoints.map((p) => `${p.x},${p.y}`).join(' ')"
- fill="none"
- />
- </svg>
- <div
- v-for="p in block.linePoints"
- :key="p.key"
- class="panel-trend__node"
- :class="{ 'is-peak': !block.showInlineLabels && p.annotate }"
- :style="{ left: `${p.xPct}%`, bottom: `${p.yPct}%` }"
- :data-trend="p.key"
- :aria-label="`${p.label}:${p.displayText || p.count}`"
- @mouseenter="showChartTip($event, p.label, p.displayText || p.count, 'var(--cyan-hi, #00b8ff)')"
- @mouseleave="hideChartTip"
- >
- <b v-if="p.annotate" class="panel-trend__vlabel" data-trend-count>{{ p.displayText || p.count }}</b>
- <i class="panel-trend__dot" aria-hidden="true"></i>
- </div>
- </div>
- </div>
- <div class="panel-trend__axis" data-trend-axis>
- <EllipsisText
- v-for="(point, i) in block.points"
- :key="point.key"
- tag="span"
- class="panel-trend__label"
- :style="{ left: `${block.linePoints[i] ? block.linePoints[i].xPct : 0}%` }"
- :text="block.axisIndices.includes(i) ? point.label : ''"
- />
- </div>
- </template>
- <template v-else>
- <div
- v-for="point in block.points"
- :key="point.key"
- class="panel-trend__point"
- :data-trend="point.key"
- @mouseenter="showChartTip($event, point.label, point.displayText || point.count, 'var(--cyan, #01abea)')"
- @mouseleave="hideChartTip"
- >
- <i class="panel-trend__bar" :style="{ height: `${Math.max(2, Math.round(point.ratio * 100))}%` }"></i>
- <span class="panel-trend__count" data-trend-count>{{ point.displayText || point.count }}</span>
- <EllipsisText class="panel-trend__label" :text="point.label" />
- </div>
- </template>
- <p v-if="!block.points.length" class="panel-block__empty" data-trend-empty>该窗口没有事件记录</p>
- </div>
- <!--
- 百分比堆叠条(2026-10-13 维护者图表选型:多类别占比)。
- 一根条按占比分段(domain 算好 start/宽度),下方图例逐行列出计数——
- 0 计数的行保留图例(含义可查),不占条宽。
- 2026-10-13 维护者反馈:分段按语义 tone 着色(工单状态=语义色);
- 同语义多行(两个待办态都是 lv3)用 shade 深浅区分。
- 票 37 走查扩展:分段悬停**放大 + 详情浮层**(如「蓝色:待派单:1」);
- 分段是绝对定位的窄条,放大用 scaleY+Y 向拉伸、宽度保持(不挤邻居)。
- 票 43:段间小间隔=`left`/`width` 各自独立取整露出的底色缝(绘制缺陷,
- 非语义边界)。边界改由 domain 的接力取整(`startPct`/`widthPct`,
- 每段起点=上一段终点)给出整数百分点,连续由构造保证。
- -->
- <div v-else-if="block.kind === 'stacked'" class="panel-stacked" data-stacked-list>
- <div class="panel-stacked__bar" role="img" :aria-label="`${block.label}(合计 ${block.total})`">
- <i
- v-for="row in block.rows"
- :key="row.key"
- v-show="row.ratio > 0"
- class="panel-stacked__seg"
- :data-stacked-seg="row.key"
- :style="{
- left: `${row.startPct}%`,
- width: `${row.widthPct}%`,
- '--seg-shade': row.shade,
- '--seg-color': stackedColor(row),
- }"
- :title="`${row.label} ${row.count}`"
- @mouseenter="showChartTip($event, row.label, row.count, stackedColor(row))"
- @mouseleave="hideChartTip"
- ></i>
- </div>
- <ul class="panel-stacked__legend" data-stacked-legend>
- <li
- v-for="row in block.rows"
- :key="row.key"
- class="panel-stacked__row"
- :data-stacked="row.key"
- @mouseenter="showChartTip($event, row.label, row.count, stackedColor(row))"
- @mouseleave="hideChartTip"
- >
- <i class="panel-stacked__swatch" :style="{ '--seg-color': stackedColor(row) }" aria-hidden="true"></i>
- <EllipsisText class="panel-stacked__label" :text="row.label" />
- <b class="panel-stacked__count" data-stacked-count>{{ row.count }}</b>
- </li>
- </ul>
- </div>
- <!--
- 环形图(2026-10-13 维护者反馈:条形图太多易视觉疲劳——「类别占比、类别少」
- 场景改用环形图,中心读数合计)。扇区与图例按语义 tone 着色(行业行=行业色)。
- 带 `action` 时图例行可点(联动写全局筛选,与条形图同一条接缝)。
- 与上方饼图的分工:饼图是点位状态分布专用(选中高亮 + 地图图例同源)。
- 票 43(重开)交互修复:可见环带太窄(42/26),真实指针落在中心孔或
- 环带外都不触发——「只有悬停图例才出详情」的根因。改画**透明整楔命中区**
- (`hitPath`,内半径 0)承接悬停/聚焦,热区与视觉扇区同角度同边界;
- 扇区 ⇄ 图例**双向联动**(任一侧悬停/聚焦,两侧同时亮 + 同一个共享浮层),
- 手感对齐已验收横条统计(整行即热区)。键盘焦点在命中楔形上,
- 可访问名称=「类别:数量」;中心合计 `pointer-events:none` 让位给命中区。
- -->
- <div v-else-if="block.kind === 'donut'" class="panel-donut" data-donut-list>
- <template v-if="block.total > 0">
- <div class="panel-donut__wrap">
- <svg class="panel-donut__chart" viewBox="0 0 100 100" role="group" :aria-label="block.label">
- <path
- v-for="slice in block.slices"
- :key="slice.key"
- class="panel-donut__slice"
- :class="{ 'is-hovered': isChartHovered(chartScope(index, block), slice.key) }"
- :d="slice.path"
- :fill="barColor(slice.tone)"
- :data-donut-slice="slice.key"
- aria-hidden="true"
- ></path>
- <path
- v-for="slice in block.slices"
- :key="`hit-${slice.key}`"
- class="panel-donut__hit"
- :d="slice.hitPath"
- :data-donut-hit="slice.key"
- tabindex="0"
- role="img"
- :aria-label="`${slice.label}:${slice.count}`"
- @mouseenter="showChartTip($event, slice.label, slice.count, barColor(slice.tone)); setChartHover(chartScope(index, block), slice.key)"
- @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
- @focus="showChartTip($event, slice.label, slice.count, barColor(slice.tone)); setChartHover(chartScope(index, block), slice.key)"
- @blur="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
- ></path>
- </svg>
- <b class="panel-donut__total" data-donut-total>{{ block.total }}</b>
- </div>
- <ul class="panel-donut__legend" data-donut-legend>
- <component
- :is="block.action && row.actionKey ? 'button' : 'li'"
- v-for="row in block.rows"
- :key="row.key"
- class="panel-donut__row"
- :class="{ 'panel-donut__row--button': block.action && row.actionKey, 'is-hovered': isChartHovered(chartScope(index, block), row.key) }"
- :type="block.action && row.actionKey ? 'button' : null"
- :data-donut-row="row.key"
- :data-donut-action="block.action || null"
- :title="block.action && row.actionKey ? `把「${row.label}」设为全局筛选条件` : null"
- @click="block.action && row.actionKey ? emit('panel-action', { kind: block.action, key: row.actionKey }) : null"
- @mouseenter="showChartTip($event, row.label, row.count, barColor(row.tone)); setChartHover(chartScope(index, block), row.key)"
- @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), row.key)"
- >
- <i class="panel-donut__swatch" :style="{ background: barColor(row.tone) }" aria-hidden="true"></i>
- <EllipsisText class="panel-donut__label" :text="row.label" />
- <b class="panel-donut__count" data-donut-count>{{ row.count }}</b>
- </component>
- </ul>
- </template>
- <p v-else class="panel-block__empty" data-donut-empty>该维度当前没有记录</p>
- </div>
- <!--
- 票 14:状态分布**饼图**(决策 02 的 Q12/Q19)。消费全局未筛分布(与顶部
- 状态栏共用同一份),只**高亮当前选中**——它不是筛选入口,不可点;
- 全局条件唯一可操作面在地图顶部状态栏。
- 票 43(重开):与环形图同一修复——透明整楔命中区(`hitPath`)承接
- 悬停/聚焦(可见环带太窄是「只有图例触发」的根因),扇区 ⇄ 图例双向
- 联动,浮层与放大两侧同步;外层 svg `role="group"` 让名称可达。
- -->
- <div v-else-if="block.kind === 'pie'" class="panel-pie" data-pie>
- <template v-if="block.ready && block.total > 0">
- <svg class="panel-pie__chart" viewBox="0 0 100 100" role="group" :aria-label="block.label">
- <path
- v-for="slice in block.slices"
- :key="slice.key"
- class="panel-pie__slice"
- :class="{ 'is-dim': !slice.selected && block.hasSelection, 'is-hovered': isChartHovered(chartScope(index, block), slice.key) }"
- :d="slice.path"
- :fill="pieToneColor(slice.tone, slice.key)"
- :data-pie-slice="slice.key"
- :data-pie-selected="slice.selected ? 'yes' : 'no'"
- aria-hidden="true"
- ></path>
- <path
- v-for="slice in block.slices"
- :key="`hit-${slice.key}`"
- class="panel-pie__hit"
- :d="slice.hitPath"
- :data-pie-hit="slice.key"
- tabindex="0"
- role="img"
- :aria-label="`${slice.label}:${slice.count}`"
- @mouseenter="showChartTip($event, slice.label, slice.count, pieToneColor(slice.tone, slice.key)); setChartHover(chartScope(index, block), slice.key)"
- @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
- @focus="showChartTip($event, slice.label, slice.count, pieToneColor(slice.tone, slice.key)); setChartHover(chartScope(index, block), slice.key)"
- @blur="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
- ></path>
- </svg>
- <ul class="panel-pie__legend" data-pie-legend>
- <li
- v-for="row in block.rows"
- :key="row.key"
- class="panel-pie__row"
- :class="{ 'is-selected': row.selected, 'is-hovered': isChartHovered(chartScope(index, block), row.key) }"
- :data-pie-row="row.key"
- :data-pie-selected="row.selected ? 'yes' : 'no'"
- @mouseenter="showChartTip($event, row.label, row.count, pieToneColor(row.tone, row.key)); setChartHover(chartScope(index, block), row.key)"
- @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), row.key)"
- >
- <!--
- 票 39 二轮(维护者走查反馈):图例行与一张图顶部状态栏**同一形态**——
- `legendIconFor` 单源图标 + 文字(StatusTag 传 icon 就只画图标、不画形状,
- 票 46 返工定稿的同一条接缝)。颜色不再走 pieToneColor 这份第二映射:
- StatusTag 的 tone 类解析出完全相同的 token,顶部图例也是这么取色的。
- -->
- <StatusTag :tone="row.tone" :label="row.label" :icon="row.icon || ''" />
- <b class="panel-pie__count" data-pie-count>{{ row.count }}</b>
- </li>
- </ul>
- </template>
- <!-- 全局分布未取到:如实说明(不伪造全零饼图,票 05 的纪律)。 -->
- <p v-else-if="block.note" class="panel-block__empty" data-pie-note>{{ block.note }}</p>
- <!-- 取到了但全为 0:这是真实值,不是缺失。 -->
- <p v-else class="panel-block__empty" data-pie-empty>当前范围内没有监测点与未关联设备</p>
- </div>
- <!-- 键值行 -->
- <dl v-else-if="block.kind === 'rows'" class="panel-rows" data-row-list>
- <div v-for="row in block.rows" :key="row.key" class="panel-row" :data-row="row.key">
- <dt>{{ row.label }}</dt>
- <dd :class="{ 'is-muted': isPlaceholder(row.value) }">{{ row.value }}</dd>
- </div>
- </dl>
- <!--
- 条目清单:两种形态共用这一块 —— 普通条目(带可定位点位)与**台账卡片**
- (整张卡可点,走与地图相同的两级详情)。判据是行上有没有 `code`。
- 票 40:清单卡升级为「事件卡片」形态(样式参考当前预警面板的事件卡,
- 维护者 2026-10-16:卡片高度不需要那么高,主要参考设计及交互效果)。
- 整卡可点:有 `code` 的卡(台账监测点)打开两级详情;没有 `code` 的卡
- (运行概况异常设备)走既有 `locate` 接缝 —— 诚实的三种「点不动」反馈
- + 设备级详情,不新造第二条路径。卡内按钮自己 `.stop`,不二次触发。
- 票 64:行数由 `windowOf(block)` 给 —— domain 声明 `paginate` 的块
- 每页 ≤10(统一分页规范),其余块交出原数组,画法与行为一字不改。
- -->
- <div v-else-if="block.kind === 'list'" class="panel-list" data-item-list>
- <article
- v-for="row in windowOf(block).rows"
- :key="row.key"
- class="panel-item"
- :class="{ 'panel-item--clickable': row.code || (row.points && row.points.length) }"
- :data-item="row.key"
- :data-ledger-card="row.code || null"
- @click="onCardClick(row, $event)"
- >
- <div class="panel-item__head">
- <!-- 有编码的行整张卡可点(台账);没有编码的只有点位按钮可点(票 17 的形态)。
- 票 37:标题与下面三行说明一样走**共用省略方法**(越界省略 + hover 读全)。 -->
- <EllipsisText
- v-if="row.code"
- tag="button"
- type="button"
- class="panel-item__title panel-item__title--button"
- :text="row.name"
- hint="点击打开两级详情"
- @click.stop="emit('open-card', row.code)"
- />
- <EllipsisText v-else class="panel-item__title" :text="row.title" />
- <!-- 色键必带文字(票 06:标签不能只靠颜色);文案由 domain 给 -->
- <StatusTag v-if="row.tone && row.toneLabel" :tone="row.tone" :label="row.toneLabel" />
- </div>
- <EllipsisText v-if="row.meta && row.meta.length" tag="p" class="panel-item__meta" :text="row.meta.join(' · ')" />
- <!-- data-data-time 是走查断言面(票 15 的 t15 闸门按源码钉住 `data-data-time>数据时间` 这个形状,
- 属性与正文必须同行);文本走插槽,title 仍拿完整原文(票 37 的共用省略方法)。 -->
- <EllipsisText v-if="row.dataTime" tag="p" class="panel-item__meta" :text="`数据时间 ${row.dataTime}`" data-data-time>数据时间 {{ row.dataTime }}</EllipsisText>
- <EllipsisText v-if="row.reason" tag="p" class="panel-item__meta panel-item__reason" data-ledger-reason :text="row.reason" />
- <!-- 徽标(票 18 的台账 N+1):取到才画,没取到显示"取数中/未取到",**不画 0** -->
- <p v-if="row.badge" class="panel-item__badge" data-ledger-badge>
- 设备 {{ row.badge.total }} · 报警 {{ row.badge.alarm }} · 预警 {{ row.badge.warning }} · 离线 {{ row.badge.offline }}
- </p>
- <p v-else-if="row.badgePending" class="panel-item__badge is-muted" data-ledger-badge-pending>设备统计取数中…</p>
- <div v-if="row.points && row.points.length" class="panel-item__points">
- <button
- v-for="point in row.points"
- :key="point.key"
- type="button"
- class="panel-locate"
- :data-locate-point="point.pointCode"
- :title="`在地图上定位 ${point.label || point.pointCode}`"
- @click.stop="emit('locate', point.pointCode)"
- >
- 定位 {{ point.pointCode }}
- </button>
- </div>
- </article>
- <!--
- 该段自己的成败:后端 `UNAVAILABLE`(没取到)与 `EMPTY`(确实没有)分得开。
- 它**优先于**下面的空列表提示,且不挂在 `v-else-if` 上 ——
- 原来挂在空列表之后时,两个分支的条件在本契约里互斥
- (有 stateText 就必然没有 rows),那个分支永远不执行,是条死代码。
- -->
- <p v-if="block.stateText" class="panel-block__empty" data-item-state>{{ block.stateText }}</p>
- <p v-else-if="!windowOf(block).total" class="panel-block__empty" data-item-empty>
- {{ block.emptyText }}
- </p>
- <!--
- 票 64:统一分页控件(共享 PagerNav,规范条文 2/3/4/9 + 跳页节)。
- 只有 domain 声明 `paginate`(前端集合分页)或 `serverPage`
- (服务端页码分页,行已是后端当前页)的块才画;一页不画(条文 4,
- 由控件自己按 pageCount>1 收口)。总数取**过滤前**集合长度或后端
- `pagination.total`,不由当前页反推(条文 7)。
- -->
- <PagerNav
- v-if="block.paginate === true || block.serverPage"
- :page="windowOf(block).page"
- :page-count="windowOf(block).pageCount"
- :total="windowOf(block).total"
- :list-name="block.label"
- @page="(target) => gotoListPage(block, target)"
- />
- <!--
- 票 61:翻页区的**如实口径说明**(多窗合并范围「已合并 N 个时间窗 ·
- 已加载 M 条」)。它是口径不是控件——一页不到、控件不画时也要出现,
- 所以不挂在 PagerNav 的 v-if 里。
- -->
- <p v-if="block.pageNote" class="panel-block__page-note" data-list-page-note>{{ block.pageNote }}</p>
- </div>
- </section>
- <!--
- 票 62:「加载下一页监测点」按钮与 `ledger-more` 接缝已退役 ——
- 台账装载改**自动渐进链**(与一张图点位数据同款纪律),界面只对已装载集合
- 做页码分页;「还有没有下一批」由页脚 `pageNote` 如实说明,不再靠一个按钮。
- -->
- <!-- 字段缺失:只说条数,路径进 tooltip(票 11 的立场) -->
- <p
- v-if="view.missing.count"
- class="panel-blocks__missing"
- data-panel-missing
- :data-missing-count="view.missing.count"
- :title="view.missing.tooltip"
- >
- {{ view.missing.text }}
- </p>
- </template>
- <!-- 没内容:如实说(失败/空/不可用各有措辞,来自 domain) -->
- <p v-else class="panel-blocks__placeholder" data-panel-placeholder>{{ view.placeholder }}</p>
- <!-- 图表 hover 详情浮层(票 37 走查扩展):锚在悬停元素上方,pointer-events:none 不挡交互 -->
- <div
- v-if="chartTip"
- class="chart-tip"
- :style="{ left: `${chartTip.x}px`, top: `${chartTip.y}px`, '--tip-color': chartTip.color }"
- data-chart-tip
- >
- <i class="chart-tip__swatch" aria-hidden="true"></i>
- <span class="chart-tip__label">{{ chartTip.label }}</span>
- <b class="chart-tip__value">{{ chartTip.value }}</b>
- </div>
- </div>
- </template>
- <script setup>
- import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
- import { DETAIL_PLACEHOLDER } from '../domain/detailFlow.js'
- import { sectionPending } from '../domain/panelBlocks.js'
- import { pagedRows, readListPage, resetListPages, writeListPage } from '../domain/pagination.js'
- import CockpitIcon from './CockpitIcon.vue'
- import EllipsisText from './EllipsisText.vue'
- import PagerNav from './PagerNav.vue'
- import StatusTag from './StatusTag.vue'
- const props = defineProps({
- /**
- * 算好的视图计划(来自 `domain/panelViews.js` 的 `panelView()`)。
- * 组件**不认识 panelId**:分发归注册表,画法归 `block.kind`。
- */
- view: { type: Object, required: true },
- })
- // 票 62:`ledger-more` 已退役(台账装载是自动链,界面不再有「加载下一页」入口)。
- const emit = defineEmits(['locate', 'open-card', 'badge-needed', 'panel-action', 'list-page'])
- /**
- * 票 40:整卡可点(交互参考「当前预警」的事件卡片)。
- * · 有 `code` 的卡(台账监测点)→ 打开两级详情(与标题按钮同一出口,不新造路径);
- * · 没有 `code` 的卡(运行概况异常设备)→ 走既有 `locate` 接缝:诚实的三种
- * 「点不动」反馈 + 设备级详情(`monitoringStore.locateCode` 的既有分流)。
- * 卡内按钮已 `.stop`,走到这里的就是卡片空白处的点击;再兜一层 closest('button')
- * 防将来有人忘了加 stop —— 双保险便宜,误触发的代价(飞镜头)不便宜。
- */
- function onCardClick(row, event) {
- if (event && event.target && typeof event.target.closest === 'function' && event.target.closest('button')) return
- if (row.code) {
- emit('open-card', row.code)
- return
- }
- const point = (row.points || [])[0]
- if (point) emit('locate', point.pointCode)
- }
- /** 还没取过数(界面说"正在取数",不说"没有数据")。 */
- const pending = computed(() => sectionPending(props.view.section))
- // 票 56(第二轮):所有面板顶部的摘要说明统一折叠,**只有点击**才展开/收起。
- // 只有一份状态(`summaryOpen`):hover / focus 都不碰它,所以鼠标掠过不会弹开、
- // 移开也不会把点击展开的内容收掉。展开态是纯展示状态,不进 store(面板私有、即挂即弃)。
- const summaryOpen = ref(false)
- const summaryDetailsId = computed(() => `panel-summary-details-${props.view.name || 'default'}`)
- /**
- * 折叠进去的摘要条目。三条纪律:
- * · **措辞与有无都由 domain 决定**,渲染层只按字段是否存在决定画不画,不重写、不合成;
- * · 每条**保留原有的 `data-*` 断言面**(走查脚本 t17/t18/t20 读的还是同一个选择器与同一句话)。
- * `v-show` 折叠时元素仍在 DOM 里,文本读得到 —— 折叠只改呈现,不改信息;
- * · 本表是顶部口径说明的**唯一**消费点([t56 闸门](../../.scratch/cockpit-real-data-wayfinding/verify/t56-panel-summary-disclosure.mjs)
- * E 组按 domain 声明双向锁定:domain 新增 `*Note`/`gap`/`note` 而不进这张表 → 判红;表内字段在 domain 已无声明 → 判红)。
- *
- * 后三条(`excludedNote` / `linkNote` / `note`)是第三轮补的:它们和前七条是同一件事,
- * 但渲染层**从来没有消费点**——「故障口径」「两族口径」「联动出口」「口径剔除」这些必须
- * 让值班人员看见的说明实际是隐形的。补法与票 49/50 的裁定一致:与其逐条发现,
- * 不如把完备性做成机制。
- */
- const SUMMARY_SOURCES = [
- { key: 'industry', field: 'industryNote', hook: 'data-panel-industry' },
- { key: 'type', field: 'typeNote', hook: 'data-panel-type-note' },
- { key: 'event', field: 'eventNote', hook: 'data-panel-event-note' },
- { key: 'range', field: 'rangeNote', hook: 'data-panel-range-note' },
- { key: 'gap', field: 'gap', hook: 'data-panel-gap', isGap: true },
- { key: 'offline', field: 'offlineNote', hook: 'data-panel-offline-note' },
- { key: 'page', field: 'pageNote', hook: 'data-panel-page-note' },
- { key: 'locate', field: 'locateNote', hook: 'data-locate-note' },
- { key: 'excluded', field: 'excludedNote', hook: 'data-panel-excluded-note' },
- { key: 'link', field: 'linkNote', hook: 'data-panel-link-note' },
- { key: 'note', field: 'note', hook: 'data-panel-note' },
- // 票 64:运行信息的数据来源与口径说明(设施实体适配器逐条生成、一条指标对应一个设施、
- // 不跨行业合并)。表是顶部说明的唯一消费点——domain 新增说明字段而不进这张表即判红
- // (t56 C 组的双向普查)。
- { key: 'source', field: 'sourceNote', hook: 'data-panel-source-note' },
- ]
- const summaryItems = computed(() =>
- SUMMARY_SOURCES.flatMap(({ key, field, hook, isGap }) => {
- const text = props.view[field]
- if (!text) return []
- return [{ key, text, isGap: Boolean(isGap), attrs: { [hook]: '', 'data-summary': key } }]
- }),
- )
- /**
- * 折叠态也要说得出「有没有要关注的东西」——只留标题会把口径缺口读成「一切正常」。
- * 判据是 domain 的措辞本身(`'当前窗口没有离线设备'` 是 `monitoringPanels.js` 给的无离线句),
- * 渲染层不自己算任何数。`note` / `gap` / `excludedNote` 同属口径说明一档。
- */
- const summaryStatus = computed(() => {
- const view = props.view
- if (view.offlineNote && view.offlineNote !== '当前窗口没有离线设备') return '有离线设备'
- if (view.gap || view.note || view.excludedNote) return '有口径提示'
- return `${summaryItems.value.length} 条说明`
- })
- /**
- * 票 38:维度切换的**组件本地状态**(键=面板名 + 块标题,值=当前维度键)。
- * 为什么放组件而不是 store:「换哪个维度看」不是全局条件(与 industry/district/deviceType
- * 不同层,切模式/刷新都不该动它);两个维度的行 domain 已经算完,这里只决定显示哪一份。
- * 键不用块对象:视图计划每次重算都生成新块对象,按对象记会被刷新顶掉(表现为「切了又跳回」)。
- */
- const dimensionOverrides = ref({})
- function blockKey(block) {
- return `${props.view.name}|${block.label}`
- }
- // ---- 票 64:list 块的统一分页(运行指标列表实测 6772 条,全量直渲=一屏几千张卡)----
- //
- // 四条纪律:
- // · **只有 domain 声明 `paginate` 的块切窗口**:同一个 list 画法服务全仓多处清单,
- // 管理信息明细(实测 9 行)与预警分析重点识别是规范排除项,应急保障另有专票(63),
- // 渲染层不猜、不顺手全翻;未声明的块 `windowOf` 原样交出 `block.rows`(同一引用、零 slice)。
- // · **翻页算术只有一份**(条文 11):窗口、越界夹回、页码寻址与复位全在
- // `domain/pagination.js`(`listPageKey`/`readListPage`/`writeListPage`/`resetListPages`
- // 共用同一个键函数——读写键不一致就会造出「该复位的没复位」这类最难查的漂移),
- // 控件只有 `PagerNav.vue` 一份;本组件不出现 ceil/trunc/rows.slice/自画 pager。
- // · **复位=显式清槽**(条文 5/6):domain 给每块一个 `pageScope`(选中行业 + 片区),
- // 它变了就清掉那一块的页码槽;定时刷新时它不变 ⇒ 页码保持。
- // 第一版把条件编进页码键,被条文 6 判住:切走再切回会读到旧槽位停在第 3 页,
- // 而「切回来」同样是一次条件变化。清槽与票 46/59「换设备清页码」同一条成例。
- // · **数据变少越界由 `pagedRows` 夹回**(条文 6),界面上的页码即夹回后的值。
- const listPages = ref({})
- /**
- * 分页块的条件快照(`{ [块归属键]: pageScope }`)。只收分页块——非分页块没有页码,
- * 也就没有可复位的槽;把它们放进来会让这张表每次刷新都变,白清一次别的块。
- */
- const listScopeSnapshot = computed(() => {
- const snapshot = {}
- for (const block of props.view.blocks || []) {
- if (block.paginate === true) snapshot[blockKey(block)] = block.pageScope || ''
- }
- return snapshot
- })
- // 哪一块的条件变了,就只清那一块的页码(别的面板/别的块不受牵连)。
- // 只按**值**逐块比较:computed 每次重算都是新对象,按对象身份比会把每次刷新都当成换筛选。
- watch(listScopeSnapshot, (next, prev) => {
- const before = prev || {}
- const changed = Object.keys(next).filter((key) => before[key] !== next[key])
- if (changed.length) listPages.value = resetListPages(listPages.value, changed)
- })
- /** 这一块当前生效的页码(没记过=第 1 页)。 */
- function pageOf(block) {
- return readListPage(listPages.value, blockKey(block))
- }
- /**
- * 这一块要画几条:分页块交窗口行,服务端分页块交**后端当前页**,其余块交全部行
- * (引用不变,既有渲染零影响)。两种分页形态共用同一个 PagerNav,只是页码来源不同:
- * · `paginate`(前端集合):渲染层自己的 `listPages` 切窗口;
- * · `serverPage`(服务端页码,票 61):行已是后端当前页,页码/总数取后端回显,
- * 翻页 emit `list-page` 交给 store 发请求。
- */
- function windowOf(block) {
- const rows = block.rows || []
- if (block.serverPage) {
- const page = Number(block.serverPage.page) || 1
- const pageCount = Number(block.serverPage.pageCount) || 1
- const total = Number.isFinite(Number(block.serverPage.total)) ? Number(block.serverPage.total) : rows.length
- return { rows, page, pageCount: Math.max(1, pageCount), total }
- }
- if (block.paginate !== true) return { rows, page: 1, pageCount: 1, total: rows.length }
- return pagedRows(rows, pageOf(block))
- }
- /**
- * 翻页唯一出口(与票 59 同一条):前后页/页码直达/跳页都带**目标页码(1 起)**回来。
- * 前端集合分页只存状态(越界由 domain `pagedRows` 在下一帧夹回);服务端分页把目标页
- * 交给父组件/store 发请求(页码状态在 store,块里不存第二份)。
- */
- function gotoListPage(block, target) {
- if (block.serverPage) {
- emit('list-page', { label: block.label, page: Number(target) || 1 })
- return
- }
- listPages.value = writeListPage(listPages.value, blockKey(block), target)
- }
- function activeDimension(block) {
- const override = dimensionOverrides.value[blockKey(block)]
- if (override && (block.dimensions || []).some((dim) => dim.key === override)) return override
- return block.active || (block.dimensions && block.dimensions[0] ? block.dimensions[0].key : '')
- }
- function currentDim(block) {
- const key = activeDimension(block)
- return (block.dimensions || []).find((dim) => dim.key === key) || null
- }
- function setDimension(block, key) {
- dimensionOverrides.value = { ...dimensionOverrides.value, [blockKey(block)]: key }
- }
- function isPlaceholder(text) {
- return text === DETAIL_PLACEHOLDER
- }
- /**
- * 一条 KPI 读数的**完整原文**(值 + 单位),供共用省略方法当 `title` 用(票 37)。
- * 显示仍走模板里的子元素(单位是独立的 `<em>`,有自己的弱化样式),
- * 但 title 取的是**整条读数**——所以窄列里被省略时,hover 读到的是"12 台"而不是"12"。
- *
- * 票 54:domain 可给 `full` 声明「屏上短读、原文另有其值」(数据时间的短读法 + 完整 ISO)。
- * 渲染层只认这个通用字段,不再按 `item.key` 特判哪个读数是时间(票 38 立的同一条纪律)。
- */
- function kpiReading(item) {
- const unit = item.unit && !isPlaceholder(item.value) ? ` ${item.unit}` : ''
- return `${item.full || item.value}${unit}`
- }
- /**
- * 票 14:饼图扇区的填充色。色键 → token CSS 变量(色值只在 tokens.css 声明一份,
- * 票 06 的单一口径;`var()` 在 SVG 属性里同样生效)。
- */
- function pieToneColor(tone, key) {
- const stateColors = {
- OFFLINE: 'var(--point-offline)',
- EMPTY: 'var(--point-empty)',
- UNKNOWN: 'var(--point-unknown)',
- UNASSOCIATED: 'var(--point-unassociated)',
- }
- if (stateColors[key]) return stateColors[key]
- const map = {
- lv1: 'var(--lv1, #ff3b52)',
- lv2: 'var(--lv2, #ffb020)',
- lv3: 'var(--lv3, #01abea)',
- lv4: 'var(--lv4, #27d17a)',
- }
- return map[tone] || 'var(--neutral, #5680b0)'
- }
- // ---- 图表 hover 交互(2026-10-14 维护者走查要求:悬停放大 + 数据详情浮层)----
- //
- // 三种图(堆叠条分段 / 折线数据点 / 饼·环形扇区)共用**一个**浮层状态与同一个
- // `.chart-tip` 浮层——「悬停放大」由各图自己的 CSS `:hover` / `.is-hovered` 完成,
- // 这里只负责「详情显示什么、浮在哪」。为什么不用原生 `title`:原生提示有 ~1s 延迟、
- // 样式不可控且与「放大」反馈不同步;面板内自绘浮层与 hover 是同一帧的。
- // 纯展示层状态:不进 store(面板私有、即挂即弃,不构成全局条件)。
- /** 当前悬停的图表详情(`{ label, value, color, x, y }`;null = 无悬停)。 */
- const chartTip = ref(null)
- /**
- * 票 43(第三轮根因):联动状态必须用 **shallowRef**,不能用 `ref`。
- * `ref(null)` 存入 `{owner: block}` 后会被 Vue **深层响应式代理**——读回的
- * `chartHover.value.owner` 是 block 的 reactive 代理,而模板 v-for 拿到的 `block`
- * 是原始对象,`===` 恒 false → `.is-hovered` 永不生效(扇区不放大、图例不亮),
- * 而浮层不依赖这个比较所以照常出现——这正是维护者看到的「只有详情、没有联动」。
- * `shallowRef` 不做深层包装(整体替换即触发渲染)。
- * (第四轮进一步发现:光保持身份还不够,见下方 `chartScope`——键根本不该是对象。)
- */
- const chartHover = shallowRef(null)
- /**
- * 票 43(第四轮根因):**不能拿对象身份当键**。外壳时钟每秒 `clockText` 变化会重渲染,
- * 而 `:view="panelViewOf(panelId)"` 是模板里的**方法调用**——每次重渲染都重算出一批全新的
- * block 对象。于是鼠标没动、hover 还在,下一次 tick 的 `block` 已经不是存进去的那个,
- * `owner === block` 静默变 false → 放大与联动高亮"自己消失"(浮层存的是标量所以不受影响,
- * 症状正是「详情还在、高亮掉了」)。
- * 修法=键换成**跨重算稳定的标量 scope**:视图名 + 块序号 + 块类型 + 块标题。
- * 这四样在同一面板同一块的数据刷新前后不变,既不会因重渲染漂移,
- * 也仍区分并排的同类图(视图名/序号不同)与同块内不同行(key 不同)。
- */
- function chartScope(index, block) {
- return `${props.view.name}|${index}|${block.kind}|${block.label || ''}`
- }
- function setChartHover(scope, key) {
- chartHover.value = { scope, key }
- }
- function clearChartHover(scope, key) {
- const current = chartHover.value
- if (current && current.scope === scope && current.key === key) {
- chartHover.value = null
- }
- }
- function isChartHovered(scope, key) {
- const current = chartHover.value
- return !!current && current.scope === scope && current.key === key
- }
- /** 图表元素悬停 → 记录详情与浮层锚点(相对 `.panel-blocks` 根,避免随滚动漂移)。 */
- function showChartTip(event, label, value, color) {
- const root = rootRef.value
- if (!root || !event.currentTarget) return
- const rect = root.getBoundingClientRect()
- const host = event.currentTarget.getBoundingClientRect()
- const x = host.left + host.width / 2 - rect.left
- const y = host.top - rect.top
- // 面板底部/顶部被滚动裁切时,把浮层移到元素下方,避免悬停详情跑出可视区域。
- const top = Math.max(8, y)
- chartTip.value = {
- label,
- value,
- color,
- x,
- y: top,
- }
- }
- /** 离开图表元素:收起浮层(除非已移入浮层本身——浮层 pointer-events:none,不需要)。 */
- function hideChartTip() {
- chartTip.value = null
- }
- /**
- * 分类色键 → CSS 颜色(2026-10-13 维护者要求:图表用行业语义色区分类别)。
- * `industry:*` → 行业 token(与行业切图/地图标记同源);`lv*`/`neutral` → 语义 token;
- * `chart:N` → 图表色序 token。空键回主色(无语义的行仍可辨)。
- */
- const BAR_TONE_COLORS = {
- 'industry:gas': 'var(--industry-gas, #fe3b53)',
- 'industry:water': 'var(--industry-water, #01abea)',
- 'industry:drainage': 'var(--industry-drainage, #27d17a)',
- 'industry:manhole': 'var(--industry-manhole, #a970ff)',
- lv1: 'var(--lv1, #ff3b52)',
- lv2: 'var(--lv2, #ffb020)',
- lv3: 'var(--lv3, #01abea)',
- lv4: 'var(--lv4, #27d17a)',
- neutral: 'var(--neutral, #5680b0)',
- 'point-offline': 'var(--point-offline)',
- 'point-empty': 'var(--point-empty)',
- 'point-unknown': 'var(--point-unknown)',
- 'point-unassociated': 'var(--point-unassociated)',
- 'chart:1': 'var(--chart-c1, #01abea)',
- 'chart:2': 'var(--chart-c2, #00b8ff)',
- 'chart:3': 'var(--chart-c3, #33b3ed)',
- 'chart:4': 'var(--chart-c4, #14c2ff)',
- 'chart:5': 'var(--chart-c5, #27d17a)',
- 'chart:6': 'var(--chart-c6, #ffb020)',
- 'chart:7': 'var(--chart-c7, #a970ff)',
- 'chart:8': 'var(--chart-c8, #5680b0)',
- }
- function barColor(tone) {
- return BAR_TONE_COLORS[tone] || 'var(--cyan, #01abea)'
- }
- /**
- * 堆叠条分段色:语义 tone 优先;该语义在块内**重复出现**时(如工单四个待办态都是 lv3)
- * 用 shade 深浅区分——语义色与图表色按 shade 插值,行间可辨。纯函数(toneShared 由 domain 判定)。
- */
- function stackedColor(row) {
- const base = barColor(row.tone)
- if (!row.toneShared) return base
- return `color-mix(in srgb, ${base} ${Math.round(100 - (row.shade ?? 0.5) * 55)}%, var(--chart-c2, #00b8ff))`
- }
- // ---- 台账卡片的设备徽标:**卡片进视口才要**(票 18 的 Q14 裁定;票 62 收窄到当前窗口)----
- //
- // 为什么必须是"进视口才拉"而不是"一页出来就全拉":台账的分页窗口横跨「监测点 + 未关联设备」,
- // 一页可能几十张卡,而用户常常只看最上面几张;更关键的是**逐点位一次请求**(本端点不给设备编码,
- // 见 `domain/monitoringPanels.js` 的说明),无脑全拉会在一屏之内打出一串并发请求。
- //
- // 票 62:台账接上统一页码分页后,**DOM 里只有当前页窗口的卡片**(≤10 张),
- // 于是"进视口才拉"自然只覆盖当前窗口 —— 翻到第 2 页才会为第 11–20 张要徽标。
- // 这比旧形态(一页 500 张卡都进观察集)负载低一个量级,是票面 Acceptance 第 3 条要的形态。
- //
- // 组件不自己取数(那一层在 store)——它只回答「这张卡露出来了」,
- // 由外壳转给 `monitoringStore.loadLedgerBadge()`。请求过的点位由 store 记账,不会重复要。
- //
- // 注意 ref 挂在**根元素**上而不是那个 `v-for` 里的 `div`:`v-for` 上的 ref 在 Vue 3 里
- // 收回来的是**数组**(每个面板实例一个元素),`listRef.value.querySelectorAll` 会直接抛
- // "not a function" —— 而且抛在 `onMounted` 里会把整页的后续挂载打断
- // (真机走查第一轮就是这样:一个组件报错,A8b/D1/D2/D3 一起红)。
- const rootRef = ref(null)
- let observer = null
- /** 已经交给外面要过徽标的点位(组件这侧也记一份,避免 observer 反复触发同一张卡)。 */
- const requested = new Set()
- function observeCards() {
- if (typeof IntersectionObserver === 'undefined' || !rootRef.value) return
- for (const card of rootRef.value.querySelectorAll('[data-ledger-card]')) {
- const code = card.dataset.ledgerCard || ''
- if (!code || requested.has(code)) continue
- requested.add(code)
- observer.observe(card)
- }
- }
- onMounted(() => {
- if (typeof IntersectionObserver === 'undefined') {
- // 没有 IntersectionObserver 时**退化为全要**:徽标是可选增强,不该因此整块不显示。
- // 票 62:只覆盖**当前页窗口**的行(DOM 窗口与视图窗口同一份 `windowOf`)。
- for (const block of props.view.blocks || []) {
- if (block.kind !== 'list') continue
- for (const row of windowOf(block).rows) {
- if (row.code && !requested.has(row.code)) {
- requested.add(row.code)
- emit('badge-needed', row.code)
- }
- }
- }
- return
- }
- observer = new IntersectionObserver(
- (entries) => {
- for (const entry of entries) {
- if (!entry.isIntersecting) continue
- const code = (entry.target.dataset && entry.target.dataset.ledgerCard) || ''
- // 要过就不再要(store 也会挡一道,这里是省一次事件转发)。
- if (code) emit('badge-needed', code)
- observer.unobserve(entry.target)
- }
- },
- // 提前 80px 触发:等卡片真正露出来再拉,用户会先看到"取数中"再看到数字。
- { rootMargin: '80px 0px' },
- )
- observeCards()
- })
- // 分页追加、或换筛选后重渲染:新卡片要补进观察集。
- watch(
- () => props.view.blocks,
- () => observeCards(),
- { deep: false },
- )
- onBeforeUnmount(() => {
- if (observer) {
- observer.disconnect()
- observer = null
- }
- requested.clear()
- })
- </script>
- <style scoped>
- .panel-summary-disclosure {
- position: relative;
- /* 头部是 flex-wrap 容器:折叠入口独占一行(`flex-basis: 100%`),长句才能在栏宽内换行,
- 不被 max-content 顶出横向溢出(票 13 的「右侧被裁」是同一类缺陷)。 */
- flex: 1 1 100%;
- min-width: 0;
- }
- .panel-summary-disclosure__toggle {
- display: inline-flex;
- align-items: center;
- gap: calc(5 * var(--u, 1px));
- min-height: calc(24 * var(--u, 1px));
- padding: calc(2 * var(--u, 1px)) calc(7 * var(--u, 1px));
- border: 1px solid rgba(1, 171, 234, 0.38);
- border-radius: 3px;
- background: rgba(0, 20, 72, 0.58);
- color: var(--text-2, #88b7e8);
- font: inherit;
- font-size: var(--fs-caption, 10.5px);
- cursor: pointer;
- }
- .panel-summary-disclosure__toggle:hover,
- .panel-summary-disclosure__toggle:focus-visible {
- border-color: var(--cyan-hi, #00b8ff);
- color: var(--text-strong, #fff);
- outline: none;
- }
- .panel-summary-disclosure__toggle b { color: var(--text, #d7ecff); font-weight: 700; }
- .panel-summary-disclosure__toggle i { color: var(--muted, #5680b0); font-style: normal; }
- .panel-summary-disclosure__details {
- display: grid;
- gap: calc(3 * var(--u, 1px));
- margin-top: calc(4 * var(--u, 1px));
- padding: calc(4 * var(--u, 1px)) calc(7 * var(--u, 1px));
- border-left: 2px solid var(--cyan, #01abea);
- background: rgba(0, 20, 72, 0.42);
- }
- /* 展开区内每条摘要自己收口:等宽长串(统计窗口的 ISO 时间)按字符断行,不溢出面板。 */
- .panel-summary-disclosure__details > span {
- min-width: 0;
- overflow-wrap: anywhere;
- }
- .panel-summary-disclosure__details > .panel-blocks__gap {
- margin: 0;
- }
- .panel-blocks,
- .panel-blocks *,
- .panel-blocks *::before,
- .panel-blocks *::after {
- box-sizing: border-box;
- }
- .panel-blocks {
- /* 图表 hover 详情浮层(.chart-tip)的定位基准:坐标按本根元素算。 */
- position: relative;
- display: grid;
- gap: calc(6 * var(--u, 1px));
- align-content: start;
- min-height: 0;
- }
- .panel-blocks__head {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: var(--space-2, 8px);
- }
- .panel-blocks__note {
- margin: 0;
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- }
- /* 契约缺口:用「关注」色而不是错误色 —— 它不是故障,是后端还没有这个字段。 */
- .panel-blocks__gap {
- margin: 0;
- padding: calc(3 * var(--u, 1px)) var(--space-2, 8px);
- border-left: 2px solid var(--lv2, #ffb020);
- background: rgba(255, 176, 32, 0.08);
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- line-height: 1.5;
- }
- .panel-block {
- display: grid;
- gap: calc(3 * var(--u, 1px));
- }
- .panel-block__title {
- margin: 0;
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- font-weight: 700;
- letter-spacing: 0.06em;
- }
- .panel-kpis {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(calc(78 * var(--u, 1px)), 1fr));
- gap: var(--space-2, 8px);
- }
- /* 票 54:监测概览四卡在常规宽度固定 2×2,避免 auto-fit 展开成一行。
- **降级按容器判、不按视口判**:面板栏宽是 `--column-w = clamp(280px, 330u, 460px)`
- (styles/tokens.css:135),`--u = clamp(1px, 0.0694vw, 2px)`。栏内容宽实测:1920 视口
- ≈410px、1440 视口(u 收到 1)≈308px —— 两档都稳在 2×2。写视口 media query 等于写一条
- 在大屏驾驶舱里永不触发的规则,所以这里查的是**容器**宽度。
- **260px 这个阈值的来源**(不是拍的):单格宽 =(容器 − gap 8u)/2;读数行 = 单格 − 卡片
- 左右内边距 16u;`--fs-kpi = 20u` 下「10-12 15:15」11 个等宽字符 ≈ 110u。
- 读数不省略要求 单格 ≥ 126u → 容器 ≥ 2×126 + 8 = 260u。低于它退单列。
- 两档都不横向溢出:列宽 `minmax(0, 1fr)`(不是 auto),卡片内文本走共用省略方法。
- 形状说明:**容器查不了自己的尺寸**(CSS containment 的规则,`container-type` 加在谁身上,
- `@container` 就只对谁的后代生效),所以降级写在子卡上 `grid-column: 1 / -1`
- 横贯两列,而不是改父元素的 `grid-template-columns`。
- `container-type` 只加在本变体上:本元素内没有绝对定位后代(图表 hover 浮层的定位基准
- 仍是 `.panel-blocks` 根),因此不影响其它块的布局。 */
- .panel-kpis--two-by-two {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- container-type: inline-size;
- container-name: panel-kpi-grid;
- }
- @container panel-kpi-grid (max-width: 260px) {
- .panel-kpis--two-by-two .panel-kpi {
- grid-column: 1 / -1;
- }
- }
- .panel-kpi {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- align-items: center;
- column-gap: calc(5 * var(--u, 1px));
- row-gap: 1px;
- padding: calc(4 * var(--u, 1px)) var(--space-2, 8px);
- border: 1px solid rgba(1, 171, 234, 0.24);
- border: 1px solid rgba(1, 171, 234, 0.35);
- /* 2026-10-23 维护者要求:面板背景取消颜色渐变后,颜色渐变转移到数据统计指标卡片 ——
- .panel-kpi 用 tokens 的 --stat-card-bg(亮科技蓝 → 深蓝,色相可感知)。 */
- border-radius: calc(8 * var(--u, 1px));
- background: var(--stat-card-bg, linear-gradient(180deg, rgba(0, 82, 155, 0.45), rgba(0, 28, 70, 0.6)));
- }
- /* KPI 图标:与标签同一行,弱发光(同面板标题图标的弱辉光档)。
- 颜色由容器 currentColor 给(KPI_ICON_COLORS 语义色),mask 图标本体跟随。 */
- .panel-kpi__icon {
- display: inline-flex;
- align-items: center;
- color: var(--cyan, #01abea);
- filter: drop-shadow(0 0 4px rgba(1, 171, 234, 0.45));
- }
- /* 票 37:越界省略与 hover 完整原文都归 `EllipsisText.vue` 的 `.ellipsis-text` 一份声明;
- 本文件里的类只管字体、颜色与排布。 */
- .panel-kpi__label {
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- }
- /* 数值行跨两列(图标 + 标签在上一行,数值独占下一行全宽);
- 口径说明(hint)**同样独占一行**:它若落进第一列(auto 列),
- 会被说明文字把该列撑宽、把 minmax(0,1fr) 的标签列挤到 0 —— 标签被省略成"消失"
- (真机走查 2026-10-14:在线率 / 预警已解除率两张卡的标题整行不见,根因即此)。 */
- .panel-kpi__value,
- .panel-kpi__value--link,
- .panel-kpi__hint {
- grid-column: 1 / -1;
- }
- /* 票 38 第二轮(反馈第 1 条):整行独占**回归**,但改由 domain 的通用 `wide` 标志声明
- (概览基础设施的管线里程数值最长,独占一行排版更好看)。
- 与第一轮删掉的那版区别:那时是渲染层按 `item.key === 'lengthKm'` 特判(第二份布局语义),
- 现在组件只认 `item.wide` 这个通用呈现意图,「谁独占」的决定权在 domain。
- 列布局沿用 HEAD 原样(图标 / 标签 / 右对齐数值三列)——「占一整行」由 `grid-column`
- 表达,不需要顺带改写卡片内部排布。 */
- .panel-kpi--wide {
- grid-column: 1 / -1;
- grid-template-columns: auto minmax(0, 1fr) auto;
- padding: var(--space-2);
- }
- .panel-kpi--wide .panel-kpi__value {
- grid-column: 3;
- text-align: right;
- font-size: var(--fs-kpi);
- }
- .panel-kpi__value {
- min-width: 0;
- color: var(--text-strong, #fff);
- font-family: var(--num, monospace);
- /* 批次 2(票 15):KPI 数值是设计稿最显眼的档位(20px Rajdhani),单列 --fs-kpi。 */
- font-size: var(--fs-kpi, 20px);
- line-height: 1.2;
- }
- /* 票 54:原先这里有一条**时间读数专用降档**(把数据时间压到 caption 字号),
- 它正是「数据时间与上一行四卡不在同一视觉层级」的根因。所有 KPI 数值共用
- `.panel-kpi__value` 一档之后它失去全部消费点,按规格 03「无消费点不提交」整条删除;
- 短读法归 domain 声明(`createdAtKpi`),渲染层不再按 `item.key` 认哪个读数是时间。 */
- .panel-kpi__value em {
- color: var(--muted, #5680b0);
- font-style: normal;
- font-size: var(--fs-caption, 10.5px);
- }
- /* 票 20:可点的 KPI 读数(联动出口)。抹掉按钮默认外观,但保留可聚焦与 hover 反馈 ——
- 与 `.panel-bar--button` 同一立场:可点的东西要在键盘上也能到达。 */
- .panel-kpi__value--link {
- padding: 0;
- border: 0;
- background: transparent;
- font: inherit;
- font-family: var(--num, monospace);
- font-size: var(--fs-kpi, 20px);
- text-align: left;
- cursor: pointer;
- border-bottom: 1px dashed rgba(1, 171, 234, 0.45);
- }
- .panel-kpi__value--link:hover,
- .panel-kpi__value--link:focus-visible {
- color: var(--text-strong);
- border-bottom-color: var(--cyan, #01abea);
- }
- .panel-kpi__value--link:focus-visible {
- outline: 1px solid var(--cyan, #01abea);
- outline-offset: 1px;
- }
- /* 可点分布的 hover 反馈与状态分布行同款(`block.action` 版)。 */
- .panel-bars--clickable .panel-bar--button:hover .panel-bar__count,
- .panel-bars--clickable .panel-bar--button:focus-visible .panel-bar__count {
- color: var(--text);
- }
- .panel-kpi__hint {
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- }
- .panel-bars {
- display: grid;
- gap: calc(2 * var(--u, 1px));
- }
- /* 票 38:dimbars 的维度切换标签 + 柱列。柱/条复用 .panel-bar 的同一套画法,
- 这里只管切换器本身——小胶囊按钮,选中态与顶栏筛选 chips 同一视觉语言(is-active)。 */
- .panel-dimbars {
- display: grid;
- gap: var(--space-2, 8px);
- }
- .panel-dimbars__tabs {
- display: flex;
- gap: calc(4 * var(--u, 1px));
- }
- .panel-dimbars__tab {
- padding: calc(2 * var(--u, 1px)) calc(8 * var(--u, 1px));
- border: 1px solid rgba(1, 171, 234, 0.24);
- border-radius: calc(8 * var(--u, 1px));
- background: var(--inset, rgba(2, 10, 20, 0.55));
- color: var(--text-2, #88b7e8);
- font-family: var(--font-topbar);
- font-size: var(--fs-caption, 10.5px);
- cursor: pointer;
- }
- .panel-dimbars__tab.is-active {
- color: var(--text-strong, #fff);
- border-color: rgba(0, 184, 255, 0.85);
- background: rgba(1, 171, 234, 0.16);
- }
- .panel-dimbars__tab:focus-visible {
- outline: 1px solid var(--cyan, #01abea);
- outline-offset: 2px;
- }
- /* 可点的状态分布行:按钮要抹掉浏览器默认外观,但仍保留可聚焦与 hover 反馈。 */
- .panel-bars--clickable .panel-bar--button {
- width: 100%;
- padding: 1px 0;
- border: 0;
- background: transparent;
- font: inherit;
- text-align: left;
- cursor: pointer;
- }
- .panel-bar--button:hover .panel-bar__label,
- .panel-bar--button:focus-visible .panel-bar__label {
- color: var(--text);
- }
- .panel-bar--button:focus-visible {
- outline: 1px solid var(--cyan, #01abea);
- outline-offset: 1px;
- }
- /* 选中态:整行加一条左边线 + 底色,不改变条长口径(选中不该被读成"计数变了")。
- 票 37 起由 `row.selected` 驱动(此前样式在、值没人传,选中行与未选中行画面上分不出)。 */
- .panel-bar--button.is-active {
- background: rgba(1, 171, 234, 0.12);
- box-shadow: inset 2px 0 0 var(--cyan, #01abea);
- }
- /* 票 44(视觉密度):文字与统计条收紧(8u→4u),读数列同档;行距不变,密度靠"贴紧"而不是"堆挤"。 */
- .panel-bar {
- display: grid;
- grid-template-columns: calc(64 * var(--u, 1px)) minmax(0, 1fr) calc(26 * var(--u, 1px));
- align-items: center;
- gap: calc(4 * var(--u, 1px));
- }
- .panel-bar__label {
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- }
- /* 轨道:5u→6u 并加内衬与 1u 发丝边 —— 条段有明确的容器边界,不再与相邻行的辉光糊在一起。 */
- .panel-bar__track {
- position: relative;
- display: block;
- height: calc(6 * var(--u, 1px));
- background: linear-gradient(180deg, rgba(1, 171, 234, 0.1), rgba(1, 171, 234, 0.22));
- box-shadow: inset 0 0 0 1px rgba(1, 171, 234, 0.12);
- }
- /* 票 44(条段分割):删掉旧的外辉光 `0 0 8px` —— 辉光越过 2u 行距渗进相邻条,
- 正是"条段之间没有明确分割"的直接原因。层次改由填充自身的顶部高光线 + 右端头帽给
- (::after 装饰层,pointer-events:none 不挡悬停;行 hover 的增亮反馈仍归 .panel-bar__fill 本体)。 */
- .panel-bar__fill {
- position: relative;
- display: block;
- height: 100%;
- background: linear-gradient(90deg, var(--cyan-d, #009de9), var(--cyan, #01abea));
- }
- .panel-bar__fill::after {
- content: '';
- position: absolute;
- inset: 0;
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0) 55%);
- box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.85);
- pointer-events: none;
- }
- .panel-bar__count {
- color: var(--text);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- text-align: right;
- }
- /* 趋势:横向分桶的柱列。高度按最大桶归一;0 也留 2% 高度,好让"这个桶存在且为 0"看得出来。 */
- .panel-trend {
- display: flex;
- align-items: flex-end;
- gap: calc(2 * var(--u, 1px));
- height: calc(52 * var(--u, 1px));
- padding-top: calc(2 * var(--u, 1px));
- }
- /* 折线形态(2026-10-13 维护者第三轮美化:带 Y 轴刻度、网格线、面积、点顶数值)。
- 容器改列布局:绘图区 + X 轴标签行;绘图区左侧留刻度槽。 */
- .panel-trend--line {
- display: grid;
- grid-template-rows: minmax(0, 1fr) auto;
- gap: calc(2 * var(--u, 1px));
- height: calc(78 * var(--u, 1px));
- padding: calc(8 * var(--u, 1px)) 0 calc(1 * var(--u, 1px));
- }
- .panel-trend__chart {
- position: relative;
- height: 100%;
- min-height: 0;
- }
- /* 绘图区:左侧让出刻度槽,SVG 与数据点/标签都以它为一个坐标基准。 */
- .panel-trend__plot {
- position: absolute;
- inset: 0 0 0 calc(20 * var(--u, 1px));
- }
- /* 网格线:0/半程/峰值三档;刻度标签落在左侧槽里。 */
- .panel-trend__grid {
- position: absolute;
- left: calc(20 * var(--u, 1px));
- right: 0;
- height: 0;
- border-top: 1px dashed rgba(1, 171, 234, 0.14);
- }
- .panel-trend__grid.is-top {
- border-top-color: rgba(1, 171, 234, 0.3);
- }
- .panel-trend__ytick {
- position: absolute;
- left: calc(-20 * var(--u, 1px));
- top: -0.55em;
- width: calc(17 * var(--u, 1px));
- color: var(--muted, #5680b0);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- font-weight: 400;
- line-height: 1;
- text-align: right;
- }
- /* 折线 + 面积:SVG 铺满绘图区,viewBox 0–100 与百分比坐标同基准。 */
- .panel-trend__line {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- overflow: visible;
- }
- .panel-trend__area {
- fill: rgba(1, 171, 234, 0.12);
- }
- .panel-trend__line-path {
- stroke: var(--cyan, #01abea);
- stroke-width: 1.6;
- stroke-linejoin: round;
- stroke-linecap: round;
- vector-effect: non-scaling-stroke;
- filter: drop-shadow(0 0 3px rgba(1, 171, 234, 0.55));
- }
- /* 数据点(HTML 叠层,保持正圆):以点自身中心对齐坐标。 */
- .panel-trend__node {
- position: absolute;
- width: calc(5 * var(--u, 1px));
- height: calc(5 * var(--u, 1px));
- transform: translate(-50%, 50%);
- }
- .panel-trend__dot {
- display: block;
- width: 100%;
- height: 100%;
- border-radius: 50%;
- background: var(--cyan-hi, #00b8ff);
- box-shadow: 0 0 4px rgba(1, 171, 234, 0.8);
- }
- .panel-trend__node.is-peak .panel-trend__dot {
- background: var(--lv2, #ffb020);
- box-shadow: 0 0 5px rgba(255, 176, 32, 0.9);
- }
- /* 点顶数值标签:悬在点上方居中,等宽数字。 */
- .panel-trend__vlabel {
- position: absolute;
- bottom: calc(100% + 2px);
- left: 50%;
- transform: translateX(-50%);
- color: var(--text, #d7ecff);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- font-weight: 700;
- line-height: 1;
- white-space: nowrap;
- }
- .panel-trend__node.is-peak .panel-trend__vlabel {
- color: var(--lv2, #ffb020);
- }
- /* X 轴标签行:绝对定位到各点 x(与绘图区同基准),按 axisEvery 抽样,首尾必显。 */
- .panel-trend__axis {
- position: relative;
- height: calc(12 * var(--u, 1px));
- margin-left: calc(20 * var(--u, 1px));
- }
- /* 票 37:`white-space: nowrap` 与省略标记一并归 `.ellipsis-text`(唯一一份)。 */
- .panel-trend__axis .panel-trend__label {
- position: absolute;
- top: 0;
- transform: translateX(-50%);
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- line-height: 1.1;
- }
- .panel-trend__axis .panel-trend__label:first-child {
- transform: none;
- }
- .panel-trend__axis .panel-trend__label:last-child {
- transform: translateX(-100%);
- }
- /* 百分比堆叠条(2026-10-13 图表选型:多类别占比)。 */
- .panel-stacked {
- display: grid;
- gap: calc(4 * var(--u, 1px));
- }
- .panel-stacked__bar {
- position: relative;
- display: flex;
- height: calc(8 * var(--u, 1px));
- background: rgba(1, 171, 234, 0.12);
- overflow: visible;
- }
- .panel-stacked__seg {
- position: absolute;
- top: 0;
- bottom: 0;
- /* 2026-10-13:分段色由语义 tone 给(--seg-color,组件注入);shade 兜底删色。 */
- background: var(--seg-color, var(--cyan, #01abea));
- }
- .panel-stacked__legend {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(calc(96 * var(--u, 1px)), 1fr));
- gap: calc(2 * var(--u, 1px)) var(--space-2, 8px);
- margin: 0;
- padding: 0;
- list-style: none;
- }
- .panel-stacked__row {
- display: flex;
- align-items: center;
- gap: calc(4 * var(--u, 1px));
- min-width: 0;
- }
- .panel-stacked__swatch {
- flex: none;
- width: calc(8 * var(--u, 1px));
- height: calc(8 * var(--u, 1px));
- background: var(--seg-color, var(--cyan, #01abea));
- }
- /* 环形图(2026-10-13:条形图疲劳后的占比形态;左图右例、中心合计)。 */
- .panel-donut {
- display: flex;
- align-items: center;
- gap: var(--space-3, 12px);
- min-width: 0;
- }
- .panel-donut__wrap {
- position: relative;
- flex: none;
- width: calc(72 * var(--u, 1px));
- height: calc(72 * var(--u, 1px));
- }
- .panel-donut__chart {
- display: block;
- width: 100%;
- height: 100%;
- }
- .panel-donut__slice {
- stroke: rgba(1, 5, 20, 0.6);
- stroke-width: 1;
- }
- .panel-donut__total {
- position: absolute;
- inset: 0;
- display: grid;
- place-items: center;
- /* 票 43(重开):中心读数让位给整楔命中区——它盖在圆心上方,
- 不关掉指针事件的话「悬停饼图中心」永远触发不了最近的扇区。 */
- pointer-events: none;
- color: var(--text-strong, #fff);
- font-family: var(--num, monospace);
- font-size: var(--fs-body, 14px);
- }
- .panel-donut__legend {
- display: grid;
- gap: calc(2 * var(--u, 1px));
- margin: 0;
- padding: 0;
- list-style: none;
- min-width: 0;
- flex: 1 1 auto;
- }
- .panel-donut__row {
- display: flex;
- align-items: center;
- gap: calc(4 * var(--u, 1px));
- min-width: 0;
- }
- /* 可点图例行(行业构成联动):抹按钮默认外观,保留 hover/focus(同 .panel-bar--button 立场)。 */
- .panel-donut__row--button {
- width: 100%;
- padding: 0;
- border: 0;
- background: transparent;
- font: inherit;
- text-align: left;
- cursor: pointer;
- }
- .panel-donut__row--button:hover .panel-donut__label,
- .panel-donut__row--button:focus-visible .panel-donut__label {
- color: var(--text-strong, #fff);
- }
- .panel-donut__row--button:focus-visible {
- outline: 1px solid var(--cyan, #01abea);
- outline-offset: 1px;
- }
- .panel-donut__swatch {
- flex: none;
- width: calc(8 * var(--u, 1px));
- height: calc(8 * var(--u, 1px));
- }
- .panel-donut__label {
- /* 票 43(重开):数值紧跟标签成对。`flex: 0 1 auto` 而不是 `1 1 auto`——
- 会生长的标签把自己撑满整行、把数值又顶回行尾(正是维护者说的"离得太远");
- 只收缩才能在"标签+数值"之间零间隙成对,同时窄面板里仍可省略。 */
- flex: 0 1 auto;
- min-width: 0;
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- }
- .panel-donut__count {
- flex: none;
- color: var(--text, #d7ecff);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- }
- .panel-stacked__label {
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- }
- .panel-stacked__count {
- flex: none;
- color: var(--text, #d7ecff);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- }
- .panel-trend__point {
- display: flex;
- flex: 1 1 0;
- flex-direction: column;
- align-items: center;
- justify-content: flex-end;
- gap: 1px;
- min-width: 0;
- height: 100%;
- }
- .panel-trend__bar {
- display: block;
- width: 100%;
- background: linear-gradient(180deg, var(--cyan, #01abea), var(--cyan-d, #009de9));
- box-shadow: 0 0 6px rgba(1, 171, 234, 0.35);
- }
- .panel-trend__count {
- color: var(--text);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- line-height: 1;
- }
- .panel-trend__label {
- max-width: 100%;
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 11.5px);
- line-height: 1.1;
- }
- /* 票 14:状态分布饼图。左图右例;未选中的扇区在"有选中"时降透明度(高亮当前选中)。 */
- .panel-pie {
- display: flex;
- align-items: center;
- gap: var(--space-3, 12px);
- }
- .panel-pie__chart {
- flex: none;
- width: calc(64 * var(--u, 1px));
- height: calc(64 * var(--u, 1px));
- }
- .panel-pie__slice {
- stroke: rgba(2, 10, 20, 0.6);
- stroke-width: 1;
- transition: opacity 0.16s;
- }
- .panel-pie__slice.is-dim {
- opacity: 0.3;
- }
- .panel-pie__legend {
- display: grid;
- gap: 1px;
- margin: 0;
- padding: 0;
- list-style: none;
- min-width: 0;
- }
- .panel-pie__row {
- display: flex;
- align-items: center;
- /* 票 43(重开):数值紧跟标签成对(原 space-between 把数字顶到最右,
- 296px 面板里标签与数值隔整行宽,没有关联感)。与堆叠图例同一贴法。 */
- gap: var(--space-2, 8px);
- min-width: 0;
- border-radius: 2px;
- }
- .panel-pie__row.is-selected .panel-pie__count {
- color: var(--text);
- text-shadow: 0 0 8px rgba(1, 171, 234, 0.6);
- }
- .panel-pie__count {
- flex: none;
- color: var(--text);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- }
- .panel-rows {
- display: grid;
- gap: calc(2 * var(--u, 1px));
- margin: 0;
- }
- .panel-row {
- display: grid;
- grid-template-columns: calc(64 * var(--u, 1px)) minmax(0, 1fr);
- align-items: baseline;
- gap: var(--space-2, 8px);
- border-bottom: 1px dashed rgba(1, 171, 234, 0.14);
- }
- /* 票 55:rows 口径块与共享 bars/donut 图例统一字号、行高和间距。
- 只作用于 rows renderer,避免把面板正文或其它块的字号一起放大/缩小。 */
- .panel-row dt,
- .panel-row dd {
- font-size: var(--fs-caption, 10.5px);
- line-height: 1.35;
- }
- .panel-row dt {
- color: var(--muted, #5680b0);
- }
- .panel-row dd {
- margin: 0;
- color: var(--text, #d7ecff);
- word-break: break-word;
- }
- .panel-list {
- display: grid;
- gap: calc(4 * var(--u, 1px));
- }
- /* 票 40:清单卡升级为「事件卡片」形态(样式参考当前预警面板的事件卡;维护者:
- 卡片高度不需要那么高,主要参考设计及交互效果)。独立卡框 + 内衬底 + 紧凑两列网格,
- 比事件卡更矮(行距与内边距再收一档)。渲染形态只有一份 —— 应急保障/管理单位等
- 同一 list 块的卡片一并升级,不为单个面板长第二套样式。 */
- .panel-item {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: calc(2 * var(--u, 1px)) calc(8 * var(--u, 1px));
- align-items: start;
- padding: calc(4 * var(--u, 1px)) calc(6 * var(--u, 1px));
- border: 1px solid var(--panel-line, rgba(1, 171, 234, 0.35));
- border-radius: 4px;
- background: var(--inset, rgba(0, 31, 92, 0.5));
- min-width: 0;
- }
- /* 可点卡(台账有 code、异常设备有定位点):交互语言与事件卡同一套 ——
- hover/focus 高亮边框、底色抬亮、左缘压 3px 竖条。 */
- .panel-item--clickable {
- cursor: pointer;
- transition: border-color 0.15s, background-color 0.15s, box-shadow 0.15s;
- }
- .panel-item--clickable:hover,
- .panel-item--clickable:focus-within {
- border-color: var(--cyan-hi, #00b8ff);
- background: rgba(0, 82, 168, 0.5);
- box-shadow: inset 3px 0 0 var(--cyan-hi, #00b8ff);
- }
- .panel-item__head {
- grid-column: 1 / -1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-2, 8px);
- }
- /* 原因/徽标/定位按钮行横贯整卡;两条 meta(说明与数据时间)并排一行收矮卡高。 */
- .panel-item__reason,
- .panel-item__badge,
- .panel-item__points {
- grid-column: 1 / -1;
- }
- .panel-item__title {
- min-width: 0;
- color: var(--text, #d7ecff);
- font-size: var(--fs-caption, 10.5px);
- }
- /* 台账卡片标题是可点按钮:外观与文字标题一致,但可聚焦、有 hover。 */
- .panel-item__title--button {
- padding: 0;
- border: 0;
- background: transparent;
- font: inherit;
- font-size: var(--fs-caption, 10.5px);
- text-align: left;
- cursor: pointer;
- }
- .panel-item__title--button:hover,
- .panel-item__title--button:focus-visible {
- color: var(--cyan, #01abea);
- }
- .panel-item__title--button:focus-visible {
- outline: 1px solid var(--cyan, #01abea);
- outline-offset: 1px;
- }
- .panel-item__meta {
- margin: 0;
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- }
- /* 状态原因与徽标:低一档亮度,不跟标题抢注意力。 */
- .panel-item__reason,
- .panel-item__badge {
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- }
- .panel-item__points {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-1, 4px);
- }
- /* 「加载下一页」:与面板内其它按钮同一套视觉。 */
- .panel-more {
- justify-self: start;
- padding: 1px var(--space-2, 8px);
- border: 1px solid rgba(1, 171, 234, 0.4);
- border-radius: calc(8 * var(--u, 1px));
- background: rgba(2, 9, 27, 0.6);
- color: var(--text-2, #88b7e8);
- font: inherit;
- font-size: var(--fs-caption, 10.5px);
- cursor: pointer;
- }
- .panel-more:hover,
- .panel-more:focus-visible {
- border-color: var(--cyan, #01abea);
- color: var(--text);
- }
- /* 可定位按钮:小、可点、有 hover 反馈(它是面板与地图之间唯一的交互面)。 */
- .panel-locate {
- padding: 1px var(--space-2, 8px);
- border: 1px solid rgba(1, 171, 234, 0.45);
- border-radius: calc(8 * var(--u, 1px));
- background: rgba(2, 9, 27, 0.6);
- color: var(--cyan, #01abea);
- font: inherit;
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- white-space: nowrap;
- cursor: pointer;
- }
- .panel-locate:hover,
- .panel-locate:focus-visible {
- border-color: var(--cyan, #01abea);
- background: rgba(1, 171, 234, 0.14);
- color: var(--text);
- }
- .panel-block__empty,
- .panel-block__page-note,
- .panel-blocks__placeholder,
- .panel-blocks__missing,
- .is-muted {
- margin: 0;
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- }
- .panel-blocks__missing {
- color: var(--lv2, #ffb020);
- }
- /* ================================================================
- 图表 hover 交互(2026-10-14 维护者走查要求:悬停放大 + 数据详情浮层)。
- 放大 = 各图自己的一档 transform(过渡 0.15s,出离即还原);
- 详情 = 面板内一个共享浮层(.chart-tip),锚在悬停元素上方居中。
- ================================================================ */
- /* 折线数据点:点放大 + 光晕增强(数据点是 5u 的小圆,放大 1.8 倍才可感)。 */
- .panel-trend__node {
- cursor: default;
- transition: transform 0.15s;
- }
- .panel-trend__node:hover {
- transform: translate(-50%, 50%) scale(1.8);
- }
- .panel-trend__node:hover .panel-trend__dot {
- box-shadow: 0 0 10px rgba(1, 171, 234, 1);
- }
- /* 柱列形态:柱体增亮 + 顶部上浮一档(柱是 flex 子项,scale 会糊相邻柱,改用亮度与位移)。 */
- .panel-trend__point {
- cursor: default;
- }
- .panel-trend__point:hover .panel-trend__bar {
- background: linear-gradient(180deg, var(--cyan-hi, #00b8ff), var(--cyan, #01abea));
- box-shadow: 0 0 10px rgba(1, 171, 234, 0.8);
- transform: translateY(-1px);
- }
- /* 堆叠条分段:悬停时实际放大(不挤邻居),并以亮度/描边强化反馈。
- 父条不裁切,避免 scaleY 被吞;分段自身 z-index 覆盖相邻段。 */
- .panel-stacked__seg {
- cursor: default;
- transform-origin: center;
- transition: transform 0.15s, filter 0.15s, box-shadow 0.15s;
- }
- .panel-stacked__seg:hover {
- transform: scaleY(1.8) scaleX(1.04);
- filter: brightness(1.35) saturate(1.2);
- box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.7), 0 0 8px var(--seg-color, var(--cyan, #01abea));
- z-index: 1;
- }
- /* 环形图扇区:沿圆心向外放大(transform-origin 在 SVG path 上按各自中点近似取中心即可)。
- 票 43(重开):放大/增亮改由 `.is-hovered` 驱动(联动状态),不再是扇区自己的 `:hover` ——
- 指针事件现在落在透明命中楔形上,可见扇区收不到 hover;命中区与图例行都写同一个联动状态,
- 所以「悬停扇区 ⇄ 悬停图例」两侧同时亮(对齐横条统计的整行热区手感)。
- 票 43(第三轮):放大档从 1.06 加到 **1.1 + 提亮 1.35 + 白描边**——维护者反馈
- 「只出详情、没有放大」;除联动状态本身失效(shallowRef 根因)外,1.06 在 72px 图上
- 只有 ~2px 位移也不够"明显"。命中楔形:透明填充、只接收指针;键盘焦点环画在它身上。 */
- .panel-donut__slice {
- transition: transform 0.15s, filter 0.15s;
- transform-origin: 50px 50px;
- transform-box: view-box;
- }
- .panel-donut__slice.is-hovered {
- transform: scale(1.1);
- filter: brightness(1.35);
- stroke: rgba(255, 255, 255, 0.9);
- stroke-width: 1.5;
- }
- .panel-donut__hit {
- fill: transparent;
- cursor: default;
- stroke: none;
- outline: none;
- transform-box: view-box;
- }
- .panel-donut__hit:focus-visible {
- stroke: rgba(255, 255, 255, 0.85);
- stroke-width: 1.5;
- }
- /* 饼图扇区:同环形(票 14 的 is-dim 选中降透明保留;opacity 过渡不能丢)。
- 票 43(第三轮):放大档同环形加到 1.1 + 提亮 + 白描边(64px 图上 1.05 只有 ~1.5px,不够明显)。 */
- .panel-pie__slice {
- transition: transform 0.15s, filter 0.15s, opacity 0.16s;
- transform-origin: 50px 50px;
- transform-box: view-box;
- }
- .panel-pie__slice.is-hovered {
- transform: scale(1.1);
- filter: brightness(1.35);
- stroke: rgba(255, 255, 255, 0.9);
- stroke-width: 1.5;
- }
- .panel-pie__hit {
- fill: transparent;
- cursor: default;
- stroke: none;
- outline: none;
- transform-box: view-box;
- }
- .panel-pie__hit:focus-visible {
- stroke: rgba(255, 255, 255, 0.85);
- stroke-width: 1.5;
- }
- /* 票 43(重开):图例行联动高亮(与扇区双向)——底色一档 + 文字提亮,
- 与横条统计 `.panel-bar:hover .panel-bar__fill` 同一档可感度。 */
- .panel-donut__row.is-hovered,
- .panel-pie__row.is-hovered {
- background: rgba(1, 171, 234, 0.12);
- }
- .panel-donut__row.is-hovered .panel-donut__label,
- .panel-pie__row.is-hovered .panel-pie__count {
- color: var(--text-strong, #fff);
- }
- /* 条形图行:填充条增亮(行高太小,不位移)。 */
- .panel-bar__fill {
- transition: filter 0.15s;
- }
- .panel-bar:hover .panel-bar__fill {
- filter: brightness(1.3);
- }
- /* 共享详情浮层:锚点在悬停元素上缘中点,自身再向上偏移并水平居中。
- pointer-events:none —— 浮层不拦截鼠标,移开即消失;z-index 盖过图表但不盖面板头。 */
- .chart-tip {
- position: absolute;
- z-index: 5;
- display: flex;
- align-items: center;
- gap: calc(5 * var(--u, 1px));
- padding: calc(3 * var(--u, 1px)) var(--space-2, 8px);
- border: 1px solid var(--tip-color, var(--cyan, #01abea));
- border-radius: calc(8 * var(--u, 1px));
- background: rgba(2, 10, 24, 0.92);
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5), 0 0 8px color-mix(in srgb, var(--tip-color, #01abea) 55%, transparent);
- transform: translate(-50%, calc(-100% - 6px));
- pointer-events: none;
- white-space: nowrap;
- }
- .chart-tip__swatch {
- flex: none;
- width: calc(7 * var(--u, 1px));
- height: calc(7 * var(--u, 1px));
- background: var(--tip-color, var(--cyan, #01abea));
- }
- .chart-tip__label {
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- }
- .chart-tip__value {
- color: var(--text-strong, #fff);
- font-family: var(--num, monospace);
- font-size: var(--fs-caption, 10.5px);
- }
- </style>
|