Overview.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <script setup lang="ts">
  2. /**
  3. * 总览页签(P11 迁移第一个页签)——逐字对应重构前 `app.js` 的 `overview(d)`:
  4. * ① KPI 卡片(6 个,含口径 title)
  5. * ② 需要关注(融合面链盘 rows 里 bad/warn 的台,含"查看完整依据"与卡在哪一步)
  6. * ③ 全场状态条(38 格矩阵 + 台号 + 静默台数)
  7. *
  8. * 呈现一致由 `tools/render_parity.mjs` 对拍:本组件 SSR 出来的 HTML 与现网 `/detail/v2`
  9. * 页面里对应区块的 文案/数值/类名序列/台号序列 必须一致。
  10. */
  11. import { computed } from 'vue'
  12. import { t } from '../i18n/zh'
  13. import { STEP, SEV, fmt, stateKey, stKey, stWord, unitNo, unitState, winName } from '../lib/render'
  14. import { SYS9, sysName, sysSlug } from '../lib/render2'
  15. const props = defineProps<{ d: any }>()
  16. const F = computed(() => props.d?.fus || {})
  17. const M = computed(() => props.d?.m8?.mtbf || {})
  18. const C = computed(() => props.d?.control || {})
  19. /** 损失分项按 control.states 取(与经典页同源;见 app.js:172 的实逮说明) */
  20. const LS = computed<Record<string, number>>(() => {
  21. const out: Record<string, number> = {}
  22. for (const x of (C.value.states || [])) out[x.k] = x.loss
  23. return out
  24. })
  25. const kpi = computed(() => {
  26. const c = C.value
  27. const lossLabel = t('kpi.loss', { win: winName(props.d?.win) })
  28. + (LS.value['停机(调度令)'] != null ? ` · ${t('kpi.loss_dispatch', { n: fmt(LS.value['停机(调度令)']) })}` : '')
  29. return [
  30. { v: fmt(c.avail, 1), u: '%', l: t('kpi.avail', { win: winName(props.d?.win) }), k: (c.avail >= 95 ? 'ok' : 'warn'), ti: '' },
  31. { v: fmt(M.value.停机总时长h), u: 'h', l: t('kpi.downtime', { n: fmt(M.value.停机事件数) }), k: 'warn', ti: '' },
  32. { v: fmt(M.value.MTBF_h), u: 'h', l: t('kpi.mtbo'), k: '', ti: '' },
  33. { v: fmt(LS.value['停机']), u: 'MWh', l: lossLabel, k: '', ti: '' },
  34. { v: fmt(LS.value['限电·绑定']), u: 'MWh', l: t('kpi.curtail'), k: '', ti: '' },
  35. {
  36. // 旧模板 `<b>${fmt(报警台)} <small>/ ${fmt(全场)}</small></b>` —— `vHtml` 保留 <small> 元素与节点边界
  37. vRaw: `${fmt(props.d?.kpi?.报警台)} <small>/ ${fmt(props.d?.kpi?.全场)}</small>`, u: '',
  38. l: t('kpi.alarm_units'), k: (props.d?.kpi?.报警台 ? 'warn' : 'ok'), ti: t('kpi.alarm_units.hint'),
  39. vHtml: true,
  40. },
  41. ]
  42. })
  43. const why = computed<Record<string, string>>(() => {
  44. const out: Record<string, string> = {}
  45. for (const w of (props.d?.watch || [])) out[w.t] = w.why
  46. return out
  47. })
  48. const rows = computed(() => (((F.value.链盘 || {}).rows) || []).filter((r: any) => r.lvl === 'bad' || r.lvl === 'warn'))
  49. function basis(r: any): string {
  50. const own = (why.value[r.t] || '').trim()
  51. if (own) return own
  52. const stuckKey = STEP[r.stuck] || String(r.stuck || '').toLowerCase()
  53. const stuckStep = stuckKey === 'evidence' ? t('step.site_records') : t('step.' + (stuckKey || 'evidence'))
  54. return t('ov.basis_fusion', {
  55. part: r.part || '—',
  56. done: (r.done == null ? '—' : r.done),
  57. n: ((r.steps || []).length || '—'),
  58. stuck: r.stuck ? stuckStep : t('ov.all_steps_done'),
  59. srcs: (r.srcs || []).join(' / ') || t('ov.no_srcs'),
  60. })
  61. }
  62. function stuckText(r: any): string {
  63. if (!r.stuck) return ''
  64. const stuckKey = STEP[r.stuck] || String(r.stuck || '').toLowerCase()
  65. const stuckStep = stuckKey === 'evidence' ? t('step.site_records') : t('step.' + (stuckKey || 'evidence'))
  66. return t('step.stuck_at', { step: stuckStep })
  67. }
  68. /** 九系统分布(旧 overview() 的第 4 块):与 component 页同一份 d.sysdist */
  69. const SYS_SEG = [['危险', 'critical'], ['报警', 'warning'], ['不可判', 'unknown'], ['良好', 'good'], ['优秀', 'excellent']]
  70. const systems = computed(() => {
  71. const dist = props.d?.sysdist || {}
  72. return SYS9.map((s) => {
  73. const c = (dist[s] || {}) as Record<string, number>
  74. const n = Object.values(c).reduce((a, b) => a + b, 0) || 1
  75. return { name: sysName(s), slug: sysSlug(s), dist: c, n, seg: SYS_SEG.filter(([k]) => c[k]).map(([k, v]) => ({ k, v })) }
  76. })
  77. })
  78. const mat = computed<any[]>(() => F.value.矩阵 || [])
  79. const hot = computed(() => new Set(rows.value.map((r: any) => r.t)))
  80. /** ★P25 与 v2.9.2 同款:`TIER`(中文值→键)/`TIER_CLS`(键→类名),取自旧页 app.js:49/85 */
  81. const TIER_MAP: Record<string, string> = {'定论': 'confirmed', '准定论·预警': 'near', '候选·新发': 'candidate_new', '候选': 'candidate', '候选·记基线': 'candidate_baseline', '参考': 'reference', 'INSUFFICIENT': 'insufficient', '正常': 'normal', '撤回': 'withdrawn'}
  82. const TIER_CLS: Record<string, string> = {candidate_new: 'candidate', candidate_baseline: 'candidate', normal: 'reference'}
  83. /** 旧 `tierChip` 的类名口径:映射到 TIER_CLS,取不到就用键本身 */
  84. const tierCls = (zh: unknown): string => {
  85. const k = TIER_MAP[String(zh ?? '')]
  86. if (!k) return ''
  87. return TIER_CLS[k] || k
  88. }
  89. </script>
  90. <template>
  91. <section>
  92. <div class="kpis">
  93. <div v-for="(k, i) in kpi" :key="i" class="kpi" :class="k.k" :title="k.ti || undefined">
  94. <b v-if="k.vHtml" v-html="k.vRaw"></b><b v-else>{{ k.v }}<small v-if="k.u">{{ k.u }}</small></b><span>{{ k.l }}</span>
  95. </div>
  96. </div>
  97. </section>
  98. <section>
  99. <div class="sechead">
  100. <h2>{{ t('ov.attention') }}</h2>
  101. <span class="meta">{{ t('ov.attention_n', { n: rows.length }) }}</span>
  102. <span class="meta">{{ t('ov.ranked_by') }}</span>
  103. </div>
  104. <div v-if="F.gap" class="card mb" style="margin-bottom:10px">
  105. <p class="small" style="color:var(--warning)">{{ F.gap }}</p>
  106. <p class="small muted">{{ t('ov.fus_gap_hint') }}</p>
  107. </div>
  108. <div class="cards">
  109. <div v-for="r in rows" :key="r.t" class="card" :id="`u-${r.t}`" :style="`--sev:var(--${SEV[stateKey(r.ostate) || (r.lvl === 'bad' ? 'bad' : 'warn')]})`">
  110. <div class="head">
  111. <a class="unit" :href="`/turbine/${r.t}`">{{ unitNo(r.t) }}</a>
  112. <span class="sys">{{ r.part || '' }}</span>
  113. <span class="chip" :class="stateKey(r.ostate) || (r.lvl === 'bad' ? 'bad' : 'warn')">{{ stWord(r.ostate || (r.lvl === 'bad' ? '危险' : '报警')) }}</span>
  114. </div>
  115. <details class="evidence-detail">
  116. <summary>{{ t('ov.evidence') }}</summary>
  117. <div class="why">{{ basis(r) }}</div>
  118. </details>
  119. <div class="evid">
  120. <span v-if="r.estate" class="tier" :class="tierCls(r.estate)" :title="t('tier.' + (TIER_MAP[r.estate] || 'unknown'))">{{ stWord(r.estate) }}</span>
  121. <span v-if="r.srcs && r.srcs.length" class="rv">{{ r.srcs.length }} sources</span>
  122. </div>
  123. <div v-if="stuckText(r) || r.next" class="caveat">
  124. <b>{{ t('ov.caveat') }}</b>{{ stuckText(r) }}{{ stuckText(r) && r.next ? ' — ' : '' }}{{ r.next || '' }}
  125. </div>
  126. </div>
  127. </div>
  128. </section>
  129. <section>
  130. <div class="sechead">
  131. <h2>{{ t('ov.strip') }}</h2><span class="meta">{{ t('ov.strip_hint') }}</span>
  132. <div class="legend">
  133. <span v-for="k in ['bad', 'warn', 'unk', 'ok', 'ex']" :key="k">
  134. <i :style="`background:var(--${SEV[k]})`"></i>{{ t(stKey(k)) }}
  135. </span>
  136. </div>
  137. </div>
  138. <div class="strip" :style="`--n:${mat.length || 38}`">
  139. <div class="cells">
  140. <template v-if="mat.length">
  141. <div v-for="r in mat" :key="r.turbine" class="cell" :class="[unitState(r), hot.has(r.turbine) ? 'hot' : '']"
  142. :data-u="r.turbine" :title="`${unitNo(r.turbine)} ${t(stKey(unitState(r)))}`"></div>
  143. </template>
  144. <div v-else class="quiet" style="padding:10px">{{ F.gap || t('ov.fus_gap_hint') }}</div>
  145. </div>
  146. <div class="nums">
  147. <span v-for="r in mat" :key="`n-${r.turbine}`" :class="hot.has(r.turbine) ? 'hot' : ''">{{ unitNo(r.turbine).replace('#', '') }}</span>
  148. </div>
  149. <div class="quiet">
  150. {{ rows.length ? t('ov.quiet', { n: Math.max(0, (mat.length || 38) - rows.length) }) : t('ov.quiet_all', { n: mat.length || 38 }) }}
  151. </div>
  152. </div>
  153. </section>
  154. <!-- 九系统(旧 app.js:233-241;对拍实逮:漏了这块会少 43 条文本) -->
  155. <section>
  156. <div class="sechead">
  157. <h2>{{ t('ov.systems') }}</h2><span class="meta">{{ t('ov.systems_hint') }}</span>
  158. </div>
  159. <div class="systems">
  160. <div v-for="s in systems" :key="s.name" class="sysc">
  161. <div class="t">
  162. <b>{{ s.name }}</b>
  163. <a :href="`#tab=component&sys=${encodeURIComponent(s.slug)}`">{{ t('ui.open') }}</a>
  164. </div>
  165. <div class="bar">
  166. <i v-for="k in s.seg" :key="k.k" :style="`width:${(s.dist[k.k] / s.n * 100).toFixed(1)}%;background:var(--${k.v})`"></i>
  167. </div>
  168. <div class="cnt">
  169. <span v-for="k in s.seg" :key="`c-${k.k}`"><i :style="`background:var(--${k.v})`"></i>{{ s.dist[k.k] }}</span>
  170. </div>
  171. </div>
  172. </div>
  173. </section>
  174. </template>