| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- <script setup lang="ts">
- /**
- * 总览页签(P11 迁移第一个页签)——逐字对应重构前 `app.js` 的 `overview(d)`:
- * ① KPI 卡片(6 个,含口径 title)
- * ② 需要关注(融合面链盘 rows 里 bad/warn 的台,含"查看完整依据"与卡在哪一步)
- * ③ 全场状态条(38 格矩阵 + 台号 + 静默台数)
- *
- * 呈现一致由 `tools/render_parity.mjs` 对拍:本组件 SSR 出来的 HTML 与现网 `/detail/v2`
- * 页面里对应区块的 文案/数值/类名序列/台号序列 必须一致。
- */
- import { computed } from 'vue'
- import { t } from '../i18n/zh'
- import { STEP, SEV, fmt, stateKey, stKey, stWord, unitNo, unitState, winName } from '../lib/render'
- import { SYS9, sysName, sysSlug } from '../lib/render2'
- const props = defineProps<{ d: any }>()
- const F = computed(() => props.d?.fus || {})
- const M = computed(() => props.d?.m8?.mtbf || {})
- const C = computed(() => props.d?.control || {})
- /** 损失分项按 control.states 取(与经典页同源;见 app.js:172 的实逮说明) */
- const LS = computed<Record<string, number>>(() => {
- const out: Record<string, number> = {}
- for (const x of (C.value.states || [])) out[x.k] = x.loss
- return out
- })
- const kpi = computed(() => {
- const c = C.value
- const lossLabel = t('kpi.loss', { win: winName(props.d?.win) })
- + (LS.value['停机(调度令)'] != null ? ` · ${t('kpi.loss_dispatch', { n: fmt(LS.value['停机(调度令)']) })}` : '')
- return [
- { v: fmt(c.avail, 1), u: '%', l: t('kpi.avail', { win: winName(props.d?.win) }), k: (c.avail >= 95 ? 'ok' : 'warn'), ti: '' },
- { v: fmt(M.value.停机总时长h), u: 'h', l: t('kpi.downtime', { n: fmt(M.value.停机事件数) }), k: 'warn', ti: '' },
- { v: fmt(M.value.MTBF_h), u: 'h', l: t('kpi.mtbo'), k: '', ti: '' },
- { v: fmt(LS.value['停机']), u: 'MWh', l: lossLabel, k: '', ti: '' },
- { v: fmt(LS.value['限电·绑定']), u: 'MWh', l: t('kpi.curtail'), k: '', ti: '' },
- {
- // 旧模板 `<b>${fmt(报警台)} <small>/ ${fmt(全场)}</small></b>` —— `vHtml` 保留 <small> 元素与节点边界
- vRaw: `${fmt(props.d?.kpi?.报警台)} <small>/ ${fmt(props.d?.kpi?.全场)}</small>`, u: '',
- l: t('kpi.alarm_units'), k: (props.d?.kpi?.报警台 ? 'warn' : 'ok'), ti: t('kpi.alarm_units.hint'),
- vHtml: true,
- },
- ]
- })
- const why = computed<Record<string, string>>(() => {
- const out: Record<string, string> = {}
- for (const w of (props.d?.watch || [])) out[w.t] = w.why
- return out
- })
- const rows = computed(() => (((F.value.链盘 || {}).rows) || []).filter((r: any) => r.lvl === 'bad' || r.lvl === 'warn'))
- function basis(r: any): string {
- const own = (why.value[r.t] || '').trim()
- if (own) return own
- const stuckKey = STEP[r.stuck] || String(r.stuck || '').toLowerCase()
- const stuckStep = stuckKey === 'evidence' ? t('step.site_records') : t('step.' + (stuckKey || 'evidence'))
- return t('ov.basis_fusion', {
- part: r.part || '—',
- done: (r.done == null ? '—' : r.done),
- n: ((r.steps || []).length || '—'),
- stuck: r.stuck ? stuckStep : t('ov.all_steps_done'),
- srcs: (r.srcs || []).join(' / ') || t('ov.no_srcs'),
- })
- }
- function stuckText(r: any): string {
- if (!r.stuck) return ''
- const stuckKey = STEP[r.stuck] || String(r.stuck || '').toLowerCase()
- const stuckStep = stuckKey === 'evidence' ? t('step.site_records') : t('step.' + (stuckKey || 'evidence'))
- return t('step.stuck_at', { step: stuckStep })
- }
- /** 九系统分布(旧 overview() 的第 4 块):与 component 页同一份 d.sysdist */
- const SYS_SEG = [['危险', 'critical'], ['报警', 'warning'], ['不可判', 'unknown'], ['良好', 'good'], ['优秀', 'excellent']]
- const systems = computed(() => {
- const dist = props.d?.sysdist || {}
- return SYS9.map((s) => {
- const c = (dist[s] || {}) as Record<string, number>
- const n = Object.values(c).reduce((a, b) => a + b, 0) || 1
- return { name: sysName(s), slug: sysSlug(s), dist: c, n, seg: SYS_SEG.filter(([k]) => c[k]).map(([k, v]) => ({ k, v })) }
- })
- })
- const mat = computed<any[]>(() => F.value.矩阵 || [])
- const hot = computed(() => new Set(rows.value.map((r: any) => r.t)))
- /** ★P25 与 v2.9.2 同款:`TIER`(中文值→键)/`TIER_CLS`(键→类名),取自旧页 app.js:49/85 */
- const TIER_MAP: Record<string, string> = {'定论': 'confirmed', '准定论·预警': 'near', '候选·新发': 'candidate_new', '候选': 'candidate', '候选·记基线': 'candidate_baseline', '参考': 'reference', 'INSUFFICIENT': 'insufficient', '正常': 'normal', '撤回': 'withdrawn'}
- const TIER_CLS: Record<string, string> = {candidate_new: 'candidate', candidate_baseline: 'candidate', normal: 'reference'}
- /** 旧 `tierChip` 的类名口径:映射到 TIER_CLS,取不到就用键本身 */
- const tierCls = (zh: unknown): string => {
- const k = TIER_MAP[String(zh ?? '')]
- if (!k) return ''
- return TIER_CLS[k] || k
- }
- </script>
- <template>
- <section>
- <div class="kpis">
- <div v-for="(k, i) in kpi" :key="i" class="kpi" :class="k.k" :title="k.ti || undefined">
- <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>
- </div>
- </div>
- </section>
- <section>
- <div class="sechead">
- <h2>{{ t('ov.attention') }}</h2>
- <span class="meta">{{ t('ov.attention_n', { n: rows.length }) }}</span>
- <span class="meta">{{ t('ov.ranked_by') }}</span>
- </div>
- <div v-if="F.gap" class="card mb" style="margin-bottom:10px">
- <p class="small" style="color:var(--warning)">{{ F.gap }}</p>
- <p class="small muted">{{ t('ov.fus_gap_hint') }}</p>
- </div>
- <div class="cards">
- <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')]})`">
- <div class="head">
- <a class="unit" :href="`/turbine/${r.t}`">{{ unitNo(r.t) }}</a>
- <span class="sys">{{ r.part || '' }}</span>
- <span class="chip" :class="stateKey(r.ostate) || (r.lvl === 'bad' ? 'bad' : 'warn')">{{ stWord(r.ostate || (r.lvl === 'bad' ? '危险' : '报警')) }}</span>
- </div>
- <details class="evidence-detail">
- <summary>{{ t('ov.evidence') }}</summary>
- <div class="why">{{ basis(r) }}</div>
- </details>
- <div class="evid">
- <span v-if="r.estate" class="tier" :class="tierCls(r.estate)" :title="t('tier.' + (TIER_MAP[r.estate] || 'unknown'))">{{ stWord(r.estate) }}</span>
- <span v-if="r.srcs && r.srcs.length" class="rv">{{ r.srcs.length }} sources</span>
- </div>
- <div v-if="stuckText(r) || r.next" class="caveat">
- <b>{{ t('ov.caveat') }}</b>{{ stuckText(r) }}{{ stuckText(r) && r.next ? ' — ' : '' }}{{ r.next || '' }}
- </div>
- </div>
- </div>
- </section>
- <section>
- <div class="sechead">
- <h2>{{ t('ov.strip') }}</h2><span class="meta">{{ t('ov.strip_hint') }}</span>
- <div class="legend">
- <span v-for="k in ['bad', 'warn', 'unk', 'ok', 'ex']" :key="k">
- <i :style="`background:var(--${SEV[k]})`"></i>{{ t(stKey(k)) }}
- </span>
- </div>
- </div>
- <div class="strip" :style="`--n:${mat.length || 38}`">
- <div class="cells">
- <template v-if="mat.length">
- <div v-for="r in mat" :key="r.turbine" class="cell" :class="[unitState(r), hot.has(r.turbine) ? 'hot' : '']"
- :data-u="r.turbine" :title="`${unitNo(r.turbine)} ${t(stKey(unitState(r)))}`"></div>
- </template>
- <div v-else class="quiet" style="padding:10px">{{ F.gap || t('ov.fus_gap_hint') }}</div>
- </div>
- <div class="nums">
- <span v-for="r in mat" :key="`n-${r.turbine}`" :class="hot.has(r.turbine) ? 'hot' : ''">{{ unitNo(r.turbine).replace('#', '') }}</span>
- </div>
- <div class="quiet">
- {{ rows.length ? t('ov.quiet', { n: Math.max(0, (mat.length || 38) - rows.length) }) : t('ov.quiet_all', { n: mat.length || 38 }) }}
- </div>
- </div>
- </section>
- <!-- 九系统(旧 app.js:233-241;对拍实逮:漏了这块会少 43 条文本) -->
- <section>
- <div class="sechead">
- <h2>{{ t('ov.systems') }}</h2><span class="meta">{{ t('ov.systems_hint') }}</span>
- </div>
- <div class="systems">
- <div v-for="s in systems" :key="s.name" class="sysc">
- <div class="t">
- <b>{{ s.name }}</b>
- <a :href="`#tab=component&sys=${encodeURIComponent(s.slug)}`">{{ t('ui.open') }}</a>
- </div>
- <div class="bar">
- <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>
- </div>
- <div class="cnt">
- <span v-for="k in s.seg" :key="`c-${k.k}`"><i :style="`background:var(--${k.v})`"></i>{{ s.dist[k.k] }}</span>
- </div>
- </div>
- </div>
- </section>
- </template>
|