cardbuddy 0.9.1 → 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-B9MOriNv.js → index-D1iIJ4sl.js} +14 -13
- 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 +136 -46
- package/dist/vendored/components/ComponentPalette.vue +1 -1
- package/dist/vendored/components/DebugPanel.vue +735 -90
- 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 +618 -210
- 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-CwD14gm3.css +0 -1
- 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>
|
|
@@ -250,7 +296,7 @@
|
|
|
250
296
|
</button>
|
|
251
297
|
<!-- 本地开发服务连接状态:连接图标 + 状态文案,颜色与右上角「连接」按钮实时同步(复用 devSvcClass / devSvcTitle / devSvcLabel)。
|
|
252
298
|
可点击:弹出 / 收起底部「服务日志」面板(实时镜像所连 cardbuddy 终端输出)。
|
|
253
|
-
|
|
299
|
+
仅重连 / 断开 / 未配置等_warn、danger 态着语义色,连接成功用状态栏默认色;不设选中态,仅 hover 提示可点。 -->
|
|
254
300
|
<button
|
|
255
301
|
class="statusbar__item statusbar__conn"
|
|
256
302
|
:class="devSvcClass"
|
|
@@ -448,6 +494,85 @@
|
|
|
448
494
|
</div>
|
|
449
495
|
</section>
|
|
450
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
|
+
/>
|
|
451
576
|
</aside>
|
|
452
577
|
</template>
|
|
453
578
|
|
|
@@ -457,6 +582,10 @@ import StructureTree from './StructureTree.vue'
|
|
|
457
582
|
import NodeInspector from './NodeInspector.vue'
|
|
458
583
|
import EventEditor from './EventEditor.vue'
|
|
459
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'
|
|
460
589
|
import { COMPONENT_META } from '@/runtime/toPreview.js'
|
|
461
590
|
import { copyText } from '@/runtime/clipboard.js'
|
|
462
591
|
// 结构树「粘贴」按钮的 disabled 态:剪贴板是否有可粘贴节点
|
|
@@ -467,12 +596,141 @@ import { CodeJar } from 'codejar'
|
|
|
467
596
|
import Prism from 'prismjs'
|
|
468
597
|
import 'prismjs/components/prism-json'
|
|
469
598
|
import { apiFetch, subscribeServiceLog, fetchServiceLogs } from '@/api.js'
|
|
599
|
+
// mock 覆盖仓与派生真相:与 App / 服务端共用同一实现,避免判据漂移
|
|
600
|
+
import { buildDataFromSchema, readMocks, hasMockValue } from '../../server/core/buildDataFromSchema.js'
|
|
470
601
|
// 服务日志面板:终端 ANSI(SGR)→ HTML,1:1 还原 cardbuddy 的 chalk 配色
|
|
471
602
|
import { ansiToHtml, stripAnsi } from '@/ansi.js'
|
|
472
603
|
|
|
473
604
|
/** 服务日志在前端保留的最大行数(与服务端环形缓冲同量级,超出裁剪最旧的)。 */
|
|
474
605
|
const SVC_MAX_LINES = 800
|
|
475
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
|
+
|
|
476
734
|
/**
|
|
477
735
|
* 行级别判定:只认 logger.js 的行首标签(HH:MM:SS.mmm INFO/OK/WARN/ERR),
|
|
478
736
|
* 其余输出(console.* / 第三方 / vite)统一 info,避免把卡片内容里的「错误」字样误判。
|
|
@@ -515,9 +773,14 @@ function deepEqual(a, b) {
|
|
|
515
773
|
return true
|
|
516
774
|
}
|
|
517
775
|
|
|
776
|
+
/** 深拷贝 JSON 安全值;变量默认值写入 schema / rootScope 前隔断引用 */
|
|
777
|
+
function cloneJson(v) {
|
|
778
|
+
return v === undefined ? undefined : JSON.parse(JSON.stringify(v))
|
|
779
|
+
}
|
|
780
|
+
|
|
518
781
|
export default {
|
|
519
782
|
name: 'DebugPanel',
|
|
520
|
-
components: { JsonTree, StructureTree, NodeInspector, EventEditor, LogValue },
|
|
783
|
+
components: { JsonTree, StructureTree, NodeInspector, EventEditor, LogValue, Dropdown, ConfirmDialog, NumberStepper, SetSegment },
|
|
521
784
|
props: {
|
|
522
785
|
rt: { type: Object, required: true },
|
|
523
786
|
preview: { type: Object, default: null },
|
|
@@ -532,9 +795,9 @@ export default {
|
|
|
532
795
|
// 拖拽移动成功令牌:App 在节点拖拽移动成功后自增,透传给结构树,
|
|
533
796
|
// 触发「选中被拖节点 + 仅展开其祖先链、其余全部折叠」。
|
|
534
797
|
moveCollapseToken: { type: Number, default: 0 },
|
|
535
|
-
// 当前激活的调试面板 tab
|
|
798
|
+
// 当前激活的调试面板 tab(变量/组件结构/卡片来源),由 App 统一管理,
|
|
536
799
|
// 便于画布 Ctrl/⌘ 点击定位在「组件结构」tab 激活时才生效。
|
|
537
|
-
activeTab: { type: String, default: '
|
|
800
|
+
activeTab: { type: String, default: 'vars' },
|
|
538
801
|
// 本地开发服务连接状态:复用 App 中顶栏「连接」按钮同款状态/配色(devSvcClass/devSvcTitle/devSvcLabel),
|
|
539
802
|
// 供底部状态栏展示「连接图标 + 状态文案」,与右上角按钮实时同步(颜色一致、状态一致)。
|
|
540
803
|
devSvcClass: { type: String, default: 'app__btn--warn' },
|
|
@@ -550,6 +813,9 @@ export default {
|
|
|
550
813
|
return {
|
|
551
814
|
copied: false,
|
|
552
815
|
copyTimer: null,
|
|
816
|
+
// 刚复制过默认值的变量行下标(图标变 ✓ 反馈),null=无
|
|
817
|
+
copiedVar: null,
|
|
818
|
+
varCopyTimer: null,
|
|
553
819
|
// 底部面板模式:''(收起) | 'binding'(变量绑定错误) | 'event'(事件日志 / 执行错误)
|
|
554
820
|
errorPanel: '',
|
|
555
821
|
// 事件面板内部 tab:'log'(主动日志) | 'err'(执行错误),打开面板时按是否有错误自动定位
|
|
@@ -581,20 +847,78 @@ export default {
|
|
|
581
847
|
// 事件编辑弹框目标节点(点击「事件」徽标时单独弹出,与属性面板解耦)
|
|
582
848
|
editingEventComp: null,
|
|
583
849
|
eventEditorKey: 0,
|
|
584
|
-
// Raw
|
|
585
|
-
rawMode: false,
|
|
850
|
+
// Raw JSON 编辑状态(mock 二级视图的编辑器内容与解析错误)
|
|
586
851
|
rawText: '',
|
|
587
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
|
+
],
|
|
588
873
|
// 导入来源元数据(Phase E):GET /__import-meta/:id,无侧车时为 null
|
|
589
874
|
importMeta: null,
|
|
590
875
|
tabs: [
|
|
591
|
-
{ key: '
|
|
876
|
+
{ key: 'vars', label: '变量' },
|
|
592
877
|
{ key: 'structure', label: '组件结构' },
|
|
593
878
|
{ key: 'meta', label: '卡片来源' },
|
|
594
879
|
],
|
|
595
880
|
}
|
|
596
881
|
},
|
|
597
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
|
+
},
|
|
598
922
|
/** 服务日志空态标题:按连接态区分「没配 / 没连上 / 连上了但还没输出」 */
|
|
599
923
|
svcEmptyText() {
|
|
600
924
|
if (this.devSvcConn === 'unconfigured') return '未配置本地开发服务'
|
|
@@ -692,9 +1016,13 @@ export default {
|
|
|
692
1016
|
cardId: {
|
|
693
1017
|
immediate: true,
|
|
694
1018
|
handler() {
|
|
695
|
-
this.rawMode = false
|
|
696
1019
|
this.rawError = ''
|
|
697
1020
|
this.destroyJar()
|
|
1021
|
+
// 换卡回到变量列表视图,并关掉可能开着的变量弹窗 / 删除确认
|
|
1022
|
+
this.varsMode = 'list'
|
|
1023
|
+
this.varDlg = null
|
|
1024
|
+
this.varDlgError = ''
|
|
1025
|
+
this.delVarTarget = null
|
|
698
1026
|
this.inspectingComp = null
|
|
699
1027
|
this.editingEventComp = null
|
|
700
1028
|
this.importMeta = null
|
|
@@ -711,12 +1039,49 @@ export default {
|
|
|
711
1039
|
// 切到「来源」tab 时按需拉取导入来源元数据(懒加载,消除自动 404)。
|
|
712
1040
|
activeTab(n) {
|
|
713
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
|
+
}
|
|
714
1076
|
},
|
|
715
1077
|
// schema 引用变化(撤销/重做、刷新重载会整体替换对象;就地编辑只 mutate 不换引用)
|
|
716
1078
|
// 时关闭已打开的节点属性/事件面板,避免面板持有已被替换的旧预览节点引用。
|
|
717
1079
|
schema() {
|
|
718
1080
|
this.inspectingComp = null
|
|
719
1081
|
this.editingEventComp = null
|
|
1082
|
+
// schema 整体被替换(重置 / 撤销重做 / 外部重载):mock 视图开着时同步重播种,
|
|
1083
|
+
// 否则 CodeJar 会停留在「应用」前的旧文本,重置按钮看起来像没生效
|
|
1084
|
+
if (this.varsMode === 'mock') this.seedRawFromMocks()
|
|
720
1085
|
},
|
|
721
1086
|
},
|
|
722
1087
|
mounted() {
|
|
@@ -729,6 +1094,7 @@ export default {
|
|
|
729
1094
|
beforeUnmount() {
|
|
730
1095
|
// DebugPanel 一般常驻,但保险起见卸载时销毁 CodeJar、清理可能残留的拖拽监听
|
|
731
1096
|
this.destroyJar()
|
|
1097
|
+
this.destroyDlgJar()
|
|
732
1098
|
this.cancelResize()
|
|
733
1099
|
this.stopSvcLog()
|
|
734
1100
|
},
|
|
@@ -823,18 +1189,6 @@ export default {
|
|
|
823
1189
|
/* localStorage 不可用时忽略 */
|
|
824
1190
|
}
|
|
825
1191
|
},
|
|
826
|
-
/** 进入 Raw 模式:把当前 rootScope 序列化为可编辑文本,并用 CodeJar 托管高亮 */
|
|
827
|
-
enterRaw() {
|
|
828
|
-
this.rawError = ''
|
|
829
|
-
try {
|
|
830
|
-
this.rawText = JSON.stringify(this.rt.rootScope, (k, v) => (k === '_schemaNode' ? undefined : v), 2)
|
|
831
|
-
} catch (e) {
|
|
832
|
-
this.rawText = ''
|
|
833
|
-
this.rawError = '无法序列化当前数据:' + (e && e.message)
|
|
834
|
-
}
|
|
835
|
-
this.rawMode = true
|
|
836
|
-
this.$nextTick(() => this.initJar())
|
|
837
|
-
},
|
|
838
1192
|
/** 用 CodeJar 初始化原始 JSON 编辑器:contenteditable + Prism(json) 高亮 */
|
|
839
1193
|
initJar() {
|
|
840
1194
|
const el = this.$refs.rawEditor
|
|
@@ -855,6 +1209,9 @@ export default {
|
|
|
855
1209
|
this.rawText = code
|
|
856
1210
|
if (this.rawError) this.rawError = '' // 用户再次输入即清除旧错误
|
|
857
1211
|
})
|
|
1212
|
+
// 开发期暴露 Raw 编辑器句柄供 E2E 程序化写入(与 window.__rt 同源约定);生产构建不暴露。
|
|
1213
|
+
// 合成 input 事件驱动不了 CodeJar,E2E 经 updateCode 走真实 onUpdate → 「应用」路径。
|
|
1214
|
+
if (import.meta.env.DEV) window.__rawEditor = this._jar
|
|
858
1215
|
this.$nextTick(() => el.focus())
|
|
859
1216
|
},
|
|
860
1217
|
/** 销毁 CodeJar 实例(退出/保存/卸载/切卡时调用) */
|
|
@@ -863,46 +1220,94 @@ export default {
|
|
|
863
1220
|
this._jar.destroy()
|
|
864
1221
|
}
|
|
865
1222
|
this._jar = null
|
|
1223
|
+
if (typeof window !== 'undefined' && window.__rawEditor) window.__rawEditor = null
|
|
866
1224
|
},
|
|
867
|
-
/**
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
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
|
|
872
1251
|
},
|
|
873
|
-
/**
|
|
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 视图继续编辑 */
|
|
874
1279
|
saveRaw() {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1280
|
+
// 先容错清理(注释 / 尾逗号)再严格校验:报错判定基于清理后的文本
|
|
1281
|
+
const parsed = parseJsonLenient(this.rawText)
|
|
1282
|
+
if (parsed.error) {
|
|
1283
|
+
this.rawError = 'JSON 格式错误:' + parsed.error
|
|
878
1284
|
return
|
|
879
1285
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
parsed = JSON.parse(text)
|
|
883
|
-
} catch (e) {
|
|
884
|
-
this.rawError = 'JSON 格式错误:' + e.message
|
|
1286
|
+
if (parsed.value === null || typeof parsed.value !== 'object' || Array.isArray(parsed.value)) {
|
|
1287
|
+
this.rawError = '变量 mock 必须是 JSON 对象({ ... })'
|
|
885
1288
|
return
|
|
886
1289
|
}
|
|
887
|
-
|
|
888
|
-
|
|
1290
|
+
const schema = this.schema
|
|
1291
|
+
if (!schema) {
|
|
1292
|
+
this.rawError = '当前未载入卡片'
|
|
889
1293
|
return
|
|
890
1294
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
+
// 落定一笔撤销记录(与节点属性「应用」同路径)
|
|
894
1305
|
this.$nextTick(() => this.onCommit())
|
|
895
|
-
this.destroyJar()
|
|
896
|
-
this.rawMode = false
|
|
897
1306
|
this.rawError = ''
|
|
898
1307
|
},
|
|
899
1308
|
async copyScope() {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
text = JSON.stringify(this.rt.rootScope, null, 2)
|
|
903
|
-
} catch (e) {
|
|
904
|
-
text = String(this.rt.rootScope)
|
|
905
|
-
}
|
|
1309
|
+
// 复制当前 mock 文档(编辑器所见即所得)
|
|
1310
|
+
const text = this.rawText || ''
|
|
906
1311
|
const ok = await copyText(text)
|
|
907
1312
|
if (ok) {
|
|
908
1313
|
this.copied = true
|
|
@@ -925,6 +1330,246 @@ export default {
|
|
|
925
1330
|
onCommit() {
|
|
926
1331
|
this.$emit('schema-commit')
|
|
927
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
|
+
},
|
|
928
1573
|
/** 双击结构树节点:弹出属性面板 */
|
|
929
1574
|
onInspect(comp) {
|
|
930
1575
|
this.inspectingComp = comp
|