Fault.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <script setup lang="ts">
  2. /**
  3. * 故障统计(fault)——逐字对应重构前 `app.js` 的 `fault()`(app.js:648-673)。
  4. *
  5. * 结构:警报面(月度故障率 · 次数/时长 Pareto · 可靠性×可维修性象限 · 逐台故障表 · 季节性)
  6. * + 停机面(RAM 分解 · 停机 Pareto · 停机台次表)。
  7. * 图表:旧 `charts.js` 自绘 SVG → 迁移后用 **ECharts**(用户令 2026-09-29 引入);
  8. * `__data__` 按旧图**打印出来的那批数字**给(theo/计数/时长/per_day/series…),由对拍做数据级比对。
  9. */
  10. import { computed } from 'vue'
  11. import LegacyChart from '../components/LegacyChart.vue'
  12. import { chart } from '../lib/legacyCharts'
  13. import { t } from '../i18n/zh'
  14. import { fmt, unitNo, winName } from '../lib/render'
  15. import { partName } from '../lib/render2'
  16. const props = defineProps<{ d: any }>()
  17. const F = computed<any>(() => props.d?.faults || {})
  18. const pm = computed<string[]>(() => F.value.partial || [])
  19. const M = computed<any>(() => (props.d?.m8 && !props.d.m8.err) ? props.d.m8.mtbf : null)
  20. const mo = computed<any>(() => F.value.monthly || { months: [], vals: [] })
  21. const perDay = computed<number[]>(() => mo.value.per_day || [])
  22. /** 旧 app.js:654 `i0 = pm.length ? mo.months.indexOf(pm[0]) : -1`(示例值取 pm[0] 那一月的日均) */
  23. const exampleIdx = computed(() => (pm.value.length ? (mo.value.months || []).indexOf(pm.value[0]) : -1))
  24. /** 旧模板把"— 部分/全量 … 实测 …"拼成**一个**插值(一个文本节点)⇒ 这里也合成一个字符串 */
  25. const rateTail = computed(() => {
  26. const head = pm.value.length ? t('flt.rate_partial', { m: pm.value.join('/') }) : t('flt.rate_full')
  27. // 旧代码守卫是 `(i0 >= 0 && mo.per_day)` —— 值本身可能是 null,也照样渲染(页面上会出现"日均 null 条/日")
  28. const hasExample = exampleIdx.value >= 0 && !!mo.value.per_day
  29. return head + (hasExample ? ' ' + t('flt.rate_example', { m: pm.value[0], v: exampleVal.value }) : '')
  30. })
  31. const exampleVal = computed(() => {
  32. const i = exampleIdx.value
  33. return (i >= 0 && mo.value.per_day) ? mo.value.per_day[i] : null
  34. })
  35. /** 旧 app.js:651 —— 覆盖不全时的黄字提示 */
  36. const partialWarnTxt = computed(() => pm.value.length
  37. ? t('win.partial', { m: pm.value.join('/'), c: Math.round((F.value.cov_min || 0) * 100) })
  38. : '')
  39. const basic = {
  40. grid: { left: 60, right: 20, top: 24, bottom: 46 },
  41. tooltip: { trigger: 'axis' },
  42. yAxis: { type: 'value' },
  43. }
  44. /** 月度故障率(旧 rateChart:柱取自 per_day,覆盖不足的月份另标) */
  45. const rateOption = computed(() => ({
  46. ...basic,
  47. xAxis: { type: 'category', data: mo.value.months || [] },
  48. series: [{ type: 'bar', data: perDay.value.map((v: number | null) => (v == null ? null : Math.round(v * 100) / 100)), itemStyle: { color: '#0b5fa5' } }],
  49. __data__: perDay.value.filter((v: number | null) => v != null),
  50. }))
  51. /** Pareto(旧 paretoChart(items, unit, note):items = [{k, v}])—— 值取**原值**(旧图直接写 v) */
  52. function pareto(items: any[]) {
  53. const it = (items || []).filter((x) => x && x.v > 0)
  54. return {
  55. ...basic,
  56. xAxis: { type: 'category', data: it.map((x) => x.k) },
  57. series: [{ type: 'bar', data: it.map((x) => x.v), itemStyle: { color: '#2f7dbf' } }],
  58. __data__: [], // 旧 Pareto 只把坐标刻度写成 SVG 文字(数据靠柱高/悬停),故不给数
  59. }
  60. }
  61. /** 旧 charts.js 的 `short`:取码的前导数字,取不到就截前 4 字 */
  62. const shortK = (k: unknown): string => {
  63. const m = String(k).match(/^([0-9]+)/)
  64. return m ? m[1] : String(k).slice(0, 4)
  65. }
  66. const esc = (s: unknown): string => String(s ?? '')
  67. .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
  68. /** 逐字复现旧 paretoChart 的 HTML 摘要(含 <br> 与 <b>,用 v-html 保证节点边界一致) */
  69. function paretoSummaryHtml(items: any[], unit: string, note: string): string {
  70. const it = (items || []).filter((x) => x && x.v > 0)
  71. if (!it.length) return esc(t('ch.pareto.none'))
  72. const tot = it.reduce((a, x) => a + x.v, 0)
  73. let cum = 0
  74. let k80: number | null = null
  75. for (let i = 0; i < it.length; i++) {
  76. cum += it[i].v
  77. if (k80 === null && tot && cum / tot >= 0.8) k80 = i + 1
  78. }
  79. const top3 = it.slice(0, 3)
  80. .map((x) => `<b>${esc(shortK(x.k))}</b> ${esc(String(x.k).replace(/^[0-9]+ */, '').slice(0, 18))}`)
  81. .join(' · ')
  82. const total = t('ch.pareto.total', { n: it.length, tot: fmt(Math.round(tot)), unit })
  83. const head = k80 ? `<b>${esc(t('ch.pareto.key', { k: k80 }))}</b> (${esc(total)})` : esc(total)
  84. return head + '<br>' + esc(t('ch.pareto.top3')) + top3 + (note ? '<br>' + esc(note) : '')
  85. }
  86. const paretoNSummary = computed(() => paretoSummaryHtml(F.value.pareto_n || [], t('unit.n'), t('flt.pareto_n_note')))
  87. const paretoDSummary = computed(() => paretoSummaryHtml(F.value.pareto_d || [], ' h', t('flt.pareto_d_note')))
  88. const stopParetoSummary = computed(() => paretoSummaryHtml(stopPareto.value, ' h', t('flt.stop_pareto_note')))
  89. const paretoNOption = computed(() => pareto(F.value.pareto_n || []))
  90. const paretoDOption = computed(() => pareto(F.value.pareto_d || []))
  91. /** 旧 `paretoChart` 的 k80:累计占比首次 ≥80% 的那一项(其标签用于"关键少数"摘要) */
  92. function k80Of(items: any[]): { k: string; n: number; tot: number } | null {
  93. const it = (items || []).filter((x) => x && x.v > 0)
  94. if (!it.length) return null
  95. const tot = it.reduce((a, x) => a + x.v, 0)
  96. let cum = 0
  97. for (const x of it) {
  98. cum += x.v
  99. if (tot && cum / tot >= 0.8) return { k: String(x.k), n: it.length, tot }
  100. }
  101. return { k: String(it[it.length - 1].k), n: it.length, tot }
  102. }
  103. const k80N = computed(() => k80Of(F.value.pareto_n || []))
  104. const k80D = computed(() => k80Of(F.value.pareto_d || []))
  105. /** 可靠性 × 可维修性象限(旧 quadChart:x=MTBF 对数、y=MDT、面积=累计停机) */
  106. const quadOption = computed(() => {
  107. const it = (F.value.quad || []).filter((x: any) => x && x.n > 0 && x.med > 0)
  108. return {
  109. grid: { left: 60, right: 24, top: 24, bottom: 46 },
  110. xAxis: { type: 'log', name: 'MTBF(h)' },
  111. yAxis: { type: 'value', name: 'MDT(h)' },
  112. series: [{
  113. type: 'scatter',
  114. data: it.map((x: any) => [x.mtbf ?? x.n, x.med]),
  115. symbolSize: (v: number[]) => Math.min(28, 6 + Math.sqrt(Math.max(v[0] || 1, 1))),
  116. itemStyle: { color: '#b42318', opacity: 0.6 },
  117. }],
  118. __data__: [], // 象限图是散点:坐标即数据,旧图不把数值写成文字
  119. }
  120. })
  121. const pt = computed<any[]>(() => F.value.per_t || [])
  122. const maxT = computed(() => Math.max(1, ...pt.value.map((x: any) => x.v)))
  123. const seasonal = computed<any[]>(() => (M.value && props.d?.m8?.seasonal) ? props.d.m8.seasonal : [])
  124. /** 旧 multiline 的系列配色(照抄),图例色块用同一批 */
  125. const ML_COLS = ['var(--accent)', '#D48A1E', '#2E8B6A', '#7C5295']
  126. /** 旧 multiline 的系列名:`displayText(name).slice(0, 16)` */
  127. const seasonalNames = computed(() => seasonal.value.slice(0, 4).map((x: any) => String(x.name).slice(0, 16)))
  128. const seasonalOption = computed(() => {
  129. const s = seasonal.value.slice(0, 4)
  130. const months = s.length ? s[0].months : []
  131. return {
  132. ...basic,
  133. xAxis: { type: 'category', data: months },
  134. legend: { top: 0, textStyle: { fontSize: 10 } },
  135. series: s.map((x: any) => ({ name: String(x.name).slice(0, 16), type: 'line', data: x.vals, connectNulls: false })),
  136. __data__: [], // 旧 multiline 只标刻度;系列值不写成文字
  137. }
  138. })
  139. const RT = computed<any>(() => (props.d?.rel && !props.d.rel.err) ? props.d.rel : null)
  140. const ramOption = computed(() => {
  141. const rows = (RT.value?.表 || []) as any[]
  142. return {
  143. grid: { left: 60, right: 24, top: 24, bottom: 40 },
  144. xAxis: { type: 'value', name: 'MTBF(h)' },
  145. yAxis: { type: 'value', name: 'MDT(h)' },
  146. series: [{ type: 'scatter', data: rows.filter((r) => !r.外部 && r.MTBF_h).map((r) => [r.MTBF_h, r.MDT_h]),
  147. symbolSize: 8, itemStyle: { color: '#2f7dbf', opacity: 0.5 } }],
  148. __data__: [], // 旧 ramChart 也是散点(MTBF×MDT),数值不写成文字
  149. }
  150. })
  151. /** 旧 attribBar 的分组口径(照抄):unk = 无记录停机 · un2 = 未归类 · ok = 总 − 两者 */
  152. const attribGroups = computed(() => {
  153. const rows = (RT.value?.表 || []) as any[]
  154. const unk = (rows.find((r) => r.部件 === '无记录停机') || {}).停机时长h || 0
  155. const un2 = rows.filter((r) => r.部件 === '未归类').reduce((a, r) => a + r.停机时长h, 0)
  156. const tot = RT.value?.总停机h || 0
  157. const ok = tot - unk - un2
  158. return { ok, unk, un2 }
  159. })
  160. const attribOption = computed(() => {
  161. const g = attribGroups.value
  162. return {
  163. xAxis: { type: 'value' },
  164. 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) })] },
  165. series: [{ type: 'bar', data: [Math.round(g.ok), Math.round(g.unk), Math.round(g.un2)], itemStyle: { color: '#0b5fa5' } }],
  166. __data__: [Math.round(g.ok), Math.round(g.unk), Math.round(g.un2)],
  167. }
  168. })
  169. const stopPareto = computed<any[]>(() => (props.d?.m8?.stop_pareto || []).map((x: any) => ({
  170. k: String(x.k).split(';')[0].slice(0, 26), v: x.h,
  171. })))
  172. const stopParetoOption = computed(() => pareto(stopPareto.value))
  173. const partPlain = (k: unknown): string => partName(k as any)
  174. const seasonalRaw = computed<any>(() => ({
  175. months: (seasonal.value[0] && seasonal.value[0].months) || [],
  176. series: seasonal.value.slice(0, 4).map((x: any) => ({ name: String(x.name).slice(0, 16), vals: x.vals })),
  177. xlab: t('flt.seasonal_x'),
  178. }))
  179. const top = computed<any[]>(() => M.value?.台级top || [])
  180. const mxTop = computed(() => Math.max(1, ...top.value.map((x: any) => x.时长h)))
  181. </script>
  182. <template>
  183. <section>
  184. <div class="eyebrow">{{ t('flt.eyebrow') }}</div>
  185. <!-- 旧 winBanner(d, extra):`<b>t(win.banner)</b>${extra ? ' | ' + extra : ''}` -->
  186. <div class="panel mb small" style="border-left:3px solid var(--accent)">
  187. <b>{{ t('win.banner', { win: winName(d.win) }) }}</b><template v-if="partialWarnTxt"> | <span style="color:var(--warning)">{{ partialWarnTxt }}</span></template>
  188. </div>
  189. <div class="grid2">
  190. <div class="eyebrow" style="grid-column:1/-1;margin-top:4px">
  191. {{ 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>
  192. </div>
  193. <div class="panel span2">
  194. <h3>{{ t('flt.rate') }}</h3>
  195. <LegacyChart :html="chart('rateChart', mo)" />
  196. <p class="small muted" style="margin-top:6px">
  197. <b>{{ t('flt.rate_note') }}</b> — {{ rateTail }}
  198. </p>
  199. </div>
  200. <div class="panel">
  201. <h3>{{ t('flt.pareto_n') }}</h3>
  202. <LegacyChart :html="chart('paretoChart', F.pareto_n || [], t('unit.n'), t('flt.pareto_n_note'))" />
  203. </div>
  204. <div class="panel">
  205. <h3>{{ t('flt.pareto_d') }}</h3>
  206. <LegacyChart :html="chart('paretoChart', F.pareto_d || [], ' h', t('flt.pareto_d_note'))" />
  207. </div>
  208. <div class="panel span2">
  209. <h3>{{ t('flt.quad') }}</h3>
  210. <LegacyChart :html="chart('quadChart', F.quad || [])" />
  211. <p class="small muted" style="margin-top:6px">
  212. {{ t('flt.quad_note', { n: F.n_codes, m: (F.quad || []).length }) }}
  213. <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>
  214. {{ t('flt.quad_note2') }}
  215. </p>
  216. </div>
  217. <div class="panel">
  218. <h3>{{ t('flt.per_t', { n: pt.length }) }}</h3>
  219. <div class="tblwrap">
  220. <table>
  221. <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>
  222. <tr v-for="x in pt" :key="x.k">
  223. <td><b>{{ unitNo(x.k) }}</b></td>
  224. <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>
  225. <td class="r small">{{ (x.v / F.total * 100).toFixed(1) }}%</td>
  226. </tr>
  227. </table>
  228. </div>
  229. <p class="small muted" style="margin-top:6px">{{ t('flt.per_t_note', { n: fmt(F.total) }) }}</p>
  230. </div>
  231. <div v-if="d.m8 && d.m8.err" class="panel" style="border-left:3px solid var(--critical)">
  232. <p class="small">{{ t('flt.block_err', { k: 'm8', e: d.m8.err }) }}</p>
  233. </div>
  234. <div v-if="M && seasonal.length" class="panel">
  235. <h3>{{ t('flt.seasonal') }} <span class="small muted" style="font-weight:400">({{ t('flt.seasonal_unit') }})</span></h3>
  236. <LegacyChart :html="chart('multiline', seasonalRaw)" />
  237. <p class="small muted" style="margin-top:6px">{{ t('flt.seasonal_note') }}</p>
  238. </div>
  239. <template v-if="M">
  240. <div class="eyebrow" style="grid-column:1/-1;margin-top:4px">
  241. {{ 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>
  242. </div>
  243. <div v-if="RT" class="panel span2">
  244. <h3>{{ t('flt.ram') }}</h3>
  245. <LegacyChart :html="chart('ramChart', RT && RT.表 || [], partPlain)" />
  246. <p class="small muted" style="margin-top:6px">{{ t('flt.ram_note', { a: RT.设备类停机h, b: RT.外部类停机h }) }}</p>
  247. <div style="margin-top:12px"><LegacyChart :html="chart('attribBar', RT && RT.表 || [], RT && RT.总停机h || 0)" /></div>
  248. <p class="small muted" style="margin-top:2px">{{ t('flt.attrib_note', { h: ((RT.表 || []).find((x: any) => x.部件 === '无记录停机') || {}).停机时长h || 0 }) }}</p>
  249. </div>
  250. <div class="panel">
  251. <h3>{{ t('flt.stop_pareto') }}</h3>
  252. <LegacyChart :html="chart('paretoChart', stopPareto, ' h', t('flt.stop_pareto_note'))" />
  253. <p class="small muted" style="margin-top:4px">{{ M.口径 }}</p>
  254. </div>
  255. <div class="panel">
  256. <h3>{{ t('flt.stop_units', { n: top.length }) }}</h3>
  257. <div class="tblwrap">
  258. <table>
  259. <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>
  260. <tr v-for="x in top" :key="x.t">
  261. <td><b>{{ unitNo(x.t) }}</b></td>
  262. <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>
  263. <td class="r small">{{ x.次 }}</td>
  264. </tr>
  265. </table>
  266. </div>
  267. <p class="small muted" style="margin-top:6px">{{ t('flt.stop_units_note') }}</p>
  268. </div>
  269. </template>
  270. </div>
  271. </section>
  272. </template>