Просмотр исходного кода

补回被 p71 误删的 const F => bodyHtml 里 F.value 报错、振动页整块渲染失败(iframe 也没出来); 补齐后核验: portal_cms 走同源 iframe(门户 CSS + hero 标题 + .chips 行)

zhouyang.xie 6 дней назад
Родитель
Сommit
79f1929c65

+ 15 - 8
app_frontEnd/web/src/lib/vib.ts

@@ -270,10 +270,8 @@ export function portalCmsHtml(d: any, cms: any): string {
   const total = n(kpi['全场'] ?? kpi['收录台'])
   const okn = Math.max(total - alert, 0)
   const win = String((d && d.win) || '')
-  // 窗口内报警工单条数(按窗,取 d.fus.事件.主轴.工单.总条数)
   const ev = (((fus['事件'] || {})['主轴'] || {})['工单'] || {})
   const tickets = n(ev['总条数'])
-  // CMS 侧(/api/vibcms):采集窗 / 时间范围 / 裁决分布 / 行动项 / 数据时点
   const c = cms || {}
   const grades = Array.isArray(c.grades) ? c.grades : []
   const cnt = (g: string) => grades.filter((x: any) => String(x['综合'] || '') === g).length
@@ -283,18 +281,27 @@ export function portalCmsHtml(d: any, cms: any): string {
   const cmsDate = String(c.date || '')
   const chip = (cls: string, s: string) => '<span class="chip' + (cls ? ' ' + cls : '') + '">' + s + '</span>'
   let html = PORTAL_CMS_HTML
-  // 移除门户里写死的静态数字句("TCM 六采集窗 · 320 万记录 · 160 万谱")——接口不提供记录/谱计数
+  // 门户里写死的静态数字(接口不提供记录/谱计数)先移除
   html = html.replace(/<span class="chip[^"]*">TCM[^<]*<\/span>/, '')
-  // 门户原有的两个状态 chip → 按窗实时值
-  html = html.replace(/(<span class="chip s al">)[^<]*/, '$1需关注 ' + alert + ' 台')
-  html = html.replace(/(<span class="chip s gd">)[^<]*/, '$1未见异常 ' + okn + ' 台')
-  const live = [
+  // ★P71 精确替换 hero 内的整个 .chips 容器(此前是往第一个 </div> 塞 ⇒ 位置错、显示乱)
+  const chips = [
+    chip('s al', '需关注 ' + alert + ' 台'),
+    chip('s gd', '未见异常 ' + okn + ' 台'),
+    chip('ev', '证据窗末 ' + (String(fus['证据窗末'] || c.date || '').slice(0, 10) || '—')),
     chip('', '当前时间窗 · ' + win),
     chip('ev', '窗口内报警工单 · ' + tickets + ' 条'),
     chip('', 'CMS 采集窗 ' + (cmsWin || '—') + (cmsRange ? ' · ' + cmsRange : '')),
     chip('', 'CMS 裁决 · 优秀 ' + cnt('优秀') + ' / 良好 ' + cnt('良好') + ' / 报警 ' + cnt('报警')),
     chip('', '行动项 ' + actions + ' 条' + (cmsDate ? ' · 数据时点 ' + cmsDate : '')),
   ].join('')
-  html = html.replace('</div>', live + '</div>', 1)
+  if (/<div class="chips">[\s\S]*?<\/div>/.test(html)) html = html.replace(/<div class="chips">[\s\S]*?<\/div>/, '<div class="chips">' + chips + '</div>')
+  else html = html.replace('</div>', '<div class="chips">' + chips + '</div></div>', 1)
   return html
 }
+
+/** ★P71 「振动 CMS」整页文档:门户 CSS + 该节(供同源 iframe srcdoc 使用,渲染与门户一致)。 */
+export function portalCmsDoc(d: any, cms: any, portalCss: string): string {
+  return '<!doctype html><html lang="zh" data-theme="light"><head><meta charset="utf-8">'
+    + '<base target="_top"><style>' + portalCss + '</style></head><body>'
+    + portalCmsHtml(d, cms) + '</body></html>'
+}

+ 9 - 3
app_frontEnd/web/src/views/Vibration.vue

@@ -8,7 +8,8 @@ import { vUnwrap } from '../lib/unwrap'
  */
 import {computed, ref, watch } from 'vue'
 import { t } from '../i18n/zh'
-import { FUS_SUBS, cmsHtml, eventsHtml, loopHtml, matrixHtml, portalCmsHtml, unitsHtml } from '../lib/vib'
+import { FUS_SUBS, cmsHtml, eventsHtml, loopHtml, matrixHtml, portalCmsDoc, unitsHtml } from '../lib/vib'
+import { PORTAL_CSS } from '../lib/portalSections'
 
 const props = defineProps<{ d: any; sub?: string | null; cms?: any }>()
 const emit = defineEmits<{ (e: 'sub', id: string): void }>()
@@ -31,6 +32,9 @@ watch(sub, async (s) => {
 const cmsData = computed<any>(() => props.cms ?? cmsLocal.value)
 const F = computed<any>(() => props.d?.fus)
 
+/** ★P71 门户「振动·CMS」整页文档(同源 iframe)—— 用门户 CSS 渲染,chip 为按窗实时值 */
+const portalCmsSrc = computed<string>(() => portalCmsDoc(props.d, cmsData.value, PORTAL_CSS))
+
 const bodyHtml = computed(() => {
   const f = F.value
   if (!f || f.err) return `<section><div class="panel"><p>${t('fus.unavailable', { e: String((f && f.err) || '') })}</p></div></section>`
@@ -39,7 +43,7 @@ const bodyHtml = computed(() => {
   if (sub.value === 'units') return unitsHtml(f, null)
   if (sub.value === 'events') return eventsHtml(f, props.d)
   if (sub.value === 'cms') return cmsHtml(cmsData.value)
-  if (sub.value === 'portal_cms') return portalCmsHtml(props.d, cmsData.value)   // ★P65 门户「振动·CMS」(受时间窗影响)
+  // portal_cms 由模板里的同源 iframe 渲染(见下方 portalCmsSrc)
   return `<div class="panel mb"><p class="small muted">子视图「${t('fus.sub.' + sub.value)}」尚未迁移到 Vue(迁移中)。</p></div>`
 })
 </script>
@@ -51,6 +55,8 @@ const bodyHtml = computed(() => {
       <button v-for="s in FUS_SUBS" :key="s" class="pill" :class="{ on: s === sub }" :data-fsub="s"
               @click="emit('sub', s)">{{ t('fus.sub.' + s) }}</button>
     </div>
-    <div class="unwrap-host" v-html="bodyHtml"></div>
+    <iframe v-if="sub === 'portal_cms'" class="portal-cms-frame" title="振动 · CMS"
+            :srcdoc="portalCmsSrc" style="width:100%;height:1080px;border:0;display:block"></iframe>
+    <div v-else class="unwrap-host" v-html="bodyHtml"></div>
   </section>
 </template>

+ 31 - 0
app_frontEnd/web/tools/_check_cms_frame.mjs

@@ -0,0 +1,31 @@
+
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { dirname, resolve } from 'node:path'
+import { JSDOM, VirtualConsole } from 'jsdom'
+const HERE = dirname(fileURLToPath(import.meta.url))
+const dist = resolve(HERE, '../../../release/web')
+const idx = readFileSync(dist + '/index.html', 'utf8')
+const jsFile = (idx.match(/src="([^"]+\.js)"/) || [])[1].replace(/^.*\//, '')
+const cssFile = (idx.match(/href="([^"]+\.css)"/) || [])[1].replace(/^.*\//, '')
+const code = readFileSync(dist + '/assets/' + jsFile, 'utf8')
+const css = readFileSync(dist + '/assets/' + cssFile, 'utf8')
+const fleet = readFileSync(resolve(HERE, '_fleet_fixture.json'), 'utf8')
+const vib = readFileSync(resolve(HERE, '_vibcms_fixture.json'), 'utf8')
+const dom = new JSDOM(idx.replace('</head>', '<style>' + css + '</style></head>'),
+  { url: 'http://127.0.0.1:28084/detail/v2#tab=vibration', runScripts: 'outside-only', pretendToBeVisual: true, virtualConsole: new VirtualConsole() })
+const w = dom.window
+w.fetch = async (u) => { const s = String(u); const body = /fleet/.test(s) ? fleet : (/vibcms/.test(s) ? vib : '{}')
+  return { ok: true, status: 200, json: async () => JSON.parse(body), text: async () => body } }
+try { w.eval(code) } catch (e) { console.log('EVAL_ERR ' + String(e).slice(0, 120)) }
+for (let i = 0; i < 24; i++) { await new Promise((r) => setTimeout(r, 400)); if (w.document.querySelector('[data-fsub]')) break }
+const p6 = w.document.querySelector('[data-fsub="portal_cms"]')
+if (p6) p6.dispatchEvent(new w.MouseEvent('click', { bubbles: true }))
+for (let i = 0; i < 16; i++) { await new Promise((r) => setTimeout(r, 400)); if (w.document.querySelector('.portal-cms-frame')) break }
+const f = w.document.querySelector('.portal-cms-frame')
+const sd = f ? (f.getAttribute('srcdoc') || '') : ''
+console.log('FRAME ' + (f ? 'Y' : 'N') + ' LEN ' + sd.length)
+console.log('HAS_PORTAL_CSS ' + /--ground|\.hero/.test(sd))
+console.log('HAS_HERO_TITLE ' + /振动与 SCADA 联合分析/.test(sd))
+console.log('CHIPS_ROW ' + (/<div class="chips">/.test(sd)) + ' · 当前时间窗 ' + /当前时间窗 · /.test(sd))
+console.log('CHIPS_COUNT ' + (sd.match(/class="chip/g) || []).length)

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
release/web/assets/index-C6A6HZLY.js


+ 1 - 1
release/web/index.html

@@ -4,7 +4,7 @@
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
   <title>观澜 · 风电数据分析与决策支持</title>
-  <script type="module" crossorigin src="/web/assets/index-CkEIofVw.js"></script>
+  <script type="module" crossorigin src="/web/assets/index-C6A6HZLY.js"></script>
   <link rel="stylesheet" crossorigin href="/web/assets/index-DFl-J_Hd.css">
 </head>
 <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов