| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- <script setup lang="ts">
- /**
- * 故障统计(fault)——逐字对应重构前 `app.js` 的 `fault()`(app.js:648-673)。
- *
- * 结构:警报面(月度故障率 · 次数/时长 Pareto · 可靠性×可维修性象限 · 逐台故障表 · 季节性)
- * + 停机面(RAM 分解 · 停机 Pareto · 停机台次表)。
- * 图表:旧 `charts.js` 自绘 SVG → 迁移后用 **ECharts**(用户令 2026-09-29 引入);
- * `__data__` 按旧图**打印出来的那批数字**给(theo/计数/时长/per_day/series…),由对拍做数据级比对。
- */
- import { computed } from 'vue'
- import LegacyChart from '../components/LegacyChart.vue'
- import { chart } from '../lib/legacyCharts'
- import { t } from '../i18n/zh'
- import { fmt, unitNo, winName } from '../lib/render'
- import { partName } from '../lib/render2'
- const props = defineProps<{ d: any }>()
- const F = computed<any>(() => props.d?.faults || {})
- const pm = computed<string[]>(() => F.value.partial || [])
- const M = computed<any>(() => (props.d?.m8 && !props.d.m8.err) ? props.d.m8.mtbf : null)
- const mo = computed<any>(() => F.value.monthly || { months: [], vals: [] })
- const perDay = computed<number[]>(() => mo.value.per_day || [])
- /** 旧 app.js:654 `i0 = pm.length ? mo.months.indexOf(pm[0]) : -1`(示例值取 pm[0] 那一月的日均) */
- const exampleIdx = computed(() => (pm.value.length ? (mo.value.months || []).indexOf(pm.value[0]) : -1))
- /** 旧模板把"— 部分/全量 … 实测 …"拼成**一个**插值(一个文本节点)⇒ 这里也合成一个字符串 */
- const rateTail = computed(() => {
- const head = pm.value.length ? t('flt.rate_partial', { m: pm.value.join('/') }) : t('flt.rate_full')
- // 旧代码守卫是 `(i0 >= 0 && mo.per_day)` —— 值本身可能是 null,也照样渲染(页面上会出现"日均 null 条/日")
- const hasExample = exampleIdx.value >= 0 && !!mo.value.per_day
- return head + (hasExample ? ' ' + t('flt.rate_example', { m: pm.value[0], v: exampleVal.value }) : '')
- })
- const exampleVal = computed(() => {
- const i = exampleIdx.value
- return (i >= 0 && mo.value.per_day) ? mo.value.per_day[i] : null
- })
- /** 旧 app.js:651 —— 覆盖不全时的黄字提示 */
- const partialWarnTxt = computed(() => pm.value.length
- ? t('win.partial', { m: pm.value.join('/'), c: Math.round((F.value.cov_min || 0) * 100) })
- : '')
- const basic = {
- grid: { left: 60, right: 20, top: 24, bottom: 46 },
- tooltip: { trigger: 'axis' },
- yAxis: { type: 'value' },
- }
- /** 月度故障率(旧 rateChart:柱取自 per_day,覆盖不足的月份另标) */
- const rateOption = computed(() => ({
- ...basic,
- xAxis: { type: 'category', data: mo.value.months || [] },
- series: [{ type: 'bar', data: perDay.value.map((v: number | null) => (v == null ? null : Math.round(v * 100) / 100)), itemStyle: { color: '#0b5fa5' } }],
- __data__: perDay.value.filter((v: number | null) => v != null),
- }))
- /** Pareto(旧 paretoChart(items, unit, note):items = [{k, v}])—— 值取**原值**(旧图直接写 v) */
- function pareto(items: any[]) {
- const it = (items || []).filter((x) => x && x.v > 0)
- return {
- ...basic,
- xAxis: { type: 'category', data: it.map((x) => x.k) },
- series: [{ type: 'bar', data: it.map((x) => x.v), itemStyle: { color: '#2f7dbf' } }],
- __data__: [], // 旧 Pareto 只把坐标刻度写成 SVG 文字(数据靠柱高/悬停),故不给数
- }
- }
- /** 旧 charts.js 的 `short`:取码的前导数字,取不到就截前 4 字 */
- const shortK = (k: unknown): string => {
- const m = String(k).match(/^([0-9]+)/)
- return m ? m[1] : String(k).slice(0, 4)
- }
- const esc = (s: unknown): string => String(s ?? '')
- .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
- /** 逐字复现旧 paretoChart 的 HTML 摘要(含 <br> 与 <b>,用 v-html 保证节点边界一致) */
- function paretoSummaryHtml(items: any[], unit: string, note: string): string {
- const it = (items || []).filter((x) => x && x.v > 0)
- if (!it.length) return esc(t('ch.pareto.none'))
- const tot = it.reduce((a, x) => a + x.v, 0)
- let cum = 0
- let k80: number | null = null
- for (let i = 0; i < it.length; i++) {
- cum += it[i].v
- if (k80 === null && tot && cum / tot >= 0.8) k80 = i + 1
- }
- const top3 = it.slice(0, 3)
- .map((x) => `<b>${esc(shortK(x.k))}</b> ${esc(String(x.k).replace(/^[0-9]+ */, '').slice(0, 18))}`)
- .join(' · ')
- const total = t('ch.pareto.total', { n: it.length, tot: fmt(Math.round(tot)), unit })
- const head = k80 ? `<b>${esc(t('ch.pareto.key', { k: k80 }))}</b> (${esc(total)})` : esc(total)
- return head + '<br>' + esc(t('ch.pareto.top3')) + top3 + (note ? '<br>' + esc(note) : '')
- }
- const paretoNSummary = computed(() => paretoSummaryHtml(F.value.pareto_n || [], t('unit.n'), t('flt.pareto_n_note')))
- const paretoDSummary = computed(() => paretoSummaryHtml(F.value.pareto_d || [], ' h', t('flt.pareto_d_note')))
- const stopParetoSummary = computed(() => paretoSummaryHtml(stopPareto.value, ' h', t('flt.stop_pareto_note')))
- const paretoNOption = computed(() => pareto(F.value.pareto_n || []))
- const paretoDOption = computed(() => pareto(F.value.pareto_d || []))
- /** 旧 `paretoChart` 的 k80:累计占比首次 ≥80% 的那一项(其标签用于"关键少数"摘要) */
- function k80Of(items: any[]): { k: string; n: number; tot: number } | null {
- const it = (items || []).filter((x) => x && x.v > 0)
- if (!it.length) return null
- const tot = it.reduce((a, x) => a + x.v, 0)
- let cum = 0
- for (const x of it) {
- cum += x.v
- if (tot && cum / tot >= 0.8) return { k: String(x.k), n: it.length, tot }
- }
- return { k: String(it[it.length - 1].k), n: it.length, tot }
- }
- const k80N = computed(() => k80Of(F.value.pareto_n || []))
- const k80D = computed(() => k80Of(F.value.pareto_d || []))
- /** 可靠性 × 可维修性象限(旧 quadChart:x=MTBF 对数、y=MDT、面积=累计停机) */
- const quadOption = computed(() => {
- const it = (F.value.quad || []).filter((x: any) => x && x.n > 0 && x.med > 0)
- return {
- grid: { left: 60, right: 24, top: 24, bottom: 46 },
- xAxis: { type: 'log', name: 'MTBF(h)' },
- yAxis: { type: 'value', name: 'MDT(h)' },
- series: [{
- type: 'scatter',
- data: it.map((x: any) => [x.mtbf ?? x.n, x.med]),
- symbolSize: (v: number[]) => Math.min(28, 6 + Math.sqrt(Math.max(v[0] || 1, 1))),
- itemStyle: { color: '#b42318', opacity: 0.6 },
- }],
- __data__: [], // 象限图是散点:坐标即数据,旧图不把数值写成文字
- }
- })
- const pt = computed<any[]>(() => F.value.per_t || [])
- const maxT = computed(() => Math.max(1, ...pt.value.map((x: any) => x.v)))
- const seasonal = computed<any[]>(() => (M.value && props.d?.m8?.seasonal) ? props.d.m8.seasonal : [])
- /** 旧 multiline 的系列配色(照抄),图例色块用同一批 */
- const ML_COLS = ['var(--accent)', '#D48A1E', '#2E8B6A', '#7C5295']
- /** 旧 multiline 的系列名:`displayText(name).slice(0, 16)` */
- const seasonalNames = computed(() => seasonal.value.slice(0, 4).map((x: any) => String(x.name).slice(0, 16)))
- const seasonalOption = computed(() => {
- const s = seasonal.value.slice(0, 4)
- const months = s.length ? s[0].months : []
- return {
- ...basic,
- xAxis: { type: 'category', data: months },
- legend: { top: 0, textStyle: { fontSize: 10 } },
- series: s.map((x: any) => ({ name: String(x.name).slice(0, 16), type: 'line', data: x.vals, connectNulls: false })),
- __data__: [], // 旧 multiline 只标刻度;系列值不写成文字
- }
- })
- const RT = computed<any>(() => (props.d?.rel && !props.d.rel.err) ? props.d.rel : null)
- const ramOption = computed(() => {
- const rows = (RT.value?.表 || []) as any[]
- return {
- grid: { left: 60, right: 24, top: 24, bottom: 40 },
- xAxis: { type: 'value', name: 'MTBF(h)' },
- yAxis: { type: 'value', name: 'MDT(h)' },
- series: [{ type: 'scatter', data: rows.filter((r) => !r.外部 && r.MTBF_h).map((r) => [r.MTBF_h, r.MDT_h]),
- symbolSize: 8, itemStyle: { color: '#2f7dbf', opacity: 0.5 } }],
- __data__: [], // 旧 ramChart 也是散点(MTBF×MDT),数值不写成文字
- }
- })
- /** 旧 attribBar 的分组口径(照抄):unk = 无记录停机 · un2 = 未归类 · ok = 总 − 两者 */
- const attribGroups = computed(() => {
- const rows = (RT.value?.表 || []) as any[]
- const unk = (rows.find((r) => r.部件 === '无记录停机') || {}).停机时长h || 0
- const un2 = rows.filter((r) => r.部件 === '未归类').reduce((a, r) => a + r.停机时长h, 0)
- const tot = RT.value?.总停机h || 0
- const ok = tot - unk - un2
- return { ok, unk, un2 }
- })
- const attribOption = computed(() => {
- const g = attribGroups.value
- return {
- xAxis: { type: 'value' },
- yAxis: { type: 'category', data: [t('ch.attrib.ok', { h: Math.round(g.ok), p: '' }), t('ch.attrib.none', { h: Math.round(g.unk), p: '' }), t('ch.attrib.uncls', { h: Math.round(g.un2) })] },
- series: [{ type: 'bar', data: [Math.round(g.ok), Math.round(g.unk), Math.round(g.un2)], itemStyle: { color: '#0b5fa5' } }],
- __data__: [Math.round(g.ok), Math.round(g.unk), Math.round(g.un2)],
- }
- })
- const stopPareto = computed<any[]>(() => (props.d?.m8?.stop_pareto || []).map((x: any) => ({
- k: String(x.k).split(';')[0].slice(0, 26), v: x.h,
- })))
- const stopParetoOption = computed(() => pareto(stopPareto.value))
- const partPlain = (k: unknown): string => partName(k as any)
- const seasonalRaw = computed<any>(() => ({
- months: (seasonal.value[0] && seasonal.value[0].months) || [],
- series: seasonal.value.slice(0, 4).map((x: any) => ({ name: String(x.name).slice(0, 16), vals: x.vals })),
- xlab: t('flt.seasonal_x'),
- }))
- const top = computed<any[]>(() => M.value?.台级top || [])
- const mxTop = computed(() => Math.max(1, ...top.value.map((x: any) => x.时长h)))
- </script>
- <template>
- <section>
- <div class="eyebrow">{{ t('flt.eyebrow') }}</div>
- <!-- 旧 winBanner(d, extra):`<b>t(win.banner)</b>${extra ? ' | ' + extra : ''}` -->
- <div class="panel mb small" style="border-left:3px solid var(--accent)">
- <b>{{ t('win.banner', { win: winName(d.win) }) }}</b><template v-if="partialWarnTxt"> | <span style="color:var(--warning)">{{ partialWarnTxt }}</span></template>
- </div>
- <div class="grid2">
- <div class="eyebrow" style="grid-column:1/-1;margin-top:4px">
- {{ t('flt.alarm_face') }} <span style="text-transform:none;letter-spacing:0;color:var(--muted)">{{ t('flt.alarm_meta', { n: fmt(F.total), c: F.n_codes, win: winName(d.win) }) }}</span>
- </div>
- <div class="panel span2">
- <h3>{{ t('flt.rate') }}</h3>
- <LegacyChart :html="chart('rateChart', mo)" />
- <p class="small muted" style="margin-top:6px">
- <b>{{ t('flt.rate_note') }}</b> — {{ rateTail }}
- </p>
- </div>
- <div class="panel">
- <h3>{{ t('flt.pareto_n') }}</h3>
- <LegacyChart :html="chart('paretoChart', F.pareto_n || [], t('unit.n'), t('flt.pareto_n_note'))" />
- </div>
- <div class="panel">
- <h3>{{ t('flt.pareto_d') }}</h3>
- <LegacyChart :html="chart('paretoChart', F.pareto_d || [], ' h', t('flt.pareto_d_note'))" />
- </div>
- <div class="panel span2">
- <h3>{{ t('flt.quad') }}</h3>
- <LegacyChart :html="chart('quadChart', F.quad || [])" />
- <p class="small muted" style="margin-top:6px">
- {{ t('flt.quad_note', { n: F.n_codes, m: (F.quad || []).length }) }}
- <span v-if="pm.length" style="color:var(--warning)">{{ t('flt.quad_partial', { m: pm.join('/'), c: Math.round((F.cov_min || 0) * 100) }) }}</span>
- {{ t('flt.quad_note2') }}
- </p>
- </div>
- <div class="panel">
- <h3>{{ t('flt.per_t', { n: pt.length }) }}</h3>
- <div class="tblwrap">
- <table>
- <tr><th style="width:70px">{{ t('ui.unit') }}</th><th>{{ t('ui.count') }}</th><th class="r" style="width:80px">{{ t('flt.per_t_share') }}</th></tr>
- <tr v-for="x in pt" :key="x.k">
- <td><b>{{ unitNo(x.k) }}</b></td>
- <td><i class="bar" :style="`width:${Math.round(x.v / maxT * 90)}px;background:${x.v / maxT > 0.6 ? 'var(--critical)' : 'var(--accent)'}`"></i><span class="num small">{{ fmt(x.v) }}</span></td>
- <td class="r small">{{ (x.v / F.total * 100).toFixed(1) }}%</td>
- </tr>
- </table>
- </div>
- <p class="small muted" style="margin-top:6px">{{ t('flt.per_t_note', { n: fmt(F.total) }) }}</p>
- </div>
- <div v-if="d.m8 && d.m8.err" class="panel" style="border-left:3px solid var(--critical)">
- <p class="small">{{ t('flt.block_err', { k: 'm8', e: d.m8.err }) }}</p>
- </div>
- <div v-if="M && seasonal.length" class="panel">
- <h3>{{ t('flt.seasonal') }} <span class="small muted" style="font-weight:400">({{ t('flt.seasonal_unit') }})</span></h3>
- <LegacyChart :html="chart('multiline', seasonalRaw)" />
- <p class="small muted" style="margin-top:6px">{{ t('flt.seasonal_note') }}</p>
- </div>
- <template v-if="M">
- <div class="eyebrow" style="grid-column:1/-1;margin-top:4px">
- {{ t('flt.stop_face') }} <span style="text-transform:none;letter-spacing:0;color:var(--muted)">{{ t('flt.stop_meta', { h: fmt(M.停机总时长h), n: fmt(M.停机事件数) }) }}</span>
- </div>
- <div v-if="RT" class="panel span2">
- <h3>{{ t('flt.ram') }}</h3>
- <LegacyChart :html="chart('ramChart', RT && RT.表 || [], partPlain)" />
- <p class="small muted" style="margin-top:6px">{{ t('flt.ram_note', { a: RT.设备类停机h, b: RT.外部类停机h }) }}</p>
- <div style="margin-top:12px"><LegacyChart :html="chart('attribBar', RT && RT.表 || [], RT && RT.总停机h || 0)" /></div>
- <p class="small muted" style="margin-top:2px">{{ t('flt.attrib_note', { h: ((RT.表 || []).find((x: any) => x.部件 === '无记录停机') || {}).停机时长h || 0 }) }}</p>
- </div>
- <div class="panel">
- <h3>{{ t('flt.stop_pareto') }}</h3>
- <LegacyChart :html="chart('paretoChart', stopPareto, ' h', t('flt.stop_pareto_note'))" />
- <p class="small muted" style="margin-top:4px">{{ M.口径 }}</p>
- </div>
- <div class="panel">
- <h3>{{ t('flt.stop_units', { n: top.length }) }}</h3>
- <div class="tblwrap">
- <table>
- <tr><th style="width:64px">{{ t('ui.unit') }}</th><th>{{ t('flt.stop_h') }}</th><th class="r" style="width:56px">{{ t('flt.stop_n') }}</th></tr>
- <tr v-for="x in top" :key="x.t">
- <td><b>{{ unitNo(x.t) }}</b></td>
- <td><i class="bar" :style="`width:${Math.round(x.时长h / mxTop * 90)}px;background:${x.时长h / mxTop > 0.6 ? 'var(--critical)' : 'var(--accent)'}`"></i><span class="num small">{{ fmt(x.时长h) }}</span>h</td>
- <td class="r small">{{ x.次 }}</td>
- </tr>
- </table>
- </div>
- <p class="small muted" style="margin-top:6px">{{ t('flt.stop_units_note') }}</p>
- </div>
- </template>
- </div>
- </section>
- </template>
|