probe-t64-dupkey.mjs 5.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. // 探针:重复 :key 会不会让翻页画出错内容(合成载荷:key 全同、值各不同)。
  2. import { readFileSync } from 'node:fs'
  3. import { registerHooks } from 'node:module'
  4. import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
  5. import { createRenderer, h, ref, nextTick } from '@vue/runtime-core'
  6. const root = new URL('../../../../../src/modules/cockpit/', import.meta.url)
  7. registerHooks({
  8. load(url, context, nextLoad) {
  9. if (url.endsWith('.json')) return { format: 'module', shortCircuit: true, source: `export default ${readFileSync(new URL(url), 'utf8')}` }
  10. if (url.endsWith('.vue')) {
  11. const { descriptor } = parse(readFileSync(new URL(url), 'utf8'), { filename: new URL(url).pathname })
  12. const script = compileScript(descriptor, { id: 'probe', genDefaultAs: '__sfc__' })
  13. const template = compileTemplate({
  14. source: descriptor.template.content, filename: new URL(url).pathname,
  15. id: 'probe', compilerOptions: { bindingMetadata: script.bindings },
  16. })
  17. return { format: 'module', shortCircuit: true, source: `${script.content}\n${template.code}\n__sfc__.render = render; export default __sfc__;` }
  18. }
  19. return nextLoad(url, context)
  20. },
  21. })
  22. const nodeOps = {
  23. createElement: (tag) => ({ tag, props: {}, children: [], parent: null, text: '', style: {}, addEventListener() {}, removeEventListener() {} }),
  24. createText: (text) => ({ tag: '#text', text, children: [], parent: null, props: {}, style: {}, addEventListener() {}, removeEventListener() {} }),
  25. createComment: (text) => ({ tag: '#comment', text, children: [], parent: null, props: {}, style: {}, addEventListener() {}, removeEventListener() {} }),
  26. setText: (node, text) => { node.text = text },
  27. setElementText: (node, text) => { node.text = text; node.children = [] },
  28. insert: (child, parent, anchor) => {
  29. if (child.parent) child.parent.children = child.parent.children.filter((c) => c !== child)
  30. child.parent = parent
  31. if (!parent) return
  32. if (anchor) { const i = parent.children.indexOf(anchor); parent.children.splice(i < 0 ? parent.children.length : i, 0, child) }
  33. else parent.children.push(child)
  34. },
  35. remove: (child) => { if (child.parent) child.parent.children = child.parent.children.filter((c) => c !== child); child.parent = null },
  36. parentNode: (node) => node.parent,
  37. nextSibling: (node) => (node.parent ? node.parent.children[node.parent.children.indexOf(node) + 1] || null : null),
  38. querySelector: () => null,
  39. setScopeId: () => {},
  40. patchProp: (el, key, prev, next) => { el.props[key] = next },
  41. }
  42. const { createApp } = createRenderer(nodeOps)
  43. const { default: PanelBlocks } = await import(new URL('components/PanelBlocks.vue', root))
  44. const { operationPanel } = await import(new URL('domain/infrastructurePanels.js', root))
  45. const { section } = await import(new URL('domain/sectionState.js', root))
  46. function findAll(node, predicate, acc = []) {
  47. if (!node) return acc
  48. if (predicate(node)) acc.push(node)
  49. for (const c of node.children || []) findAll(c, predicate, acc)
  50. return acc
  51. }
  52. function textOf(node) {
  53. if (!node) return ''
  54. const own = node.tag === '#text' ? node.text : typeof node.props?.text === 'string' ? node.props.text : ''
  55. return own + (node.children || []).map(textOf).join('')
  56. }
  57. // 合成载荷:699 条指标,metricCode 全同(真实 GAS 就是这个形状),value 各不相同 V0..V698
  58. const mk = (i) => ({
  59. metricCode: 'same_code', metricName: `M${i}`, category: 'SUPPLY', unit: 'MPa',
  60. value: String(i), capacityValue: '1', loadRatio: '0.5', loadState: 'NORMAL',
  61. pointCodes: [`FAC${i}`], freshness: { status: 'CURRENT' },
  62. })
  63. const raw = { industries: [{ industry: { code: 'GAS', label: '燃气' }, state: 'READY', metrics: Array.from({ length: 699 }, (_, i) => mk(i)) }], excluded: {} }
  64. const data = operationPanel.length > 2 ? null : null
  65. const { toOperatingInformation } = await import(new URL('domain/infrastructurePanels.js', root))
  66. const view = operationPanel(toOperatingInformation(raw), section('success', { loaded: true }), { scope: 'all' })
  67. const rows = view.blocks[1].rows
  68. console.log('domain row keys distinct:', new Set(rows.map((r) => r.key)).size, '/', rows.length)
  69. console.log('row[0] meta:', rows[0].meta.join(' · '))
  70. console.log('row[11] meta:', rows[11].meta.join(' · '))
  71. const viewRef = ref(view)
  72. const app = createApp({ render: () => h(PanelBlocks, { view: viewRef.value }) })
  73. app.config.warnHandler = () => {}
  74. const container = nodeOps.createElement('#root')
  75. app.mount(container)
  76. function renderedMetas() {
  77. return findAll(container, (n) => n.props && 'data-item' in n.props).map((card) => {
  78. const metas = findAll(card, (n) => n.props && n.props['class'] && String(n.props['class']).includes('panel-item__meta'))
  79. return metas.length ? textOf(metas[0]) : '(no meta)'
  80. })
  81. }
  82. console.log('page1 rendered:', renderedMetas().slice(0, 2).join(' || '))
  83. const btn2 = findAll(container, (n) => String(n.props?.['data-page-number']) === '2')[0]
  84. btn2.props.onClick()
  85. await nextTick()
  86. console.log('page2 rendered:', renderedMetas().slice(0, 2).join(' || '))
  87. console.log('page2 expected:', rows[10].meta.join(' · '), '||', rows[11].meta.join(' · '))
  88. const btn70 = findAll(container, (n) => String(n.props?.['data-page-number']) === '70')[0]
  89. btn70?.props.onClick()
  90. await nextTick()
  91. console.log('page70 rendered:', renderedMetas().join(' || '))