cardbuddy 0.9.0 → 0.10.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 +17 -2
- package/dist/cli.js +3 -0
- package/dist/config.js +7 -0
- package/dist/portGuard.js +293 -0
- package/dist/preview-editor/assets/index-C_UKS0NF.css +1 -0
- package/dist/preview-editor/assets/index-D1iIJ4sl.js +81 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/server-core/assetProxy.js +3 -22
- package/dist/server-core/buildDataFromSchema.js +36 -4
- package/dist/server-core/cardListCache.js +0 -17
- package/dist/server-core/index.js +3 -5
- package/dist/server-core/logStream.js +1 -6
- package/dist/standalone.js +24 -8
- package/dist/vendored/App.vue +187 -77
- package/dist/vendored/components/ComponentPalette.vue +1 -1
- package/dist/vendored/components/DebugPanel.vue +737 -91
- package/dist/vendored/components/NumberStepper.vue +125 -0
- package/dist/vendored/components/ProgressSlider.vue +9 -1
- package/dist/vendored/components/StructureTree.vue +2 -2
- package/dist/vendored/runtime/clipboard-node.js +0 -5
- package/dist/vendored/styles/app.css +707 -252
- package/dist/vendored/styles/card-gallery.css +3 -3
- package/dist/vendored/styles/card-select.css +13 -6
- package/dist/vendored/styles/dropdown.css +1 -2
- package/dist/vendored/styles/json-tree.css +3 -3
- package/dist/vendored/styles/node-inspector.css +7 -294
- package/dist/vendored/styles/setters.css +48 -49
- package/dist/vendored/styles/structure-tree.css +6 -11
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-CrcrVs4F.css +0 -1
- package/dist/preview-editor/assets/index-DnpHHKWl.js +0 -80
- package/dist/vendored/assets/logo.png +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
调试面板:三个标签页。
|
|
3
|
-
-
|
|
4
|
-
|
|
3
|
+
- 变量:schema.dictionary.inputParams 的增 / 删 / 改(变量名 · 中文名 · 类型 · 默认值 · 描述),
|
|
4
|
+
即卡片对外声明的入参;默认值同时是设计期 mock 基线,落到 rt.rootScope 供画布预览。
|
|
5
|
+
底部「配置变量 mock」进入二级视图,以 Raw JSON 直接编辑整份设计期数据。
|
|
5
6
|
- 组件结构:当前卡片的组件层级树(参考编辑器「结构」面板)
|
|
6
7
|
- 卡片来源:导入来源元数据
|
|
7
8
|
|
|
@@ -30,47 +31,92 @@
|
|
|
30
31
|
</div>
|
|
31
32
|
|
|
32
33
|
<div class="debug__body">
|
|
33
|
-
<template v-if="activeTab === '
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
</
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
34
|
+
<template v-if="activeTab === 'vars'">
|
|
35
|
+
<!-- 变量列表视图:管理 schema.dictionary.inputParams(变量属性的增 / 删 / 改) -->
|
|
36
|
+
<div v-if="varsMode === 'list'" class="vars">
|
|
37
|
+
<!-- 空态占位:与卡片来源 / API 调用面板同款语言 -->
|
|
38
|
+
<div v-if="!inputParams.length" class="empty-state">
|
|
39
|
+
<span class="empty-state__badge" aria-hidden="true">
|
|
40
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-settings" /></svg>
|
|
41
|
+
</span>
|
|
42
|
+
<p class="debug__empty">暂无变量</p>
|
|
43
|
+
<p class="empty-state__hint">变量与 schema.dictionary.inputParams 一一对应:绑定表达式以 $key 引用,mock 值供设计期预览</p>
|
|
44
|
+
</div>
|
|
45
|
+
<ul v-else class="vars__list">
|
|
46
|
+
<li v-for="(p, i) in inputParams" :key="p.key || i" class="vars__row" @dblclick="openVarDlg(i)">
|
|
47
|
+
<span class="vars__key">{{ p.key }}</span>
|
|
48
|
+
<span class="vars__type" :class="'vars__type--' + (p.type || 'string')">{{ p.type || 'string' }}</span>
|
|
49
|
+
<span v-if="hasMock(p)" class="vars__mock" title="已配置 mock(dictionary.mocks,设计期覆盖默认值)">m</span>
|
|
50
|
+
<span class="vars__name" :title="[p.name, p.desc].filter(Boolean).join(' · ')">{{ p.name || p.desc || '' }}</span>
|
|
51
|
+
<!-- 操作区双击不冒泡到行:连点复制/删除/编辑钮时不误开编辑弹窗 -->
|
|
52
|
+
<span class="vars__ops" @dblclick.stop>
|
|
53
|
+
<!-- 复制默认值:defaultValue 序列化后入剪贴板(容器给 2 空格缩进 JSON),无默认值时禁用 -->
|
|
54
|
+
<button
|
|
55
|
+
class="vars__op"
|
|
56
|
+
type="button"
|
|
57
|
+
:disabled="!hasDefault(p)"
|
|
58
|
+
:title="copiedVar === i ? '已复制 ✓' : hasDefault(p) ? '复制默认值' : '暂无默认值'"
|
|
59
|
+
@click="copyVarDefault(p, i)"
|
|
60
|
+
>
|
|
61
|
+
<span v-if="copiedVar === i" class="debug__copy-ok">✓</span>
|
|
62
|
+
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-copy-sm" /></svg>
|
|
63
|
+
</button>
|
|
64
|
+
<button class="vars__op" type="button" title="编辑变量" @click="openVarDlg(i)">
|
|
65
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-gear" /></svg>
|
|
66
|
+
</button>
|
|
67
|
+
<button class="vars__op vars__op--del" type="button" title="删除变量" @click="askDeleteVar(i)">
|
|
68
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-trash" /></svg>
|
|
69
|
+
</button>
|
|
70
|
+
</span>
|
|
71
|
+
</li>
|
|
72
|
+
</ul>
|
|
73
|
+
<!-- 右下角悬浮操作组:与 mock 视图悬浮条同语言,不占固定行高 -->
|
|
74
|
+
<div class="vars__fabs">
|
|
75
|
+
<button class="debug__copy" type="button" title="新增变量(写入 dictionary.inputParams)" @click="openVarDlg(null)">+ 添加变量</button>
|
|
76
|
+
<button v-if="inputParams.length" class="debug__copy" type="button" title="以 Raw JSON 编辑全部变量的设计期 mock 数据" @click="varsMode = 'mock'">配置变量 mock</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<!-- mock 二级视图:仅 Raw JSON(CodeJar + Prism 高亮);右下角操作按钮只在此出现 -->
|
|
80
|
+
<div v-else class="debug__scope-wrap debug__scope-wrap--raw">
|
|
81
|
+
<div class="vars__mockhead">
|
|
82
|
+
<button class="vars__back" type="button" title="返回变量列表" @click="leaveMock">
|
|
83
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6" /></svg>
|
|
84
|
+
变量
|
|
85
|
+
</button>
|
|
86
|
+
<span class="vars__mocktitle">变量 mock · 设计期覆盖(dictionary.mocks · Raw JSON)</span>
|
|
87
|
+
<span class="vars__mockhint">仅设计期生效 · 未列出的键回退默认值</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="debug__scope-raw">
|
|
90
|
+
<!-- 报错提示条:与顶部提示条同语言(语义色软底 + 图标 + 关闭),钉在编辑器顶部不挤占编辑区 -->
|
|
91
|
+
<div v-if="rawError" class="raw-err-bar" role="alert">
|
|
92
|
+
<svg class="raw-err-bar__icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-alert-triangle" /></svg>
|
|
93
|
+
<span class="raw-err-bar__msg">{{ rawError }}</span>
|
|
94
|
+
<button class="raw-err-bar__close" type="button" title="关闭" @click="rawError = ''">✕</button>
|
|
72
95
|
</div>
|
|
73
|
-
|
|
96
|
+
<div
|
|
97
|
+
ref="rawEditor"
|
|
98
|
+
class="debug__raw-editor"
|
|
99
|
+
spellcheck="false"
|
|
100
|
+
></div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="debug__scope-foot">
|
|
103
|
+
<!-- 右下角悬浮组内的返回:与头部 chip 同一 leaveMock 语义,手不离编辑区即可退出二级视图 -->
|
|
104
|
+
<button class="debug__copy" type="button" title="返回变量列表" @click="leaveMock">
|
|
105
|
+
<svg class="debug__copy-icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6" /></svg>返回
|
|
106
|
+
</button>
|
|
107
|
+
<span class="debug__sep"></span>
|
|
108
|
+
<span v-if="copied" class="debug__copied">已复制 ✓</span>
|
|
109
|
+
<button class="debug__copy" type="button" @click="copyScope">
|
|
110
|
+
<svg class="debug__copy-icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-copy-sm" /></svg>复制
|
|
111
|
+
</button>
|
|
112
|
+
<span class="debug__sep"></span>
|
|
113
|
+
<button class="debug__copy debug__copy--primary" type="button" @click="saveRaw">
|
|
114
|
+
<svg class="debug__copy-icn" viewBox="0 0 16 16" aria-hidden="true"><use href="#icon-check" /></svg>应用
|
|
115
|
+
</button>
|
|
116
|
+
<button class="debug__copy debug__copy--warn" type="button" @click="$emit('reset-import')" title="丢弃内存编辑,从磁盘重新载入(导入优先于编辑)">
|
|
117
|
+
<svg class="debug__copy-icn" viewBox="0 0 16 16" aria-hidden="true"><use href="#icon-reset" /></svg>重置
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
74
120
|
</div>
|
|
75
121
|
</template>
|
|
76
122
|
|
|
@@ -138,7 +184,7 @@
|
|
|
138
184
|
</dd>
|
|
139
185
|
</div>
|
|
140
186
|
</dl>
|
|
141
|
-
<p class="debug__meta-hint"
|
|
187
|
+
<p class="debug__meta-hint">导入优先于编辑:在「变量」tab →「配置变量 mock」里点「重置」,可丢弃内存编辑、从磁盘重新载入最新导入。</p>
|
|
142
188
|
</div>
|
|
143
189
|
</template>
|
|
144
190
|
</div>
|
|
@@ -249,10 +295,11 @@
|
|
|
249
295
|
<span>{{ eventCount ? eventCount + ' 条事件记录' : '无事件记录' }}</span>
|
|
250
296
|
</button>
|
|
251
297
|
<!-- 本地开发服务连接状态:连接图标 + 状态文案,颜色与右上角「连接」按钮实时同步(复用 devSvcClass / devSvcTitle / devSvcLabel)。
|
|
252
|
-
可点击:弹出 / 收起底部「服务日志」面板(实时镜像所连 cardbuddy
|
|
298
|
+
可点击:弹出 / 收起底部「服务日志」面板(实时镜像所连 cardbuddy 终端输出)。
|
|
299
|
+
仅重连 / 断开 / 未配置等_warn、danger 态着语义色,连接成功用状态栏默认色;不设选中态,仅 hover 提示可点。 -->
|
|
253
300
|
<button
|
|
254
301
|
class="statusbar__item statusbar__conn"
|
|
255
|
-
:class="
|
|
302
|
+
:class="devSvcClass"
|
|
256
303
|
type="button"
|
|
257
304
|
:title="devSvcTitle + '(点击查看服务日志)'"
|
|
258
305
|
@click="toggleErrorPanel('svclog')"
|
|
@@ -447,6 +494,85 @@
|
|
|
447
494
|
</div>
|
|
448
495
|
</section>
|
|
449
496
|
</Teleport>
|
|
497
|
+
|
|
498
|
+
<!-- 变量添加 / 编辑弹窗:与节点属性面板同构(ni-overlay / ni-panel / ni-head / ni-foot),
|
|
499
|
+
宽度 · 字号 · 控件高度 · 发丝线分隔全部沿用 node-inspector 的视觉语言;
|
|
500
|
+
默认值按类型分控件:数组 / 对象走 CodeJar 代码编辑区(高亮 + 缩进 + 大段粘贴,确定时自动格式化)、
|
|
501
|
+
字符串走多行文本域、数字走带 −/+ 的 NumberStepper、布尔走 SetSegment 分段单选。 -->
|
|
502
|
+
<transition name="modal">
|
|
503
|
+
<div v-if="varDlg" class="ni-overlay" @click.self="varDlg = null">
|
|
504
|
+
<div class="ni-panel modal-card vars-dlg" role="alertdialog" aria-modal="true" :aria-label="varDlg.index == null ? '添加变量' : '编辑变量'">
|
|
505
|
+
<div class="ni-head">
|
|
506
|
+
<span class="ni-title">{{ varDlg.index == null ? '添加变量' : '编辑变量' }}<em class="ni-cls">{{ varDlg.form.key || '未命名' }}</em></span>
|
|
507
|
+
<button class="ni-close" type="button" title="关闭" @click="varDlg = null"></button>
|
|
508
|
+
</div>
|
|
509
|
+
<div class="ni-body">
|
|
510
|
+
<div class="ni-scroll">
|
|
511
|
+
<div class="ni-field-row">
|
|
512
|
+
<label class="ni-field-label"><i>*</i>变量名</label>
|
|
513
|
+
<input class="ni-input" type="text" v-model="varDlg.form.key" placeholder="英文标识,绑定以 $key 引用" />
|
|
514
|
+
</div>
|
|
515
|
+
<div class="ni-field-row">
|
|
516
|
+
<label class="ni-field-label"><i>*</i>中文名</label>
|
|
517
|
+
<input class="ni-input" type="text" v-model="varDlg.form.name" placeholder="中文名" />
|
|
518
|
+
</div>
|
|
519
|
+
<div class="ni-field-row">
|
|
520
|
+
<label class="ni-field-label"><i>*</i>类型</label>
|
|
521
|
+
<Dropdown class="vars-dlg__ctrl" block size="sm" :value="varDlg.form.type" :options="varTypeOptions" @change="onVarTypeChange" />
|
|
522
|
+
</div>
|
|
523
|
+
<!-- 默认值按类型分四种控件:数组 / 对象 = CodeJar 代码编辑区、字符串 = 多行文本域、
|
|
524
|
+
数字 = 带 −/+ 的 NumberStepper、布尔 = 复用 SetSegment 的 true / false 分段单选。四者统一以文本承载
|
|
525
|
+
(varDlg.form.defaultValue),确定时仍由 parseDefault 按类型解析。 -->
|
|
526
|
+
<div class="ni-field-row ni-field-row--top">
|
|
527
|
+
<label class="ni-field-label">默认值</label>
|
|
528
|
+
<div v-if="isComplexType(varDlg.form.type)" ref="dlgCode" class="vars-dlg__code" spellcheck="false"></div>
|
|
529
|
+
<textarea
|
|
530
|
+
v-else-if="varDlg.form.type === 'string'"
|
|
531
|
+
class="ni-input vars-dlg__str"
|
|
532
|
+
v-model="varDlg.form.defaultValue"
|
|
533
|
+
:placeholder="scalarPlaceholder('string')"
|
|
534
|
+
></textarea>
|
|
535
|
+
<NumberStepper
|
|
536
|
+
v-else-if="varDlg.form.type === 'number'"
|
|
537
|
+
class="vars-dlg__ctrl"
|
|
538
|
+
:value="varDlg.form.defaultValue"
|
|
539
|
+
:placeholder="scalarPlaceholder('number')"
|
|
540
|
+
@input="varDlg.form.defaultValue = $event"
|
|
541
|
+
/>
|
|
542
|
+
<SetSegment
|
|
543
|
+
v-else
|
|
544
|
+
class="vars-dlg__ctrl"
|
|
545
|
+
:value="varDlg.form.defaultValue"
|
|
546
|
+
:options="{ iParamList: varBoolOptions }"
|
|
547
|
+
@input="varDlg.form.defaultValue = $event"
|
|
548
|
+
/>
|
|
549
|
+
</div>
|
|
550
|
+
<div class="ni-field-row ni-field-row--top">
|
|
551
|
+
<label class="ni-field-label">描述</label>
|
|
552
|
+
<textarea class="ni-input vars-dlg__desc" v-model="varDlg.form.desc" placeholder="可选"></textarea>
|
|
553
|
+
</div>
|
|
554
|
+
<p v-if="varDlgError" class="vars-dlg__err">{{ varDlgError }}</p>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="ni-foot">
|
|
558
|
+
<span class="ni-tip">默认值入 defaultValue;设计期覆盖见「配置变量 mock」</span>
|
|
559
|
+
<div class="ni-foot-btns">
|
|
560
|
+
<button class="ni-btn ni-btn--ghost" type="button" @click="varDlg = null">取消</button>
|
|
561
|
+
<button class="ni-btn" type="button" @click="confirmVarDlg">确定</button>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
</transition>
|
|
567
|
+
<ConfirmDialog
|
|
568
|
+
:visible="delVarTarget != null"
|
|
569
|
+
title="删除变量"
|
|
570
|
+
:message="delVarMessage"
|
|
571
|
+
:severity="delVarSeverity"
|
|
572
|
+
:confirm-text="delVarConfirmText"
|
|
573
|
+
@confirm="confirmDeleteVar"
|
|
574
|
+
@cancel="delVarTarget = null"
|
|
575
|
+
/>
|
|
450
576
|
</aside>
|
|
451
577
|
</template>
|
|
452
578
|
|
|
@@ -456,6 +582,10 @@ import StructureTree from './StructureTree.vue'
|
|
|
456
582
|
import NodeInspector from './NodeInspector.vue'
|
|
457
583
|
import EventEditor from './EventEditor.vue'
|
|
458
584
|
import LogValue from './LogValue.vue'
|
|
585
|
+
import Dropdown from './Dropdown.vue'
|
|
586
|
+
import ConfirmDialog from './ConfirmDialog.vue'
|
|
587
|
+
import NumberStepper from './NumberStepper.vue'
|
|
588
|
+
import SetSegment from './setters/SetSegment.vue'
|
|
459
589
|
import { COMPONENT_META } from '@/runtime/toPreview.js'
|
|
460
590
|
import { copyText } from '@/runtime/clipboard.js'
|
|
461
591
|
// 结构树「粘贴」按钮的 disabled 态:剪贴板是否有可粘贴节点
|
|
@@ -466,12 +596,141 @@ import { CodeJar } from 'codejar'
|
|
|
466
596
|
import Prism from 'prismjs'
|
|
467
597
|
import 'prismjs/components/prism-json'
|
|
468
598
|
import { apiFetch, subscribeServiceLog, fetchServiceLogs } from '@/api.js'
|
|
599
|
+
// mock 覆盖仓与派生真相:与 App / 服务端共用同一实现,避免判据漂移
|
|
600
|
+
import { buildDataFromSchema, readMocks, hasMockValue } from '../../server/core/buildDataFromSchema.js'
|
|
469
601
|
// 服务日志面板:终端 ANSI(SGR)→ HTML,1:1 还原 cardbuddy 的 chalk 配色
|
|
470
602
|
import { ansiToHtml, stripAnsi } from '@/ansi.js'
|
|
471
603
|
|
|
472
604
|
/** 服务日志在前端保留的最大行数(与服务端环形缓冲同量级,超出裁剪最旧的)。 */
|
|
473
605
|
const SVC_MAX_LINES = 800
|
|
474
606
|
|
|
607
|
+
/**
|
|
608
|
+
* 容错 JSON 解析(对象字面量友好):先做一遍「格式化容忍」清理,再交严格 JSON.parse ——
|
|
609
|
+
* 报错只针对对象结构本身非法(括号不配对 / 悬空冒号 / 未闭合字符串等),
|
|
610
|
+
* 手写习惯一律容忍并规范化:无引号键 / 单引号字符串 / 注释 / 尾逗号 /
|
|
611
|
+
* 分号当逗号 / 重复逗号 / undefined·NaN·Infinity 归 null。
|
|
612
|
+
* 字符串字面量原样搬运(含转义),不会误伤内容里的 // 、,} 等字符;
|
|
613
|
+
* 清理后的标准 JSON 由调用方(应用 / 确定)回灌编辑器完成「保存即格式化」。
|
|
614
|
+
* @param {string} text
|
|
615
|
+
* @returns {{ value: any } | { error: string }}
|
|
616
|
+
*/
|
|
617
|
+
function parseJsonLenient(text) {
|
|
618
|
+
const src = String(text == null ? '' : text).replace(/^\uFEFF/, '')
|
|
619
|
+
let out = ''
|
|
620
|
+
let i = 0
|
|
621
|
+
const n = src.length
|
|
622
|
+
const lastNonWs = () => {
|
|
623
|
+
let k = out.length - 1
|
|
624
|
+
while (k >= 0 && /\s/.test(out[k])) k -= 1
|
|
625
|
+
return k
|
|
626
|
+
}
|
|
627
|
+
// 逗号去重入栈: leading 逗号(紧跟 [ { )与连续逗号都不输出
|
|
628
|
+
const pushComma = () => {
|
|
629
|
+
const k = lastNonWs()
|
|
630
|
+
if (k >= 0 && out[k] !== ',' && out[k] !== '[' && out[k] !== '{') out += ','
|
|
631
|
+
}
|
|
632
|
+
const skipWs = (from) => {
|
|
633
|
+
let k = from
|
|
634
|
+
while (k < n && /\s/.test(src[k])) k += 1
|
|
635
|
+
return k
|
|
636
|
+
}
|
|
637
|
+
while (i < n) {
|
|
638
|
+
const ch = src[i]
|
|
639
|
+
// 双引号字符串:原样搬运(含转义)
|
|
640
|
+
if (ch === '"') {
|
|
641
|
+
let j = i + 1
|
|
642
|
+
while (j < n) {
|
|
643
|
+
if (src[j] === '\\') { j += 2; continue }
|
|
644
|
+
if (src[j] === '"') { j += 1; break }
|
|
645
|
+
j += 1
|
|
646
|
+
}
|
|
647
|
+
out += src.slice(i, j)
|
|
648
|
+
i = j
|
|
649
|
+
continue
|
|
650
|
+
}
|
|
651
|
+
// 单引号字符串:转双引号(内部双引号转义、\' 还原为裸引号)
|
|
652
|
+
if (ch === '\'') {
|
|
653
|
+
let j = i + 1
|
|
654
|
+
let body = ''
|
|
655
|
+
while (j < n) {
|
|
656
|
+
const c = src[j]
|
|
657
|
+
if (c === '\\' && j + 1 < n) {
|
|
658
|
+
const nx = src[j + 1]
|
|
659
|
+
if (nx === '\'') { body += '\''; j += 2; continue }
|
|
660
|
+
if (nx === '"') { body += '\\\"'; j += 2; continue }
|
|
661
|
+
body += c + nx
|
|
662
|
+
j += 2
|
|
663
|
+
continue
|
|
664
|
+
}
|
|
665
|
+
if (c === '\'') { j += 1; break }
|
|
666
|
+
if (c === '"') { body += '\\\"'; j += 1; continue }
|
|
667
|
+
body += c
|
|
668
|
+
j += 1
|
|
669
|
+
}
|
|
670
|
+
out += '"' + body + '"'
|
|
671
|
+
i = j
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
// 注释:// 与 /* */
|
|
675
|
+
if (ch === '/' && src[i + 1] === '/') {
|
|
676
|
+
while (i < n && src[i] !== '\n') i += 1
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
if (ch === '/' && src[i + 1] === '*') {
|
|
680
|
+
const close = src.indexOf('*/', i + 2)
|
|
681
|
+
i = close === -1 ? n : close + 2
|
|
682
|
+
continue
|
|
683
|
+
}
|
|
684
|
+
// 分号视同逗号;逗号去重
|
|
685
|
+
if (ch === ';' || ch === ',') {
|
|
686
|
+
pushComma()
|
|
687
|
+
i += 1
|
|
688
|
+
continue
|
|
689
|
+
}
|
|
690
|
+
// 标识符:后跟冒号 → 键,补双引号;否则原样(true/false/null 等关键字)
|
|
691
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
692
|
+
let j = i
|
|
693
|
+
while (j < n && /[A-Za-z0-9_$]/.test(src[j])) j += 1
|
|
694
|
+
const token = src.slice(i, j)
|
|
695
|
+
if (src[skipWs(j)] === ':') out += '"' + token + '"'
|
|
696
|
+
else if (token === 'undefined' || token === 'NaN' || token === 'Infinity') out += 'null'
|
|
697
|
+
else out += token
|
|
698
|
+
i = j
|
|
699
|
+
continue
|
|
700
|
+
}
|
|
701
|
+
// 数字(含负号 / 指数):后跟冒号 → 键,补双引号;-Infinity 归 null
|
|
702
|
+
if (ch === '-' && src.startsWith('Infinity', i + 1)) {
|
|
703
|
+
out += 'null'
|
|
704
|
+
i += 9
|
|
705
|
+
continue
|
|
706
|
+
}
|
|
707
|
+
if (/[0-9]/.test(ch) || (ch === '-' && /[0-9]/.test(src[i + 1] || ''))) {
|
|
708
|
+
let j = i
|
|
709
|
+
while (j < n && /[0-9eE+\-.]/.test(src[j])) j += 1
|
|
710
|
+
const token = src.slice(i, j)
|
|
711
|
+
if (src[skipWs(j)] === ':') out += '"' + token + '"'
|
|
712
|
+
else out += token
|
|
713
|
+
i = j
|
|
714
|
+
continue
|
|
715
|
+
}
|
|
716
|
+
// 收括号:抹掉所有尾逗号
|
|
717
|
+
if (ch === '}' || ch === ']') {
|
|
718
|
+
let k = lastNonWs()
|
|
719
|
+
while (k >= 0 && out[k] === ',') {
|
|
720
|
+
out = out.slice(0, k) + out.slice(k + 1)
|
|
721
|
+
k = lastNonWs()
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
out += ch
|
|
725
|
+
i += 1
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
return { value: JSON.parse(out.trim() || '{}') }
|
|
729
|
+
} catch (e) {
|
|
730
|
+
return { error: e.message }
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
475
734
|
/**
|
|
476
735
|
* 行级别判定:只认 logger.js 的行首标签(HH:MM:SS.mmm INFO/OK/WARN/ERR),
|
|
477
736
|
* 其余输出(console.* / 第三方 / vite)统一 info,避免把卡片内容里的「错误」字样误判。
|
|
@@ -514,9 +773,14 @@ function deepEqual(a, b) {
|
|
|
514
773
|
return true
|
|
515
774
|
}
|
|
516
775
|
|
|
776
|
+
/** 深拷贝 JSON 安全值;变量默认值写入 schema / rootScope 前隔断引用 */
|
|
777
|
+
function cloneJson(v) {
|
|
778
|
+
return v === undefined ? undefined : JSON.parse(JSON.stringify(v))
|
|
779
|
+
}
|
|
780
|
+
|
|
517
781
|
export default {
|
|
518
782
|
name: 'DebugPanel',
|
|
519
|
-
components: { JsonTree, StructureTree, NodeInspector, EventEditor, LogValue },
|
|
783
|
+
components: { JsonTree, StructureTree, NodeInspector, EventEditor, LogValue, Dropdown, ConfirmDialog, NumberStepper, SetSegment },
|
|
520
784
|
props: {
|
|
521
785
|
rt: { type: Object, required: true },
|
|
522
786
|
preview: { type: Object, default: null },
|
|
@@ -531,9 +795,9 @@ export default {
|
|
|
531
795
|
// 拖拽移动成功令牌:App 在节点拖拽移动成功后自增,透传给结构树,
|
|
532
796
|
// 触发「选中被拖节点 + 仅展开其祖先链、其余全部折叠」。
|
|
533
797
|
moveCollapseToken: { type: Number, default: 0 },
|
|
534
|
-
// 当前激活的调试面板 tab
|
|
798
|
+
// 当前激活的调试面板 tab(变量/组件结构/卡片来源),由 App 统一管理,
|
|
535
799
|
// 便于画布 Ctrl/⌘ 点击定位在「组件结构」tab 激活时才生效。
|
|
536
|
-
activeTab: { type: String, default: '
|
|
800
|
+
activeTab: { type: String, default: 'vars' },
|
|
537
801
|
// 本地开发服务连接状态:复用 App 中顶栏「连接」按钮同款状态/配色(devSvcClass/devSvcTitle/devSvcLabel),
|
|
538
802
|
// 供底部状态栏展示「连接图标 + 状态文案」,与右上角按钮实时同步(颜色一致、状态一致)。
|
|
539
803
|
devSvcClass: { type: String, default: 'app__btn--warn' },
|
|
@@ -549,6 +813,9 @@ export default {
|
|
|
549
813
|
return {
|
|
550
814
|
copied: false,
|
|
551
815
|
copyTimer: null,
|
|
816
|
+
// 刚复制过默认值的变量行下标(图标变 ✓ 反馈),null=无
|
|
817
|
+
copiedVar: null,
|
|
818
|
+
varCopyTimer: null,
|
|
552
819
|
// 底部面板模式:''(收起) | 'binding'(变量绑定错误) | 'event'(事件日志 / 执行错误)
|
|
553
820
|
errorPanel: '',
|
|
554
821
|
// 事件面板内部 tab:'log'(主动日志) | 'err'(执行错误),打开面板时按是否有错误自动定位
|
|
@@ -580,20 +847,78 @@ export default {
|
|
|
580
847
|
// 事件编辑弹框目标节点(点击「事件」徽标时单独弹出,与属性面板解耦)
|
|
581
848
|
editingEventComp: null,
|
|
582
849
|
eventEditorKey: 0,
|
|
583
|
-
// Raw
|
|
584
|
-
rawMode: false,
|
|
850
|
+
// Raw JSON 编辑状态(mock 二级视图的编辑器内容与解析错误)
|
|
585
851
|
rawText: '',
|
|
586
852
|
rawError: '',
|
|
853
|
+
// 变量 tab 二级视图:'list'(变量列表,管理 inputParams)| 'mock'(Raw JSON 编辑 mock 数据)
|
|
854
|
+
varsMode: 'list',
|
|
855
|
+
// 变量添加 / 编辑弹窗:null | { index: null|number, form: { key, name, type, defaultValue, desc } }
|
|
856
|
+
varDlg: null,
|
|
857
|
+
varDlgError: '',
|
|
858
|
+
// 待删除变量下标(ConfirmDialog 确认中)
|
|
859
|
+
delVarTarget: null,
|
|
860
|
+
varTypeOptions: [
|
|
861
|
+
{ key: 'string', value: '字符串 string' },
|
|
862
|
+
{ key: 'number', value: '数字 number' },
|
|
863
|
+
{ key: 'boolean', value: '布尔 boolean' },
|
|
864
|
+
{ key: 'array', value: '数组 array' },
|
|
865
|
+
{ key: 'object', value: '对象 object' },
|
|
866
|
+
],
|
|
867
|
+
// 布尔默认值的分段单选项(交 SetSegment 渲染,即属性面板同款 .st-seg):
|
|
868
|
+
// 值仍以文本承载('true' / 'false'),与 parseDefault 的解析口径一致
|
|
869
|
+
varBoolOptions: [
|
|
870
|
+
{ key: 'true', value: 'true' },
|
|
871
|
+
{ key: 'false', value: 'false' },
|
|
872
|
+
],
|
|
587
873
|
// 导入来源元数据(Phase E):GET /__import-meta/:id,无侧车时为 null
|
|
588
874
|
importMeta: null,
|
|
589
875
|
tabs: [
|
|
590
|
-
{ key: '
|
|
876
|
+
{ key: 'vars', label: '变量' },
|
|
591
877
|
{ key: 'structure', label: '组件结构' },
|
|
592
878
|
{ key: 'meta', label: '卡片来源' },
|
|
593
879
|
],
|
|
594
880
|
}
|
|
595
881
|
},
|
|
596
882
|
computed: {
|
|
883
|
+
/** 设计期 mock 覆盖表:schema.dictionary.mocks(key → 值);m 徽标与 mock 编辑器的唯一数据源 */
|
|
884
|
+
mocks() {
|
|
885
|
+
return readMocks(this.schema) || {}
|
|
886
|
+
},
|
|
887
|
+
/** 变量定义列表:schema.dictionary.inputParams(变量 tab 的唯一数据源) */
|
|
888
|
+
inputParams() {
|
|
889
|
+
const dict = this.schema && this.schema.dictionary
|
|
890
|
+
return dict && Array.isArray(dict.inputParams) ? dict.inputParams : []
|
|
891
|
+
},
|
|
892
|
+
/** 删除确认文案:点名变量与影响面 */
|
|
893
|
+
/** 待删变量的引用统计(绑定表达式 / 事件脚本 / 其它文本),供删除确认分级 */
|
|
894
|
+
delVarRefs() {
|
|
895
|
+
const p = this.delVarTarget == null ? null : this.inputParams[this.delVarTarget]
|
|
896
|
+
return this.scanVarRefs(p && p.key)
|
|
897
|
+
},
|
|
898
|
+
/** 被引用 = danger(红·禁止图标,明确破坏面);未被引用 = warn(橙黄·三角,普通不可逆操作) */
|
|
899
|
+
delVarSeverity() {
|
|
900
|
+
const r = this.delVarRefs
|
|
901
|
+
return r.binding + r.event + r.other > 0 ? 'danger' : 'warn'
|
|
902
|
+
},
|
|
903
|
+
delVarConfirmText() {
|
|
904
|
+
return this.delVarSeverity === 'danger' ? '仍要删除' : '删除'
|
|
905
|
+
},
|
|
906
|
+
delVarMessage() {
|
|
907
|
+
const p = this.delVarTarget == null ? null : this.inputParams[this.delVarTarget]
|
|
908
|
+
if (!p) return ''
|
|
909
|
+
// 中文名与 key 相同时不重复展示括号
|
|
910
|
+
const nm = p.name && p.name !== p.key ? '(' + p.name + ')' : ''
|
|
911
|
+
const r = this.delVarRefs
|
|
912
|
+
const total = r.binding + r.event + r.other
|
|
913
|
+
if (total > 0) {
|
|
914
|
+
const parts = []
|
|
915
|
+
if (r.binding) parts.push(`绑定表达式 ${r.binding} 处`)
|
|
916
|
+
if (r.event) parts.push(`事件脚本 ${r.event} 处`)
|
|
917
|
+
if (r.other) parts.push(`其它文本 ${r.other} 处`)
|
|
918
|
+
return `变量「${p.key}」${nm}仍被${parts.join('、')}引用(共 ${total} 处)。删除后这些引用将取不到值,画布可能出现空白或执行报错,请先迁移再删除。`
|
|
919
|
+
}
|
|
920
|
+
return `变量「${p.key}」${nm}当前未被任何绑定表达式或事件脚本引用。删除后变量定义与 mock 值一并移除,不可恢复。`
|
|
921
|
+
},
|
|
597
922
|
/** 服务日志空态标题:按连接态区分「没配 / 没连上 / 连上了但还没输出」 */
|
|
598
923
|
svcEmptyText() {
|
|
599
924
|
if (this.devSvcConn === 'unconfigured') return '未配置本地开发服务'
|
|
@@ -691,9 +1016,13 @@ export default {
|
|
|
691
1016
|
cardId: {
|
|
692
1017
|
immediate: true,
|
|
693
1018
|
handler() {
|
|
694
|
-
this.rawMode = false
|
|
695
1019
|
this.rawError = ''
|
|
696
1020
|
this.destroyJar()
|
|
1021
|
+
// 换卡回到变量列表视图,并关掉可能开着的变量弹窗 / 删除确认
|
|
1022
|
+
this.varsMode = 'list'
|
|
1023
|
+
this.varDlg = null
|
|
1024
|
+
this.varDlgError = ''
|
|
1025
|
+
this.delVarTarget = null
|
|
697
1026
|
this.inspectingComp = null
|
|
698
1027
|
this.editingEventComp = null
|
|
699
1028
|
this.importMeta = null
|
|
@@ -710,12 +1039,49 @@ export default {
|
|
|
710
1039
|
// 切到「来源」tab 时按需拉取导入来源元数据(懒加载,消除自动 404)。
|
|
711
1040
|
activeTab(n) {
|
|
712
1041
|
if (n === 'meta') this.fetchImportMeta()
|
|
1042
|
+
// 离开变量 tab:mock 编辑器随视图卸载,销毁 CodeJar 防泄漏;回到列表视图
|
|
1043
|
+
if (n !== 'vars') {
|
|
1044
|
+
this.varsMode = 'list'
|
|
1045
|
+
this.rawError = ''
|
|
1046
|
+
this.destroyJar()
|
|
1047
|
+
this.varDlg = null
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
// 变量弹窗开合:打开且为数组 / 对象类型时挂载 CodeJar 代码编辑区;关闭即销毁
|
|
1051
|
+
varDlg(n) {
|
|
1052
|
+
if (!n) {
|
|
1053
|
+
this.destroyDlgJar()
|
|
1054
|
+
return
|
|
1055
|
+
}
|
|
1056
|
+
this.varDlgError = ''
|
|
1057
|
+
if (this.isComplexType(n.form.type)) this.$nextTick(() => this.initDlgJar())
|
|
1058
|
+
},
|
|
1059
|
+
// 类型切换:四种默认值控件互换,旧文本按新类型规整(能沿用则沿用,不能则给该类型的空态);
|
|
1060
|
+
// 容器类型还要等 DOM 换出后重新挂 CodeJar
|
|
1061
|
+
'varDlg.form.type'(n, o) {
|
|
1062
|
+
if (!this.varDlg || n === o) return
|
|
1063
|
+
this.varDlg.form.defaultValue = this.normalizeDefaultText(n, this.varDlg.form.defaultValue)
|
|
1064
|
+
if (this.isComplexType(n)) this.$nextTick(() => this.initDlgJar())
|
|
1065
|
+
else this.destroyDlgJar()
|
|
1066
|
+
},
|
|
1067
|
+
// 进入 mock 二级视图:序列化 rootScope 交 CodeJar 托管;退出即销毁实例
|
|
1068
|
+
varsMode(n) {
|
|
1069
|
+
if (n === 'mock') {
|
|
1070
|
+
this.seedRawFromMocks()
|
|
1071
|
+
this.$nextTick(() => this.initJar())
|
|
1072
|
+
} else {
|
|
1073
|
+
this.rawError = ''
|
|
1074
|
+
this.destroyJar()
|
|
1075
|
+
}
|
|
713
1076
|
},
|
|
714
1077
|
// schema 引用变化(撤销/重做、刷新重载会整体替换对象;就地编辑只 mutate 不换引用)
|
|
715
1078
|
// 时关闭已打开的节点属性/事件面板,避免面板持有已被替换的旧预览节点引用。
|
|
716
1079
|
schema() {
|
|
717
1080
|
this.inspectingComp = null
|
|
718
1081
|
this.editingEventComp = null
|
|
1082
|
+
// schema 整体被替换(重置 / 撤销重做 / 外部重载):mock 视图开着时同步重播种,
|
|
1083
|
+
// 否则 CodeJar 会停留在「应用」前的旧文本,重置按钮看起来像没生效
|
|
1084
|
+
if (this.varsMode === 'mock') this.seedRawFromMocks()
|
|
719
1085
|
},
|
|
720
1086
|
},
|
|
721
1087
|
mounted() {
|
|
@@ -728,6 +1094,7 @@ export default {
|
|
|
728
1094
|
beforeUnmount() {
|
|
729
1095
|
// DebugPanel 一般常驻,但保险起见卸载时销毁 CodeJar、清理可能残留的拖拽监听
|
|
730
1096
|
this.destroyJar()
|
|
1097
|
+
this.destroyDlgJar()
|
|
731
1098
|
this.cancelResize()
|
|
732
1099
|
this.stopSvcLog()
|
|
733
1100
|
},
|
|
@@ -822,18 +1189,6 @@ export default {
|
|
|
822
1189
|
/* localStorage 不可用时忽略 */
|
|
823
1190
|
}
|
|
824
1191
|
},
|
|
825
|
-
/** 进入 Raw 模式:把当前 rootScope 序列化为可编辑文本,并用 CodeJar 托管高亮 */
|
|
826
|
-
enterRaw() {
|
|
827
|
-
this.rawError = ''
|
|
828
|
-
try {
|
|
829
|
-
this.rawText = JSON.stringify(this.rt.rootScope, (k, v) => (k === '_schemaNode' ? undefined : v), 2)
|
|
830
|
-
} catch (e) {
|
|
831
|
-
this.rawText = ''
|
|
832
|
-
this.rawError = '无法序列化当前数据:' + (e && e.message)
|
|
833
|
-
}
|
|
834
|
-
this.rawMode = true
|
|
835
|
-
this.$nextTick(() => this.initJar())
|
|
836
|
-
},
|
|
837
1192
|
/** 用 CodeJar 初始化原始 JSON 编辑器:contenteditable + Prism(json) 高亮 */
|
|
838
1193
|
initJar() {
|
|
839
1194
|
const el = this.$refs.rawEditor
|
|
@@ -854,6 +1209,9 @@ export default {
|
|
|
854
1209
|
this.rawText = code
|
|
855
1210
|
if (this.rawError) this.rawError = '' // 用户再次输入即清除旧错误
|
|
856
1211
|
})
|
|
1212
|
+
// 开发期暴露 Raw 编辑器句柄供 E2E 程序化写入(与 window.__rt 同源约定);生产构建不暴露。
|
|
1213
|
+
// 合成 input 事件驱动不了 CodeJar,E2E 经 updateCode 走真实 onUpdate → 「应用」路径。
|
|
1214
|
+
if (import.meta.env.DEV) window.__rawEditor = this._jar
|
|
857
1215
|
this.$nextTick(() => el.focus())
|
|
858
1216
|
},
|
|
859
1217
|
/** 销毁 CodeJar 实例(退出/保存/卸载/切卡时调用) */
|
|
@@ -862,46 +1220,94 @@ export default {
|
|
|
862
1220
|
this._jar.destroy()
|
|
863
1221
|
}
|
|
864
1222
|
this._jar = null
|
|
1223
|
+
if (typeof window !== 'undefined' && window.__rawEditor) window.__rawEditor = null
|
|
865
1224
|
},
|
|
866
|
-
/**
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
this.
|
|
1225
|
+
/** 变量弹窗「默认值」代码编辑区(数组 / 对象):CodeJar + Prism(json),与 mock Raw 编辑器同方案 */
|
|
1226
|
+
initDlgJar() {
|
|
1227
|
+
const el = this.$refs.dlgCode
|
|
1228
|
+
if (!el) return
|
|
1229
|
+
this.destroyDlgJar()
|
|
1230
|
+
const highlight = (editor) => {
|
|
1231
|
+
editor.innerHTML = Prism.highlight(editor.textContent, Prism.languages.json, 'json')
|
|
1232
|
+
}
|
|
1233
|
+
this._dlgJar = CodeJar(el, highlight, {
|
|
1234
|
+
tab: ' ',
|
|
1235
|
+
indentOn: /[({[]$/,
|
|
1236
|
+
addClosing: true,
|
|
1237
|
+
history: true,
|
|
1238
|
+
})
|
|
1239
|
+
this._dlgJar.updateCode(this.varDlg ? this.varDlg.form.defaultValue : '')
|
|
1240
|
+
this._dlgJar.onUpdate((code) => {
|
|
1241
|
+
if (this.varDlg) this.varDlg.form.defaultValue = code
|
|
1242
|
+
if (this.varDlgError) this.varDlgError = ''
|
|
1243
|
+
})
|
|
1244
|
+
// 开发期暴露弹窗代码区句柄供 E2E 程序化写入(合成 input 事件驱动不了 CodeJar);生产不暴露
|
|
1245
|
+
if (import.meta.env.DEV) window.__dlgEditor = this._dlgJar
|
|
1246
|
+
},
|
|
1247
|
+
destroyDlgJar() {
|
|
1248
|
+
if (this._dlgJar && this._dlgJar.destroy) this._dlgJar.destroy()
|
|
1249
|
+
this._dlgJar = null
|
|
1250
|
+
if (typeof window !== 'undefined' && window.__dlgEditor) window.__dlgEditor = null
|
|
871
1251
|
},
|
|
872
|
-
/**
|
|
1252
|
+
/** 标量 → 容器切换时的种子文本:旧文本形状匹配则格式化沿用,否则给空容器骨架 */
|
|
1253
|
+
complexSeed(type, text) {
|
|
1254
|
+
const empty = type === 'array' ? '[]' : '{}'
|
|
1255
|
+
const t = String(text == null ? '' : text).trim()
|
|
1256
|
+
if (!t) return empty
|
|
1257
|
+
try {
|
|
1258
|
+
const v = JSON.parse(t)
|
|
1259
|
+
const shapeOk = type === 'array' ? Array.isArray(v) : !!v && typeof v === 'object'
|
|
1260
|
+
return shapeOk ? JSON.stringify(v, null, 2) : empty
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
return empty
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
/**
|
|
1266
|
+
* 默认值统一以文本承载,但四种类型「合法文本」的形态不同:切类型 / 打开弹窗时按目标类型规整,
|
|
1267
|
+
* 免得把上一类型的字面量塞进新控件(如布尔的 'true' 落进数字步进框)。
|
|
1268
|
+
* 字符串原样保留(含首尾空白与换行)——占位提示已明示「字符串原样保存」。
|
|
1269
|
+
*/
|
|
1270
|
+
normalizeDefaultText(type, text) {
|
|
1271
|
+
if (this.isComplexType(type)) return this.complexSeed(type, text)
|
|
1272
|
+
const t = String(text == null ? '' : text).trim()
|
|
1273
|
+
if (type === 'number') return t !== '' && Number.isFinite(Number(t)) ? String(Number(t)) : ''
|
|
1274
|
+
if (type === 'boolean') return /^(true|1)$/i.test(t) ? 'true' : 'false'
|
|
1275
|
+
return String(text == null ? '' : text)
|
|
1276
|
+
},
|
|
1277
|
+
/** 应用 Raw 编辑:校验 JSON 合法性与形状 → 写入 dictionary.mocks → 重算预览数据(默认值之上叠 mock);
|
|
1278
|
+
* 写入即按 2 空格标准格式回灌编辑器;留在 mock 视图继续编辑 */
|
|
873
1279
|
saveRaw() {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1280
|
+
// 先容错清理(注释 / 尾逗号)再严格校验:报错判定基于清理后的文本
|
|
1281
|
+
const parsed = parseJsonLenient(this.rawText)
|
|
1282
|
+
if (parsed.error) {
|
|
1283
|
+
this.rawError = 'JSON 格式错误:' + parsed.error
|
|
877
1284
|
return
|
|
878
1285
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
parsed = JSON.parse(text)
|
|
882
|
-
} catch (e) {
|
|
883
|
-
this.rawError = 'JSON 格式错误:' + e.message
|
|
1286
|
+
if (parsed.value === null || typeof parsed.value !== 'object' || Array.isArray(parsed.value)) {
|
|
1287
|
+
this.rawError = '变量 mock 必须是 JSON 对象({ ... })'
|
|
884
1288
|
return
|
|
885
1289
|
}
|
|
886
|
-
|
|
887
|
-
|
|
1290
|
+
const schema = this.schema
|
|
1291
|
+
if (!schema) {
|
|
1292
|
+
this.rawError = '当前未载入卡片'
|
|
888
1293
|
return
|
|
889
1294
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1295
|
+
if (!schema.dictionary || typeof schema.dictionary !== 'object') {
|
|
1296
|
+
schema.dictionary = { dictionaryList: [], inputParams: [] }
|
|
1297
|
+
}
|
|
1298
|
+
schema.dictionary.mocks = parsed.value
|
|
1299
|
+
// 画布数据 = defaultValue 之上按 key 叠加非空 mock;watcher 会把 rootScope 回写两仓
|
|
1300
|
+
this.rt.setRootScope(buildDataFromSchema(schema, { includeMocks: true }))
|
|
1301
|
+
const formatted = JSON.stringify(parsed.value, null, 2)
|
|
1302
|
+
this.rawText = formatted
|
|
1303
|
+
if (this._jar) this._jar.updateCode(formatted)
|
|
1304
|
+
// 落定一笔撤销记录(与节点属性「应用」同路径)
|
|
893
1305
|
this.$nextTick(() => this.onCommit())
|
|
894
|
-
this.destroyJar()
|
|
895
|
-
this.rawMode = false
|
|
896
1306
|
this.rawError = ''
|
|
897
1307
|
},
|
|
898
1308
|
async copyScope() {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
text = JSON.stringify(this.rt.rootScope, null, 2)
|
|
902
|
-
} catch (e) {
|
|
903
|
-
text = String(this.rt.rootScope)
|
|
904
|
-
}
|
|
1309
|
+
// 复制当前 mock 文档(编辑器所见即所得)
|
|
1310
|
+
const text = this.rawText || ''
|
|
905
1311
|
const ok = await copyText(text)
|
|
906
1312
|
if (ok) {
|
|
907
1313
|
this.copied = true
|
|
@@ -924,6 +1330,246 @@ export default {
|
|
|
924
1330
|
onCommit() {
|
|
925
1331
|
this.$emit('schema-commit')
|
|
926
1332
|
},
|
|
1333
|
+
// —— 变量管理(schema.dictionary.inputParams)——
|
|
1334
|
+
/** 是否已配置 mock(dictionary.mocks 有数据;与 buildDataFromSchema 同判据)→ 列表行 m 徽标 */
|
|
1335
|
+
hasMock(param) {
|
|
1336
|
+
return hasMockValue(this.mocks, param.key)
|
|
1337
|
+
},
|
|
1338
|
+
/** 是否已配置默认值:仅 undefined / null / 空串算「无」;false 与 0 都是有效默认值 */
|
|
1339
|
+
hasDefault(param) {
|
|
1340
|
+
const v = param ? param.defaultValue : undefined
|
|
1341
|
+
if (v === undefined || v === null) return false
|
|
1342
|
+
if (typeof v === 'string') return v.length > 0
|
|
1343
|
+
return true
|
|
1344
|
+
},
|
|
1345
|
+
/** 默认值 → 剪贴板文本:与 openVarDlg 同口径(容器给 2 空格缩进 JSON,标量 String()) */
|
|
1346
|
+
varDefaultText(param) {
|
|
1347
|
+
const v = param ? param.defaultValue : undefined
|
|
1348
|
+
if (v === undefined || v === null) return ''
|
|
1349
|
+
if (typeof v === 'object') {
|
|
1350
|
+
try {
|
|
1351
|
+
return JSON.stringify(v, null, 2)
|
|
1352
|
+
} catch {
|
|
1353
|
+
return String(v)
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return String(v)
|
|
1357
|
+
},
|
|
1358
|
+
/** 复制变量默认值;成功后图标短暂变 ✓(与接口调用面板同款反馈节奏) */
|
|
1359
|
+
async copyVarDefault(param, i) {
|
|
1360
|
+
const text = this.varDefaultText(param)
|
|
1361
|
+
if (!text) return
|
|
1362
|
+
const ok = await copyText(text)
|
|
1363
|
+
if (ok) {
|
|
1364
|
+
this.copiedVar = i
|
|
1365
|
+
if (this.varCopyTimer) clearTimeout(this.varCopyTimer)
|
|
1366
|
+
this.varCopyTimer = setTimeout(() => {
|
|
1367
|
+
if (this.copiedVar === i) this.copiedVar = null
|
|
1368
|
+
}, 1500)
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
isComplexType(type) {
|
|
1372
|
+
return type === 'array' || type === 'object'
|
|
1373
|
+
},
|
|
1374
|
+
// 布尔改用单选(无占位提示),故这里只剩数字与字符串两支
|
|
1375
|
+
scalarPlaceholder(type) {
|
|
1376
|
+
if (type === 'number') return '数字,如 0 / 99.5'
|
|
1377
|
+
return '字符串原样保存'
|
|
1378
|
+
},
|
|
1379
|
+
onVarTypeChange(type) {
|
|
1380
|
+
if (this.varDlg) this.varDlg.form.type = type
|
|
1381
|
+
},
|
|
1382
|
+
/** 打开添加(index=null)/ 编辑(index=下标)弹窗;默认值统一以文本承载,确定时按类型解析 */
|
|
1383
|
+
openVarDlg(index) {
|
|
1384
|
+
const param = index == null ? null : this.inputParams[index]
|
|
1385
|
+
const type = param ? param.type || 'string' : 'string'
|
|
1386
|
+
const raw = param ? param.defaultValue : undefined
|
|
1387
|
+
let text = ''
|
|
1388
|
+
if (raw !== undefined && raw !== null) {
|
|
1389
|
+
text = typeof raw === 'object' ? JSON.stringify(raw, null, 2) : String(raw)
|
|
1390
|
+
}
|
|
1391
|
+
// 按类型规整:布尔补出 'false' 让单选有落点,数字剔除非法字面量,容器格式化
|
|
1392
|
+
text = this.normalizeDefaultText(type, text)
|
|
1393
|
+
this.varDlgError = ''
|
|
1394
|
+
this.varDlg = {
|
|
1395
|
+
index,
|
|
1396
|
+
form: {
|
|
1397
|
+
key: param ? param.key || '' : '',
|
|
1398
|
+
name: param ? param.name || '' : '',
|
|
1399
|
+
type,
|
|
1400
|
+
defaultValue: text,
|
|
1401
|
+
desc: param ? param.desc || '' : '',
|
|
1402
|
+
},
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
/** 默认值文本 → 类型化值;返回 { value } 或 { error } */
|
|
1406
|
+
parseDefault(type, text) {
|
|
1407
|
+
const t = String(text == null ? '' : text).trim()
|
|
1408
|
+
if (this.isComplexType(type)) {
|
|
1409
|
+
if (!t) return { value: type === 'array' ? [] : {} }
|
|
1410
|
+
// 与 mock 编辑器同套容错:先清理注释 / 尾逗号再严格校验
|
|
1411
|
+
const parsed = parseJsonLenient(t)
|
|
1412
|
+
if (parsed.error) return { error: '默认值不是合法 JSON:' + parsed.error }
|
|
1413
|
+
const v = parsed.value
|
|
1414
|
+
if (type === 'array' && !Array.isArray(v)) return { error: '默认值需为 JSON 数组' }
|
|
1415
|
+
if (type === 'object' && (typeof v !== 'object' || v === null || Array.isArray(v))) return { error: '默认值需为 JSON 对象' }
|
|
1416
|
+
return { value: v }
|
|
1417
|
+
}
|
|
1418
|
+
if (type === 'number') {
|
|
1419
|
+
if (!t) return { value: 0 }
|
|
1420
|
+
const n = Number(t)
|
|
1421
|
+
if (!Number.isFinite(n)) return { error: '默认值不是合法数字' }
|
|
1422
|
+
return { value: n }
|
|
1423
|
+
}
|
|
1424
|
+
if (type === 'boolean') {
|
|
1425
|
+
if (!t) return { value: false }
|
|
1426
|
+
if (t === 'true' || t === '1') return { value: true }
|
|
1427
|
+
if (t === 'false' || t === '0') return { value: false }
|
|
1428
|
+
return { error: '布尔默认值仅接受 true / false' }
|
|
1429
|
+
}
|
|
1430
|
+
return { value: String(text == null ? '' : text) }
|
|
1431
|
+
},
|
|
1432
|
+
/** 确定:写入 inputParams 并同步 rootScope(mock 基线),再记一笔历史 */
|
|
1433
|
+
confirmVarDlg() {
|
|
1434
|
+
const dlg = this.varDlg
|
|
1435
|
+
if (!dlg) return
|
|
1436
|
+
const form = dlg.form
|
|
1437
|
+
const key = String(form.key || '').trim()
|
|
1438
|
+
const name = String(form.name || '').trim()
|
|
1439
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1440
|
+
this.varDlgError = '变量名需为字母 / 下划线开头的英文标识(绑定以 $key 引用)'
|
|
1441
|
+
return
|
|
1442
|
+
}
|
|
1443
|
+
if (this.inputParams.some((p, i) => p.key === key && i !== dlg.index)) {
|
|
1444
|
+
this.varDlgError = '已存在同名变量 ' + key
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
if (!name) {
|
|
1448
|
+
this.varDlgError = '中文名不能为空'
|
|
1449
|
+
return
|
|
1450
|
+
}
|
|
1451
|
+
const parsed = this.parseDefault(form.type, form.defaultValue)
|
|
1452
|
+
if (parsed.error) {
|
|
1453
|
+
this.varDlgError = parsed.error
|
|
1454
|
+
return
|
|
1455
|
+
}
|
|
1456
|
+
// 容器类型:保存即按 2 空格标准 JSON 格式化回灌(编辑器与存档同形)
|
|
1457
|
+
if (this.isComplexType(form.type)) {
|
|
1458
|
+
const formatted = JSON.stringify(parsed.value, null, 2)
|
|
1459
|
+
form.defaultValue = formatted
|
|
1460
|
+
if (this._dlgJar) this._dlgJar.updateCode(formatted)
|
|
1461
|
+
}
|
|
1462
|
+
const schema = this.schema
|
|
1463
|
+
if (!schema) {
|
|
1464
|
+
this.varDlgError = '当前未载入卡片'
|
|
1465
|
+
return
|
|
1466
|
+
}
|
|
1467
|
+
if (!schema.dictionary || typeof schema.dictionary !== 'object') {
|
|
1468
|
+
schema.dictionary = { dictionaryList: [], inputParams: [] }
|
|
1469
|
+
}
|
|
1470
|
+
if (!Array.isArray(schema.dictionary.inputParams)) schema.dictionary.inputParams = []
|
|
1471
|
+
const params = schema.dictionary.inputParams
|
|
1472
|
+
const root = this.rt.rootScope
|
|
1473
|
+
const mocks = readMocks(schema) || {}
|
|
1474
|
+
const value = cloneJson(parsed.value)
|
|
1475
|
+
if (dlg.index == null) {
|
|
1476
|
+
params.push({ key, name, type: form.type, defaultValue: cloneJson(value), desc: String(form.desc || ''), rule: { type: '', options: {} }, children: [] })
|
|
1477
|
+
// 已有 mock 覆盖时画布继续用 mock,默认值只入 defaultValue 仓
|
|
1478
|
+
if (!hasMockValue(mocks, key)) root[key] = value
|
|
1479
|
+
} else {
|
|
1480
|
+
const param = params[dlg.index]
|
|
1481
|
+
if (!param) return
|
|
1482
|
+
const oldKey = param.key
|
|
1483
|
+
param.key = key
|
|
1484
|
+
param.name = name
|
|
1485
|
+
param.type = form.type
|
|
1486
|
+
param.desc = String(form.desc || '')
|
|
1487
|
+
param.defaultValue = cloneJson(value)
|
|
1488
|
+
if (oldKey !== key) {
|
|
1489
|
+
// 改名时 mock 覆盖跟随迁移,避免旧 key 的 mock 变成孤儿 / 新 key 丢覆盖
|
|
1490
|
+
if (Object.prototype.hasOwnProperty.call(mocks, oldKey)) {
|
|
1491
|
+
mocks[key] = mocks[oldKey]
|
|
1492
|
+
delete mocks[oldKey]
|
|
1493
|
+
}
|
|
1494
|
+
if (root && Object.prototype.hasOwnProperty.call(root, oldKey)) delete root[oldKey]
|
|
1495
|
+
}
|
|
1496
|
+
if (!hasMockValue(mocks, key)) root[key] = value
|
|
1497
|
+
}
|
|
1498
|
+
this.varDlg = null
|
|
1499
|
+
this.onCommit()
|
|
1500
|
+
},
|
|
1501
|
+
askDeleteVar(index) {
|
|
1502
|
+
this.delVarTarget = index
|
|
1503
|
+
},
|
|
1504
|
+
/** 删除:inputParams 移除 + rootScope 清键,再记一笔历史 */
|
|
1505
|
+
/**
|
|
1506
|
+
* 扫描变量引用面:遍历 schema(跳过 dictionary 两仓与 compiled 回声产物),
|
|
1507
|
+
* 按两种引用形态匹配——表达式里的 `$key`、事件脚本里的 `state.key` / `state['key']`;
|
|
1508
|
+
* 分类计数:绑定表达式(itemValue 为字符串)/ 事件脚本(code 键)/ 其它文本。
|
|
1509
|
+
* 仅供删除确认分级(被引用=danger / 未被引用=warn),不做写操作。
|
|
1510
|
+
*/
|
|
1511
|
+
scanVarRefs(key) {
|
|
1512
|
+
const refs = { binding: 0, event: 0, other: 0 }
|
|
1513
|
+
const schema = this.schema
|
|
1514
|
+
if (!key || !schema || typeof schema !== 'object') return refs
|
|
1515
|
+
const esc = String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1516
|
+
const boundary = '(?![A-Za-z0-9_$])'
|
|
1517
|
+
const dollarRe = new RegExp('\\$' + esc + boundary)
|
|
1518
|
+
const stateRe = new RegExp('state\\s*(?:\\.\\s*' + esc + boundary + '|\\[\\s*[\'"]' + esc + '[\'"]\\s*\\])')
|
|
1519
|
+
const hit = (s) => dollarRe.test(s) || stateRe.test(s)
|
|
1520
|
+
const walk = (v) => {
|
|
1521
|
+
if (v == null) return
|
|
1522
|
+
if (Array.isArray(v)) {
|
|
1523
|
+
for (const item of v) walk(item)
|
|
1524
|
+
return
|
|
1525
|
+
}
|
|
1526
|
+
if (typeof v !== 'object') return
|
|
1527
|
+
for (const [k, val] of Object.entries(v)) {
|
|
1528
|
+
if (k === 'dictionary' || k === 'compiled') continue
|
|
1529
|
+
if (typeof val === 'string') {
|
|
1530
|
+
if (!hit(val)) continue
|
|
1531
|
+
if (k === 'itemValue') refs.binding += 1
|
|
1532
|
+
else if (k === 'code') refs.event += 1
|
|
1533
|
+
else refs.other += 1
|
|
1534
|
+
} else {
|
|
1535
|
+
walk(val)
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
walk(schema)
|
|
1540
|
+
return refs
|
|
1541
|
+
},
|
|
1542
|
+
confirmDeleteVar() {
|
|
1543
|
+
const param = this.delVarTarget == null ? null : this.inputParams[this.delVarTarget]
|
|
1544
|
+
this.delVarTarget = null
|
|
1545
|
+
if (!param || !this.schema || !this.schema.dictionary) return
|
|
1546
|
+
const params = this.schema.dictionary.inputParams || []
|
|
1547
|
+
const idx = params.indexOf(param)
|
|
1548
|
+
if (idx >= 0) params.splice(idx, 1)
|
|
1549
|
+
// mock 覆盖一并清除,避免留下孤儿 key 与误报的 m 徽标
|
|
1550
|
+
const mocks = readMocks(this.schema) || {}
|
|
1551
|
+
if (Object.prototype.hasOwnProperty.call(mocks, param.key)) delete mocks[param.key]
|
|
1552
|
+
const root = this.rt.rootScope
|
|
1553
|
+
if (root && Object.prototype.hasOwnProperty.call(root, param.key)) delete root[param.key]
|
|
1554
|
+
this.onCommit()
|
|
1555
|
+
},
|
|
1556
|
+
/** 从 mock 二级视图回到变量列表(编辑器随视图卸载,watcher 负责销毁 CodeJar) */
|
|
1557
|
+
leaveMock() {
|
|
1558
|
+
this.varsMode = 'list'
|
|
1559
|
+
},
|
|
1560
|
+
/** 以当前 dictionary.mocks 播种 mock 编辑器文本(进入视图 / schema 被替换时共用) */
|
|
1561
|
+
seedRawFromMocks() {
|
|
1562
|
+
this.rawError = ''
|
|
1563
|
+
try {
|
|
1564
|
+
// mock 编辑器只编辑覆盖仓 dictionary.mocks(默认值在变量弹窗里改);空表即 {}
|
|
1565
|
+
this.rawText = JSON.stringify(this.mocks, null, 2)
|
|
1566
|
+
} catch (e) {
|
|
1567
|
+
this.rawText = ''
|
|
1568
|
+
this.rawError = '无法序列化当前数据:' + (e && e.message)
|
|
1569
|
+
return
|
|
1570
|
+
}
|
|
1571
|
+
if (this._jar) this._jar.updateCode(this.rawText)
|
|
1572
|
+
},
|
|
927
1573
|
/** 双击结构树节点:弹出属性面板 */
|
|
928
1574
|
onInspect(comp) {
|
|
929
1575
|
this.inspectingComp = comp
|