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,806 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
调试面板:四个标签页。
|
|
3
|
+
- 接口调用:ynet.callJSAPI / postMessage 的调用流水
|
|
4
|
+
- 数据绑定:当前 rootScope 的实时 JSON(初始数据 / 落盘 / 保存高亮比对源)。
|
|
5
|
+
本地调试的所有交互只写「运行态」,不会改这里,故调试中此 tab 保持不变、保存不亮。
|
|
6
|
+
- 组件结构:当前卡片的组件层级树(参考编辑器「结构」面板)
|
|
7
|
+
- 卡片来源:导入来源元数据
|
|
8
|
+
|
|
9
|
+
运行态(rt.state)已从标签页移出:底部状态栏新增「运行态」指示器,点击在面板内
|
|
10
|
+
弹出可拖拽高度的只读面板,默认收起。rt.state 是运行期交互态(Input 输入 /
|
|
11
|
+
RadioButton·CheckGroup 选择 / globalSet 等只写这里),不污染 rootScope、不触发保存
|
|
12
|
+
高亮,事件代码里 `this.data.xxx` 也读这里,调试变更永不落盘。
|
|
13
|
+
|
|
14
|
+
注:「绑定错误」已移至底部状态栏(类 VSCode 面板),点击状态栏问题指示器在
|
|
15
|
+
底部弹出错误面板查看,不再作为此处的标签页。
|
|
16
|
+
-->
|
|
17
|
+
<template>
|
|
18
|
+
<aside class="debug">
|
|
19
|
+
<div class="debug__tabs">
|
|
20
|
+
<button
|
|
21
|
+
v-for="t in tabs"
|
|
22
|
+
:key="t.key"
|
|
23
|
+
:class="['debug__tab', { 'debug__tab--active': activeTab === t.key }]"
|
|
24
|
+
@click="$emit('update:activeTab', t.key)"
|
|
25
|
+
>
|
|
26
|
+
{{ t.label }}
|
|
27
|
+
<span v-if="t.key === 'api' && apiCalls.length" class="debug__count">{{ apiCalls.length }}</span>
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="debug__body">
|
|
32
|
+
<template v-if="activeTab === 'api'">
|
|
33
|
+
<button
|
|
34
|
+
class="debug__clear"
|
|
35
|
+
type="button"
|
|
36
|
+
:disabled="!apiCalls.length"
|
|
37
|
+
title="清空日志"
|
|
38
|
+
@click="clearApiLog"
|
|
39
|
+
>
|
|
40
|
+
<!-- 垃圾桶图标:清空日志 -->
|
|
41
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
42
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-trash" /></svg>
|
|
43
|
+
</button>
|
|
44
|
+
<div class="debug__placeholder"></div>
|
|
45
|
+
<p v-if="!apiCalls.length" class="debug__empty">暂无接口调用</p>
|
|
46
|
+
<ul class="debug__list debug__list--api">
|
|
47
|
+
<li v-for="(c, i) in apiCalls" :key="i" class="debug__item debug__item--api">
|
|
48
|
+
<div class="debug__api-row">
|
|
49
|
+
<span class="debug__kind">{{ c.api }}</span>
|
|
50
|
+
<button
|
|
51
|
+
class="debug__copy-ic"
|
|
52
|
+
type="button"
|
|
53
|
+
:title="copiedIndex === i ? '已复制 ✓' : '复制数据(不含方法名)'"
|
|
54
|
+
@click="copyApiPayload(c, i)"
|
|
55
|
+
>
|
|
56
|
+
<svg v-if="copiedIndex !== i" width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
57
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-copy-sm" /></svg>
|
|
58
|
+
<span v-else class="debug__copy-ok">✓</span>
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
<!-- 返回数据:结构化折叠树(Chrome 控制台风格) -->
|
|
62
|
+
<ApiPayloadTree :value="c.payload" />
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<template v-else-if="activeTab === 'scope'">
|
|
68
|
+
<div class="debug__scope-wrap" :class="{ 'debug__scope-wrap--raw': rawMode }">
|
|
69
|
+
<!-- Raw 模式:直接编辑原始 JSON(CodeJar + Prism 语法高亮) -->
|
|
70
|
+
<template v-if="rawMode">
|
|
71
|
+
<div class="debug__scope-raw">
|
|
72
|
+
<div
|
|
73
|
+
ref="rawEditor"
|
|
74
|
+
class="debug__raw-editor"
|
|
75
|
+
spellcheck="false"
|
|
76
|
+
></div>
|
|
77
|
+
<p v-if="rawError" class="debug__raw-err">{{ rawError }}</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="debug__scope-foot">
|
|
80
|
+
<button class="debug__copy" type="button" @click="saveRaw">应用</button>
|
|
81
|
+
<button class="debug__copy" type="button" @click="exitRaw">取消</button>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
<!-- 默认模式:JSON 树 -->
|
|
85
|
+
<template v-else>
|
|
86
|
+
<div class="debug__scope-tree">
|
|
87
|
+
<JsonTree :data="rt.rootScope" :rt="rt" :editable="true" :schema="schema" />
|
|
88
|
+
</div>
|
|
89
|
+
<div class="debug__scope-foot">
|
|
90
|
+
<span v-if="copied" class="debug__copied">已复制 ✓</span>
|
|
91
|
+
<button class="debug__copy" type="button" @click="enterRaw">Raw</button>
|
|
92
|
+
<button class="debug__copy" type="button" @click="copyScope">复制</button>
|
|
93
|
+
<span class="debug__sep"></span>
|
|
94
|
+
<button class="debug__copy debug__copy--primary" type="button" @click="onCommit">应用</button>
|
|
95
|
+
<button class="debug__copy" type="button" @click="$emit('reset-import')" title="丢弃内存编辑,从磁盘重新载入(导入优先于编辑)">重置</button>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<template v-else-if="activeTab === 'structure'">
|
|
102
|
+
<StructureTree :preview="preview" :schema="schema" :card-code="cardId" :selected-node-id="selectedNodeId" :move-collapse-token="moveCollapseToken" :can-paste="canPaste" @edit-event="onEditEvent" @inspect="onInspect" @select-node="onSelectNode" @add-node="onAddNode" @move-node="onMoveNode" @hover-node="onHoverNode" @copy-selected="$emit('copy-selected')" @paste="$emit('paste')" @duplicate="$emit('duplicate')" @delete-selected="$emit('delete-selected')" />
|
|
103
|
+
</template>
|
|
104
|
+
|
|
105
|
+
<template v-else-if="activeTab === 'meta'">
|
|
106
|
+
<div class="debug__meta">
|
|
107
|
+
<p v-if="!importMeta" class="debug__empty">该卡尚未经「同步远程」导入,无导入来源元数据。<br />本地新建卡 / 未同步卡均如此。</p>
|
|
108
|
+
<dl v-else class="debug__meta-list">
|
|
109
|
+
<div class="debug__meta-row">
|
|
110
|
+
<dt>导入接口</dt>
|
|
111
|
+
<dd><code>{{ importMeta.interface }}</code> <span class="debug__meta-muted">({{ importMeta.source }})</span></dd>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="debug__meta-row">
|
|
114
|
+
<dt>远端基址</dt>
|
|
115
|
+
<dd><code>{{ importMeta.baseUrl }}</code></dd>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="debug__meta-row">
|
|
118
|
+
<dt>cardId</dt>
|
|
119
|
+
<dd><code>{{ importMeta.cardId }}</code></dd>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="debug__meta-row">
|
|
122
|
+
<dt>cardCode</dt>
|
|
123
|
+
<dd><code>{{ importMeta.cardCode }}</code></dd>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="debug__meta-row" v-if="importMeta.cardName">
|
|
126
|
+
<dt>卡片标题</dt>
|
|
127
|
+
<dd>{{ importMeta.cardName }}</dd>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="debug__meta-row">
|
|
130
|
+
<dt>应用 / 空间</dt>
|
|
131
|
+
<dd><code>{{ importMeta.appId }}</code> / <code>{{ importMeta.workspaceId }}</code></dd>
|
|
132
|
+
</div>
|
|
133
|
+
<div class="debug__meta-row">
|
|
134
|
+
<dt>同步时间</dt>
|
|
135
|
+
<dd>{{ formatTime(importMeta.syncedAt) }}</dd>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="debug__meta-row">
|
|
138
|
+
<dt>落库位置</dt>
|
|
139
|
+
<dd><code>src/cards/{{ importMeta.cardCode }}/</code></dd>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="debug__meta-row">
|
|
142
|
+
<dt>本地状态</dt>
|
|
143
|
+
<dd>
|
|
144
|
+
<span v-if="importMeta.dirty" class="debug__tag debug__tag--warn" title="本地已编辑,偏离导入镜像">已编辑(偏离导入镜像)</span>
|
|
145
|
+
<span v-else class="debug__tag debug__tag--ok">与导入镜像一致</span>
|
|
146
|
+
</dd>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="debug__meta-row" v-if="importMeta.lastExport">
|
|
149
|
+
<dt>最近导出</dt>
|
|
150
|
+
<dd>
|
|
151
|
+
<span v-if="importMeta.lastExport.ok" class="debug__tag debug__tag--ok">读回校验通过 ✓</span>
|
|
152
|
+
<span v-else class="debug__tag debug__tag--err">读回校验未通过 ✗</span>
|
|
153
|
+
<span class="debug__meta-muted">({{ formatTime(importMeta.lastExport.at) }})</span>
|
|
154
|
+
<span v-if="importMeta.lastExport.readBackError" class="debug__meta-err"> · {{ importMeta.lastExport.readBackError }}</span>
|
|
155
|
+
</dd>
|
|
156
|
+
</div>
|
|
157
|
+
</dl>
|
|
158
|
+
<p class="debug__meta-hint">导入优先于编辑:点「数据绑定」tab 的「重置为导入镜像」可丢弃内存编辑、从磁盘重新载入最新导入。</p>
|
|
159
|
+
</div>
|
|
160
|
+
</template>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<!--
|
|
164
|
+
节点属性面板:组件结构 tab 双击节点弹出(可编辑写回)。
|
|
165
|
+
事件编辑已并入本面板的「事件」页签(meta 驱动),不再有独立弹层。
|
|
166
|
+
-->
|
|
167
|
+
<NodeInspector
|
|
168
|
+
v-if="inspectingComp"
|
|
169
|
+
:key="inspectKey"
|
|
170
|
+
:comp="inspectingComp"
|
|
171
|
+
:comp-names="compNames"
|
|
172
|
+
:rt="rt"
|
|
173
|
+
@apply="onApply"
|
|
174
|
+
@commit="onCommit"
|
|
175
|
+
@close="inspectingComp = null"
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
<!--
|
|
179
|
+
事件编辑弹框:点击结构树「事件」徽标时单独弹出,与属性面板解耦。
|
|
180
|
+
只编辑 eventactions,不涉及属性/样式。
|
|
181
|
+
-->
|
|
182
|
+
<Transition name="ee-drawer">
|
|
183
|
+
<EventEditor
|
|
184
|
+
v-if="editingEventComp"
|
|
185
|
+
:key="'evt-' + eventEditorKey"
|
|
186
|
+
:comp="editingEventComp"
|
|
187
|
+
:comp-names="compNames"
|
|
188
|
+
:rt="rt"
|
|
189
|
+
@apply="onApply"
|
|
190
|
+
@commit="onCommit"
|
|
191
|
+
@close="editingEventComp = null"
|
|
192
|
+
/>
|
|
193
|
+
</Transition>
|
|
194
|
+
|
|
195
|
+
<!--
|
|
196
|
+
运行态面板:默认收起;点击底部状态栏「运行态」指示器弹出。面板高度由头部
|
|
197
|
+
拖拽调整(startResize,无专门拖拽节点,与预览区宽度调节同一套 mousedown 机制),
|
|
198
|
+
并持久化到 localStorage。展示 rt.state 实时 JSON(运行期交互态,只读),不污染绑定数据、
|
|
199
|
+
不触发保存高亮、调试变更永不落盘。
|
|
200
|
+
-->
|
|
201
|
+
<section
|
|
202
|
+
class="statepanel"
|
|
203
|
+
ref="statePanel"
|
|
204
|
+
v-show="statePanelOpen"
|
|
205
|
+
>
|
|
206
|
+
<div class="statepanel__head" @mousedown.prevent="startResize">
|
|
207
|
+
<span class="statepanel__title">运行态 <code>rt.state</code></span>
|
|
208
|
+
<button class="statepanel__close" type="button" title="收起" @click="statePanelOpen = false">✕</button>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="statepanel__tree">
|
|
211
|
+
<JsonTree :data="rt.state" :rt="rt" :editable="false" />
|
|
212
|
+
</div>
|
|
213
|
+
</section>
|
|
214
|
+
|
|
215
|
+
<!--
|
|
216
|
+
底部状态栏(仅右侧调试面板内、贴近 VSCode 布局):左侧「问题」指示器展示
|
|
217
|
+
绑定错误数,点击在面板底部弹出/收起错误面板;右侧展示当前卡片与卡片总数。
|
|
218
|
+
-->
|
|
219
|
+
<section class="statusbar">
|
|
220
|
+
<button
|
|
221
|
+
class="statusbar__item statusbar__item--prob"
|
|
222
|
+
:class="{ 'is-open': statePanelOpen }"
|
|
223
|
+
type="button"
|
|
224
|
+
@click="toggleStatePanel"
|
|
225
|
+
:title="statePanelOpen ? '收起运行态面板' : '展开运行态面板(可拖拽调整高度)'"
|
|
226
|
+
>
|
|
227
|
+
<span class="statusbar__icon" :class="{ 'is-dirty': stateDirty }">◉</span>
|
|
228
|
+
<span>运行态</span>
|
|
229
|
+
</button>
|
|
230
|
+
<button
|
|
231
|
+
class="statusbar__item statusbar__item--prob"
|
|
232
|
+
:class="{ 'is-error': bindingErrors.length, 'is-ok': !bindingErrors.length }"
|
|
233
|
+
type="button"
|
|
234
|
+
@click="bindingErrors.length && toggleErrorPanel('binding')"
|
|
235
|
+
:title="bindingErrors.length ? '查看绑定错误' : '无绑定错误'"
|
|
236
|
+
>
|
|
237
|
+
<span class="statusbar__icon">{{ bindingErrors.length ? '⚠' : '✓' }}</span>
|
|
238
|
+
<span>{{ bindingErrors.length ? bindingErrors.length + ' 个绑定错误' : '无绑定错误' }}</span>
|
|
239
|
+
</button>
|
|
240
|
+
<button
|
|
241
|
+
class="statusbar__item statusbar__item--prob"
|
|
242
|
+
:class="eventStatusClass"
|
|
243
|
+
type="button"
|
|
244
|
+
:disabled="!eventCount"
|
|
245
|
+
@click="eventCount && toggleErrorPanel('event')"
|
|
246
|
+
:title="eventStatusTitle"
|
|
247
|
+
>
|
|
248
|
+
<span class="statusbar__icon">{{ eventCount ? (eventErrors.length ? '⚠' : '☰') : '✓' }}</span>
|
|
249
|
+
<span>{{ eventCount ? eventCount + ' 条事件' : '无事件' }}</span>
|
|
250
|
+
</button>
|
|
251
|
+
<!-- 本地开发服务连接状态:连接图标 + 状态文案,颜色与右上角「连接」按钮实时同步(复用 devSvcClass / devSvcTitle / devSvcLabel) -->
|
|
252
|
+
<span
|
|
253
|
+
class="statusbar__item statusbar__conn"
|
|
254
|
+
:class="devSvcClass"
|
|
255
|
+
:title="devSvcTitle"
|
|
256
|
+
>
|
|
257
|
+
<svg class="statusbar__conn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-link" /></svg>
|
|
258
|
+
<span>{{ devSvcLabel }}</span>
|
|
259
|
+
</span>
|
|
260
|
+
<span class="statusbar__spacer"></span>
|
|
261
|
+
<span class="statusbar__item statusbar__item--right">{{ cardId }}</span>
|
|
262
|
+
<span class="statusbar__item statusbar__item--right">{{ cardCount }} 张卡片</span>
|
|
263
|
+
</section>
|
|
264
|
+
|
|
265
|
+
<section class="errpanel" ref="errPanel" v-show="errorPanel">
|
|
266
|
+
<div class="errpanel__head" @mousedown.prevent="startResize">
|
|
267
|
+
<span class="errpanel__title">{{ errorPanel === 'event' ? '事件面板' : '绑定错误' }}</span>
|
|
268
|
+
<span class="errpanel__actions">
|
|
269
|
+
<button
|
|
270
|
+
v-if="errorPanel === 'event'"
|
|
271
|
+
class="errpanel__iconbtn errpanel__clear"
|
|
272
|
+
type="button"
|
|
273
|
+
:disabled="!logs.length"
|
|
274
|
+
title="清空日志(不影响错误)"
|
|
275
|
+
@click="clearEventLogs"
|
|
276
|
+
>
|
|
277
|
+
<!-- 垃圾桶图标:清空日志 -->
|
|
278
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
279
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-trash" /></svg>
|
|
280
|
+
</button>
|
|
281
|
+
<button class="errpanel__iconbtn errpanel__close" type="button" title="关闭" @click="errorPanel = ''">✕</button>
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="errpanel__body">
|
|
285
|
+
<!-- 事件面板:日志 + 错误 两区块,用于捕获主动日志与执行错误 -->
|
|
286
|
+
<template v-if="errorPanel === 'event'">
|
|
287
|
+
<div class="ep__sec-title">日志 ({{ logs.length }})</div>
|
|
288
|
+
<p v-if="!logs.length" class="debug__empty">无日志 ✓</p>
|
|
289
|
+
<ul v-else class="debug__list debug__list--log">
|
|
290
|
+
<li v-for="(l, i) in logs" :key="'l' + i" class="log__item" :class="'log--' + l.level">
|
|
291
|
+
<span class="log__tag" :class="'log__tag--' + l.level">{{ l.level }}</span>
|
|
292
|
+
<div class="log__content">
|
|
293
|
+
<template v-if="l.args && l.args.length">
|
|
294
|
+
<LogValue v-for="(a, ai) in l.args" :key="ai" :value="a" />
|
|
295
|
+
</template>
|
|
296
|
+
<span v-else class="log__empty">—</span>
|
|
297
|
+
</div>
|
|
298
|
+
</li>
|
|
299
|
+
</ul>
|
|
300
|
+
<div class="ep__sec-title">错误 ({{ eventErrors.length }})</div>
|
|
301
|
+
<p v-if="!eventErrors.length" class="debug__empty">无错误 ✓</p>
|
|
302
|
+
<ul v-else class="debug__list">
|
|
303
|
+
<li v-for="(e, i) in eventErrors" :key="'e' + i" class="debug__item debug__item--err">
|
|
304
|
+
<span class="debug__kind">[{{ e.kind }}]</span> {{ e.msg }}
|
|
305
|
+
</li>
|
|
306
|
+
</ul>
|
|
307
|
+
</template>
|
|
308
|
+
<!-- 绑定错误面板:仅错误列表 -->
|
|
309
|
+
<template v-else>
|
|
310
|
+
<p v-if="!panelErrors.length" class="debug__empty">无错误 ✓</p>
|
|
311
|
+
<ul v-else class="debug__list">
|
|
312
|
+
<li v-for="(e, i) in panelErrors" :key="i" class="debug__item debug__item--err">
|
|
313
|
+
<span class="debug__kind">[{{ e.kind }}]</span> {{ e.msg }}
|
|
314
|
+
</li>
|
|
315
|
+
</ul>
|
|
316
|
+
</template>
|
|
317
|
+
</div>
|
|
318
|
+
</section>
|
|
319
|
+
</aside>
|
|
320
|
+
</template>
|
|
321
|
+
|
|
322
|
+
<script>
|
|
323
|
+
import JsonTree from './JsonTree.vue'
|
|
324
|
+
import ApiPayloadTree from './ApiPayloadTree.vue'
|
|
325
|
+
import StructureTree from './StructureTree.vue'
|
|
326
|
+
import NodeInspector from './NodeInspector.vue'
|
|
327
|
+
import EventEditor from './EventEditor.vue'
|
|
328
|
+
import LogValue from './LogValue.vue'
|
|
329
|
+
import { COMPONENT_META } from '@/runtime/toPreview.js'
|
|
330
|
+
import { copyText } from '@/runtime/clipboard.js'
|
|
331
|
+
// 结构树「粘贴」按钮的 disabled 态:剪贴板是否有可粘贴节点
|
|
332
|
+
import { nodeClipboard } from '@/runtime/clipboard-node.js'
|
|
333
|
+
// Raw 编辑器的代码高亮:复用项目既有的 CodeJar + Prism 方案(与 SetCode/SetExpr 一致),
|
|
334
|
+
// 仅切换语法为 json(Prism 自带 json grammar,无需额外安装)。
|
|
335
|
+
import { CodeJar } from 'codejar'
|
|
336
|
+
import Prism from 'prismjs'
|
|
337
|
+
import 'prismjs/components/prism-json'
|
|
338
|
+
import { apiFetch } from '@/api.js'
|
|
339
|
+
|
|
340
|
+
// copyText 已抽到 @/runtime/clipboard.js,供 DebugPanel 与 NodeInspector 共用
|
|
341
|
+
|
|
342
|
+
// 判断运行态是否与绑定数据基线发生偏离(用于状态栏「运行态」小点变黄)
|
|
343
|
+
function deepEqual(a, b) {
|
|
344
|
+
if (a === b) return true
|
|
345
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false
|
|
346
|
+
const ka = Object.keys(a)
|
|
347
|
+
const kb = Object.keys(b)
|
|
348
|
+
if (ka.length !== kb.length) return false
|
|
349
|
+
for (const k of ka) {
|
|
350
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false
|
|
351
|
+
if (!deepEqual(a[k], b[k])) return false
|
|
352
|
+
}
|
|
353
|
+
return true
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export default {
|
|
357
|
+
name: 'DebugPanel',
|
|
358
|
+
components: { JsonTree, ApiPayloadTree, StructureTree, NodeInspector, EventEditor, LogValue },
|
|
359
|
+
props: {
|
|
360
|
+
rt: { type: Object, required: true },
|
|
361
|
+
preview: { type: Object, default: null },
|
|
362
|
+
schema: { type: Object, default: null },
|
|
363
|
+
cardId: { type: String, default: '' },
|
|
364
|
+
// 预览重载令牌:loadCard(含刷新/同步后自动重载/撤销重做)每次自增,
|
|
365
|
+
// 用于让「卡片来源」tab 在卡片被重新载入磁盘内容时也重拉导入来源元数据,
|
|
366
|
+
// 修复「同步后点刷新,来源信息不更新」的问题。
|
|
367
|
+
reloadToken: { type: Number, default: 0 },
|
|
368
|
+
cardCount: { type: Number, default: 0 },
|
|
369
|
+
selectedNodeId: { type: String, default: '' },
|
|
370
|
+
// 拖拽移动成功令牌:App 在节点拖拽移动成功后自增,透传给结构树,
|
|
371
|
+
// 触发「选中被拖节点 + 仅展开其祖先链、其余全部折叠」。
|
|
372
|
+
moveCollapseToken: { type: Number, default: 0 },
|
|
373
|
+
// 当前激活的调试面板 tab(接口调用/绑定数据/组件结构/卡片来源),由 App 统一管理,
|
|
374
|
+
// 便于画布 Ctrl/⌘ 点击定位在「组件结构」tab 激活时才生效。
|
|
375
|
+
activeTab: { type: String, default: 'api' },
|
|
376
|
+
// 本地开发服务连接状态:复用 App 中顶栏「连接」按钮同款状态/配色(devSvcClass/devSvcTitle/devSvcLabel),
|
|
377
|
+
// 供底部状态栏展示「连接图标 + 状态文案」,与右上角按钮实时同步(颜色一致、状态一致)。
|
|
378
|
+
devSvcClass: { type: String, default: 'app__btn--warn' },
|
|
379
|
+
devSvcTitle: { type: String, default: '' },
|
|
380
|
+
devSvcLabel: { type: String, default: '' },
|
|
381
|
+
},
|
|
382
|
+
data() {
|
|
383
|
+
return {
|
|
384
|
+
copied: false,
|
|
385
|
+
copyTimer: null,
|
|
386
|
+
// 接口调用 tab:刚复制过数据的条目下标(用于图标变 ✓ 反馈),null=无
|
|
387
|
+
copiedIndex: null,
|
|
388
|
+
apiCopyTimer: null,
|
|
389
|
+
// 底部错误面板模式:''(收起) | 'binding'(绑定错误) | 'event'(事件执行错误)
|
|
390
|
+
errorPanel: '',
|
|
391
|
+
// 运行态面板:默认收起;点击状态栏「运行态」指示器切换
|
|
392
|
+
statePanelOpen: false,
|
|
393
|
+
// 运行态 / 绑定错误 / 事件 三个底部面板互斥,共用同一高度变量(px);
|
|
394
|
+
// 可被顶部抓手拖拽调整,并持久化到 localStorage(cardbuddy:debug-panel-height)
|
|
395
|
+
panelH: this.loadPanelH(),
|
|
396
|
+
// 双击结构树节点后弹出的属性面板目标节点
|
|
397
|
+
inspectingComp: null,
|
|
398
|
+
// 换节点时强制重建面板(内部含 CodeJar 这类托管 DOM 的控件)
|
|
399
|
+
inspectKey: 0,
|
|
400
|
+
// 事件编辑弹框目标节点(点击「事件」徽标时单独弹出,与属性面板解耦)
|
|
401
|
+
editingEventComp: null,
|
|
402
|
+
eventEditorKey: 0,
|
|
403
|
+
// Raw 编辑模式状态
|
|
404
|
+
rawMode: false,
|
|
405
|
+
rawText: '',
|
|
406
|
+
rawError: '',
|
|
407
|
+
// 导入来源元数据(Phase E):GET /__import-meta/:id,无侧车时为 null
|
|
408
|
+
importMeta: null,
|
|
409
|
+
tabs: [
|
|
410
|
+
{ key: 'scope', label: '绑定数据' },
|
|
411
|
+
{ key: 'structure', label: '组件结构' },
|
|
412
|
+
{ key: 'api', label: '接口调用' },
|
|
413
|
+
{ key: 'meta', label: '卡片来源' },
|
|
414
|
+
],
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
computed: {
|
|
418
|
+
// 依赖 apiRev,使接口调用流水在 pushApiCall / clearApiCalls 后实时刷新
|
|
419
|
+
// (apiCalls 本身是非响应式数组,模板直接读它不会触发重渲染)
|
|
420
|
+
//
|
|
421
|
+
// ⚠️ 必须 slice() 返回**新数组**,不能直接 return 原数组引用:
|
|
422
|
+
// Vue 3.4+ 引入了「computed 值稳定性」——重算后若新值与旧值 Object.is 相等,
|
|
423
|
+
// 就不再通知下游 effect。原数组是同一个引用,即使 apiRev 自增触发了重算,
|
|
424
|
+
// 渲染 effect 也收不到通知,界面纹丝不动;直到别的原因(如点 tab 改了 this.tab)
|
|
425
|
+
// 引起重渲染,才把积压的日志一次性刷出来——这正是「切 tab 才看得到」的成因。
|
|
426
|
+
apiCalls() {
|
|
427
|
+
void this.rt.apiRev
|
|
428
|
+
return this.rt.apiCalls.slice()
|
|
429
|
+
},
|
|
430
|
+
// 依赖 revision,使 setData 后数据绑定实时刷新
|
|
431
|
+
scopeText() {
|
|
432
|
+
void this.rt.revision
|
|
433
|
+
try {
|
|
434
|
+
return JSON.stringify(this.rt.rootScope, null, 2)
|
|
435
|
+
} catch (e) {
|
|
436
|
+
return '(无法序列化)'
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
// 绑定类错误(bind / vshow / render / refresh),依赖 errorRev 保证实时刷新
|
|
440
|
+
// 同 apiCalls:必须 slice() 出新引用,否则 Vue 3.4+ 的 computed 值稳定性
|
|
441
|
+
// 会判定「值没变」而不通知渲染 effect,状态栏计数将停留在旧值。
|
|
442
|
+
bindingErrors() {
|
|
443
|
+
void this.rt.errorRev
|
|
444
|
+
return this.rt.errors.slice()
|
|
445
|
+
},
|
|
446
|
+
// 事件执行错误(customAction / listener),由运行时单独存放,跨渲染保留
|
|
447
|
+
eventErrors() {
|
|
448
|
+
void this.rt.errorRev
|
|
449
|
+
return this.rt.eventErrors.slice()
|
|
450
|
+
},
|
|
451
|
+
// 事件主动日志(console.*),依赖 logRev 保证实时刷新
|
|
452
|
+
logs() {
|
|
453
|
+
void this.rt.logRev
|
|
454
|
+
return this.rt.eventLogs.slice()
|
|
455
|
+
},
|
|
456
|
+
/** 事件面板合并计数器:事件执行错误 + 主动日志(状态栏单一「事件」指示器使用) */
|
|
457
|
+
eventCount() {
|
|
458
|
+
return this.eventErrors.length + this.logs.length
|
|
459
|
+
},
|
|
460
|
+
/** 合并指示器配色:有错误→红;仅日志→蓝;皆无→灰 */
|
|
461
|
+
eventStatusClass() {
|
|
462
|
+
if (this.eventErrors.length) return 'is-error'
|
|
463
|
+
if (this.logs.length) return 'is-log'
|
|
464
|
+
return 'is-ok'
|
|
465
|
+
},
|
|
466
|
+
eventStatusTitle() {
|
|
467
|
+
if (this.eventErrors.length) return '查看事件面板(含错误)'
|
|
468
|
+
if (this.logs.length) return '查看事件面板(含日志)'
|
|
469
|
+
return '无事件记录'
|
|
470
|
+
},
|
|
471
|
+
panelErrors() {
|
|
472
|
+
return this.errorPanel === 'event' ? this.eventErrors : this.bindingErrors
|
|
473
|
+
},
|
|
474
|
+
compNames() {
|
|
475
|
+
const map = {}
|
|
476
|
+
for (const key in COMPONENT_META) {
|
|
477
|
+
map[key] = COMPONENT_META[key].name || key
|
|
478
|
+
}
|
|
479
|
+
map.Root = '根'
|
|
480
|
+
return map
|
|
481
|
+
},
|
|
482
|
+
/** 剪贴板是否有可粘贴节点(实时反映 nodeClipboard.node 是否为空) */
|
|
483
|
+
canPaste() {
|
|
484
|
+
return !!nodeClipboard.node
|
|
485
|
+
},
|
|
486
|
+
/**
|
|
487
|
+
* 运行态是否与「绑定数据」基线(rt.rootScope)发生偏离。
|
|
488
|
+
* 一旦用户在调试中改动运行态(输入 / 选择 / globalSet 等),state 即与 rootScope
|
|
489
|
+
* 不一致 → 状态栏「运行态」小点变黄,提示当前有未落库的临时调试值。
|
|
490
|
+
* 读取 state/rootScope 的响应式键建立细粒度依赖;revision 覆盖换卡 / reseed。
|
|
491
|
+
*/
|
|
492
|
+
stateDirty() {
|
|
493
|
+
void this.rt.revision
|
|
494
|
+
return !deepEqual(this.rt.state, this.rt.rootScope)
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
watch: {
|
|
498
|
+
// 切换卡片时退出 Raw 模式,避免编辑内容与新卡片数据不一致;
|
|
499
|
+
// 同时关闭属性面板,避免展示旧卡片节点;并拉取导入来源元数据。
|
|
500
|
+
cardId: {
|
|
501
|
+
immediate: true,
|
|
502
|
+
handler() {
|
|
503
|
+
this.rawMode = false
|
|
504
|
+
this.rawError = ''
|
|
505
|
+
this.destroyJar()
|
|
506
|
+
this.inspectingComp = null
|
|
507
|
+
this.editingEventComp = null
|
|
508
|
+
this.importMeta = null
|
|
509
|
+
// 懒加载:仅当用户停留在「来源」tab 时才拉取(见 maybeFetchMeta),
|
|
510
|
+
// 避免每次切换/加载卡片都向无侧车的本地卡打 404(问题 #7)。
|
|
511
|
+
this.maybeFetchMeta()
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
// 预览被重新载入(刷新按钮 / 同步后自动重载 / 撤销重做)时:cardId 未变,
|
|
515
|
+
// 但磁盘上的 .import-meta.json 可能已被「同步远程」更新,须同步重拉来源元数据。
|
|
516
|
+
reloadToken() {
|
|
517
|
+
this.maybeFetchMeta()
|
|
518
|
+
},
|
|
519
|
+
// 切到「来源」tab 时按需拉取导入来源元数据(懒加载,消除自动 404)。
|
|
520
|
+
activeTab(n) {
|
|
521
|
+
if (n === 'meta') this.fetchImportMeta()
|
|
522
|
+
},
|
|
523
|
+
// schema 引用变化(撤销/重做、刷新重载会整体替换对象;就地编辑只 mutate 不换引用)
|
|
524
|
+
// 时关闭已打开的节点属性/事件面板,避免面板持有已被替换的旧预览节点引用。
|
|
525
|
+
schema() {
|
|
526
|
+
this.inspectingComp = null
|
|
527
|
+
this.editingEventComp = null
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
mounted() {
|
|
531
|
+
// 初始化两个底部面板高度为持久化值(默认 240px)。拖拽由面板头部(已有节点,
|
|
532
|
+
// 非专门拖拽节点)经 startResize 处理,与预览区宽度调节同一套 mousedown 机制。
|
|
533
|
+
const h = this.panelH
|
|
534
|
+
if (this.$refs.statePanel) this.$refs.statePanel.style.height = h + 'px'
|
|
535
|
+
if (this.$refs.errPanel) this.$refs.errPanel.style.height = h + 'px'
|
|
536
|
+
},
|
|
537
|
+
beforeUnmount() {
|
|
538
|
+
// DebugPanel 一般常驻,但保险起见卸载时销毁 CodeJar、清理可能残留的拖拽监听
|
|
539
|
+
this.destroyJar()
|
|
540
|
+
this.cancelResize()
|
|
541
|
+
},
|
|
542
|
+
methods: {
|
|
543
|
+
// 三个底部面板互斥:运行态 / 绑定错误 / 事件 不会同时出现。
|
|
544
|
+
// 点开运行态即关闭错误/事件面板。
|
|
545
|
+
toggleStatePanel() {
|
|
546
|
+
this.statePanelOpen = !this.statePanelOpen
|
|
547
|
+
if (this.statePanelOpen) this.errorPanel = ''
|
|
548
|
+
},
|
|
549
|
+
toggleErrorPanel(kind) {
|
|
550
|
+
// 再次点击同一指示器则收起;点击另一指示器则切换列表
|
|
551
|
+
this.errorPanel = this.errorPanel === kind ? '' : kind
|
|
552
|
+
// 与运行态面板互斥:点开错误/事件面板即关闭运行态
|
|
553
|
+
if (this.errorPanel) this.statePanelOpen = false
|
|
554
|
+
},
|
|
555
|
+
/**
|
|
556
|
+
* 拖拽调整底部面板(运行态 / 绑定错误 / 事件)高度:挂在面板头部(已有 .statepanel__head /
|
|
557
|
+
* .errpanel__head,无专门拖拽节点),与预览区宽度调节同一套 mousedown 机制。
|
|
558
|
+
* 在头部按下后监听 document 的 mousemove / mouseup;向上拖(clientY 减小)增大面板高度,
|
|
559
|
+
* 向下拖减小,符合底部停靠面板的直觉。高度夹在 [120px, 视窗 60%],并实时同步到两个面板
|
|
560
|
+
* (三者互斥共用 panelH),鼠标松开才持久化到 localStorage(避免拖拽过程频繁写盘)。
|
|
561
|
+
* 头部上的关闭/清空等按钮不触发拖拽,避免与点击冲突。
|
|
562
|
+
*/
|
|
563
|
+
startResize(e) {
|
|
564
|
+
if (e.target.closest('button')) return
|
|
565
|
+
const startY = e.clientY
|
|
566
|
+
const startH = this.panelH
|
|
567
|
+
const minH = 120
|
|
568
|
+
const maxH = Math.max(minH, Math.round(window.innerHeight * 0.6))
|
|
569
|
+
const apply = (h) => {
|
|
570
|
+
this.panelH = h
|
|
571
|
+
if (this.$refs.statePanel) this.$refs.statePanel.style.height = h + 'px'
|
|
572
|
+
if (this.$refs.errPanel) this.$refs.errPanel.style.height = h + 'px'
|
|
573
|
+
}
|
|
574
|
+
const onMove = (ev) => {
|
|
575
|
+
const delta = startY - ev.clientY // 向上拖 = 正值 = 面板变高
|
|
576
|
+
apply(Math.min(maxH, Math.max(minH, startH + delta)))
|
|
577
|
+
}
|
|
578
|
+
const onUp = () => {
|
|
579
|
+
document.body.style.cursor = ''
|
|
580
|
+
document.body.style.userSelect = ''
|
|
581
|
+
document.removeEventListener('mousemove', onMove)
|
|
582
|
+
document.removeEventListener('mouseup', onUp)
|
|
583
|
+
this._resizeMove = this._resizeUp = null
|
|
584
|
+
this.savePanelH()
|
|
585
|
+
}
|
|
586
|
+
this._resizeMove = onMove
|
|
587
|
+
this._resizeUp = onUp
|
|
588
|
+
document.body.style.cursor = 'row-resize'
|
|
589
|
+
document.body.style.userSelect = 'none'
|
|
590
|
+
document.addEventListener('mousemove', onMove)
|
|
591
|
+
document.addEventListener('mouseup', onUp)
|
|
592
|
+
},
|
|
593
|
+
/** 清理可能残留的拖拽监听(组件卸载或异常中断时调用) */
|
|
594
|
+
cancelResize() {
|
|
595
|
+
if (this._resizeMove) document.removeEventListener('mousemove', this._resizeMove)
|
|
596
|
+
if (this._resizeUp) document.removeEventListener('mouseup', this._resizeUp)
|
|
597
|
+
this._resizeMove = this._resizeUp = null
|
|
598
|
+
document.body.style.cursor = ''
|
|
599
|
+
document.body.style.userSelect = ''
|
|
600
|
+
},
|
|
601
|
+
/** 读取持久化的底部面板高度(localStorage)。无记录 / 损坏 / 越界时回退 240px。 */
|
|
602
|
+
loadPanelH() {
|
|
603
|
+
try {
|
|
604
|
+
const v = localStorage.getItem('cardbuddy:debug-panel-height')
|
|
605
|
+
if (v != null) {
|
|
606
|
+
const h = parseInt(v, 10)
|
|
607
|
+
if (!Number.isNaN(h)) {
|
|
608
|
+
const minH = 120
|
|
609
|
+
const maxH = Math.max(minH, Math.round(window.innerHeight * 0.6))
|
|
610
|
+
return Math.min(maxH, Math.max(minH, h))
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} catch (e) {
|
|
614
|
+
/* localStorage 不可用时忽略 */
|
|
615
|
+
}
|
|
616
|
+
return 240
|
|
617
|
+
},
|
|
618
|
+
/** 持久化底部面板高度到 localStorage(px)。 */
|
|
619
|
+
savePanelH() {
|
|
620
|
+
try {
|
|
621
|
+
localStorage.setItem('cardbuddy:debug-panel-height', String(Math.round(this.panelH)))
|
|
622
|
+
} catch (e) {
|
|
623
|
+
/* localStorage 不可用时忽略 */
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
/** 进入 Raw 模式:把当前 rootScope 序列化为可编辑文本,并用 CodeJar 托管高亮 */
|
|
627
|
+
enterRaw() {
|
|
628
|
+
this.rawError = ''
|
|
629
|
+
try {
|
|
630
|
+
this.rawText = JSON.stringify(this.rt.rootScope, (k, v) => (k === '_schemaNode' ? undefined : v), 2)
|
|
631
|
+
} catch (e) {
|
|
632
|
+
this.rawText = ''
|
|
633
|
+
this.rawError = '无法序列化当前数据:' + (e && e.message)
|
|
634
|
+
}
|
|
635
|
+
this.rawMode = true
|
|
636
|
+
this.$nextTick(() => this.initJar())
|
|
637
|
+
},
|
|
638
|
+
/** 用 CodeJar 初始化原始 JSON 编辑器:contenteditable + Prism(json) 高亮 */
|
|
639
|
+
initJar() {
|
|
640
|
+
const el = this.$refs.rawEditor
|
|
641
|
+
if (!el) return
|
|
642
|
+
// 复用 CodeJar 时先销毁旧实例,避免重复绑定到同一 DOM
|
|
643
|
+
this.destroyJar()
|
|
644
|
+
const highlight = (editor) => {
|
|
645
|
+
editor.innerHTML = Prism.highlight(editor.textContent, Prism.languages.json, 'json')
|
|
646
|
+
}
|
|
647
|
+
this._jar = CodeJar(el, highlight, {
|
|
648
|
+
tab: ' ',
|
|
649
|
+
indentOn: /[({[]$/,
|
|
650
|
+
addClosing: true,
|
|
651
|
+
history: true,
|
|
652
|
+
})
|
|
653
|
+
this._jar.updateCode(this.rawText || '')
|
|
654
|
+
this._jar.onUpdate((code) => {
|
|
655
|
+
this.rawText = code
|
|
656
|
+
if (this.rawError) this.rawError = '' // 用户再次输入即清除旧错误
|
|
657
|
+
})
|
|
658
|
+
this.$nextTick(() => el.focus())
|
|
659
|
+
},
|
|
660
|
+
/** 销毁 CodeJar 实例(退出/保存/卸载/切卡时调用) */
|
|
661
|
+
destroyJar() {
|
|
662
|
+
if (this._jar && this._jar.destroy) {
|
|
663
|
+
this._jar.destroy()
|
|
664
|
+
}
|
|
665
|
+
this._jar = null
|
|
666
|
+
},
|
|
667
|
+
/** 取消 Raw 编辑:放弃改动,切回默认树形模式 */
|
|
668
|
+
exitRaw() {
|
|
669
|
+
this.rawMode = false
|
|
670
|
+
this.rawError = ''
|
|
671
|
+
this.destroyJar()
|
|
672
|
+
},
|
|
673
|
+
/** 保存 Raw 编辑:先校验 JSON 合法性与类型,通过后替换 rootScope */
|
|
674
|
+
saveRaw() {
|
|
675
|
+
const text = (this.rawText || '').trim()
|
|
676
|
+
if (!text) {
|
|
677
|
+
this.rawError = '内容不能为空'
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
let parsed
|
|
681
|
+
try {
|
|
682
|
+
parsed = JSON.parse(text)
|
|
683
|
+
} catch (e) {
|
|
684
|
+
this.rawError = 'JSON 格式错误:' + e.message
|
|
685
|
+
return
|
|
686
|
+
}
|
|
687
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
688
|
+
this.rawError = '数据绑定必须是对象({ ... })'
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
this.rt.setRootScope(parsed)
|
|
692
|
+
// 等待 watcher 把 rootScope 同步进 currentSchema.dictionary 后再落定历史,
|
|
693
|
+
// 使本次 Raw 编辑作为一笔撤销记录(与「应用」按钮同路径)。
|
|
694
|
+
this.$nextTick(() => this.onCommit())
|
|
695
|
+
this.destroyJar()
|
|
696
|
+
this.rawMode = false
|
|
697
|
+
this.rawError = ''
|
|
698
|
+
},
|
|
699
|
+
async copyScope() {
|
|
700
|
+
let text = ''
|
|
701
|
+
try {
|
|
702
|
+
text = JSON.stringify(this.rt.rootScope, null, 2)
|
|
703
|
+
} catch (e) {
|
|
704
|
+
text = String(this.rt.rootScope)
|
|
705
|
+
}
|
|
706
|
+
const ok = await copyText(text)
|
|
707
|
+
if (ok) {
|
|
708
|
+
this.copied = true
|
|
709
|
+
if (this.copyTimer) clearTimeout(this.copyTimer)
|
|
710
|
+
this.copyTimer = setTimeout(() => {
|
|
711
|
+
this.copied = false
|
|
712
|
+
}, 1500)
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
/** 结构树「事件」徽标:打开属性面板并直接落到事件页签 */
|
|
716
|
+
onEditEvent(comp) {
|
|
717
|
+
this.editingEventComp = comp
|
|
718
|
+
this.eventEditorKey++
|
|
719
|
+
},
|
|
720
|
+
/** 节点属性 / 事件面板「应用」(实时编辑):仅内存重建预览,不落盘、不记历史 */
|
|
721
|
+
onApply() {
|
|
722
|
+
this.$emit('schema-changed')
|
|
723
|
+
},
|
|
724
|
+
/** 节点属性 / 事件面板「应用」按钮:重建预览 + 提交一次历史(写入 undo/redo 栈) */
|
|
725
|
+
onCommit() {
|
|
726
|
+
this.$emit('schema-commit')
|
|
727
|
+
},
|
|
728
|
+
/** 双击结构树节点:弹出属性面板 */
|
|
729
|
+
onInspect(comp) {
|
|
730
|
+
this.inspectingComp = comp
|
|
731
|
+
this.inspectKey++
|
|
732
|
+
},
|
|
733
|
+
onSelectNode(id) {
|
|
734
|
+
this.$emit('select-node', id)
|
|
735
|
+
},
|
|
736
|
+
/** 结构树某行「+」:透传到 App 打开组件选择面板 */
|
|
737
|
+
onAddNode(comp) {
|
|
738
|
+
this.$emit('add-node', comp)
|
|
739
|
+
},
|
|
740
|
+
onMoveNode(payload) {
|
|
741
|
+
this.$emit('move-node', payload)
|
|
742
|
+
},
|
|
743
|
+
/** 结构树节点 hover:透传到 App,驱动画布参考线框 */
|
|
744
|
+
onHoverNode(payload) {
|
|
745
|
+
this.$emit('hover-node', payload)
|
|
746
|
+
},
|
|
747
|
+
/** 清空接口调用流水:调用运行时 clearApiCalls(清空数组 + 自增 apiRev),
|
|
748
|
+
* 因 apiCalls computed 返回 slice() 新引用,触发实时重渲染 */
|
|
749
|
+
clearApiLog() {
|
|
750
|
+
this.rt.clearApiCalls()
|
|
751
|
+
},
|
|
752
|
+
/** 清空事件主动日志(事件面板「清空日志」按钮);错误不清除 */
|
|
753
|
+
clearEventLogs() {
|
|
754
|
+
this.rt.clearEventLogs()
|
|
755
|
+
},
|
|
756
|
+
/** 拉取当前卡片的导入来源元数据(Phase E 来源 tab) */
|
|
757
|
+
maybeFetchMeta() {
|
|
758
|
+
if (this.activeTab === 'meta') this.fetchImportMeta()
|
|
759
|
+
},
|
|
760
|
+
async fetchImportMeta() {
|
|
761
|
+
const id = this.cardId
|
|
762
|
+
if (!id) {
|
|
763
|
+
this.importMeta = null
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
const res = await apiFetch(`/__import-meta/${encodeURIComponent(id)}`)
|
|
768
|
+
if (res.ok) {
|
|
769
|
+
const r = await res.json()
|
|
770
|
+
this.importMeta = r.meta || null
|
|
771
|
+
} else {
|
|
772
|
+
this.importMeta = null
|
|
773
|
+
}
|
|
774
|
+
} catch {
|
|
775
|
+
this.importMeta = null
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
/** ISO 时间转人读字符串(本地时区) */
|
|
779
|
+
formatTime(iso) {
|
|
780
|
+
if (!iso) return '-'
|
|
781
|
+
try {
|
|
782
|
+
return new Date(iso).toLocaleString('zh-CN', { hour12: false })
|
|
783
|
+
} catch {
|
|
784
|
+
return iso
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
/** 复制某条接口调用的「返回数据」(即 payload 全文),不含方法名(api) */
|
|
788
|
+
async copyApiPayload(c, i) {
|
|
789
|
+
let text = ''
|
|
790
|
+
try {
|
|
791
|
+
text = JSON.stringify(c.payload, null, 2)
|
|
792
|
+
} catch {
|
|
793
|
+
text = String(c.payload)
|
|
794
|
+
}
|
|
795
|
+
const ok = await copyText(text)
|
|
796
|
+
if (ok) {
|
|
797
|
+
this.copiedIndex = i
|
|
798
|
+
if (this.apiCopyTimer) clearTimeout(this.apiCopyTimer)
|
|
799
|
+
this.apiCopyTimer = setTimeout(() => {
|
|
800
|
+
if (this.copiedIndex === i) this.copiedIndex = null
|
|
801
|
+
}, 1500)
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
}
|
|
806
|
+
</script>
|