cardbuddy 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -0
- package/dist/cli.js +76 -0
- package/dist/config.js +63 -0
- package/dist/core.js +2 -0
- package/dist/preview-editor/assets/apple-touch-icon-BQcES9QK.png +0 -0
- package/dist/preview-editor/assets/default-image-wzZYQ86K.png +0 -0
- package/dist/preview-editor/assets/favicon-16-Cg6Nrosk.png +0 -0
- package/dist/preview-editor/assets/favicon-32-B_1744sp.png +0 -0
- package/dist/preview-editor/assets/index-BYB0hngG.js +72 -0
- package/dist/preview-editor/assets/index-CpBZEQDy.css +1 -0
- package/dist/preview-editor/assets/site-icon-CFMif8z4.png +0 -0
- package/dist/preview-editor/assets/svg-symbols-B2exBrAr.svg +118 -0
- package/dist/preview-editor/index.html +17 -0
- package/dist/server-core/_vendor/finmall-env.mjs +96 -0
- package/dist/server-core/assetProxy.js +154 -0
- package/dist/server-core/bbox.js +153 -0
- package/dist/server-core/browser.js +108 -0
- package/dist/server-core/buildDataFromSchema.js +50 -0
- package/dist/server-core/cardFiles.js +188 -0
- package/dist/server-core/cardHistory.js +156 -0
- package/dist/server-core/cardImage.js +85 -0
- package/dist/server-core/cardImportMeta.js +139 -0
- package/dist/server-core/cardListCache.js +68 -0
- package/dist/server-core/cardOpenFolder.js +135 -0
- package/dist/server-core/cardSave.js +127 -0
- package/dist/server-core/cardSidecar.js +72 -0
- package/dist/server-core/cardSync.js +259 -0
- package/dist/server-core/cardValidate.js +134 -0
- package/dist/server-core/index.js +82 -0
- package/dist/server-core/logger.js +241 -0
- package/dist/server-core/nodeModels.js +52 -0
- package/dist/server-core/renderStatus.js +207 -0
- package/dist/server-core/screenshot.js +123 -0
- package/dist/server-core/sliceStatic.js +44 -0
- package/dist/standalone.js +447 -0
- package/dist/vendored/App.vue +2330 -0
- package/dist/vendored/api.js +142 -0
- package/dist/vendored/assets/apple-touch-icon.png +0 -0
- package/dist/vendored/assets/default-image.png +0 -0
- package/dist/vendored/assets/favicon-16.png +0 -0
- package/dist/vendored/assets/favicon-32.png +0 -0
- package/dist/vendored/assets/logo.png +0 -0
- package/dist/vendored/assets/site-icon.png +0 -0
- package/dist/vendored/assets/svg-symbols.svg +118 -0
- package/dist/vendored/assets/svgIcons/icons.svg +54 -0
- package/dist/vendored/assets/svgIcons/index.js +39 -0
- package/dist/vendored/components/AbsoluteLayout.vue +42 -0
- package/dist/vendored/components/AlertCardInstance.vue +58 -0
- package/dist/vendored/components/AlertCardLayer.vue +59 -0
- package/dist/vendored/components/ApiPayloadTree.vue +168 -0
- package/dist/vendored/components/AppToast.vue +37 -0
- package/dist/vendored/components/Button.vue +37 -0
- package/dist/vendored/components/Canvas.vue +65 -0
- package/dist/vendored/components/CardGallery.vue +243 -0
- package/dist/vendored/components/CardPreview.vue +72 -0
- package/dist/vendored/components/CardSelect.vue +200 -0
- package/dist/vendored/components/CheckGroup.vue +131 -0
- package/dist/vendored/components/Column.vue +25 -0
- package/dist/vendored/components/ComponentPalette.vue +294 -0
- package/dist/vendored/components/ConfirmDialog.vue +39 -0
- package/dist/vendored/components/CustomPlaceholder.vue +33 -0
- package/dist/vendored/components/DatePickerOverlay.vue +345 -0
- package/dist/vendored/components/DebugPanel.vue +806 -0
- package/dist/vendored/components/DeviceOverlay.vue +70 -0
- package/dist/vendored/components/Divider.vue +22 -0
- package/dist/vendored/components/Dropdown.vue +191 -0
- package/dist/vendored/components/EventEditor.vue +247 -0
- package/dist/vendored/components/GridLayout.vue +67 -0
- package/dist/vendored/components/HoverGuides.vue +122 -0
- package/dist/vendored/components/Icon.vue +34 -0
- package/dist/vendored/components/Image.vue +34 -0
- package/dist/vendored/components/Input.vue +90 -0
- package/dist/vendored/components/JsonTree.vue +516 -0
- package/dist/vendored/components/ListLayout.vue +68 -0
- package/dist/vendored/components/LogValue.vue +51 -0
- package/dist/vendored/components/Map.vue +110 -0
- package/dist/vendored/components/MultiColumn.vue +49 -0
- package/dist/vendored/components/MultiText.vue +58 -0
- package/dist/vendored/components/NodeInspector.vue +295 -0
- package/dist/vendored/components/PickerOverlay.vue +131 -0
- package/dist/vendored/components/ProgressSlider.vue +167 -0
- package/dist/vendored/components/PropControl.vue +241 -0
- package/dist/vendored/components/PropTree.vue +128 -0
- package/dist/vendored/components/RadioButton.vue +74 -0
- package/dist/vendored/components/RemotePreview.vue +171 -0
- package/dist/vendored/components/RenderTree.vue +437 -0
- package/dist/vendored/components/Root.vue +30 -0
- package/dist/vendored/components/Row.vue +25 -0
- package/dist/vendored/components/SelectionOverlay.vue +105 -0
- package/dist/vendored/components/StructureTree.vue +949 -0
- package/dist/vendored/components/SvgCode.vue +19 -0
- package/dist/vendored/components/SvgSymbols.vue +30 -0
- package/dist/vendored/components/Switch.vue +38 -0
- package/dist/vendored/components/Text.vue +32 -0
- package/dist/vendored/components/Video.vue +70 -0
- package/dist/vendored/components/ViewContainer.vue +52 -0
- package/dist/vendored/components/base.js +12 -0
- package/dist/vendored/components/icons.js +47 -0
- package/dist/vendored/components/index.js +58 -0
- package/dist/vendored/components/setters/SetBgPosition.vue +64 -0
- package/dist/vendored/components/setters/SetBgSize.vue +90 -0
- package/dist/vendored/components/setters/SetCode.vue +90 -0
- package/dist/vendored/components/setters/SetColor.vue +244 -0
- package/dist/vendored/components/setters/SetCustomSelect.vue +58 -0
- package/dist/vendored/components/setters/SetExpr.vue +107 -0
- package/dist/vendored/components/setters/SetGradient.vue +125 -0
- package/dist/vendored/components/setters/SetGrid9.vue +68 -0
- package/dist/vendored/components/setters/SetIconSelect.vue +67 -0
- package/dist/vendored/components/setters/SetIfForm.vue +61 -0
- package/dist/vendored/components/setters/SetImage.vue +43 -0
- package/dist/vendored/components/setters/SetList.vue +139 -0
- package/dist/vendored/components/setters/SetNumber.vue +118 -0
- package/dist/vendored/components/setters/SetNumberStyle.vue +113 -0
- package/dist/vendored/components/setters/SetPadding.vue +77 -0
- package/dist/vendored/components/setters/SetSegment.vue +160 -0
- package/dist/vendored/components/setters/SetSelect.vue +51 -0
- package/dist/vendored/components/setters/SetSwitch.vue +49 -0
- package/dist/vendored/components/setters/SetText.vue +52 -0
- package/dist/vendored/components/setters/props.js +23 -0
- package/dist/vendored/fidelity/baseline-messageOverview.json +87 -0
- package/dist/vendored/fidelity/calibration.json +29 -0
- package/dist/vendored/fidelity/fidelity-messageOverview-after.html +114 -0
- package/dist/vendored/fidelity/fidelity-messageOverview-after.json +272 -0
- package/dist/vendored/fidelity/fidelity-messageOverview.html +185 -0
- package/dist/vendored/fidelity/fidelity-messageOverview.json +336 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +262 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +502 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +201 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +417 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +125 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +338 -0
- package/dist/vendored/index.html +16 -0
- package/dist/vendored/main.js +36 -0
- package/dist/vendored/meta/action-meta.json +141 -0
- package/dist/vendored/meta/component-meta.json +2714 -0
- package/dist/vendored/meta/directive-meta.json +40 -0
- package/dist/vendored/meta/style-meta.json +1086 -0
- package/dist/vendored/runtime/StyleParser.js +179 -0
- package/dist/vendored/runtime/adapter.js +166 -0
- package/dist/vendored/runtime/alert-card.js +125 -0
- package/dist/vendored/runtime/app-toast.js +33 -0
- package/dist/vendored/runtime/assets.js +16 -0
- package/dist/vendored/runtime/canvas-context.js +141 -0
- package/dist/vendored/runtime/card-runtime.js +289 -0
- package/dist/vendored/runtime/clipboard-node.js +83 -0
- package/dist/vendored/runtime/clipboard.js +29 -0
- package/dist/vendored/runtime/evaluate.js +152 -0
- package/dist/vendored/runtime/event-model.js +300 -0
- package/dist/vendored/runtime/hoverGuide.js +31 -0
- package/dist/vendored/runtime/jsapi-mock.js +256 -0
- package/dist/vendored/runtime/node-model.js +217 -0
- package/dist/vendored/runtime/property.js +364 -0
- package/dist/vendored/runtime/selectionOverlay.js +53 -0
- package/dist/vendored/runtime/toPreview.js +648 -0
- package/dist/vendored/standalone-main.js +36 -0
- package/dist/vendored/standalone.html +12 -0
- package/dist/vendored/styles/app.css +2565 -0
- package/dist/vendored/styles/card-gallery.css +234 -0
- package/dist/vendored/styles/card-select.css +281 -0
- package/dist/vendored/styles/components.css +383 -0
- package/dist/vendored/styles/dropdown.css +337 -0
- package/dist/vendored/styles/json-tree.css +308 -0
- package/dist/vendored/styles/node-inspector.css +1003 -0
- package/dist/vendored/styles/setters.css +962 -0
- package/dist/vendored/styles/standalone.css +27 -0
- package/dist/vendored/styles/structure-tree.css +320 -0
- package/package.json +42 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="visible" class="rp-mask" @click.self="$emit('close')">
|
|
3
|
+
<div class="rp-card" role="dialog" aria-label="远端预览二维码">
|
|
4
|
+
<div class="rp-head">
|
|
5
|
+
<span class="rp-title">远端预览(真机扫码)</span>
|
|
6
|
+
<button class="rp-x" type="button" @click="$emit('close')" aria-label="关闭">×</button>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="rp-body">
|
|
9
|
+
<div class="rp-qr-wrap">
|
|
10
|
+
<img v-if="qrDataUrl" :src="qrDataUrl" class="rp-qr" :class="{ 'rp-qr--blurred': publishing }" alt="preview qr" />
|
|
11
|
+
<div v-else class="rp-qr rp-qr--loading">{{ loading ? '准备中…' : '—' }}</div>
|
|
12
|
+
<div v-if="publishing" class="rp-qr-loading">
|
|
13
|
+
<span class="rp-spinner" />
|
|
14
|
+
<span class="rp-loading-text">正在发布预览…</span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<p class="rp-tip">手机(FinMall App)扫一扫,真机查看当前卡片预览</p>
|
|
18
|
+
|
|
19
|
+
<div class="rp-url">
|
|
20
|
+
<code class="rp-url-text">{{ url || '—' }}</code>
|
|
21
|
+
<button class="rp-copy" type="button" :disabled="!url" @click="copyUrl">
|
|
22
|
+
{{ copied ? '已复制 ✓' : '复制链接' }}
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
<p v-if="warn" class="rp-warn">{{ warn }}</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
import QRCode from 'qrcode'
|
|
33
|
+
import { apiFetch } from '@/api.js'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 复刻 ubomp-h5-sub 的 PreviewPopover.vue(onPopoverShow)远端预览逻辑:
|
|
37
|
+
* 1) 先显示「简版」二维码:app://previewCard?cardId=<id>(对齐 PreviewPopover 第80行)
|
|
38
|
+
* 2) 调后端 /__preview 走全链路:
|
|
39
|
+
* prepreview 保存(IDC10010)→ 取 code(IDC10015)→ 发布测试版(IDC10020)→ 通知真机(IDC10021)
|
|
40
|
+
* 3) 再显示「完整版」二维码:app://previewCard?cardId=<id>&cardCode=<code>&cardVersion=00000000&socket=<wss>
|
|
41
|
+
* (对齐 PreviewPopover 第92行,socket = CARD_WEBSOCKET + /websocket/card/<id>)
|
|
42
|
+
* 二维码编码的是原生 App scheme(非 http 地址),由 FinMall App 扫码后自行渲染卡片。
|
|
43
|
+
* 参考 ubomp-h5-sub/src/modules/inner_card/views/editor/components/PreviewPopover.vue
|
|
44
|
+
*/
|
|
45
|
+
export default {
|
|
46
|
+
name: 'RemotePreview',
|
|
47
|
+
props: {
|
|
48
|
+
visible: { type: Boolean, default: false },
|
|
49
|
+
// 远端卡片 id(schema.pageId,数值型),对应 ubomp 的 cardId / $route.query.id
|
|
50
|
+
pageId: { type: [String, Number], default: '' },
|
|
51
|
+
// 当前卡片完整 schema(含 pageId),用于「预览前先保存」当前编辑内容
|
|
52
|
+
schema: { type: Object, default: null },
|
|
53
|
+
// 远端卡片 code(可选;留空时由 /__preview 经 IDC10015 取回)
|
|
54
|
+
cardCode: { type: String, default: '' },
|
|
55
|
+
},
|
|
56
|
+
data() {
|
|
57
|
+
return {
|
|
58
|
+
qrDataUrl: '',
|
|
59
|
+
url: '',
|
|
60
|
+
loading: false,
|
|
61
|
+
publishing: false,
|
|
62
|
+
copied: false,
|
|
63
|
+
copyTimer: null,
|
|
64
|
+
warn: '',
|
|
65
|
+
code: '',
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
watch: {
|
|
69
|
+
visible(v) {
|
|
70
|
+
if (v) this.open()
|
|
71
|
+
},
|
|
72
|
+
pageId() {
|
|
73
|
+
if (this.visible) this.open()
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
beforeUnmount() {
|
|
77
|
+
if (this.copyTimer) clearTimeout(this.copyTimer)
|
|
78
|
+
},
|
|
79
|
+
methods: {
|
|
80
|
+
buildSocket(cardId) {
|
|
81
|
+
// 对应 ubomp config/uat.env.js:CARD_WEBSOCKET = wss://<host>/pmb-web
|
|
82
|
+
// 域名不硬编码:从 .env.finmall 的 FINMALL_BASE_URL 经 vite 暴露到
|
|
83
|
+
// import.meta.env 读取(默认 UAT 域名),部署到其他环境时改配置即可。
|
|
84
|
+
const base = import.meta.env.FINMALL_BASE_URL || 'https://uat-pkg.finmall.com'
|
|
85
|
+
const wsHost = base.replace(/^https?:/, 'wss:')
|
|
86
|
+
return `${wsHost}/pmb-web/websocket/card/${cardId}`
|
|
87
|
+
},
|
|
88
|
+
fullUrl(cardId, cardCode) {
|
|
89
|
+
const socket = this.buildSocket(cardId)
|
|
90
|
+
return (
|
|
91
|
+
`app://previewCard?cardId=${encodeURIComponent(cardId)}` +
|
|
92
|
+
`&cardCode=${encodeURIComponent(cardCode || '')}` +
|
|
93
|
+
`&cardVersion=00000000` +
|
|
94
|
+
`&socket=${encodeURIComponent(socket)}`
|
|
95
|
+
)
|
|
96
|
+
},
|
|
97
|
+
async makeQr(text) {
|
|
98
|
+
return QRCode.toDataURL(text, {
|
|
99
|
+
width: 220,
|
|
100
|
+
margin: 1,
|
|
101
|
+
errorCorrectionLevel: 'H',
|
|
102
|
+
color: { dark: '#10131a', light: '#ffffff' },
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
async open() {
|
|
106
|
+
this.qrDataUrl = ''
|
|
107
|
+
this.url = ''
|
|
108
|
+
this.loading = true
|
|
109
|
+
this.publishing = false
|
|
110
|
+
this.copied = false
|
|
111
|
+
this.warn = ''
|
|
112
|
+
this.code = this.cardCode || ''
|
|
113
|
+
const cardId = String(this.pageId || '').trim()
|
|
114
|
+
if (!cardId) {
|
|
115
|
+
this.warn = '当前卡片缺少 pageId(远端卡片 id),无法生成预览二维码'
|
|
116
|
+
this.loading = false
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
// 1) 简版二维码(仅 cardId)
|
|
120
|
+
this.url = `app://previewCard?cardId=${encodeURIComponent(cardId)}`
|
|
121
|
+
try {
|
|
122
|
+
this.qrDataUrl = await this.makeQr(this.url)
|
|
123
|
+
} catch (e) {
|
|
124
|
+
this.warn = '二维码生成失败:' + (e && e.message)
|
|
125
|
+
}
|
|
126
|
+
// 2) 走远端全链路:保存 → 取 code → 发布 → 通知
|
|
127
|
+
this.publishing = true
|
|
128
|
+
try {
|
|
129
|
+
const resp = await apiFetch('/__preview', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'content-type': 'application/json' },
|
|
132
|
+
body: JSON.stringify({ schema: this.schema || { pageId: cardId } }),
|
|
133
|
+
})
|
|
134
|
+
const data = await resp.json().catch(() => ({}))
|
|
135
|
+
if (!resp.ok) throw new Error(data.message || 'preview failed')
|
|
136
|
+
if (data.cardCode !== undefined) this.code = data.cardCode
|
|
137
|
+
// 3) 完整版二维码(带 cardCode / socket)
|
|
138
|
+
this.url = this.fullUrl(cardId, this.code)
|
|
139
|
+
this.qrDataUrl = await this.makeQr(this.url)
|
|
140
|
+
} catch (e) {
|
|
141
|
+
this.warn = '远端发布失败:' + (e && e.message)
|
|
142
|
+
} finally {
|
|
143
|
+
this.publishing = false
|
|
144
|
+
this.loading = false
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
async copyUrl() {
|
|
148
|
+
if (!this.url) return
|
|
149
|
+
try {
|
|
150
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
151
|
+
await navigator.clipboard.writeText(this.url)
|
|
152
|
+
} else {
|
|
153
|
+
const ta = document.createElement('textarea')
|
|
154
|
+
ta.value = this.url
|
|
155
|
+
ta.style.position = 'fixed'
|
|
156
|
+
ta.style.opacity = '0'
|
|
157
|
+
document.body.appendChild(ta)
|
|
158
|
+
ta.select()
|
|
159
|
+
document.execCommand('copy')
|
|
160
|
+
document.body.removeChild(ta)
|
|
161
|
+
}
|
|
162
|
+
this.copied = true
|
|
163
|
+
if (this.copyTimer) clearTimeout(this.copyTimer)
|
|
164
|
+
this.copyTimer = setTimeout(() => { this.copied = false }, 1500)
|
|
165
|
+
} catch (e) {
|
|
166
|
+
this.warn = '复制失败:' + (e && e.message)
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
</script>
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
递归渲染组件:消费 buildPreview(schema).view 这棵预览树。
|
|
3
|
+
对齐官方渲染模块 2130 的 `v`(单组件)/ `g`(作用域插槽)/ `h`(事件 handler)/
|
|
4
|
+
`p`(applyVars)/ `d`(evalExpr)。
|
|
5
|
+
|
|
6
|
+
注意:本组件**没有 <template>**,完全由 <script> 里的 render() 函数驱动——
|
|
7
|
+
SFC 中若存在 <template> 会覆盖 script 的 render 选项,导致渲染为空。
|
|
8
|
+
|
|
9
|
+
要点:
|
|
10
|
+
- className -> 注册表取 Vue 组件;取不到画未知组件占位但仍渲染子树。
|
|
11
|
+
- 数据绑定:applyVars(properties, scope) / applyVars(style.properties, scope)。
|
|
12
|
+
- 列表(ListLayout / GridLayout)用 makeSlotScope 构造 item 作用域后,
|
|
13
|
+
以具名默认插槽(Vue 3 slots 对象)把「模板子节点」按 value 逐项渲染
|
|
14
|
+
(对齐官方 `g` 的 Object.assign({}, o, item, { $$:item, _index:index }))。
|
|
15
|
+
- 事件:eventactions 中 actionType=customAction 的,生成
|
|
16
|
+
new Function("event", code) 并在触发时以 {state,data,setData,...} 为 this 执行。
|
|
17
|
+
- **加载/初始化动作**(eventKey 属于「就绪类」生命周期:
|
|
18
|
+
'' / onReady / onLoad / onCreate)在 `created`(首帧之前)由 runLoadActions
|
|
19
|
+
执行一次;它们**不**注册成被动事件 handler,避免与 Root/组件自身的 emit 重复触发。
|
|
20
|
+
这样数据(如 assetOverview 的 ratio)在首帧渲染前就位,不会首帧 NaN 再依赖
|
|
21
|
+
异步重渲染补救。
|
|
22
|
+
- 其余事件(click / change / onMeasured / selectChange / action …)注册成被动
|
|
23
|
+
handler,由用户交互或组件(如 Canvas 测量后 emit onMeasured)触发。
|
|
24
|
+
- v-show:渲染期按 showType(00/01/02) + condition.expression 实时 evalExpr 计算。
|
|
25
|
+
-->
|
|
26
|
+
<script>
|
|
27
|
+
import { h } from 'vue'
|
|
28
|
+
import RenderTree from './RenderTree.vue'
|
|
29
|
+
import { getComponent, SCOPED_SLOT_COMPONENTS } from './index.js'
|
|
30
|
+
import { applyVars, makeSlotScope, evalExpr, makeRenderScope } from '../runtime/evaluate.js'
|
|
31
|
+
import CustomPlaceholder from './CustomPlaceholder.vue'
|
|
32
|
+
import { setSelectionEl, clearSelectionEl } from '../runtime/selectionOverlay.js'
|
|
33
|
+
|
|
34
|
+
// 「就绪类」生命周期事件 key:这些不是用户交互,而是卡片/组件加载完成时应自动执行的
|
|
35
|
+
// 初始化动作(如 assetOverview 根节点挂在 onReady 上的 ratio 计算)。
|
|
36
|
+
// 它们在 created(首帧之前)由 runLoadActions 执行一次,且不注册成被动 handler。
|
|
37
|
+
const LIFECYCLE_KEYS = new Set(['', 'onReady', 'onLoad', 'onCreate'])
|
|
38
|
+
|
|
39
|
+
function isLifecycleKey(key) {
|
|
40
|
+
return LIFECYCLE_KEYS.has(key || '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Vue 2 的 `on: { eventKey: fn }` 事件对象 → Vue 3 的 onXxx 监听 prop。
|
|
45
|
+
* 与运行时 emit 解析规则一致:emit('foo') 对应 prop `onFoo`,
|
|
46
|
+
* emit('onMeasured') 对应 `onOnMeasured`,中划线 key 先驼峰化。
|
|
47
|
+
*/
|
|
48
|
+
function toHandlerKey(key) {
|
|
49
|
+
const camel = String(key).replace(/-(\w)/g, (_, c) => c.toUpperCase())
|
|
50
|
+
return 'on' + camel.charAt(0).toUpperCase() + camel.slice(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** CSS 选择器转义:用于按 data-id 精确查询首个匹配元素(列表重复项定位)。 */
|
|
54
|
+
function cssEscape(v) {
|
|
55
|
+
return (typeof window !== 'undefined' && window.CSS && CSS.escape)
|
|
56
|
+
? CSS.escape(String(v))
|
|
57
|
+
: String(v).replace(/["\\\]]/g, '\\$&')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default {
|
|
61
|
+
name: 'RenderTree',
|
|
62
|
+
inject: ['adapter', 'cardRuntime'],
|
|
63
|
+
props: {
|
|
64
|
+
node: { type: Object, required: true },
|
|
65
|
+
scope: { type: Object, default: () => ({}) },
|
|
66
|
+
selectedNodeId: { type: String, default: '' },
|
|
67
|
+
},
|
|
68
|
+
methods: {
|
|
69
|
+
/** 取某节点的「直接子节点」:优先用默认插槽里的 children,否则用 components */
|
|
70
|
+
getChildNodes(node) {
|
|
71
|
+
const slots = node.slots || []
|
|
72
|
+
if (slots.length && slots[0].components && slots[0].components.length) {
|
|
73
|
+
return slots[0].components
|
|
74
|
+
}
|
|
75
|
+
return node.components || []
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/** 取 customAction 脚本代码(兼容 code / compiled 两种导出字段) */
|
|
79
|
+
actionCode(ea) {
|
|
80
|
+
const fn = (ea.action && ea.action.properties && ea.action.properties.function) || {}
|
|
81
|
+
return fn.code || fn.compiled || ''
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/** v-show 可见性:00 始终显示;01 条件真显示;02 条件真隐藏 */
|
|
85
|
+
isVisible(node, scope) {
|
|
86
|
+
const rt = this.cardRuntime
|
|
87
|
+
const vshow = node.directives && node.directives['v-show']
|
|
88
|
+
if (!vshow) return true
|
|
89
|
+
const showType = vshow.showType || '00'
|
|
90
|
+
if (showType === '00') return true
|
|
91
|
+
const cond = vshow.condition || {}
|
|
92
|
+
const expr = cond.expression
|
|
93
|
+
if (!expr) return showType === '01' // 无表达式兜底:01 显 / 02 隐
|
|
94
|
+
try {
|
|
95
|
+
const res = evalExpr(expr, scope)
|
|
96
|
+
return showType === '01' ? !!res : !res
|
|
97
|
+
} catch (e) {
|
|
98
|
+
rt.pushError('vshow', `${node.className}: [${expr}] -> ${e.message}`)
|
|
99
|
+
return showType === '01'
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** 收集绑定到具体事件的 handler(就绪类生命周期动作不在此处理,改由 runLoadActions 在 created 执行) */
|
|
104
|
+
buildHandlers(node, scope) {
|
|
105
|
+
const rt = this.cardRuntime
|
|
106
|
+
const handlers = {}
|
|
107
|
+
// 用户交互触发的事件:直接执行,交互前不回写 rootScope。
|
|
108
|
+
// 设计:输入框 / 选择等交互只写运行期克隆 rt.state,而事件代码里 `this.data.xxx`
|
|
109
|
+
// 也直接读 rt.state(对齐官方运行时——`this.data` 即含实时输入的工作数据,
|
|
110
|
+
// 仅「保存到服务器」才持久化)。因此本地调试全程不碰 rt.rootScope(初始数据 /
|
|
111
|
+
// 保存基线),永不触发保存高亮、也不作为最终数据落盘。生命周期动作不走此路径。
|
|
112
|
+
;(node.eventactions || []).forEach((ea) => {
|
|
113
|
+
const ev = ea.event || {}
|
|
114
|
+
const evKey = ev.eventKey || ''
|
|
115
|
+
if (isLifecycleKey(evKey)) return // 初始化动作在 created 由 runLoadActions 统一处理
|
|
116
|
+
const at = ea.action || {}
|
|
117
|
+
const atype = at.actionType
|
|
118
|
+
if (atype === 'customAction') {
|
|
119
|
+
const code = this.actionCode(ea)
|
|
120
|
+
handlers[evKey] = (event) => this.runCustomAction(code, scope, event, node)
|
|
121
|
+
} else {
|
|
122
|
+
handlers[evKey] = (event) => this.runSystemAction(atype, at.properties, scope, event, node)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
return handlers
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/** 执行一条 customAction 脚本 */
|
|
129
|
+
runCustomAction(code, scope, event, node) {
|
|
130
|
+
const rt = this.cardRuntime
|
|
131
|
+
// this.state 必须「读取当前节点作用域、写入独立的 rt.state」:
|
|
132
|
+
// - 读:走 scope 原对象。列表项场景下 scope 就是该项本身,弹窗用
|
|
133
|
+
// `this.state` 取选中项才不会失效(原实现 this.state===scope 正是靠此);
|
|
134
|
+
// 主卡读 this.state.x 也仍是 rootScope 原值。
|
|
135
|
+
// - 写:this.state.x = v 与 setData 都劫持到 rt.state,绝不动 rootScope,
|
|
136
|
+
// 从而不污染持久化数据、不触发「保存」脏标记(修复上轮交互误判已编辑)。
|
|
137
|
+
// 显示层经 makeRenderScope(rt.state, 局部作用域) 按「局部优先于 state」合并,仍读得到运行期写入
|
|
138
|
+
// (根节点局部作用域即 rt.state 本身,故根层级 state 写入恒优先生效;列表项 itemScope 优先于全局)。
|
|
139
|
+
const stateProxy = new Proxy(scope, {
|
|
140
|
+
set(_t, k, v) {
|
|
141
|
+
rt.state[k] = v
|
|
142
|
+
return true
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
const ctx = {
|
|
146
|
+
state: stateProxy,
|
|
147
|
+
// `this.data` 在运行时读取运行期克隆 rt.state(而非 rt.rootScope):
|
|
148
|
+
// - 对齐官方语义——运行时 `this.data` 即「含 Input 实时写入的工作数据」,
|
|
149
|
+
// 脚本里 `this.data.payAmount` / `this.data.remark` 读到的就是当前交互值;
|
|
150
|
+
// - rt.rootScope 仅作「初始数据 / 保存基线」,调试交互(输入、选择、标签
|
|
151
|
+
// 切换、globalSet)一律只写 rt.state,**绝不**回写 rt.rootScope,
|
|
152
|
+
// 故 historyDirty 永不因调试变真、保存按钮不亮、存档也只存初始数据。
|
|
153
|
+
data: rt.state,
|
|
154
|
+
setData(patch) {
|
|
155
|
+
if (patch && typeof patch === 'object') Object.assign(rt.state, patch)
|
|
156
|
+
rt.markDirty()
|
|
157
|
+
},
|
|
158
|
+
setRootData(patch) {
|
|
159
|
+
if (patch && typeof patch === 'object') Object.assign(rt.rootScope, patch)
|
|
160
|
+
rt.markDirty()
|
|
161
|
+
},
|
|
162
|
+
addEventListener(name, fn) {
|
|
163
|
+
rt.addEventListener(name, fn)
|
|
164
|
+
},
|
|
165
|
+
postMessage(name, payload) {
|
|
166
|
+
rt.postMessage(name, payload)
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
// 事件代码的 console.* 主动日志:注入一个与全局 console 同签名的对象,作为
|
|
170
|
+
// new Function 的第 2 个形参(形参遮蔽全局 console),既打到真实控制台,
|
|
171
|
+
// 又汇入 rt.eventLogs 供「事件面板」展示。不修改全局 console,避免污染/误伤。
|
|
172
|
+
const evtConsole = {
|
|
173
|
+
log: (...a) => { console.log(...a); rt.pushLog('log', a) },
|
|
174
|
+
info: (...a) => { console.info(...a); rt.pushLog('info', a) },
|
|
175
|
+
warn: (...a) => { console.warn(...a); rt.pushLog('warn', a) },
|
|
176
|
+
error: (...a) => { console.error(...a); rt.pushLog('error', a) },
|
|
177
|
+
debug: (...a) => { console.debug(...a); rt.pushLog('debug', a) },
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
// eslint-disable-next-line no-new-func
|
|
181
|
+
const fn = new Function('event', 'console', code || '')
|
|
182
|
+
fn.apply(ctx, [event, evtConsole])
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// 错误面板里带上节点 id,便于在结构树里定位出错节点(如 Row[row_4w])
|
|
185
|
+
const nodeTag = node && node.id ? `${node.className}(${node.id})` : node.className
|
|
186
|
+
rt.pushError('customAction', `${nodeTag}: ${e.message}`)
|
|
187
|
+
// eslint-disable-next-line no-console
|
|
188
|
+
console.error('[customAction error]', nodeTag, e)
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/** 系统动作(closeAlertCard / sendAiChatMessage 等):转给 ynet.callJSAPI 桩 */
|
|
193
|
+
runSystemAction(atype, props, scope, event, node) {
|
|
194
|
+
const rt = this.cardRuntime
|
|
195
|
+
let ynet = typeof window !== 'undefined' ? window.ynet : null
|
|
196
|
+
if (!ynet && typeof window !== 'undefined') {
|
|
197
|
+
ynet = window.ynet = {
|
|
198
|
+
callJSAPI(api, p, cb) {
|
|
199
|
+
rt.pushApiCall(api, p)
|
|
200
|
+
if (typeof cb === 'function') {
|
|
201
|
+
try {
|
|
202
|
+
cb({})
|
|
203
|
+
} catch (e) {
|
|
204
|
+
/* ignore */
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const p = props || {}
|
|
211
|
+
if (atype === 'closeAlertCard') ynet.callJSAPI('closeAlertCard', p, p && p.callback)
|
|
212
|
+
else if (atype === 'sendAiChatMessage') ynet.callJSAPI('sendAiChatMessage', { message: (p && p.message) || '' }, undefined)
|
|
213
|
+
else ynet.callJSAPI(atype, p, undefined)
|
|
214
|
+
rt.pushApiCall(atype, p)
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/** 加载/初始化动作(就绪类生命周期 eventKey 的 customAction),created 执行一次(mounted 补跑但按节点去重) */
|
|
218
|
+
runLoadActions() {
|
|
219
|
+
const rt = this.cardRuntime
|
|
220
|
+
const node = this.node
|
|
221
|
+
if (!node.__loadRan) {
|
|
222
|
+
node.__loadRan = true
|
|
223
|
+
;(node.eventactions || []).forEach((ea) => {
|
|
224
|
+
const ev = ea.event || {}
|
|
225
|
+
if (!isLifecycleKey(ev.eventKey || '')) return
|
|
226
|
+
if (((ea.action || {}).actionType) !== 'customAction') return
|
|
227
|
+
const code = this.actionCode(ea)
|
|
228
|
+
if (!code) return
|
|
229
|
+
// 初始化动作的作用域必须是「局部作用域(根节点即 rt.state)优先」的合并视图:
|
|
230
|
+
// - 读 this.state.x 取 rt.state(根节点 scope 已由 App.vue 换成 rt.state;深拷贝副本见 seedState),
|
|
231
|
+
// 绝不触碰 rootScope 嵌套对象;
|
|
232
|
+
// - 写 this.state.x / setData 经 set 陷阱只落 rt.state,不污染持久化数据绑定。
|
|
233
|
+
// 否则(如 assetOverview 根节点 onReady 里 `this.state.listData.forEach(e => e.ratio=...)`)
|
|
234
|
+
// 会顺着「this.scope===rt.rootScope」的共享引用把 ratio 写进 rootScope,
|
|
235
|
+
// 导致切卡即被保存脏标记误判为「已编辑」。交互事件路径仍直接用 this.scope(保持列表项回填语义)。
|
|
236
|
+
this.runCustomAction(code, makeRenderScope(rt.state, this.scope), undefined, node)
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/** 递归渲染一个节点,返回 vnode 或 null */
|
|
242
|
+
renderNode(node) {
|
|
243
|
+
if (node.visible === false) return null
|
|
244
|
+
const rt = this.cardRuntime
|
|
245
|
+
const scope = makeRenderScope(rt.state, this.scope)
|
|
246
|
+
if (!this.isVisible(node, scope)) return null
|
|
247
|
+
|
|
248
|
+
const comp = getComponent(node.className)
|
|
249
|
+
|
|
250
|
+
// 未知组件:画占位但保留子树
|
|
251
|
+
if (!comp) {
|
|
252
|
+
const children = this.getChildNodes(node).map((c) => this.renderSub(c, scope))
|
|
253
|
+
return h(CustomPlaceholder, { customClass: node.className, title: '未知组件' }, () => children)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const props = applyVars(node.properties || {}, scope, rt.getBindingBuffer())
|
|
257
|
+
const styleProps = applyVars((node.style && node.style.properties) || {}, scope, rt.getBindingBuffer())
|
|
258
|
+
// Vue 3:props 与事件监听(onXxx)统一平铺在同一个 vnode props 对象里
|
|
259
|
+
const vnodeProps = Object.assign({}, props, { styleProps })
|
|
260
|
+
// 在组件根元素挂载调试用 data 属性(data-cid / data-class),便于结构树定位、
|
|
261
|
+
// 事件面板与自动化测试精准选择对应节点(不影响渲染与布局)。
|
|
262
|
+
vnodeProps['data-cid'] = node.componentId || node.id
|
|
263
|
+
vnodeProps['data-class'] = node.className
|
|
264
|
+
// 节点唯一 id(与结构树选中键 selectedNodeId 同源),供选中覆盖层在列表重复项
|
|
265
|
+
// 中精确定位「第一个」匹配元素(querySelector 取文档顺序首个)。
|
|
266
|
+
vnodeProps['data-id'] = node.id
|
|
267
|
+
const handlers = this.buildHandlers(node, scope)
|
|
268
|
+
for (const key in handlers) vnodeProps[toHandlerKey(key)] = handlers[key]
|
|
269
|
+
|
|
270
|
+
// 输入框双向回写:官方平台会把 Input 的实时输入值写回卡片「工作数据」,
|
|
271
|
+
// 脚本里统一以 `this.data.xxx` 读取(见 creditCardPayment 的 `this.data.payAmount`、
|
|
272
|
+
// dreamPlan 的 `this.data.targetName`、transfersjump 的 `this.data.remark`)。
|
|
273
|
+
// 组件 meta 里 Input 没有可绑定事件(events: []),卡片无法自己接线,
|
|
274
|
+
// 所以由渲染层兜底:value 绑定形如 `$key` 时,输入即写回同名字段。
|
|
275
|
+
//
|
|
276
|
+
// 关键设计(对齐 RadioButton/CheckGroup 交互态语义 + 官方运行时语义):
|
|
277
|
+
// - 输入只写运行期克隆 `rt.state`,绝不碰持久化 `rt.rootScope`,因此打字过程
|
|
278
|
+
// 不会触发「保存到本地」脏标记(historyDirty 比对的是 rootScope 与磁盘版本);
|
|
279
|
+
// - 事件代码里 `this.data.xxx` 直接读 `rt.state`(见 runCustomAction 的
|
|
280
|
+
// `data: rt.state`),故 `this.data.remark` 等读到的就是当前实时输入值,
|
|
281
|
+
// 无需任何「交互前回写」;显示层经 makeRenderScope 按局部优先合并读取即时回显。
|
|
282
|
+
// state 是响应式的,赋值本身就会触发依赖它的节点重渲染,无需 markDirty。
|
|
283
|
+
if (node.className === 'Input' && !vnodeProps.onInput) {
|
|
284
|
+
const expr = ((node.properties && node.properties.__vars__) || {}).value
|
|
285
|
+
const m = /^\s*\$([A-Za-z_$][\w$]*)\s*$/.exec(String(expr || ''))
|
|
286
|
+
if (m) {
|
|
287
|
+
const key = m[1]
|
|
288
|
+
vnodeProps.onInput = (v) => {
|
|
289
|
+
rt.state[key] = v
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 单选/复选框双向回写:组件仅 emit('change', next) 而不自维护选中态,
|
|
295
|
+
// selected / checkedSet 完全由传入的 value 决定。运行时需把新值写回 value
|
|
296
|
+
// 绑定的变量(与 Input 同理,卡片脚本统一以 `this.data.xxx` 读取),否则点击
|
|
297
|
+
// 只会触发事件、选中态永不刷新。仅当用户未在 schema 自定义 change handler 时兜底。
|
|
298
|
+
if ((node.className === 'RadioButton' || node.className === 'CheckGroup') && !vnodeProps.onChange) {
|
|
299
|
+
const expr = ((node.properties && node.properties.__vars__) || {}).value
|
|
300
|
+
const m = /^\s*\$([A-Za-z_$][\w$]*)\s*$/.exec(String(expr || ''))
|
|
301
|
+
if (m) {
|
|
302
|
+
const key = m[1]
|
|
303
|
+
vnodeProps.onChange = (v) => {
|
|
304
|
+
// 单选/复选框的运行时选择是「交互态」而非「卡片数据编辑」:只写运行期
|
|
305
|
+
// state(绝不碰持久化 rootScope),因此不会触发「保存到本地」脏标记
|
|
306
|
+
// (historyDirty 比对的是 rt.rootScope 与磁盘版本),切卡/重载也不落盘。
|
|
307
|
+
// 显示层经 makeRenderScope 按局部优先合并读取,选中态即时刷新;组件级
|
|
308
|
+
// value 完全由 prop 驱动(无本地状态),须 markDirty 自增 revision
|
|
309
|
+
// 强制 RadioButton/CheckGroup 重算选中态。
|
|
310
|
+
rt.state[key] = v
|
|
311
|
+
rt.markDirty()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 滑块双向回写:ProgressSlider 仅 emit('change'/'selectChange', v) 而不自维护值,
|
|
317
|
+
// 进度条宽度与滑块块位置完全由传入的 value 决定。运行时须把新值写回 value 绑定的
|
|
318
|
+
// 变量(卡片脚本统一以 `this.data.xxx` 读取),否则拖动只会触发事件、滑块位置
|
|
319
|
+
// (进度条 / 滑块块)永不刷新,表现为"滑不动"。
|
|
320
|
+
//
|
|
321
|
+
// 关键:值回写是**平台内置绑定**(对齐线上语义——拖动即写回绑定变量),与用户自定义
|
|
322
|
+
// 的 change/selectChange 动作是「叠加」关系,绝不能因为用户绑了自定义 handler 就跳过
|
|
323
|
+
// 回写(否则像本仓库 all-components-test 卡那样「绑了 change 日志」的滑块就彻底滑不动)。
|
|
324
|
+
// 故此处无论用户是否自定义 handler 都写回,并把自定义 handler 与回写**组合**执行。
|
|
325
|
+
if (node.className === 'ProgressSlider') {
|
|
326
|
+
const expr = ((node.properties && node.properties.__vars__) || {}).value
|
|
327
|
+
const m = /^\s*\$([A-Za-z_$][\w$]*)\s*$/.exec(String(expr || ''))
|
|
328
|
+
if (m) {
|
|
329
|
+
const key = m[1]
|
|
330
|
+
const writeBack = (v) => {
|
|
331
|
+
// 滑块拖动是「交互态」而非「卡片数据编辑」:只写运行期 state(绝不碰
|
|
332
|
+
// 持久化 rootScope),不触发「保存到本地」脏标记,切卡/重载也不落盘;
|
|
333
|
+
// 显示层经 makeRenderScope 按局部优先合并读取即时回显,markDirty 自增
|
|
334
|
+
// revision 强制进度条宽度与滑块块位置重算。
|
|
335
|
+
rt.state[key] = v
|
|
336
|
+
rt.markDirty()
|
|
337
|
+
}
|
|
338
|
+
// 组合:先跑用户自定义 handler(若有),再执行平台内置值回写;
|
|
339
|
+
// 用 finally 保证即使用户 handler 抛错,回写仍生效。
|
|
340
|
+
const userOnChange = vnodeProps.onChange
|
|
341
|
+
const userOnSelectChange = vnodeProps.onSelectChange
|
|
342
|
+
vnodeProps.onChange = (v) => {
|
|
343
|
+
try {
|
|
344
|
+
if (userOnChange) userOnChange(v)
|
|
345
|
+
} finally {
|
|
346
|
+
writeBack(v)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
vnodeProps.onSelectChange = (v) => {
|
|
350
|
+
try {
|
|
351
|
+
if (userOnSelectChange) userOnSelectChange(v)
|
|
352
|
+
} finally {
|
|
353
|
+
writeBack(v)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const children = this.getChildNodes(node)
|
|
360
|
+
|
|
361
|
+
// 带作用域插槽的容器:把「模板子节点」按 value 逐项渲染
|
|
362
|
+
if (SCOPED_SLOT_COMPONENTS.has(node.className)) {
|
|
363
|
+
return h(comp, vnodeProps, {
|
|
364
|
+
default: (slotProps) => {
|
|
365
|
+
const itemScope = makeSlotScope(scope, slotProps.item, slotProps.index)
|
|
366
|
+
return children.map((c) => this.renderSub(c, itemScope))
|
|
367
|
+
},
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 普通组件:子节点直接渲染进默认插槽(函数式插槽,Vue 3 推荐写法)
|
|
372
|
+
return h(comp, vnodeProps, () => children.map((c) => this.renderSub(c, scope)))
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
/** 渲染子节点:包一层 RenderTree 以获得独立生命周期(mounted/事件作用域) */
|
|
376
|
+
renderSub(node, scope) {
|
|
377
|
+
return h(RenderTree, { node, scope, selectedNodeId: this.selectedNodeId })
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/** 根据选中状态驱动覆盖层:选中时把根元素交给覆盖层贴合,取消时归还 */
|
|
381
|
+
updateHighlight() {
|
|
382
|
+
const el = this.$el
|
|
383
|
+
if (!el || el.nodeType !== 1) return
|
|
384
|
+
const id = this.node.id
|
|
385
|
+
const isSelected = this.selectedNodeId && id === this.selectedNodeId
|
|
386
|
+
if (isSelected) {
|
|
387
|
+
// 列表(ListLayout/GridLayout)会把同一模板节点渲染成多个共享 id 的 DOM 实例,
|
|
388
|
+
// 全部满足选中条件。若直接把本实例 el 交给覆盖层,各实例依次覆盖会令高亮落在末项;
|
|
389
|
+
// 故显式取文档顺序中第一个匹配元素(即顶部项),保证高亮落在其一,并与
|
|
390
|
+
// Ctrl/⌘ 点击「定位」行为一致(均选中第一个)。
|
|
391
|
+
const screen = typeof document !== 'undefined' ? document.querySelector('.phone__screen') : null
|
|
392
|
+
const target = screen ? screen.querySelector('[data-id="' + cssEscape(String(id)) + '"]') : null
|
|
393
|
+
setSelectionEl(target || el, id)
|
|
394
|
+
} else {
|
|
395
|
+
clearSelectionEl(id)
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
render() {
|
|
401
|
+
// 读取 revision 建立「整树重渲染」依赖
|
|
402
|
+
void this.cardRuntime.revision
|
|
403
|
+
// 根节点渲染开始时重置绑定错误缓冲区:本轮渲染重新收集(渲染结束后
|
|
404
|
+
// 在 mounted/updated 里提交)。不能在此处直接清空响应式错误列表,
|
|
405
|
+
// 否则会在渲染中读写 observable 导致无限更新循环,且会误清事件错误。
|
|
406
|
+
if (this.node.className === 'Root') this.cardRuntime.startBindingPass()
|
|
407
|
+
return this.renderNode(this.node)
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
created() {
|
|
411
|
+
// 加载/初始化动作(就绪类生命周期 eventKey:'' / onReady / onLoad / onCreate 的
|
|
412
|
+
// customAction,如 assetOverview 根节点 onReady 里计算 ratio)。必须在「首帧渲染
|
|
413
|
+
// 之前」执行,否则首帧会因 ratio 等字段尚未计算而渲染成 NaN%,之后虽能靠 revision
|
|
414
|
+
// 自增触发重渲染,但某些环境下(如 HMR 状态异常、浏览器缓存)这次异步重渲染可能不
|
|
415
|
+
// 触发,导致百分比长期停留在 NaN%。提前到 created 可从根本上消除该时序风险。
|
|
416
|
+
this.runLoadActions()
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
mounted() {
|
|
420
|
+
// 首帧之后仍补跑一次:兼容那些需要在 DOM 就绪后才安全执行的加载动作;
|
|
421
|
+
// runLoadActions 内部按节点去重,不会重复执行同一动作。
|
|
422
|
+
this.runLoadActions()
|
|
423
|
+
this.$nextTick(() => this.updateHighlight())
|
|
424
|
+
// 首帧渲染结束:提交本轮收集到的绑定错误(渲染期之外,响应式安全)
|
|
425
|
+
if (this.node.className === 'Root') this.cardRuntime.commitBindingPass()
|
|
426
|
+
},
|
|
427
|
+
updated() {
|
|
428
|
+
// 每次重渲染结束后同样提交一轮绑定错误
|
|
429
|
+
if (this.node.className === 'Root') this.cardRuntime.commitBindingPass()
|
|
430
|
+
},
|
|
431
|
+
watch: {
|
|
432
|
+
selectedNodeId() {
|
|
433
|
+
this.updateHighlight()
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!-- 官方 module 2089 / 535:卡片根容器,挂载后抛 onReady -->
|
|
2
|
+
<template>
|
|
3
|
+
<div ref="root" class="ce_root" :style="styleObj">
|
|
4
|
+
<slot />
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script>
|
|
9
|
+
import StyleParser from '../runtime/StyleParser.js'
|
|
10
|
+
import base from './base.js'
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
name: 'CeRoot',
|
|
14
|
+
emits: ['onReady'],
|
|
15
|
+
mixins: [base],
|
|
16
|
+
props: {
|
|
17
|
+
styleProps: { type: Object, default: () => ({}) },
|
|
18
|
+
},
|
|
19
|
+
computed: {
|
|
20
|
+
styleObj() {
|
|
21
|
+
const parsed = new StyleParser(this, this.styleProps).parse()
|
|
22
|
+
// 官方是 Object.assign({fontSize}, parsed),即 style 里的 fontSize 优先
|
|
23
|
+
return Object.assign({ fontSize: this.adapter.doSize(14, 'font') }, parsed)
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
mounted() {
|
|
27
|
+
this.$emit('onReady', {})
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!-- 官方 module 2093 / 537(updateGap 是 IE 兼容分支,省略) -->
|
|
2
|
+
<template>
|
|
3
|
+
<div ref="row" class="ce_row" :style="styleObj" @click="$emit('click', $event)">
|
|
4
|
+
<slot />
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script>
|
|
9
|
+
import StyleParser from '../runtime/StyleParser.js'
|
|
10
|
+
import base from './base.js'
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
name: 'CeRow',
|
|
14
|
+
emits: ['click'],
|
|
15
|
+
mixins: [base],
|
|
16
|
+
props: {
|
|
17
|
+
styleProps: { type: Object, default: () => ({}) },
|
|
18
|
+
},
|
|
19
|
+
computed: {
|
|
20
|
+
styleObj() {
|
|
21
|
+
return new StyleParser(this, this.styleProps).parse()
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
</script>
|