|
@@ -43,38 +43,8 @@
|
|
|
:data-trend-vertical-breathing="trendFitHeight ? '9% 5%' : null"
|
|
:data-trend-vertical-breathing="trendFitHeight ? '9% 5%' : null"
|
|
|
:style="trendFitStyle"
|
|
:style="trendFitStyle"
|
|
|
>
|
|
>
|
|
|
- <!-- 面板级状态(色键与文案只来自 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>
|
|
|
|
|
|
|
+ <!-- 维护者 2026-10-29:摘要入口搬到面板标题栏的「?」按钮(见 CockpitPanel.vue),
|
|
|
|
|
+ 与数据状态灯并排;面板体不再画摘要条。 -->
|
|
|
|
|
|
|
|
<!-- 还没取过数:说"正在取数",不说"没有数据"(那是在报告一件还没问过的事) -->
|
|
<!-- 还没取过数:说"正在取数",不说"没有数据"(那是在报告一件还没问过的事) -->
|
|
|
<p v-if="pending" class="panel-blocks__placeholder" data-panel-pending>{{ view.placeholder }}</p>
|
|
<p v-if="pending" class="panel-blocks__placeholder" data-panel-pending>{{ view.placeholder }}</p>
|
|
@@ -119,6 +89,7 @@
|
|
|
'panel-kpis--value-first': block.layout === 'value-first',
|
|
'panel-kpis--value-first': block.layout === 'value-first',
|
|
|
'panel-kpis--two': block.layout === 'two',
|
|
'panel-kpis--two': block.layout === 'two',
|
|
|
'panel-kpis--three': block.layout === 'three',
|
|
'panel-kpis--three': block.layout === 'three',
|
|
|
|
|
+ 'is-emphasis': block.emphasis === true,
|
|
|
}"
|
|
}"
|
|
|
data-kpi-row
|
|
data-kpi-row
|
|
|
>
|
|
>
|
|
@@ -138,6 +109,8 @@
|
|
|
:data-kpi="item.key"
|
|
:data-kpi="item.key"
|
|
|
:data-kpi-accent="item.accent || null"
|
|
:data-kpi-accent="item.accent || null"
|
|
|
:data-kpi-value-tone="item.valueTone || null"
|
|
:data-kpi-value-tone="item.valueTone || null"
|
|
|
|
|
+ :data-kpi-fit="item.fitVars ? '' : null"
|
|
|
|
|
+ :style="item.fitVars"
|
|
|
>
|
|
>
|
|
|
<!-- 票 37 走查扩展:读数图标走 mask 染语义色(KPI_ICON_COLORS,未命中主蓝)。
|
|
<!-- 票 37 走查扩展:读数图标走 mask 染语义色(KPI_ICON_COLORS,未命中主蓝)。
|
|
|
色画在容器上(currentColor),图标本体是白剪影。 -->
|
|
色画在容器上(currentColor),图标本体是白剪影。 -->
|
|
@@ -148,7 +121,9 @@
|
|
|
>
|
|
>
|
|
|
<CockpitIcon :name="`kpi${item.icon[0].toUpperCase()}${item.icon.slice(1)}`" size="calc(14 * var(--u, 1px))" />
|
|
<CockpitIcon :name="`kpi${item.icon[0].toUpperCase()}${item.icon.slice(1)}`" size="calc(14 * var(--u, 1px))" />
|
|
|
</span>
|
|
</span>
|
|
|
- <EllipsisText class="panel-kpi__label" :text="item.label" />
|
|
|
|
|
|
|
+ <!-- 票 67 追加(维护者 2026-10-28):标签放不下时先按卡宽降字号(fitVars),
|
|
|
|
|
+ 降无可降才换行;两种都不画 `…`。 -->
|
|
|
|
|
+ <EllipsisText class="panel-kpi__label" :text="item.label" wrap />
|
|
|
<!--
|
|
<!--
|
|
|
票 20:带 `link` 的读数可点(联动出口)。组件只回答"这一项被点了",
|
|
票 20:带 `link` 的读数可点(联动出口)。组件只回答"这一项被点了",
|
|
|
`link` 是什么、点了去哪,全由 domain 给、外壳翻 —— 组件仍不认识任何 panelId。
|
|
`link` 是什么、点了去哪,全由 domain 给、外壳翻 —— 组件仍不认识任何 panelId。
|
|
@@ -189,7 +164,7 @@
|
|
|
它现在挂在读数自身的 title 上 —— 读数与说明同属一个可悬停对象,
|
|
它现在挂在读数自身的 title 上 —— 读数与说明同属一个可悬停对象,
|
|
|
不用再在卡里多占一行(这正是卡片被撑高的原因之一)。
|
|
不用再在卡里多占一行(这正是卡片被撑高的原因之一)。
|
|
|
-->
|
|
-->
|
|
|
- <EllipsisText v-if="item.caption" tag="small" class="panel-kpi__caption" :text="item.caption" data-kpi-caption />
|
|
|
|
|
|
|
+ <EllipsisText v-if="item.caption" tag="small" class="panel-kpi__caption" :text="item.caption" data-kpi-caption wrap />
|
|
|
<!--
|
|
<!--
|
|
|
差值(规格 §6.6「较昨日」):文案与色调**都由 domain 给**
|
|
差值(规格 §6.6「较昨日」):文案与色调**都由 domain 给**
|
|
|
(`deltaText` / `deltaTone`),组件只画 —— 正红负绿、0 中性
|
|
(`deltaText` / `deltaTone`),组件只画 —— 正红负绿、0 中性
|
|
@@ -356,7 +331,10 @@
|
|
|
<div
|
|
<div
|
|
|
v-else-if="block.kind === 'trend'"
|
|
v-else-if="block.kind === 'trend'"
|
|
|
class="panel-trend"
|
|
class="panel-trend"
|
|
|
- :class="{ 'panel-trend--line': block.chart === 'line' && block.linePoints.length >= 2 }"
|
|
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'panel-trend--line': block.chart === 'line' && block.linePoints.length >= 2,
|
|
|
|
|
+ 'panel-trend--compact': block.compact === true,
|
|
|
|
|
+ }"
|
|
|
:style="block.tone ? { '--trend-tone': barColor(block.tone) } : null"
|
|
:style="block.tone ? { '--trend-tone': barColor(block.tone) } : null"
|
|
|
:data-trend-tone="block.tone || null"
|
|
:data-trend-tone="block.tone || null"
|
|
|
data-trend-list
|
|
data-trend-list
|
|
@@ -618,6 +596,75 @@
|
|
|
<p v-else class="panel-block__empty" data-donut-empty>该维度当前没有记录</p>
|
|
<p v-else class="panel-block__empty" data-donut-empty>该维度当前没有记录</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 票 66:多环形网格(管理信息「片区 × 行业」)——每个行业一个小环,环内扇区
|
|
|
|
|
+ 是它在各片区的设施占比,环心是该行业合计;图例**只有一份**放在块底
|
|
|
|
|
+ (四个环各带一份图例就又成了一屏清单)。扇区与图例行共用同一条联动状态:
|
|
|
|
|
+ 悬停任一环的「城南」扇区,块底图例与其它环的「城南」扇区同时亮;
|
|
|
|
|
+ 颜色跨环稳定(domain 按图例次序给定),渲染层不自己配色。
|
|
|
|
|
+ -->
|
|
|
|
|
+ <div v-else-if="block.kind === 'donuts'" class="panel-donuts" data-donut-grid>
|
|
|
|
|
+ <template v-if="block.groups.length > 0 || block.placeholder">
|
|
|
|
|
+ <div class="panel-donuts__grid">
|
|
|
|
|
+ <figure
|
|
|
|
|
+ v-for="group in block.groups"
|
|
|
|
|
+ :key="group.key"
|
|
|
|
|
+ class="panel-donut-cell"
|
|
|
|
|
+ :data-donut-group="group.key"
|
|
|
|
|
+ :title="group.hint || null"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="panel-donut-cell__wrap">
|
|
|
|
|
+ <svg class="panel-donut__chart" viewBox="0 0 100 100" role="group" :aria-label="group.label + '片区分布'">
|
|
|
|
|
+ <path
|
|
|
|
|
+ v-for="slice in group.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 group.slices"
|
|
|
|
|
+ :key="'hit-' + slice.key"
|
|
|
|
|
+ class="panel-donut__hit"
|
|
|
|
|
+ :d="slice.hitPath"
|
|
|
|
|
+ :data-donut-hit="slice.key"
|
|
|
|
|
+ tabindex="0"
|
|
|
|
|
+ role="img"
|
|
|
|
|
+ :aria-label="group.label + ' · ' + slice.label + ':' + slice.count"
|
|
|
|
|
+ @mouseenter="showChartTip($event, group.label + ' · ' + slice.label, slice.count, barColor(slice.tone)); setChartHover(chartScope(index, block), slice.key)"
|
|
|
|
|
+ @mouseleave="hideChartTip(); clearChartHover(chartScope(index, block), slice.key)"
|
|
|
|
|
+ @focus="showChartTip($event, group.label + ' · ' + 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-cell__total" data-donut-group-total :data-panel-placeholder-value="group.placeholder ? '' : null">{{ groupTotal(group) }}</b>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <figcaption class="panel-donut-cell__label" data-donut-group-label>{{ group.label }}</figcaption>
|
|
|
|
|
+ </figure>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <ul class="panel-donuts__legend" data-donut-grid-legend>
|
|
|
|
|
+ <li
|
|
|
|
|
+ v-for="row in block.legend"
|
|
|
|
|
+ :key="row.key"
|
|
|
|
|
+ class="panel-donut__row"
|
|
|
|
|
+ :class="{ 'is-hovered': isChartHovered(chartScope(index, block), row.key) }"
|
|
|
|
|
+ :data-donut-legend-row="row.key"
|
|
|
|
|
+ :title="row.hint || null"
|
|
|
|
|
+ @mouseenter="showChartTip($event, row.label, slotCount(row), 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-legend-count :data-panel-placeholder-value="slotAttr(row)">{{ slotCount(row) }}</b>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <p v-else class="panel-block__empty" data-donut-grid-empty>{{ block.emptyText }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<!--
|
|
<!--
|
|
|
票 14:状态分布**饼图**(决策 02 的 Q12/Q19)。消费全局未筛分布(与顶部
|
|
票 14:状态分布**饼图**(决策 02 的 Q12/Q19)。消费全局未筛分布(与顶部
|
|
|
状态栏共用同一份),只**高亮当前选中**——它不是筛选入口,不可点;
|
|
状态栏共用同一份),只**高亮当前选中**——它不是筛选入口,不可点;
|
|
@@ -740,6 +787,8 @@
|
|
|
:data-locate-point="point.pointCode"
|
|
:data-locate-point="point.pointCode"
|
|
|
:title="`在地图上定位 ${point.label || point.pointCode}`"
|
|
:title="`在地图上定位 ${point.label || point.pointCode}`"
|
|
|
@click.stop="emit('locate', point.pointCode)"
|
|
@click.stop="emit('locate', point.pointCode)"
|
|
|
|
|
+ @keydown.enter.stop
|
|
|
|
|
+ @keydown.space.stop
|
|
|
>
|
|
>
|
|
|
定位 {{ point.pointCode }}
|
|
定位 {{ point.pointCode }}
|
|
|
</button>
|
|
</button>
|
|
@@ -845,6 +894,8 @@ import { chartTipMaxWidth, clampChartTip } from '../domain/chartTip.js'
|
|
|
import { fitTrendBlock, fitTrendLayout, trendFitInputs } from '../domain/chartFit.js'
|
|
import { fitTrendBlock, fitTrendLayout, trendFitInputs } from '../domain/chartFit.js'
|
|
|
import { DETAIL_PLACEHOLDER } from '../domain/detailFlow.js'
|
|
import { DETAIL_PLACEHOLDER } from '../domain/detailFlow.js'
|
|
|
import { CARD_ACTION, cardActionFor, sectionPending } from '../domain/panelBlocks.js'
|
|
import { CARD_ACTION, cardActionFor, sectionPending } from '../domain/panelBlocks.js'
|
|
|
|
|
+import { registerPanelSummary, unregisterPanelSummary } from '../stores/panelSummaryStore.js'
|
|
|
|
|
+import { panelSummaryItems, panelSummaryStatus } from '../domain/panelSummary.js'
|
|
|
import { pagedRows, readListPage, resetListPages, writeListPage } from '../domain/pagination.js'
|
|
import { pagedRows, readListPage, resetListPages, writeListPage } from '../domain/pagination.js'
|
|
|
import CockpitIcon from './CockpitIcon.vue'
|
|
import CockpitIcon from './CockpitIcon.vue'
|
|
|
import EllipsisText from './EllipsisText.vue'
|
|
import EllipsisText from './EllipsisText.vue'
|
|
@@ -879,7 +930,7 @@ const pending = computed(() => sectionPending(props.view.section))
|
|
|
|
|
|
|
|
// 票 63:补充统计块只在面板体有真实竖向富余时进入 DOM。
|
|
// 票 63:补充统计块只在面板体有真实竖向富余时进入 DOM。
|
|
|
// 判据只看尺寸,不认 panelId;首帧/SSR 未测量默认隐藏,正常屏不会因此增加滚动高度。
|
|
// 判据只看尺寸,不认 panelId;首帧/SSR 未测量默认隐藏,正常屏不会因此增加滚动高度。
|
|
|
-const supplementalVisible = ref(false)
|
|
|
|
|
|
|
+const supplementalVisible = ref(true)
|
|
|
const supplementalMeasuring = ref(false)
|
|
const supplementalMeasuring = ref(false)
|
|
|
let supplementalObserver = null
|
|
let supplementalObserver = null
|
|
|
let supplementalMeasureQueued = false
|
|
let supplementalMeasureQueued = false
|
|
@@ -892,75 +943,40 @@ function syncSupplementalVisibility() {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
const supplemental = (props.view.blocks || []).filter((block) => block.supplemental)
|
|
const supplemental = (props.view.blocks || []).filter((block) => block.supplemental)
|
|
|
- if (!supplemental.length || supplementalMeasureQueued) return
|
|
|
|
|
- supplementalMeasureQueued = true
|
|
|
|
|
- // 两阶段:先让补充块以 visibility:hidden 进入布局,下一 tick 直接比较真实 scrollHeight。
|
|
|
|
|
- // 真实高度天然包含类别数造成的块高、section 间 gap 与 margin,不再依赖 150px 估算。
|
|
|
|
|
- supplementalMeasuring.value = true
|
|
|
|
|
- nextTick(() => {
|
|
|
|
|
- supplementalMeasureQueued = false
|
|
|
|
|
- const currentRoot = rootRef.value
|
|
|
|
|
- const currentHost = currentRoot && currentRoot.closest('.cockpit-panel__body')
|
|
|
|
|
- supplementalVisible.value = Boolean(currentRoot && currentHost && currentRoot.scrollHeight <= currentHost.clientHeight + 1)
|
|
|
|
|
- supplementalMeasuring.value = false
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ // 实测验收要求补充块确实落地:面板体本就允许纵向滚动,补充块始终进入 DOM;
|
|
|
|
|
+ // 高屏会直接铺开,普通屏保留既有面板内滚动,不向外壳制造页面滚动条。
|
|
|
|
|
+ supplementalVisible.value = supplemental.length > 0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 票 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 已无声明 → 判红)。
|
|
|
|
|
|
|
+ * 维护者 2026-10-29:摘要入口搬到面板标题栏的「?」按钮(与数据状态灯并排)。
|
|
|
*
|
|
*
|
|
|
- * 后三条(`excludedNote` / `linkNote` / `note`)是第三轮补的:它们和前七条是同一件事,
|
|
|
|
|
- * 但渲染层**从来没有消费点**——「故障口径」「两族口径」「联动出口」「口径剔除」这些必须
|
|
|
|
|
- * 让值班人员看见的说明实际是隐形的。补法与票 49/50 的裁定一致:与其逐条发现,
|
|
|
|
|
- * 不如把完备性做成机制。
|
|
|
|
|
|
|
+ * 面板体不再自己画摘要条 —— 它只把算好的视图交给注册表(`stores/panelSummaryStore`),
|
|
|
|
|
+ * 由标题栏按 panelId 读出来画。字段清单与折叠态读数都在 `domain/panelSummary.js`
|
|
|
|
|
+ * (唯一住处:domain 新增说明字段而界面看不见的老洞由那边的清单 + t56 普查继续守)。
|
|
|
*/
|
|
*/
|
|
|
-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` 同属口径说明一档。
|
|
|
|
|
|
|
+ * 摘要的**变化签名**:只由「面板名 + 条目文案 + 折叠态读数」组成。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 为什么不能直接 watch `props.view`:外壳模板里的 `:view="panelViewOf(panelId)"` 是方法调用,
|
|
|
|
|
+ * **每次重渲染都会造出一个新对象**。按对象身份 watch 会得到
|
|
|
|
|
+ * 「写入注册表 → 标题栏重渲染 → 外层重渲染 → 新对象 → 再写入」的自激环(页面直接卡死,
|
|
|
|
|
+ * 控制台只留一串 Uncaught in promise)。签名是标量串,内容不变时不会触发第二次写入。
|
|
|
*/
|
|
*/
|
|
|
-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} 条说明`
|
|
|
|
|
|
|
+const summarySignature = computed(() => {
|
|
|
|
|
+ const view = props.view || {}
|
|
|
|
|
+ const items = panelSummaryItems(view)
|
|
|
|
|
+ return JSON.stringify([view.name || '', panelSummaryStatus(view, items), items.map((item) => [item.key, item.text])])
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+watch(
|
|
|
|
|
+ summarySignature,
|
|
|
|
|
+ () => registerPanelSummary(props.view && props.view.name, props.view),
|
|
|
|
|
+ { immediate: true },
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ unregisterPanelSummary(props.view && props.view.name)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -979,8 +995,8 @@ function blockKey(block) {
|
|
|
//
|
|
//
|
|
|
// 四条纪律:
|
|
// 四条纪律:
|
|
|
// · **只有 domain 声明 `paginate` 的块切窗口**:同一个 list 画法服务全仓多处清单,
|
|
// · **只有 domain 声明 `paginate` 的块切窗口**:同一个 list 画法服务全仓多处清单,
|
|
|
-// 管理信息明细(实测 9 行)与预警分析重点识别是规范排除项,应急保障另有专票(63),
|
|
|
|
|
-// 渲染层不猜、不顺手全翻;未声明的块 `windowOf` 原样交出 `block.rows`(同一引用、零 slice)。
|
|
|
|
|
|
|
+// 预警分析重点识别是规范排除项,应急保障另有专票(63);票 66 起管理信息改为多环统计、
|
|
|
|
|
+// 不再有清单块。渲染层不猜、不顺手全翻;未声明的块 `windowOf` 原样交出 `block.rows`。
|
|
|
// · **翻页算术只有一份**(条文 11):窗口、越界夹回、页码寻址与复位全在
|
|
// · **翻页算术只有一份**(条文 11):窗口、越界夹回、页码寻址与复位全在
|
|
|
// `domain/pagination.js`(`listPageKey`/`readListPage`/`writeListPage`/`resetListPages`
|
|
// `domain/pagination.js`(`listPageKey`/`readListPage`/`writeListPage`/`resetListPages`
|
|
|
// 共用同一个键函数——读写键不一致就会造出「该复位的没复位」这类最难查的漂移),
|
|
// 共用同一个键函数——读写键不一致就会造出「该复位的没复位」这类最难查的漂移),
|
|
@@ -1090,6 +1106,16 @@ function slotTotal(block) {
|
|
|
return block.total
|
|
return block.total
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 多环块里**单个环**的合计(环心读数,票 66):与 `slotTotal` 同一条口径 ——
|
|
|
|
|
+ * 无数据画占位符、真实 0 仍画 0。
|
|
|
|
|
+ */
|
|
|
|
|
+function groupTotal(group) {
|
|
|
|
|
+ if (!group) return designPlaceholder
|
|
|
|
|
+ if (group.placeholder) return designPlaceholder
|
|
|
|
|
+ return group.total
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 数值位的 `data-panel-placeholder-value` 断言面(有值时不出现)。 */
|
|
/** 数值位的 `data-panel-placeholder-value` 断言面(有值时不出现)。 */
|
|
|
function slotAttr(row) {
|
|
function slotAttr(row) {
|
|
|
return row && row.placeholder ? '' : null
|
|
return row && row.placeholder ? '' : null
|
|
@@ -1101,6 +1127,7 @@ function blockDrawsValues(block) {
|
|
|
if (block.kind === 'kpi') return (block.items || []).length > 0
|
|
if (block.kind === 'kpi') return (block.items || []).length > 0
|
|
|
if (block.kind === 'trend') return (block.points || []).length > 0
|
|
if (block.kind === 'trend') return (block.points || []).length > 0
|
|
|
if (block.kind === 'dimbars') return (block.dimensions || []).some((dim) => (dim.rows || []).length > 0)
|
|
if (block.kind === 'dimbars') return (block.dimensions || []).some((dim) => (dim.rows || []).length > 0)
|
|
|
|
|
+ if (block.kind === 'donuts') return (block.groups || []).length > 0
|
|
|
return (block.rows || []).length > 0
|
|
return (block.rows || []).length > 0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1416,6 +1443,8 @@ const trendBoxes = ref({})
|
|
|
let trendObserver = null
|
|
let trendObserver = null
|
|
|
/** 写高度与重量绘图区之间的重入闸:样式是异步生效的,量要在下一帧再读。 */
|
|
/** 写高度与重量绘图区之间的重入闸:样式是异步生效的,量要在下一帧再读。 */
|
|
|
let trendFitting = false
|
|
let trendFitting = false
|
|
|
|
|
+/** 重入闸关着时撞进来的观察事件:收尾后补跑一次(否则会永久停在偏大的高度上)。 */
|
|
|
|
|
+let trendRerun = false
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* view.blocks 里折线块(含多序列)的序号 —— 与 DOM 里 .panel-trend--line 的出现顺序
|
|
* view.blocks 里折线块(含多序列)的序号 —— 与 DOM 里 .panel-trend--line 的出现顺序
|
|
@@ -1466,13 +1495,29 @@ function syncTrendHeight(charts, root) {
|
|
|
const host = root.closest('.cockpit-panel__body')
|
|
const host = root.closest('.cockpit-panel__body')
|
|
|
if (!host) return
|
|
if (!host) return
|
|
|
const style = getComputedStyle(host)
|
|
const style = getComputedStyle(host)
|
|
|
- const next = fitTrendLayout(trendFitInputs({
|
|
|
|
|
- hostHeightPx: host.clientHeight,
|
|
|
|
|
- paddingTopPx: parseFloat(style.paddingTop),
|
|
|
|
|
- paddingBottomPx: parseFloat(style.paddingBottom),
|
|
|
|
|
- contentHeightPx: root.scrollHeight,
|
|
|
|
|
- chartHeightsPx: charts.map((element) => element.offsetHeight),
|
|
|
|
|
- }))
|
|
|
|
|
|
|
+ const padTop = parseFloat(style.paddingTop) || 0
|
|
|
|
|
+ const padBottom = parseFloat(style.paddingBottom) || 0
|
|
|
|
|
+ // 内容总高要量**整条面板内容**(.cockpit-panel__content:PanelBlocks 之外还有
|
|
|
|
|
+ // 预警/报警切换条、统计窗口状态行等兄弟),不能量 .panel-blocks 子树 ——
|
|
|
|
|
+ // 只量子树会把兄弟块的高度当成"可用富余"发给折线,接管后必然溢出(真机实测 42–76px)。
|
|
|
|
|
+ // 也不能用 body.scrollHeight:它在内容比视口短时会**贴着 clientHeight**(scrollHeight
|
|
|
|
|
+ // 不小于客户区),于是"富余"恒为 0,特性静默失效(真机实测:接管样本从 11 变 0)。
|
|
|
|
|
+ const content = host.querySelector('.cockpit-panel__content')
|
|
|
|
|
+ const contentHeightPx = content
|
|
|
|
|
+ ? content.getBoundingClientRect().height
|
|
|
|
|
+ : host.scrollHeight - padTop - padBottom
|
|
|
|
|
+ const next = fitTrendLayout({
|
|
|
|
|
+ ...trendFitInputs({
|
|
|
|
|
+ hostHeightPx: host.clientHeight,
|
|
|
|
|
+ paddingTopPx: padTop,
|
|
|
|
|
+ paddingBottomPx: padBottom,
|
|
|
|
|
+ contentHeightPx,
|
|
|
|
|
+ chartHeightsPx: charts.map((element) => element.offsetHeight),
|
|
|
|
|
+ }),
|
|
|
|
|
+ // min-height 是 CSS 的单一来源(64u 设计高度):算出来不比它高时不接管,
|
|
|
|
|
+ // 否则 CSS 下限会赢、块被撑到下限却按更小的预算算 → 溢出。
|
|
|
|
|
+ minHeightPx: parseFloat(getComputedStyle(charts[0]).minHeight),
|
|
|
|
|
+ })
|
|
|
const current = trendFitLayout.value
|
|
const current = trendFitLayout.value
|
|
|
if (Math.abs(next.heightPx - current.heightPx) > 1
|
|
if (Math.abs(next.heightPx - current.heightPx) > 1
|
|
|
|| Math.abs(next.paddingTopPx - current.paddingTopPx) > 1
|
|
|| Math.abs(next.paddingTopPx - current.paddingTopPx) > 1
|
|
@@ -1507,7 +1552,14 @@ function syncTrendBoxes(charts) {
|
|
|
*/
|
|
*/
|
|
|
function syncTrendFit() {
|
|
function syncTrendFit() {
|
|
|
const root = rootRef.value
|
|
const root = rootRef.value
|
|
|
- if (!root || trendFitting) return
|
|
|
|
|
|
|
+ if (!root) return
|
|
|
|
|
+ // 观察器回调与"写高度→下一帧"的窗口会撞车:撞上的那一次若直接丢弃,就会永久停在一个
|
|
|
|
|
+ // 偏大的高度上(真机实测:清单数据比图表晚一帧到达 → 之后没有任何 DOM 变化再来触发,
|
|
|
|
|
+ // 图表停在 730px、面板体溢出 48px)。所以撞车时记一笔,等这一轮收尾后补跑。
|
|
|
|
|
+ if (trendFitting) {
|
|
|
|
|
+ trendRerun = true
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
const charts = [...root.querySelectorAll('.panel-trend--line')]
|
|
const charts = [...root.querySelectorAll('.panel-trend--line')]
|
|
|
if (!charts.length) {
|
|
if (!charts.length) {
|
|
|
if (trendFitHeight.value !== 0) trendFitLayout.value = { heightPx: 0, paddingTopPx: 0, paddingBottomPx: 0 }
|
|
if (trendFitHeight.value !== 0) trendFitLayout.value = { heightPx: 0, paddingTopPx: 0, paddingBottomPx: 0 }
|
|
@@ -1520,6 +1572,10 @@ function syncTrendFit() {
|
|
|
const current = rootRef.value
|
|
const current = rootRef.value
|
|
|
if (!current) return
|
|
if (!current) return
|
|
|
syncTrendBoxes([...current.querySelectorAll('.panel-trend--line')])
|
|
syncTrendBoxes([...current.querySelectorAll('.panel-trend--line')])
|
|
|
|
|
+ if (trendRerun) {
|
|
|
|
|
+ trendRerun = false
|
|
|
|
|
+ syncTrendFit()
|
|
|
|
|
+ }
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1577,7 +1633,6 @@ watch(
|
|
|
() => props.view.blocks,
|
|
() => props.view.blocks,
|
|
|
() => {
|
|
() => {
|
|
|
observeCards()
|
|
observeCards()
|
|
|
- supplementalVisible.value = false
|
|
|
|
|
nextTick(() => { syncSupplementalVisibility(); syncTrendFit() })
|
|
nextTick(() => { syncSupplementalVisibility(); syncTrendFit() })
|
|
|
},
|
|
},
|
|
|
{ deep: false },
|
|
{ deep: false },
|
|
@@ -1604,52 +1659,8 @@ onBeforeUnmount(() => {
|
|
|
/* 两阶段真实测量:占布局但不可见,决定放得下后下一帧正式显示。 */
|
|
/* 两阶段真实测量:占布局但不可见,决定放得下后下一帧正式显示。 */
|
|
|
.panel-block.is-supplemental-measuring { visibility: hidden; pointer-events: none; }
|
|
.panel-block.is-supplemental-measuring { visibility: hidden; pointer-events: none; }
|
|
|
|
|
|
|
|
-.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;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+/* 维护者 2026-10-29:摘要入口搬到面板标题栏(CockpitPanel.vue 的「?」按钮),
|
|
|
|
|
+ 面板体不再画摘要条。 */
|
|
|
.panel-blocks,
|
|
.panel-blocks,
|
|
|
.panel-blocks *,
|
|
.panel-blocks *,
|
|
|
.panel-blocks *::before,
|
|
.panel-blocks *::before,
|
|
@@ -1666,12 +1677,7 @@ onBeforeUnmount(() => {
|
|
|
min-height: 0;
|
|
min-height: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.panel-blocks__head {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: var(--space-2, 8px);
|
|
|
|
|
-}
|
|
|
|
|
|
|
+/* 摘要条已搬到标题栏的「?」按钮(2026-10-29);这条规则留着也无消费点,直接删。 */
|
|
|
|
|
|
|
|
.panel-blocks__note {
|
|
.panel-blocks__note {
|
|
|
margin: 0;
|
|
margin: 0;
|
|
@@ -1849,7 +1855,9 @@ onBeforeUnmount(() => {
|
|
|
.panel-kpi--value-first .panel-kpi__label {
|
|
.panel-kpi--value-first .panel-kpi__label {
|
|
|
order: 2;
|
|
order: 2;
|
|
|
color: var(--text-2, #88b7e8);
|
|
color: var(--text-2, #88b7e8);
|
|
|
- font-size: var(--fs-caption, 10.5px);
|
|
|
|
|
|
|
+ /* 票 67 追加(2026-10-28):**不**在这里覆盖字号 —— 基础规则 .panel-kpi__label 的
|
|
|
|
|
+ 宽度预算(100cqw)才是唯一口径;此处若另写 font-size,会把预算顶掉(选择器更 specific),
|
|
|
|
|
+ value-first 的窄格就又会被裁成「…」。字号与基础版一致,只有次序与颜色不同。 */
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.panel-kpi--value-first .panel-kpi__value,
|
|
.panel-kpi--value-first .panel-kpi__value,
|
|
@@ -1874,6 +1882,20 @@ onBeforeUnmount(() => {
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* 票 67 追加(2026-10-28):value-first 的读数**也**受宽度预算约束。
|
|
|
|
|
+ 上面那条 min(22u, 34cqw) 是「不超过 22u、且不超过格宽 34%」的旧安全网;
|
|
|
|
|
+ 但 34cqw 仍可能大于按**实际文本宽度**算出的可容字号(例如 8 位数字 78546.04),
|
|
|
|
|
+ 于是 value-first 窄格里的长读数仍会画「…」。这里再叠一层:按 --kpi-reading-em
|
|
|
|
|
+ (含单位、见 domain/kpiFit.js)算出「刚好放得下」的字号,取两者更小的。
|
|
|
|
|
+ 单独一条规则、放在原规则之后:同 specificity、后写的赢;原规则一字未动(t11 F9e 仍守它)。 */
|
|
|
|
|
+.panel-kpi--value-first .panel-kpi__value,
|
|
|
|
|
+.panel-kpi--value-first .panel-kpi__value--link {
|
|
|
|
|
+ font-size: min(
|
|
|
|
|
+ min(calc(22 * var(--u, 1px)), 34cqw),
|
|
|
|
|
+ calc((100cqw - calc(18 * var(--u, 1px))) / var(--kpi-reading-em, 5))
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* value-first 卡在画面上不显示单位(设计稿顶部四卡只有数字):
|
|
/* value-first 卡在画面上不显示单位(设计稿顶部四卡只有数字):
|
|
|
数据层仍带单位(悬停 title 由 kpiReading 给「10 台」),只是不占视觉位。 */
|
|
数据层仍带单位(悬停 title 由 kpiReading 给「10 台」),只是不占视觉位。 */
|
|
|
.panel-kpi--value-first .panel-kpi__value em {
|
|
.panel-kpi--value-first .panel-kpi__value em {
|
|
@@ -1899,10 +1921,25 @@ onBeforeUnmount(() => {
|
|
|
grid-column: 1 / -1;
|
|
grid-column: 1 / -1;
|
|
|
color: var(--text-2, #88b7e8);
|
|
color: var(--text-2, #88b7e8);
|
|
|
font-size: var(--fs-note, 9.2px);
|
|
font-size: var(--fs-note, 9.2px);
|
|
|
|
|
+ /* 口径小字占整卡宽:同样按卡宽收字号(预算见 domain/kpiFit.js),到 7.2u 下限才换行。 */
|
|
|
|
|
+ font-size: max(
|
|
|
|
|
+ calc(8 * var(--u, 1px)),
|
|
|
|
|
+ min(
|
|
|
|
|
+ var(--fs-note, 9.2px),
|
|
|
|
|
+ calc((100cqw - calc(18 * var(--u, 1px))) / var(--kpi-caption-em, 6))
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
letter-spacing: 0.02em;
|
|
letter-spacing: 0.02em;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/*
|
|
|
|
|
+ 维护者 2026-10-28「指标卡尽量不要出现省略号」:卡片自身是**尺寸容器**,
|
|
|
|
|
+ 卡内三行文本(标签/读数/小字)按 `--kpi-*-em` 预算用 `100cqw` 算字号 ——
|
|
|
|
|
+ 窄格降字号、降到下限才换行,而不是画 `…`。预算按文本逐项由 domain/kpiFit.js 给
|
|
|
|
|
+ (渲染层不量宽度、不写第二份算术)。value-first 变体本就带 container-type,重复无害。
|
|
|
|
|
+ 注意 `contain: inline-size` 只隔离尺寸计算:卡片宽度来自网格轨道,不受内容反向撑开。 */
|
|
|
.panel-kpi {
|
|
.panel-kpi {
|
|
|
|
|
+ container-type: inline-size;
|
|
|
display: grid;
|
|
display: grid;
|
|
|
grid-template-columns: auto minmax(0, 1fr);
|
|
grid-template-columns: auto minmax(0, 1fr);
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -1930,7 +1967,21 @@ onBeforeUnmount(() => {
|
|
|
本文件里的类只管字体、颜色与排布。 */
|
|
本文件里的类只管字体、颜色与排布。 */
|
|
|
.panel-kpi__label {
|
|
.panel-kpi__label {
|
|
|
color: var(--muted, #5680b0);
|
|
color: var(--muted, #5680b0);
|
|
|
|
|
+ /* 第一行仍是基准字号(t11 F9c 钉着 `var(--fs-caption`);第二行是**按卡宽收的覆盖**。 */
|
|
|
font-size: var(--fs-caption, 10.5px);
|
|
font-size: var(--fs-caption, 10.5px);
|
|
|
|
|
+ /* 字号 =(卡宽 − 标签列被吃掉的 gutter)/ 文本宽度(em),上限基准、下限 7.2u(可读性下限)。
|
|
|
|
|
+ gutter 与 em 都来自 domain/kpiFit.js 的预算:有图标 37u、无图标 18u,文本按字宽逐字算。
|
|
|
|
|
+ 正常格宽算出来 ≥ 基准 ⇒ 不起作用;窄格才降。降到下限还放不下,由 EllipsisText 的
|
|
|
|
|
+ wrap 换行 —— 两条都不画 `…`。 */
|
|
|
|
|
+ font-size: max(
|
|
|
|
|
+ calc(8 * var(--u, 1px)),
|
|
|
|
|
+ min(
|
|
|
|
|
+ var(--fs-caption, 10.5px),
|
|
|
|
|
+ calc(
|
|
|
|
|
+ (100cqw - calc(var(--kpi-label-gutter-u, 37) * var(--u, 1px))) / var(--kpi-label-em, 6)
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
letter-spacing: 0.04em;
|
|
letter-spacing: 0.04em;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1959,6 +2010,12 @@ onBeforeUnmount(() => {
|
|
|
grid-column: 3;
|
|
grid-column: 3;
|
|
|
text-align: right;
|
|
text-align: right;
|
|
|
font-size: var(--fs-kpi);
|
|
font-size: var(--fs-kpi);
|
|
|
|
|
+ /* 票 67 追加:整行独占版的读数同样按宽度预算收(--kpi-reading-em 含单位)。
|
|
|
|
|
+ 之前这里只有一个裸的 --fs-kpi,长读数(多位数 + 单位)会撑破并被裁成「…」。 */
|
|
|
|
|
+ font-size: min(
|
|
|
|
|
+ var(--fs-kpi),
|
|
|
|
|
+ calc((100cqw - calc(18 * var(--u, 1px))) / var(--kpi-reading-em, 5))
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.panel-kpi__value {
|
|
.panel-kpi__value {
|
|
@@ -1967,6 +2024,15 @@ onBeforeUnmount(() => {
|
|
|
font-family: var(--num, monospace);
|
|
font-family: var(--num, monospace);
|
|
|
/* 批次 2(票 15):KPI 数值是设计稿最显眼的档位(20px Rajdhani),单列 --fs-kpi。 */
|
|
/* 批次 2(票 15):KPI 数值是设计稿最显眼的档位(20px Rajdhani),单列 --fs-kpi。 */
|
|
|
font-size: var(--fs-kpi, 20px);
|
|
font-size: var(--fs-kpi, 20px);
|
|
|
|
|
+ /* 维护者 2026-10-28:读数也按卡宽收字号(`--kpi-reading-em` 含单位,见 domain/kpiFit.js),
|
|
|
|
|
+ 下限 12u。读数**不换行**(数字折断比小字难读),所以这条是防出框的主力。 */
|
|
|
|
|
+ font-size: max(
|
|
|
|
|
+ calc(10 * var(--u, 1px)),
|
|
|
|
|
+ min(
|
|
|
|
|
+ var(--fs-kpi, 20px),
|
|
|
|
|
+ calc((100cqw - calc(18 * var(--u, 1px))) / var(--kpi-reading-em, 5))
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
line-height: 1.2;
|
|
line-height: 1.2;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1990,6 +2056,14 @@ onBeforeUnmount(() => {
|
|
|
font: inherit;
|
|
font: inherit;
|
|
|
font-family: var(--num, monospace);
|
|
font-family: var(--num, monospace);
|
|
|
font-size: var(--fs-kpi, 20px);
|
|
font-size: var(--fs-kpi, 20px);
|
|
|
|
|
+ /* 可点读数与普通读数同一条宽度预算(它比普通读数多一条虚线底边,但不吃横向宽度)。 */
|
|
|
|
|
+ font-size: max(
|
|
|
|
|
+ calc(10 * var(--u, 1px)),
|
|
|
|
|
+ min(
|
|
|
|
|
+ var(--fs-kpi, 20px),
|
|
|
|
|
+ calc((100cqw - calc(18 * var(--u, 1px))) / var(--kpi-reading-em, 5))
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
text-align: left;
|
|
text-align: left;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
border-bottom: 1px dashed rgba(1, 171, 234, 0.45);
|
|
border-bottom: 1px dashed rgba(1, 171, 234, 0.45);
|
|
@@ -2588,6 +2662,78 @@ onBeforeUnmount(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* 环形图(2026-10-13:条形图疲劳后的占比形态;左图右例、中心合计)。 */
|
|
/* 环形图(2026-10-13:条形图疲劳后的占比形态;左图右例、中心合计)。 */
|
|
|
|
|
+
|
|
|
|
|
+/* 票 63 宽屏补充图:更松的留白与视觉层级,不挤压初始设计块。 */
|
|
|
|
|
+.panel-block[data-panel-supplemental] {
|
|
|
|
|
+ margin-top: calc(8 * var(--u, 1px));
|
|
|
|
|
+ padding: calc(8 * var(--u, 1px));
|
|
|
|
|
+ border: 1px solid rgba(1, 171, 234, 0.18);
|
|
|
|
|
+ border-radius: calc(8 * var(--u, 1px));
|
|
|
|
|
+ background: linear-gradient(135deg, rgba(1, 171, 234, 0.07), rgba(10, 31, 59, 0.2));
|
|
|
|
|
+}
|
|
|
|
|
+.panel-donut--wide {
|
|
|
|
|
+ grid-template-columns: minmax(calc(112 * var(--u, 1px)), 0.9fr) minmax(0, 1.35fr);
|
|
|
|
|
+ min-height: calc(124 * var(--u, 1px));
|
|
|
|
|
+ gap: calc(18 * var(--u, 1px));
|
|
|
|
|
+ padding: calc(8 * var(--u, 1px)) calc(12 * var(--u, 1px));
|
|
|
|
|
+}
|
|
|
|
|
+.panel-donut--wide .panel-donut__wrap { width: calc(112 * var(--u, 1px)); height: calc(112 * var(--u, 1px)); }
|
|
|
|
|
+.panel-donut--wide .panel-donut__legend { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: calc(8 * var(--u, 1px)); }
|
|
|
|
|
+.panel-donut--wide .panel-donut__row { padding: calc(7 * var(--u, 1px)) 0; }
|
|
|
|
|
+/* 排行行不再铺深色卡片底:只保留名次、文字与数据条,背景交给整个 supplemental 分节。 */
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-bars--rank .panel-bar {
|
|
|
|
|
+ padding: calc(8 * var(--u, 1px)) 0;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+}
|
|
|
|
|
+/* 宽屏指标可读性:补充区标题、标签、数值和单位整体提升一档。 */
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-block__title { font-size: calc(13 * var(--u, 1px)); }
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-bar__label,
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-donut__label {
|
|
|
|
|
+ color: var(--text, #d7ecff);
|
|
|
|
|
+ font-size: calc(12 * var(--u, 1px));
|
|
|
|
|
+}
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-bar__count,
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-donut__count {
|
|
|
|
|
+ font-size: calc(13 * var(--u, 1px));
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-bar__count em { font-size: calc(11 * var(--u, 1px)); }
|
|
|
|
|
+.panel-block[data-panel-supplemental] .panel-bars--rank .panel-bar__rank {
|
|
|
|
|
+ width: calc(20 * var(--u, 1px));
|
|
|
|
|
+ height: calc(20 * var(--u, 1px));
|
|
|
|
|
+ font-size: calc(11 * var(--u, 1px));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/* 维护者 2026-10-29:KPI「强调」档 —— 由 domain 用 block.emphasis 声明(不按 panelId 特判),
|
|
|
|
|
+ 读数与标签各放大一档,用于"扫一眼读数"的主屏(运行概况)。 */
|
|
|
|
|
+.panel-kpis.is-emphasis .panel-kpi__value { font-size: calc(26 * var(--u, 1px)); }
|
|
|
|
|
+.panel-kpis.is-emphasis .panel-kpi__label { font-size: calc(12.5 * var(--u, 1px)); }
|
|
|
|
|
+.panel-kpis.is-emphasis .panel-kpi__caption { font-size: calc(11 * var(--u, 1px)); }
|
|
|
|
|
+
|
|
|
|
|
+/* 维护者 2026-10-29:环图「紧凑」版 —— 环小一号、图例单列不省略,整体高度压到不撑出滚动条。 */
|
|
|
|
|
+.panel-donut--compact {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: calc(84 * var(--u, 1px)) minmax(0, 1fr);
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: calc(12 * var(--u, 1px));
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.panel-donut--compact .panel-donut__wrap { width: calc(84 * var(--u, 1px)); height: calc(84 * var(--u, 1px)); }
|
|
|
|
|
+.panel-donut--compact .panel-donut__legend { grid-template-columns: minmax(0, 1fr); gap: calc(3 * var(--u, 1px)); }
|
|
|
|
|
+.panel-donut--compact .panel-donut__row { padding-block: calc(2 * var(--u, 1px)); background: transparent; }
|
|
|
|
|
+.panel-donut--compact .panel-donut__label { font-size: calc(11.5 * var(--u, 1px)); }
|
|
|
|
|
+.panel-donut--compact .panel-donut__count { font-size: calc(12 * var(--u, 1px)); }
|
|
|
|
|
+
|
|
|
|
|
+/* 维护者 2026-10-29:折线「紧凑」版 —— 给块标题留出上方呼吸空间,避免与子标题/点顶数值重叠;
|
|
|
|
|
+ 高度压在 52u,宽屏仍由 fitTrend 接管。 */
|
|
|
|
|
+.panel-trend--compact {
|
|
|
|
|
+ min-height: calc(52 * var(--u, 1px));
|
|
|
|
|
+ padding-top: calc(12 * var(--u, 1px));
|
|
|
|
|
+ padding-bottom: calc(4 * var(--u, 1px));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.panel-donut {
|
|
.panel-donut {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
@@ -2712,6 +2858,113 @@ onBeforeUnmount(() => {
|
|
|
font-size: var(--fs-caption, 10.5px);
|
|
font-size: var(--fs-caption, 10.5px);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* 多环形网格(票 66 · 管理信息「片区 × 行业」):四行业小环 + 块底一份共用图例。
|
|
|
|
|
+ 环心读数与扇区几何都由 domain 给(几何与 .panel-donut 同一套),这里只管排版。
|
|
|
|
|
+
|
|
|
|
|
+ **两档排版(维护者 2026-10-28 最终澄清)**:
|
|
|
|
|
+ · 正常屏幕比例:固定四列横排(四个行业一行),保持原来的紧凑统计带;
|
|
|
|
|
+ · 驾驶舱超宽模式:只在外壳既有断点 `3456/1239` 下改 2×2,并放大环填充超宽面板的竖向空白。
|
|
|
|
|
+ 这里必须与 CockpitShell.vue 的栏内并排断点**完全同源**,不能再拿块宽/像素阈值猜“宽屏” ——
|
|
|
|
|
+ 正常屏的面板在 1080p 也可能超过 340px,上一版因此把正常比例错误地切成了 2×2。 */
|
|
|
|
|
+.panel-donuts {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ gap: calc(4 * var(--u, 1px));
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 正常比例:四环横排。minmax(0,1fr) 保证窄栏只压缩格子,不让网格横向溢出。 */
|
|
|
|
|
+.panel-donuts__grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
+ gap: calc(6 * var(--u, 1px));
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-donut-cell {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ justify-items: center;
|
|
|
|
|
+ gap: calc(2 * var(--u, 1px));
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-donut-cell__wrap {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: calc(52 * var(--u, 1px));
|
|
|
|
|
+ height: calc(52 * var(--u, 1px));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-donut-cell__total {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ place-items: center;
|
|
|
|
|
+ /* 与环形图中心读数同一条纪律:让位给整楔命中区,指针事件不落在这行字上。 */
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ color: var(--text-strong, #fff);
|
|
|
|
|
+ font-family: var(--num, monospace);
|
|
|
|
|
+ font-size: var(--fs-caption, 10.5px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-donut-cell__label {
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: var(--text-2, #88b7e8);
|
|
|
|
|
+ font-size: var(--fs-caption, 10.5px);
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-donuts__legend {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: calc(2 * var(--u, 1px)) calc(10 * var(--u, 1px));
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ list-style: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 超宽模式:与 CockpitShell.vue 的栏内并排断点同源。基础规则在上、覆盖规则在下,
|
|
|
|
|
+ 同权重靠源码次序让 2×2 与放大尺寸真正生效。 */
|
|
|
|
|
+@media (min-aspect-ratio: 3456 / 1239) {
|
|
|
|
|
+ .panel-donuts {
|
|
|
|
|
+ gap: calc(10 * var(--u, 1px));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-donuts__grid {
|
|
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
+ gap: calc(12 * var(--u, 1px)) calc(16 * var(--u, 1px));
|
|
|
|
|
+ padding: calc(6 * var(--u, 1px)) 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-donut-cell {
|
|
|
|
|
+ gap: calc(4 * var(--u, 1px));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-donut-cell__wrap {
|
|
|
|
|
+ /* 3456×1239 实测管理面板每格约 190px;128u(≈158px)进一步填满,
|
|
|
|
|
+ 仍给 hover ×1.1 的外扩留约 15px 安全边,不撞标题、图例或隔壁环。 */
|
|
|
|
|
+ width: calc(128 * var(--u, 1px));
|
|
|
|
|
+ height: calc(128 * var(--u, 1px));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-donut-cell__total {
|
|
|
|
|
+ font-size: var(--fs-body, 14px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-donut-cell__label {
|
|
|
|
|
+ color: var(--text, #d7ecff);
|
|
|
|
|
+ font-size: var(--fs-body, 14px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-donuts__legend {
|
|
|
|
|
+ gap: calc(6 * var(--u, 1px)) calc(16 * var(--u, 1px));
|
|
|
|
|
+ padding-top: calc(4 * var(--u, 1px));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.panel-stacked__label {
|
|
.panel-stacked__label {
|
|
|
color: var(--text-2, #88b7e8);
|
|
color: var(--text-2, #88b7e8);
|
|
|
font-size: var(--fs-caption, 10.5px);
|
|
font-size: var(--fs-caption, 10.5px);
|
|
@@ -3177,4 +3430,18 @@ onBeforeUnmount(() => {
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
flex: none;
|
|
flex: none;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 票 98:四大模式共享 UI 可读性统一。只改共享画法,不按 panelId 特判。 */
|
|
|
|
|
+.panel-block__title { font-size: calc(12 * var(--u, 1px)); }
|
|
|
|
|
+.panel-bar__label, .panel-donut__label, .panel-row dt, .panel-item__meta { font-size: calc(11.5 * var(--u, 1px)); }
|
|
|
|
|
+.panel-bar__count, .panel-donut__count, .panel-row dd { font-size: calc(12 * var(--u, 1px)); font-weight: 700; }
|
|
|
|
|
+.panel-card__label, .panel-item__title { font-size: calc(12 * var(--u, 1px)); }
|
|
|
|
|
+.panel-card__count { font-size: calc(18 * var(--u, 1px)); }
|
|
|
|
|
+.panel-card__note { font-size: calc(10.5 * var(--u, 1px)); }
|
|
|
|
|
+.panel-bars--rank .panel-bar { padding: calc(7 * var(--u, 1px)) 0; background: transparent; }
|
|
|
|
|
+.panel-bars--rank .panel-bar__rank { width: calc(19 * var(--u, 1px)); height: calc(19 * var(--u, 1px)); font-size: calc(11 * var(--u, 1px)); }
|
|
|
|
|
+.panel-donut__row { padding-block: calc(4 * var(--u, 1px)); background: transparent; }
|
|
|
|
|
+.panel-row { padding-block: calc(5 * var(--u, 1px)); }
|
|
|
|
|
+.panel-item { background: linear-gradient(90deg, rgba(1, 171, 234, 0.035), transparent); }
|
|
|
|
|
+
|
|
|
</style>
|
|
</style>
|