| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- <script setup lang="ts">
- /**
- * 发电能力与曲线(generation)——逐字对应重构前 `app.js` 的 `generation()`(app.js:770-787)。
- *
- * 数据:`d.m9`(β 排布/封顶参数/尺子)· `d.control.curve`(候选机)· `d.sysmx_span` ·
- * `curves`(`/api/curves?win=…` 异步,服务端首次可能是 building)。
- * 图表:旧 `charts.js` 自绘 → ECharts;`__data__` 给"旧图真的写成文字"的数字(中位/规范线),
- * 其余是坐标轴刻度(旧图同样只标刻度)。
- */
- import { computed } from 'vue'
- import EChart from '../components/EChart.vue'
- import { t } from '../i18n/zh'
- import { unitNo } from '../lib/render'
- import { dv, esc } from '../lib/vib'
- const props = defineProps<{ d: any; curves?: any; pending?: boolean }>()
- const M9 = computed<any>(() => (props.d?.m9 && !props.d.m9.err) ? props.d.m9 : null)
- const F = computed<any>(() => props.d?.control || null)
- const GEN = computed<any>(() => props.curves || null)
- const sp = computed(() => (props.d?.sysmx_span || []).join(' ~ ') || '')
- const stats = (spec: any) => {
- const gs = Object.entries(spec?.分组 || {}).map(([k, v]) => ({ v: parseFloat(k), n: v as number, raw: k }))
- if (!gs.length) return null
- const vals = gs.map((g) => g.v).sort((a, b) => a - b)
- const med = vals.length % 2 ? vals[(vals.length - 1) / 2] : (vals[vals.length / 2 - 1] + vals[vals.length / 2]) / 2
- return { gs, med }
- }
- const capStats = computed(() => ({ p: stats(M9.value?.P封顶), w: stats(M9.value?.ω封顶) }))
- const capOption = computed(() => {
- const P = capStats.value.p, W9 = capStats.value.w
- const series: any[] = []
- 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 } })
- 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 } })
- return {
- grid: { left: 56, right: 36, top: 20, bottom: 24 },
- xAxis: { type: 'value' }, yAxis: { type: 'value', min: -0.5, max: 1.5, show: false },
- series,
- __data__: [P?.med, W9?.med].filter((x) => x != null) as number[],
- }
- })
- const betaOuts = computed<any[]>(() => (M9.value?.β_schedule?.离群 || []))
- const betaMed = computed<number>(() => M9.value?.β_schedule?.['全场同档差中位'] || 0)
- const betaOption = computed(() => ({
- grid: { left: 56, right: 60, top: 20, bottom: 24 },
- xAxis: { type: 'value' }, yAxis: { type: 'category', data: betaOuts.value.map((o) => unitNo(o.t)) },
- series: [{ type: 'bar', data: betaOuts.value.map((o) => o.平均偏), itemStyle: { color: '#0b5fa5' } }],
- __data__: [betaMed.value],
- }))
- /** 旧模板:`.map(c => `<a…>${unitNo(c.t)}</a> ${c.dev}% ${dv(c.判)}`).join(' | ')` —— " | " 粘在前一项尾部 */
- const candsHtml = computed(() => {
- const cs = (F.value?.curve?.cands || []) as any[]
- return cs.length
- ? cs.map((c) => `<a href="/turbine/${c.t}">${unitNo(c.t)}</a> ${c.dev}% ${dv(c.判)}`).join(' | ')
- : t('gen.no_cands')
- })
- /** 旧模板把机组链接拼进注里(`units` 参数是 HTML)⇒ 这里用 v-html 保持同一节点结构 */
- const betaNoteHtml = computed(() => t('gen.beta_note', {
- units: betaOuts.value.map((x) => `<a href="/turbine/${x.t}">${unitNo(x.t)}</a>`).join(' / '),
- k: betaOuts.value.length
- ? Math.round(Math.max(...betaOuts.value.map((x) => x.平均偏)) / Math.max(betaMed.value || 0, 0.01))
- : '—',
- v: dv(M9.value?.β_schedule?.判),
- }))
- const rulerRows = computed<any[]>(() => Object.entries(M9.value?.尺子 || {}).map(([k, v]: any) => ({ k, r: v.极差 / v.σ })))
- const rulerOption = computed(() => ({
- grid: { left: 100, right: 80, top: 20, bottom: 24 },
- xAxis: { type: 'value' }, yAxis: { type: 'category', data: rulerRows.value.map((r) => r.k) },
- series: [{ type: 'bar', data: rulerRows.value.map((r) => r.r), itemStyle: { color: '#0b5fa5' } }],
- __data__: [3.7], // 旧图把规范线 3.7 写成文字(t('ch.ruler.norm', …))
- }))
- /** 曲线图组:旧 `CH.multiline(Object.assign({zero:false}, f))` -> ECharts 折线 */
- const figs = computed<any[]>(() => (GEN.value?.figs || []))
- function figOption(f: any) {
- const months = f.months || f.x || []
- return {
- grid: { left: 56, right: 20, top: 28, bottom: 40 },
- legend: { top: 0, textStyle: { fontSize: 10 } },
- xAxis: { type: 'category', data: months },
- yAxis: { type: 'value', scale: true },
- series: (f.series || []).map((s: any) => ({ name: String(s.name), type: 'line', data: s.vals, connectNulls: false })),
- __data__: [],
- }
- }
- </script>
- <template>
- <section>
- <div class="eyebrow">{{ t('gen.eyebrow') }}</div>
- <div class="panel mb small" style="border-left:3px solid var(--accent)">
- <b>{{ t('gen.fixed_win') }}</b> {{ sp ? t('gen.win_range', { span: sp }) : '' }}
- <div class="muted" style="margin-top:3px">{{ t('gen.win_why') }}</div>
- </div>
- <div v-if="d.m9 && d.m9.err" class="panel mb" style="border-left:3px solid var(--critical)">
- <p class="small">{{ t('flt.block_err', { k: 'm9', e: dv(d.m9.err) }) }}</p>
- </div>
- <div v-if="M9" class="panel mb" :style="pending ? 'border-left:3px solid var(--accent)' : ''">
- <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>
- <div style="display:flex;flex-wrap:wrap;gap:18px">
- <div style="flex:1 1 300px;min-width:280px">
- <div class="small" style="font-weight:600;margin-bottom:4px">{{ t('gen.cap') }}</div>
- <EChart :option="capOption" height="170px" />
- <p class="small muted" style="margin-top:4px">{{ t('gen.cap_note') }}</p>
- </div>
- <div style="flex:1 1 300px;min-width:280px">
- <div class="small" style="font-weight:600;margin-bottom:4px">{{ t('gen.beta') }}</div>
- <EChart :option="betaOption" height="170px" />
- <p class="small muted" style="margin-top:4px" v-html="betaNoteHtml"></p>
- <div class="small" style="font-weight:600;margin:12px 0 4px">{{ t('gen.ruler') }}</div>
- <EChart :option="rulerOption" height="170px" />
- <p class="small muted" style="margin-top:4px">{{ t('gen.ruler_note') }}</p>
- </div>
- </div>
- </div>
- <div v-if="F && F.curve" class="panel mb">
- <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>
- <p class="small" v-html="candsHtml"></p>
- </div>
- <div class="eyebrow">{{ t('gen.figs') }}</div>
- <div v-if="GEN && GEN.err" class="panel" style="border-left:3px solid var(--critical)"><p class="small">{{ esc(GEN.err) }}</p></div>
- <div v-else-if="GEN && GEN.building" class="panel" style="border-left:3px solid var(--accent)">
- <p class="small"><b>{{ t('gen.recomputing') }}</b> {{ dv(GEN.curves?.note || '') }}</p>
- <div class="skel" style="width:60%;margin-top:10px"></div><div class="skel" style="width:80%;margin-top:8px"></div>
- </div>
- <div v-else-if="!GEN" class="panel">
- <p class="small muted">{{ t('gen.loading') }}</p>
- <div class="skel" style="width:60%;margin-top:10px"></div><div class="skel" style="width:80%;margin-top:8px"></div>
- </div>
- <div v-else class="grid2">
- <div v-for="(f, i) in figs" :key="i" class="panel">
- <h3>{{ dv(f.title || '') }}<span v-if="f.unit"> <span class="small muted" style="font-weight:400">({{ dv(f.unit) }})</span></span></h3>
- <EChart :option="figOption(f)" height="260px" />
- <!-- 旧 multiline 的 HTML 图例(不是画布):系列名逐条列出 -->
- <div class="legend" style="margin-top:6px;flex-wrap:wrap">
- <span v-for="(s, k) in (f.series || [])" :key="k"><i :style="`background:${ML_COLS[k % 4]}`"></i>{{ String(s.name) }}</span>
- </div>
- <p v-if="f.note" class="small muted" style="margin-top:6px">{{ dv(f.note) }}</p>
- </div>
- </div>
- </section>
- </template>
|