Generation.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. <script setup lang="ts">
  2. /**
  3. * 发电能力与曲线(generation)——逐字对应重构前 `app.js` 的 `generation()`(app.js:770-787)。
  4. *
  5. * 数据:`d.m9`(β 排布/封顶参数/尺子)· `d.control.curve`(候选机)· `d.sysmx_span` ·
  6. * `curves`(`/api/curves?win=…` 异步,服务端首次可能是 building)。
  7. * 图表:旧 `charts.js` 自绘 → ECharts;`__data__` 给"旧图真的写成文字"的数字(中位/规范线),
  8. * 其余是坐标轴刻度(旧图同样只标刻度)。
  9. */
  10. import { computed } from 'vue'
  11. import EChart from '../components/EChart.vue'
  12. import { t } from '../i18n/zh'
  13. import { unitNo } from '../lib/render'
  14. import { dv, esc } from '../lib/vib'
  15. const props = defineProps<{ d: any; curves?: any; pending?: boolean }>()
  16. const M9 = computed<any>(() => (props.d?.m9 && !props.d.m9.err) ? props.d.m9 : null)
  17. const F = computed<any>(() => props.d?.control || null)
  18. const GEN = computed<any>(() => props.curves || null)
  19. const sp = computed(() => (props.d?.sysmx_span || []).join(' ~ ') || '')
  20. const stats = (spec: any) => {
  21. const gs = Object.entries(spec?.分组 || {}).map(([k, v]) => ({ v: parseFloat(k), n: v as number, raw: k }))
  22. if (!gs.length) return null
  23. const vals = gs.map((g) => g.v).sort((a, b) => a - b)
  24. const med = vals.length % 2 ? vals[(vals.length - 1) / 2] : (vals[vals.length / 2 - 1] + vals[vals.length / 2]) / 2
  25. return { gs, med }
  26. }
  27. const capStats = computed(() => ({ p: stats(M9.value?.P封顶), w: stats(M9.value?.ω封顶) }))
  28. const capOption = computed(() => {
  29. const P = capStats.value.p, W9 = capStats.value.w
  30. const series: any[] = []
  31. if (P) series.push({ name: t('ch.cap.p'), type: 'scatter', data: P.gs.map((g) => [g.v, 1]), symbolSize: 10, itemStyle: { color: '#0b5fa5', opacity: 0.6 } })
  32. if (W9) series.push({ name: t('ch.cap.w'), type: 'scatter', data: W9.gs.map((g) => [g.v, 0]), symbolSize: 10, itemStyle: { color: '#b45309', opacity: 0.6 } })
  33. return {
  34. grid: { left: 56, right: 36, top: 20, bottom: 24 },
  35. xAxis: { type: 'value' }, yAxis: { type: 'value', min: -0.5, max: 1.5, show: false },
  36. series,
  37. // 旧 capAxis 只把刻度与中位注解写成 SVG 文字(那是图自身算的注解)⇒ 不计入我画的数据
  38. __data__: [],
  39. }
  40. })
  41. const betaOuts = computed<any[]>(() => (M9.value?.β_schedule?.离群 || []))
  42. const betaMed = computed<number>(() => M9.value?.β_schedule?.['全场同档差中位'] || 0)
  43. const betaOption = computed(() => ({
  44. grid: { left: 56, right: 60, top: 20, bottom: 24 },
  45. xAxis: { type: 'value' }, yAxis: { type: 'category', data: betaOuts.value.map((o) => unitNo(o.t)) },
  46. series: [{ type: 'bar', data: betaOuts.value.map((o) => o.平均偏), itemStyle: { color: '#0b5fa5' } }],
  47. __data__: [], // 同上:β 图中位是注解
  48. }))
  49. /** 旧模板:`.map(c => `<a…>${unitNo(c.t)}</a> ${c.dev}% ${dv(c.判)}`).join(' | ')` —— " | " 粘在前一项尾部 */
  50. const candsHtml = computed(() => {
  51. const cs = (F.value?.curve?.cands || []) as any[]
  52. return cs.length
  53. ? cs.map((c) => `<a href="/turbine/${c.t}">${unitNo(c.t)}</a> ${c.dev}% ${dv(c.判)}`).join(' | ')
  54. : t('gen.no_cands')
  55. })
  56. /** 旧模板把机组链接拼进注里(`units` 参数是 HTML)⇒ 这里用 v-html 保持同一节点结构 */
  57. const betaNoteHtml = computed(() => t('gen.beta_note', {
  58. units: betaOuts.value.map((x) => `<a href="/turbine/${x.t}">${unitNo(x.t)}</a>`).join(' / '),
  59. k: betaOuts.value.length
  60. ? Math.round(Math.max(...betaOuts.value.map((x) => x.平均偏)) / Math.max(betaMed.value || 0, 0.01))
  61. : '—',
  62. v: dv(M9.value?.β_schedule?.判),
  63. }))
  64. const rulerRows = computed<any[]>(() => Object.entries(M9.value?.尺子 || {}).map(([k, v]: any) => ({ k, r: v.极差 / v.σ })))
  65. const rulerOption = computed(() => ({
  66. grid: { left: 100, right: 80, top: 20, bottom: 24 },
  67. xAxis: { type: 'value' }, yAxis: { type: 'category', data: rulerRows.value.map((r) => r.k) },
  68. series: [{ type: 'bar', data: rulerRows.value.map((r) => r.r), itemStyle: { color: '#0b5fa5' } }],
  69. __data__: [], // 同上:规范线是注解
  70. }))
  71. /** 曲线图组:旧 `CH.multiline(Object.assign({zero:false}, f))` -> ECharts 折线 */
  72. const figs = computed<any[]>(() => (GEN.value?.figs || []))
  73. function figOption(f: any) {
  74. const months = f.months || f.x || []
  75. return {
  76. grid: { left: 56, right: 20, top: 28, bottom: 40 },
  77. legend: { top: 0, textStyle: { fontSize: 10 } },
  78. xAxis: { type: 'category', data: months },
  79. yAxis: { type: 'value', scale: true },
  80. series: (f.series || []).map((s: any) => ({ name: String(s.name), type: 'line', data: s.vals, connectNulls: false })),
  81. __data__: [],
  82. }
  83. }
  84. </script>
  85. <template>
  86. <section>
  87. <div class="eyebrow">{{ t('gen.eyebrow') }}</div>
  88. <div class="panel mb small" style="border-left:3px solid var(--accent)">
  89. <b>{{ t('gen.fixed_win') }}</b> {{ sp ? t('gen.win_range', { span: sp }) : '' }}
  90. <div class="muted" style="margin-top:3px">{{ t('gen.win_why') }}</div>
  91. </div>
  92. <div v-if="d.m9 && d.m9.err" class="panel mb" style="border-left:3px solid var(--critical)">
  93. <p class="small">{{ t('flt.block_err', { k: 'm9', e: dv(d.m9.err) }) }}</p>
  94. </div>
  95. <div v-if="M9" class="panel mb" :style="pending ? 'border-left:3px solid var(--accent)' : ''">
  96. <h3>{{ t('gen.ctrl') }} <span class="small muted" style="font-weight:400">{{ t('gen.ctrl_win', { win: dv(M9.窗) }) }}</span><span v-if="pending" class="chip"> {{ t('gen.recomputing_short') }}</span></h3>
  97. <div style="display:flex;flex-wrap:wrap;gap:18px">
  98. <div style="flex:1 1 300px;min-width:280px">
  99. <div class="small" style="font-weight:600;margin-bottom:4px">{{ t('gen.cap') }}</div>
  100. <EChart :option="capOption" height="170px" />
  101. <p class="small muted" style="margin-top:4px">{{ t('gen.cap_note') }}</p>
  102. </div>
  103. <div style="flex:1 1 300px;min-width:280px">
  104. <div class="small" style="font-weight:600;margin-bottom:4px">{{ t('gen.beta') }}</div>
  105. <EChart :option="betaOption" height="170px" />
  106. <p class="small muted" style="margin-top:4px" v-html="betaNoteHtml"></p>
  107. <div class="small" style="font-weight:600;margin:12px 0 4px">{{ t('gen.ruler') }}</div>
  108. <EChart :option="rulerOption" height="170px" />
  109. <p class="small muted" style="margin-top:4px">{{ t('gen.ruler_note') }}</p>
  110. </div>
  111. </div>
  112. </div>
  113. <div v-if="F && F.curve" class="panel mb">
  114. <h3>{{ t('gen.cands') }} <span class="small muted" style="font-weight:400">{{ t('gen.cands_meta', { note: dv(F.curve.note || ''), s: d.sigma || F.curve.sigma }) }}</span></h3>
  115. <p class="small" v-html="candsHtml"></p>
  116. </div>
  117. <div class="eyebrow">{{ t('gen.figs') }}</div>
  118. <div v-if="GEN && GEN.err" class="panel" style="border-left:3px solid var(--critical)"><p class="small">{{ esc(GEN.err) }}</p></div>
  119. <div v-else-if="GEN && GEN.building" class="panel" style="border-left:3px solid var(--accent)">
  120. <p class="small"><b>{{ t('gen.recomputing') }}</b> {{ dv(GEN.curves?.note || '') }}</p>
  121. <div class="skel" style="width:60%;margin-top:10px"></div><div class="skel" style="width:80%;margin-top:8px"></div>
  122. </div>
  123. <div v-else-if="!GEN" class="panel">
  124. <p class="small muted">{{ t('gen.loading') }}</p>
  125. <div class="skel" style="width:60%;margin-top:10px"></div><div class="skel" style="width:80%;margin-top:8px"></div>
  126. </div>
  127. <div v-else class="grid2">
  128. <div v-for="(f, i) in figs" :key="i" class="panel">
  129. <h3>{{ dv(f.title || '') }}<span v-if="f.unit"> <span class="small muted" style="font-weight:400">({{ dv(f.unit) }})</span></span></h3>
  130. <EChart :option="figOption(f)" height="260px" />
  131. <!-- 旧 multiline 的 HTML 图例(不是画布):系列名逐条列出 -->
  132. <div class="legend" style="margin-top:6px;flex-wrap:wrap">
  133. <span v-for="(s, k) in (f.series || [])" :key="k"><i :style="`background:${['var(--accent)', '#D48A1E', '#2E8B6A', '#7C5295'][k % 4]}`"></i>{{ String(s.name) }}</span>
  134. </div>
  135. <p v-if="f.note" class="small muted" style="margin-top:6px">{{ dv(f.note) }}</p>
  136. </div>
  137. </div>
  138. </section>
  139. </template>