cardbuddy 0.9.1 → 0.11.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.
Files changed (64) hide show
  1. package/README.md +21 -6
  2. package/dist/cli.js +17 -6
  3. package/dist/config.js +20 -2
  4. package/dist/portGuard.js +293 -0
  5. package/dist/preview-editor/assets/index-B4VZgNSK.css +1 -0
  6. package/dist/preview-editor/assets/index-jl4YJzKv.js +180 -0
  7. package/dist/preview-editor/index.html +2 -2
  8. package/dist/server-core/_vendor/finmall-env.mjs +4 -4
  9. package/dist/server-core/ai-chat.js +3710 -0
  10. package/dist/server-core/assetProxy.js +3 -22
  11. package/dist/server-core/buildDataFromSchema.js +36 -4
  12. package/dist/server-core/cardFiles.js +95 -2
  13. package/dist/server-core/cardImage.js +14 -0
  14. package/dist/server-core/cardListCache.js +0 -17
  15. package/dist/server-core/cardSave.js +2 -0
  16. package/dist/server-core/cardValidate.js +3 -0
  17. package/dist/server-core/index.js +6 -5
  18. package/dist/server-core/logStream.js +1 -6
  19. package/dist/server-core/sliceUpload.js +8 -1
  20. package/dist/standalone.js +24 -8
  21. package/dist/vendored/App.vue +405 -125
  22. package/dist/vendored/api.js +4 -4
  23. package/dist/vendored/assets/svgIcons/icons.svg +28 -0
  24. package/dist/vendored/components/AiSettings.vue +392 -0
  25. package/dist/vendored/components/CardGallery.vue +51 -5
  26. package/dist/vendored/components/ChatComposer.vue +413 -0
  27. package/dist/vendored/components/ChatMessage.vue +383 -0
  28. package/dist/vendored/components/ChatPanel.vue +937 -0
  29. package/dist/vendored/components/ComponentPalette.vue +1 -1
  30. package/dist/vendored/components/DebugPanel.vue +732 -90
  31. package/dist/vendored/components/Dropdown.vue +4 -2
  32. package/dist/vendored/components/MarkdownBody.vue +64 -0
  33. package/dist/vendored/components/NewCardDialog.vue +132 -0
  34. package/dist/vendored/components/NumberStepper.vue +125 -0
  35. package/dist/vendored/components/ProgressSlider.vue +9 -1
  36. package/dist/vendored/components/RemotePreview.vue +1 -1
  37. package/dist/vendored/components/StructureTree.vue +2 -2
  38. package/dist/vendored/main.js +1 -0
  39. package/dist/vendored/runtime/adapter.js +1 -1
  40. package/dist/vendored/runtime/ai-api.js +165 -0
  41. package/dist/vendored/runtime/clipboard-node.js +0 -5
  42. package/dist/vendored/runtime/markdown.js +140 -0
  43. package/dist/vendored/skills/.manifest.json +1101 -0
  44. package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +249 -0
  45. package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +678 -0
  46. package/dist/vendored/skills/dsl-card-generator/scripts/derive_data_json.py +81 -0
  47. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +105 -0
  48. package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +624 -0
  49. package/dist/vendored/skills/finmall-card-sync/scripts/api_batch_sync.mjs +900 -0
  50. package/dist/vendored/skills/finmall-card-sync/scripts/slice_upload.mjs +699 -0
  51. package/dist/vendored/skills/shared/finmall-config.mjs +124 -0
  52. package/dist/vendored/styles/ai-chat.css +1850 -0
  53. package/dist/vendored/styles/app.css +725 -213
  54. package/dist/vendored/styles/card-gallery.css +48 -10
  55. package/dist/vendored/styles/card-select.css +13 -6
  56. package/dist/vendored/styles/dropdown.css +1 -2
  57. package/dist/vendored/styles/json-tree.css +3 -3
  58. package/dist/vendored/styles/node-inspector.css +13 -289
  59. package/dist/vendored/styles/setters.css +48 -49
  60. package/dist/vendored/styles/structure-tree.css +6 -11
  61. package/package.json +1 -1
  62. package/dist/preview-editor/assets/index-B9MOriNv.js +0 -80
  63. package/dist/preview-editor/assets/index-CwD14gm3.css +0 -1
  64. package/dist/vendored/assets/logo.png +0 -0
@@ -1,7 +1,8 @@
1
1
  <!--
2
2
  调试面板:三个标签页。
3
- - 数据绑定:当前 rootScope 的实时 JSON(初始数据 / 落盘 / 保存高亮比对源)。
4
- 本地调试的所有交互只写「运行态」,不会改这里,故调试中此 tab 保持不变、保存不亮。
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 === 'scope'">
34
- <div class="debug__scope-wrap" :class="{ 'debug__scope-wrap--raw': rawMode }">
35
- <!-- Raw 模式:直接编辑原始 JSON(CodeJar + Prism 语法高亮) -->
36
- <template v-if="rawMode">
37
- <div class="debug__scope-raw">
38
- <div
39
- ref="rawEditor"
40
- class="debug__raw-editor"
41
- spellcheck="false"
42
- ></div>
43
- <p v-if="rawError" class="debug__raw-err">{{ rawError }}</p>
44
- </div>
45
- <div class="debug__scope-foot">
46
- <button class="debug__copy debug__copy--primary" type="button" @click="saveRaw">
47
- <svg class="debug__copy-icn" viewBox="0 0 16 16" aria-hidden="true"><use href="#icon-check" /></svg>应用
48
- </button>
49
- <button class="debug__copy" type="button" @click="exitRaw">取消</button>
50
- </div>
51
- </template>
52
- <!-- 默认模式:JSON 树 -->
53
- <template v-else>
54
- <div class="debug__scope-tree">
55
- <JsonTree :data="rt.rootScope" :rt="rt" :editable="true" :schema="schema" />
56
- </div>
57
- <div class="debug__scope-foot">
58
- <span v-if="copied" class="debug__copied">已复制 ✓</span>
59
- <button class="debug__copy" type="button" title="切换为 Raw JSON 编辑" @click="enterRaw">
60
- <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="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" /></svg>Raw
61
- </button>
62
- <button class="debug__copy" type="button" @click="copyScope">
63
- <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>复制
64
- </button>
65
- <span class="debug__sep"></span>
66
- <button class="debug__copy debug__copy--primary" type="button" @click="onCommit">
67
- <svg class="debug__copy-icn" viewBox="0 0 16 16" aria-hidden="true"><use href="#icon-check" /></svg>应用
68
- </button>
69
- <button class="debug__copy debug__copy--warn" type="button" @click="$emit('reset-import')" title="丢弃内存编辑,从磁盘重新载入(导入优先于编辑)">
70
- <svg class="debug__copy-icn" viewBox="0 0 16 16" aria-hidden="true"><use href="#icon-reset" /></svg>重置
71
- </button>
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
+ <!-- 复制变量名:复制当前变量 key(绑定以 $key 引用),无变量名时禁用 -->
54
+ <button
55
+ class="vars__op"
56
+ type="button"
57
+ :disabled="!p.key"
58
+ :title="copiedVar === i ? '已复制 ✓' : p.key ? '复制变量名' : '暂无变量名'"
59
+ @click="copyVarName(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
- </template>
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">导入优先于编辑:点「数据绑定」tab 的「重置为导入镜像」可丢弃内存编辑、从磁盘重新载入最新导入。</p>
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
- 语义色本身已表达连接态,故不设选中态,仅 hover 提示可点。 -->
299
+ 仅重连 / 断开 / 未配置等_warn、danger 态着语义色,连接成功用状态栏默认色;不设选中态,仅 hover 提示可点。 -->
254
300
  <button
255
301
  class="statusbar__item statusbar__conn"
256
302
  :class="devSvcClass"
@@ -448,6 +494,101 @@
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
+ <!-- 类型下拉:触发器与选项都走「中文名 + 彩色类型标签」,标签直接复用变量列表的
522
+ .vars__type / .vars__type--<type>(同一套 --type-* 语义色),两处视觉完全一致 -->
523
+ <Dropdown class="vars-dlg__ctrl" block size="sm" :value="varDlg.form.type" :options="varTypeOptions" @change="onVarTypeChange">
524
+ <template #current="{ opt, text }">
525
+ <span v-if="opt" class="vars-type-pick">
526
+ <span class="vars-type-pick__name">{{ opt.value }}</span>
527
+ <span class="vars__type" :class="'vars__type--' + opt.key">{{ opt.key }}</span>
528
+ </span>
529
+ <template v-else>{{ text }}</template>
530
+ </template>
531
+ <template #option="{ opt }">
532
+ <span class="vars-type-pick">
533
+ <span class="vars-type-pick__name">{{ opt.value }}</span>
534
+ <span class="vars__type" :class="'vars__type--' + opt.key">{{ opt.key }}</span>
535
+ </span>
536
+ </template>
537
+ </Dropdown>
538
+ </div>
539
+ <!-- 默认值按类型分四种控件:数组 / 对象 = CodeJar 代码编辑区、字符串 = 多行文本域、
540
+ 数字 = 带 −/+ 的 NumberStepper、布尔 = 复用 SetSegment 的 true / false 分段单选。四者统一以文本承载
541
+ (varDlg.form.defaultValue),确定时仍由 parseDefault 按类型解析。 -->
542
+ <div class="ni-field-row ni-field-row--top">
543
+ <label class="ni-field-label">默认值</label>
544
+ <div v-if="isComplexType(varDlg.form.type)" ref="dlgCode" class="vars-dlg__code" spellcheck="false"></div>
545
+ <textarea
546
+ v-else-if="varDlg.form.type === 'string'"
547
+ class="ni-input vars-dlg__str"
548
+ v-model="varDlg.form.defaultValue"
549
+ :placeholder="scalarPlaceholder('string')"
550
+ ></textarea>
551
+ <NumberStepper
552
+ v-else-if="varDlg.form.type === 'number'"
553
+ class="vars-dlg__ctrl"
554
+ :value="varDlg.form.defaultValue"
555
+ :placeholder="scalarPlaceholder('number')"
556
+ @input="varDlg.form.defaultValue = $event"
557
+ />
558
+ <SetSegment
559
+ v-else
560
+ class="vars-dlg__ctrl"
561
+ :value="varDlg.form.defaultValue"
562
+ :options="{ iParamList: varBoolOptions }"
563
+ @input="varDlg.form.defaultValue = $event"
564
+ />
565
+ </div>
566
+ <div class="ni-field-row ni-field-row--top">
567
+ <label class="ni-field-label">描述</label>
568
+ <textarea class="ni-input vars-dlg__desc" v-model="varDlg.form.desc" placeholder="可选"></textarea>
569
+ </div>
570
+ <p v-if="varDlgError" class="vars-dlg__err">{{ varDlgError }}</p>
571
+ </div>
572
+ </div>
573
+ <div class="ni-foot">
574
+ <span class="ni-tip">默认值入 defaultValue;设计期覆盖见「配置变量 mock」</span>
575
+ <div class="ni-foot-btns">
576
+ <button class="ni-btn ni-btn--ghost" type="button" @click="varDlg = null">取消</button>
577
+ <button class="ni-btn" type="button" @click="confirmVarDlg">确定</button>
578
+ </div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+ </transition>
583
+ <ConfirmDialog
584
+ :visible="delVarTarget != null"
585
+ title="删除变量"
586
+ :message="delVarMessage"
587
+ :severity="delVarSeverity"
588
+ :confirm-text="delVarConfirmText"
589
+ @confirm="confirmDeleteVar"
590
+ @cancel="delVarTarget = null"
591
+ />
451
592
  </aside>
452
593
  </template>
453
594
 
@@ -457,6 +598,10 @@ import StructureTree from './StructureTree.vue'
457
598
  import NodeInspector from './NodeInspector.vue'
458
599
  import EventEditor from './EventEditor.vue'
459
600
  import LogValue from './LogValue.vue'
601
+ import Dropdown from './Dropdown.vue'
602
+ import ConfirmDialog from './ConfirmDialog.vue'
603
+ import NumberStepper from './NumberStepper.vue'
604
+ import SetSegment from './setters/SetSegment.vue'
460
605
  import { COMPONENT_META } from '@/runtime/toPreview.js'
461
606
  import { copyText } from '@/runtime/clipboard.js'
462
607
  // 结构树「粘贴」按钮的 disabled 态:剪贴板是否有可粘贴节点
@@ -467,12 +612,141 @@ import { CodeJar } from 'codejar'
467
612
  import Prism from 'prismjs'
468
613
  import 'prismjs/components/prism-json'
469
614
  import { apiFetch, subscribeServiceLog, fetchServiceLogs } from '@/api.js'
615
+ // mock 覆盖仓与派生真相:与 App / 服务端共用同一实现,避免判据漂移
616
+ import { buildDataFromSchema, readMocks, hasMockValue } from '../../server/core/buildDataFromSchema.js'
470
617
  // 服务日志面板:终端 ANSI(SGR)→ HTML,1:1 还原 cardbuddy 的 chalk 配色
471
618
  import { ansiToHtml, stripAnsi } from '@/ansi.js'
472
619
 
473
620
  /** 服务日志在前端保留的最大行数(与服务端环形缓冲同量级,超出裁剪最旧的)。 */
474
621
  const SVC_MAX_LINES = 800
475
622
 
623
+ /**
624
+ * 容错 JSON 解析(对象字面量友好):先做一遍「格式化容忍」清理,再交严格 JSON.parse ——
625
+ * 报错只针对对象结构本身非法(括号不配对 / 悬空冒号 / 未闭合字符串等),
626
+ * 手写习惯一律容忍并规范化:无引号键 / 单引号字符串 / 注释 / 尾逗号 /
627
+ * 分号当逗号 / 重复逗号 / undefined·NaN·Infinity 归 null。
628
+ * 字符串字面量原样搬运(含转义),不会误伤内容里的 // 、,} 等字符;
629
+ * 清理后的标准 JSON 由调用方(应用 / 确定)回灌编辑器完成「保存即格式化」。
630
+ * @param {string} text
631
+ * @returns {{ value: any } | { error: string }}
632
+ */
633
+ function parseJsonLenient(text) {
634
+ const src = String(text == null ? '' : text).replace(/^\uFEFF/, '')
635
+ let out = ''
636
+ let i = 0
637
+ const n = src.length
638
+ const lastNonWs = () => {
639
+ let k = out.length - 1
640
+ while (k >= 0 && /\s/.test(out[k])) k -= 1
641
+ return k
642
+ }
643
+ // 逗号去重入栈: leading 逗号(紧跟 [ { )与连续逗号都不输出
644
+ const pushComma = () => {
645
+ const k = lastNonWs()
646
+ if (k >= 0 && out[k] !== ',' && out[k] !== '[' && out[k] !== '{') out += ','
647
+ }
648
+ const skipWs = (from) => {
649
+ let k = from
650
+ while (k < n && /\s/.test(src[k])) k += 1
651
+ return k
652
+ }
653
+ while (i < n) {
654
+ const ch = src[i]
655
+ // 双引号字符串:原样搬运(含转义)
656
+ if (ch === '"') {
657
+ let j = i + 1
658
+ while (j < n) {
659
+ if (src[j] === '\\') { j += 2; continue }
660
+ if (src[j] === '"') { j += 1; break }
661
+ j += 1
662
+ }
663
+ out += src.slice(i, j)
664
+ i = j
665
+ continue
666
+ }
667
+ // 单引号字符串:转双引号(内部双引号转义、\' 还原为裸引号)
668
+ if (ch === '\'') {
669
+ let j = i + 1
670
+ let body = ''
671
+ while (j < n) {
672
+ const c = src[j]
673
+ if (c === '\\' && j + 1 < n) {
674
+ const nx = src[j + 1]
675
+ if (nx === '\'') { body += '\''; j += 2; continue }
676
+ if (nx === '"') { body += '\\\"'; j += 2; continue }
677
+ body += c + nx
678
+ j += 2
679
+ continue
680
+ }
681
+ if (c === '\'') { j += 1; break }
682
+ if (c === '"') { body += '\\\"'; j += 1; continue }
683
+ body += c
684
+ j += 1
685
+ }
686
+ out += '"' + body + '"'
687
+ i = j
688
+ continue
689
+ }
690
+ // 注释:// 与 /* */
691
+ if (ch === '/' && src[i + 1] === '/') {
692
+ while (i < n && src[i] !== '\n') i += 1
693
+ continue
694
+ }
695
+ if (ch === '/' && src[i + 1] === '*') {
696
+ const close = src.indexOf('*/', i + 2)
697
+ i = close === -1 ? n : close + 2
698
+ continue
699
+ }
700
+ // 分号视同逗号;逗号去重
701
+ if (ch === ';' || ch === ',') {
702
+ pushComma()
703
+ i += 1
704
+ continue
705
+ }
706
+ // 标识符:后跟冒号 → 键,补双引号;否则原样(true/false/null 等关键字)
707
+ if (/[A-Za-z_$]/.test(ch)) {
708
+ let j = i
709
+ while (j < n && /[A-Za-z0-9_$]/.test(src[j])) j += 1
710
+ const token = src.slice(i, j)
711
+ if (src[skipWs(j)] === ':') out += '"' + token + '"'
712
+ else if (token === 'undefined' || token === 'NaN' || token === 'Infinity') out += 'null'
713
+ else out += token
714
+ i = j
715
+ continue
716
+ }
717
+ // 数字(含负号 / 指数):后跟冒号 → 键,补双引号;-Infinity 归 null
718
+ if (ch === '-' && src.startsWith('Infinity', i + 1)) {
719
+ out += 'null'
720
+ i += 9
721
+ continue
722
+ }
723
+ if (/[0-9]/.test(ch) || (ch === '-' && /[0-9]/.test(src[i + 1] || ''))) {
724
+ let j = i
725
+ while (j < n && /[0-9eE+\-.]/.test(src[j])) j += 1
726
+ const token = src.slice(i, j)
727
+ if (src[skipWs(j)] === ':') out += '"' + token + '"'
728
+ else out += token
729
+ i = j
730
+ continue
731
+ }
732
+ // 收括号:抹掉所有尾逗号
733
+ if (ch === '}' || ch === ']') {
734
+ let k = lastNonWs()
735
+ while (k >= 0 && out[k] === ',') {
736
+ out = out.slice(0, k) + out.slice(k + 1)
737
+ k = lastNonWs()
738
+ }
739
+ }
740
+ out += ch
741
+ i += 1
742
+ }
743
+ try {
744
+ return { value: JSON.parse(out.trim() || '{}') }
745
+ } catch (e) {
746
+ return { error: e.message }
747
+ }
748
+ }
749
+
476
750
  /**
477
751
  * 行级别判定:只认 logger.js 的行首标签(HH:MM:SS.mmm INFO/OK/WARN/ERR),
478
752
  * 其余输出(console.* / 第三方 / vite)统一 info,避免把卡片内容里的「错误」字样误判。
@@ -515,9 +789,14 @@ function deepEqual(a, b) {
515
789
  return true
516
790
  }
517
791
 
792
+ /** 深拷贝 JSON 安全值;变量默认值写入 schema / rootScope 前隔断引用 */
793
+ function cloneJson(v) {
794
+ return v === undefined ? undefined : JSON.parse(JSON.stringify(v))
795
+ }
796
+
518
797
  export default {
519
798
  name: 'DebugPanel',
520
- components: { JsonTree, StructureTree, NodeInspector, EventEditor, LogValue },
799
+ components: { JsonTree, StructureTree, NodeInspector, EventEditor, LogValue, Dropdown, ConfirmDialog, NumberStepper, SetSegment },
521
800
  props: {
522
801
  rt: { type: Object, required: true },
523
802
  preview: { type: Object, default: null },
@@ -532,9 +811,9 @@ export default {
532
811
  // 拖拽移动成功令牌:App 在节点拖拽移动成功后自增,透传给结构树,
533
812
  // 触发「选中被拖节点 + 仅展开其祖先链、其余全部折叠」。
534
813
  moveCollapseToken: { type: Number, default: 0 },
535
- // 当前激活的调试面板 tab(绑定数据/组件结构/卡片来源),由 App 统一管理,
814
+ // 当前激活的调试面板 tab(变量/组件结构/卡片来源),由 App 统一管理,
536
815
  // 便于画布 Ctrl/⌘ 点击定位在「组件结构」tab 激活时才生效。
537
- activeTab: { type: String, default: 'scope' },
816
+ activeTab: { type: String, default: 'vars' },
538
817
  // 本地开发服务连接状态:复用 App 中顶栏「连接」按钮同款状态/配色(devSvcClass/devSvcTitle/devSvcLabel),
539
818
  // 供底部状态栏展示「连接图标 + 状态文案」,与右上角按钮实时同步(颜色一致、状态一致)。
540
819
  devSvcClass: { type: String, default: 'app__btn--warn' },
@@ -550,6 +829,9 @@ export default {
550
829
  return {
551
830
  copied: false,
552
831
  copyTimer: null,
832
+ // 刚复制过默认值的变量行下标(图标变 ✓ 反馈),null=无
833
+ copiedVar: null,
834
+ varCopyTimer: null,
553
835
  // 底部面板模式:''(收起) | 'binding'(变量绑定错误) | 'event'(事件日志 / 执行错误)
554
836
  errorPanel: '',
555
837
  // 事件面板内部 tab:'log'(主动日志) | 'err'(执行错误),打开面板时按是否有错误自动定位
@@ -581,20 +863,79 @@ export default {
581
863
  // 事件编辑弹框目标节点(点击「事件」徽标时单独弹出,与属性面板解耦)
582
864
  editingEventComp: null,
583
865
  eventEditorKey: 0,
584
- // Raw 编辑模式状态
585
- rawMode: false,
866
+ // Raw JSON 编辑状态(mock 二级视图的编辑器内容与解析错误)
586
867
  rawText: '',
587
868
  rawError: '',
869
+ // 变量 tab 二级视图:'list'(变量列表,管理 inputParams)| 'mock'(Raw JSON 编辑 mock 数据)
870
+ varsMode: 'list',
871
+ // 变量添加 / 编辑弹窗:null | { index: null|number, form: { key, name, type, defaultValue, desc } }
872
+ varDlg: null,
873
+ varDlgError: '',
874
+ // 待删除变量下标(ConfirmDialog 确认中)
875
+ delVarTarget: null,
876
+ // 类型选项:value 只留中文名,英文类型名由 Dropdown 的 current / option 插槽渲染成彩色标签
877
+ varTypeOptions: [
878
+ { key: 'string', value: '字符串' },
879
+ { key: 'number', value: '数字' },
880
+ { key: 'boolean', value: '布尔' },
881
+ { key: 'array', value: '数组' },
882
+ { key: 'object', value: '对象' },
883
+ ],
884
+ // 布尔默认值的分段单选项(交 SetSegment 渲染,即属性面板同款 .st-seg):
885
+ // 值仍以文本承载('true' / 'false'),与 parseDefault 的解析口径一致
886
+ varBoolOptions: [
887
+ { key: 'true', value: 'true' },
888
+ { key: 'false', value: 'false' },
889
+ ],
588
890
  // 导入来源元数据(Phase E):GET /__import-meta/:id,无侧车时为 null
589
891
  importMeta: null,
590
892
  tabs: [
591
- { key: 'scope', label: '绑定数据' },
893
+ { key: 'vars', label: '变量' },
592
894
  { key: 'structure', label: '组件结构' },
593
895
  { key: 'meta', label: '卡片来源' },
594
896
  ],
595
897
  }
596
898
  },
597
899
  computed: {
900
+ /** 设计期 mock 覆盖表:schema.dictionary.mocks(key → 值);m 徽标与 mock 编辑器的唯一数据源 */
901
+ mocks() {
902
+ return readMocks(this.schema) || {}
903
+ },
904
+ /** 变量定义列表:schema.dictionary.inputParams(变量 tab 的唯一数据源) */
905
+ inputParams() {
906
+ const dict = this.schema && this.schema.dictionary
907
+ return dict && Array.isArray(dict.inputParams) ? dict.inputParams : []
908
+ },
909
+ /** 删除确认文案:点名变量与影响面 */
910
+ /** 待删变量的引用统计(绑定表达式 / 事件脚本 / 其它文本),供删除确认分级 */
911
+ delVarRefs() {
912
+ const p = this.delVarTarget == null ? null : this.inputParams[this.delVarTarget]
913
+ return this.scanVarRefs(p && p.key)
914
+ },
915
+ /** 被引用 = danger(红·禁止图标,明确破坏面);未被引用 = warn(橙黄·三角,普通不可逆操作) */
916
+ delVarSeverity() {
917
+ const r = this.delVarRefs
918
+ return r.binding + r.event + r.other > 0 ? 'danger' : 'warn'
919
+ },
920
+ delVarConfirmText() {
921
+ return this.delVarSeverity === 'danger' ? '仍要删除' : '删除'
922
+ },
923
+ delVarMessage() {
924
+ const p = this.delVarTarget == null ? null : this.inputParams[this.delVarTarget]
925
+ if (!p) return ''
926
+ // 中文名与 key 相同时不重复展示括号
927
+ const nm = p.name && p.name !== p.key ? '(' + p.name + ')' : ''
928
+ const r = this.delVarRefs
929
+ const total = r.binding + r.event + r.other
930
+ if (total > 0) {
931
+ const parts = []
932
+ if (r.binding) parts.push(`绑定表达式 ${r.binding} 处`)
933
+ if (r.event) parts.push(`事件脚本 ${r.event} 处`)
934
+ if (r.other) parts.push(`其它文本 ${r.other} 处`)
935
+ return `变量「${p.key}」${nm}仍被${parts.join('、')}引用(共 ${total} 处)。删除后这些引用将取不到值,画布可能出现空白或执行报错,请先迁移再删除。`
936
+ }
937
+ return `变量「${p.key}」${nm}当前未被任何绑定表达式或事件脚本引用。删除后变量定义与 mock 值一并移除,不可恢复。`
938
+ },
598
939
  /** 服务日志空态标题:按连接态区分「没配 / 没连上 / 连上了但还没输出」 */
599
940
  svcEmptyText() {
600
941
  if (this.devSvcConn === 'unconfigured') return '未配置本地开发服务'
@@ -692,9 +1033,13 @@ export default {
692
1033
  cardId: {
693
1034
  immediate: true,
694
1035
  handler() {
695
- this.rawMode = false
696
1036
  this.rawError = ''
697
1037
  this.destroyJar()
1038
+ // 换卡回到变量列表视图,并关掉可能开着的变量弹窗 / 删除确认
1039
+ this.varsMode = 'list'
1040
+ this.varDlg = null
1041
+ this.varDlgError = ''
1042
+ this.delVarTarget = null
698
1043
  this.inspectingComp = null
699
1044
  this.editingEventComp = null
700
1045
  this.importMeta = null
@@ -711,12 +1056,49 @@ export default {
711
1056
  // 切到「来源」tab 时按需拉取导入来源元数据(懒加载,消除自动 404)。
712
1057
  activeTab(n) {
713
1058
  if (n === 'meta') this.fetchImportMeta()
1059
+ // 离开变量 tab:mock 编辑器随视图卸载,销毁 CodeJar 防泄漏;回到列表视图
1060
+ if (n !== 'vars') {
1061
+ this.varsMode = 'list'
1062
+ this.rawError = ''
1063
+ this.destroyJar()
1064
+ this.varDlg = null
1065
+ }
1066
+ },
1067
+ // 变量弹窗开合:打开且为数组 / 对象类型时挂载 CodeJar 代码编辑区;关闭即销毁
1068
+ varDlg(n) {
1069
+ if (!n) {
1070
+ this.destroyDlgJar()
1071
+ return
1072
+ }
1073
+ this.varDlgError = ''
1074
+ if (this.isComplexType(n.form.type)) this.$nextTick(() => this.initDlgJar())
1075
+ },
1076
+ // 类型切换:四种默认值控件互换,旧文本按新类型规整(能沿用则沿用,不能则给该类型的空态);
1077
+ // 容器类型还要等 DOM 换出后重新挂 CodeJar
1078
+ 'varDlg.form.type'(n, o) {
1079
+ if (!this.varDlg || n === o) return
1080
+ this.varDlg.form.defaultValue = this.normalizeDefaultText(n, this.varDlg.form.defaultValue)
1081
+ if (this.isComplexType(n)) this.$nextTick(() => this.initDlgJar())
1082
+ else this.destroyDlgJar()
1083
+ },
1084
+ // 进入 mock 二级视图:序列化 rootScope 交 CodeJar 托管;退出即销毁实例
1085
+ varsMode(n) {
1086
+ if (n === 'mock') {
1087
+ this.seedRawFromMocks()
1088
+ this.$nextTick(() => this.initJar())
1089
+ } else {
1090
+ this.rawError = ''
1091
+ this.destroyJar()
1092
+ }
714
1093
  },
715
1094
  // schema 引用变化(撤销/重做、刷新重载会整体替换对象;就地编辑只 mutate 不换引用)
716
1095
  // 时关闭已打开的节点属性/事件面板,避免面板持有已被替换的旧预览节点引用。
717
1096
  schema() {
718
1097
  this.inspectingComp = null
719
1098
  this.editingEventComp = null
1099
+ // schema 整体被替换(重置 / 撤销重做 / 外部重载):mock 视图开着时同步重播种,
1100
+ // 否则 CodeJar 会停留在「应用」前的旧文本,重置按钮看起来像没生效
1101
+ if (this.varsMode === 'mock') this.seedRawFromMocks()
720
1102
  },
721
1103
  },
722
1104
  mounted() {
@@ -729,6 +1111,7 @@ export default {
729
1111
  beforeUnmount() {
730
1112
  // DebugPanel 一般常驻,但保险起见卸载时销毁 CodeJar、清理可能残留的拖拽监听
731
1113
  this.destroyJar()
1114
+ this.destroyDlgJar()
732
1115
  this.cancelResize()
733
1116
  this.stopSvcLog()
734
1117
  },
@@ -823,18 +1206,6 @@ export default {
823
1206
  /* localStorage 不可用时忽略 */
824
1207
  }
825
1208
  },
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
1209
  /** 用 CodeJar 初始化原始 JSON 编辑器:contenteditable + Prism(json) 高亮 */
839
1210
  initJar() {
840
1211
  const el = this.$refs.rawEditor
@@ -855,6 +1226,9 @@ export default {
855
1226
  this.rawText = code
856
1227
  if (this.rawError) this.rawError = '' // 用户再次输入即清除旧错误
857
1228
  })
1229
+ // 开发期暴露 Raw 编辑器句柄供 E2E 程序化写入(与 window.__rt 同源约定);生产构建不暴露。
1230
+ // 合成 input 事件驱动不了 CodeJar,E2E 经 updateCode 走真实 onUpdate → 「应用」路径。
1231
+ if (import.meta.env.DEV) window.__rawEditor = this._jar
858
1232
  this.$nextTick(() => el.focus())
859
1233
  },
860
1234
  /** 销毁 CodeJar 实例(退出/保存/卸载/切卡时调用) */
@@ -863,46 +1237,94 @@ export default {
863
1237
  this._jar.destroy()
864
1238
  }
865
1239
  this._jar = null
1240
+ if (typeof window !== 'undefined' && window.__rawEditor) window.__rawEditor = null
866
1241
  },
867
- /** 取消 Raw 编辑:放弃改动,切回默认树形模式 */
868
- exitRaw() {
869
- this.rawMode = false
870
- this.rawError = ''
871
- this.destroyJar()
1242
+ /** 变量弹窗「默认值」代码编辑区(数组 / 对象):CodeJar + Prism(json),与 mock Raw 编辑器同方案 */
1243
+ initDlgJar() {
1244
+ const el = this.$refs.dlgCode
1245
+ if (!el) return
1246
+ this.destroyDlgJar()
1247
+ const highlight = (editor) => {
1248
+ editor.innerHTML = Prism.highlight(editor.textContent, Prism.languages.json, 'json')
1249
+ }
1250
+ this._dlgJar = CodeJar(el, highlight, {
1251
+ tab: ' ',
1252
+ indentOn: /[({[]$/,
1253
+ addClosing: true,
1254
+ history: true,
1255
+ })
1256
+ this._dlgJar.updateCode(this.varDlg ? this.varDlg.form.defaultValue : '')
1257
+ this._dlgJar.onUpdate((code) => {
1258
+ if (this.varDlg) this.varDlg.form.defaultValue = code
1259
+ if (this.varDlgError) this.varDlgError = ''
1260
+ })
1261
+ // 开发期暴露弹窗代码区句柄供 E2E 程序化写入(合成 input 事件驱动不了 CodeJar);生产不暴露
1262
+ if (import.meta.env.DEV) window.__dlgEditor = this._dlgJar
1263
+ },
1264
+ destroyDlgJar() {
1265
+ if (this._dlgJar && this._dlgJar.destroy) this._dlgJar.destroy()
1266
+ this._dlgJar = null
1267
+ if (typeof window !== 'undefined' && window.__dlgEditor) window.__dlgEditor = null
1268
+ },
1269
+ /** 标量 → 容器切换时的种子文本:旧文本形状匹配则格式化沿用,否则给空容器骨架 */
1270
+ complexSeed(type, text) {
1271
+ const empty = type === 'array' ? '[]' : '{}'
1272
+ const t = String(text == null ? '' : text).trim()
1273
+ if (!t) return empty
1274
+ try {
1275
+ const v = JSON.parse(t)
1276
+ const shapeOk = type === 'array' ? Array.isArray(v) : !!v && typeof v === 'object'
1277
+ return shapeOk ? JSON.stringify(v, null, 2) : empty
1278
+ } catch (e) {
1279
+ return empty
1280
+ }
1281
+ },
1282
+ /**
1283
+ * 默认值统一以文本承载,但四种类型「合法文本」的形态不同:切类型 / 打开弹窗时按目标类型规整,
1284
+ * 免得把上一类型的字面量塞进新控件(如布尔的 'true' 落进数字步进框)。
1285
+ * 字符串原样保留(含首尾空白与换行)——占位提示已明示「字符串原样保存」。
1286
+ */
1287
+ normalizeDefaultText(type, text) {
1288
+ if (this.isComplexType(type)) return this.complexSeed(type, text)
1289
+ const t = String(text == null ? '' : text).trim()
1290
+ if (type === 'number') return t !== '' && Number.isFinite(Number(t)) ? String(Number(t)) : ''
1291
+ if (type === 'boolean') return /^(true|1)$/i.test(t) ? 'true' : 'false'
1292
+ return String(text == null ? '' : text)
872
1293
  },
873
- /** 保存 Raw 编辑:先校验 JSON 合法性与类型,通过后替换 rootScope */
1294
+ /** 应用 Raw 编辑:校验 JSON 合法性与形状 → 写入 dictionary.mocks → 重算预览数据(默认值之上叠 mock);
1295
+ * 写入即按 2 空格标准格式回灌编辑器;留在 mock 视图继续编辑 */
874
1296
  saveRaw() {
875
- const text = (this.rawText || '').trim()
876
- if (!text) {
877
- this.rawError = '内容不能为空'
1297
+ // 先容错清理(注释 / 尾逗号)再严格校验:报错判定基于清理后的文本
1298
+ const parsed = parseJsonLenient(this.rawText)
1299
+ if (parsed.error) {
1300
+ this.rawError = 'JSON 格式错误:' + parsed.error
878
1301
  return
879
1302
  }
880
- let parsed
881
- try {
882
- parsed = JSON.parse(text)
883
- } catch (e) {
884
- this.rawError = 'JSON 格式错误:' + e.message
1303
+ if (parsed.value === null || typeof parsed.value !== 'object' || Array.isArray(parsed.value)) {
1304
+ this.rawError = '变量 mock 必须是 JSON 对象({ ... })'
885
1305
  return
886
1306
  }
887
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
888
- this.rawError = '数据绑定必须是对象({ ... })'
1307
+ const schema = this.schema
1308
+ if (!schema) {
1309
+ this.rawError = '当前未载入卡片'
889
1310
  return
890
1311
  }
891
- this.rt.setRootScope(parsed)
892
- // 等待 watcher 把 rootScope 同步进 currentSchema.dictionary 后再落定历史,
893
- // 使本次 Raw 编辑作为一笔撤销记录(与「应用」按钮同路径)。
1312
+ if (!schema.dictionary || typeof schema.dictionary !== 'object') {
1313
+ schema.dictionary = { dictionaryList: [], inputParams: [] }
1314
+ }
1315
+ schema.dictionary.mocks = parsed.value
1316
+ // 画布数据 = defaultValue 之上按 key 叠加非空 mock;watcher 会把 rootScope 回写两仓
1317
+ this.rt.setRootScope(buildDataFromSchema(schema, { includeMocks: true }))
1318
+ const formatted = JSON.stringify(parsed.value, null, 2)
1319
+ this.rawText = formatted
1320
+ if (this._jar) this._jar.updateCode(formatted)
1321
+ // 落定一笔撤销记录(与节点属性「应用」同路径)
894
1322
  this.$nextTick(() => this.onCommit())
895
- this.destroyJar()
896
- this.rawMode = false
897
1323
  this.rawError = ''
898
1324
  },
899
1325
  async copyScope() {
900
- let text = ''
901
- try {
902
- text = JSON.stringify(this.rt.rootScope, null, 2)
903
- } catch (e) {
904
- text = String(this.rt.rootScope)
905
- }
1326
+ // 复制当前 mock 文档(编辑器所见即所得)
1327
+ const text = this.rawText || ''
906
1328
  const ok = await copyText(text)
907
1329
  if (ok) {
908
1330
  this.copied = true
@@ -925,6 +1347,226 @@ export default {
925
1347
  onCommit() {
926
1348
  this.$emit('schema-commit')
927
1349
  },
1350
+ // —— 变量管理(schema.dictionary.inputParams)——
1351
+ /** 是否已配置 mock(dictionary.mocks 有数据;与 buildDataFromSchema 同判据)→ 列表行 m 徽标 */
1352
+ hasMock(param) {
1353
+ return hasMockValue(this.mocks, param.key)
1354
+ },
1355
+ /** 复制变量名(当前 key,绑定以 $key 引用);成功后图标短暂变 ✓(与接口调用面板同款反馈节奏) */
1356
+ async copyVarName(param, i) {
1357
+ const text = param && param.key ? String(param.key) : ''
1358
+ if (!text) return
1359
+ const ok = await copyText(text)
1360
+ if (ok) {
1361
+ this.copiedVar = i
1362
+ if (this.varCopyTimer) clearTimeout(this.varCopyTimer)
1363
+ this.varCopyTimer = setTimeout(() => {
1364
+ if (this.copiedVar === i) this.copiedVar = null
1365
+ }, 1500)
1366
+ }
1367
+ },
1368
+ isComplexType(type) {
1369
+ return type === 'array' || type === 'object'
1370
+ },
1371
+ // 布尔改用单选(无占位提示),故这里只剩数字与字符串两支
1372
+ scalarPlaceholder(type) {
1373
+ if (type === 'number') return '数字,如 0 / 99.5'
1374
+ return '字符串原样保存'
1375
+ },
1376
+ onVarTypeChange(type) {
1377
+ if (this.varDlg) this.varDlg.form.type = type
1378
+ },
1379
+ /** 打开添加(index=null)/ 编辑(index=下标)弹窗;默认值统一以文本承载,确定时按类型解析 */
1380
+ openVarDlg(index) {
1381
+ const param = index == null ? null : this.inputParams[index]
1382
+ const type = param ? param.type || 'string' : 'string'
1383
+ const raw = param ? param.defaultValue : undefined
1384
+ let text = ''
1385
+ if (raw !== undefined && raw !== null) {
1386
+ text = typeof raw === 'object' ? JSON.stringify(raw, null, 2) : String(raw)
1387
+ }
1388
+ // 按类型规整:布尔补出 'false' 让单选有落点,数字剔除非法字面量,容器格式化
1389
+ text = this.normalizeDefaultText(type, text)
1390
+ this.varDlgError = ''
1391
+ this.varDlg = {
1392
+ index,
1393
+ form: {
1394
+ key: param ? param.key || '' : '',
1395
+ name: param ? param.name || '' : '',
1396
+ type,
1397
+ defaultValue: text,
1398
+ desc: param ? param.desc || '' : '',
1399
+ },
1400
+ }
1401
+ },
1402
+ /** 默认值文本 → 类型化值;返回 { value } 或 { error } */
1403
+ parseDefault(type, text) {
1404
+ const t = String(text == null ? '' : text).trim()
1405
+ if (this.isComplexType(type)) {
1406
+ if (!t) return { value: type === 'array' ? [] : {} }
1407
+ // 与 mock 编辑器同套容错:先清理注释 / 尾逗号再严格校验
1408
+ const parsed = parseJsonLenient(t)
1409
+ if (parsed.error) return { error: '默认值不是合法 JSON:' + parsed.error }
1410
+ const v = parsed.value
1411
+ if (type === 'array' && !Array.isArray(v)) return { error: '默认值需为 JSON 数组' }
1412
+ if (type === 'object' && (typeof v !== 'object' || v === null || Array.isArray(v))) return { error: '默认值需为 JSON 对象' }
1413
+ return { value: v }
1414
+ }
1415
+ if (type === 'number') {
1416
+ if (!t) return { value: 0 }
1417
+ const n = Number(t)
1418
+ if (!Number.isFinite(n)) return { error: '默认值不是合法数字' }
1419
+ return { value: n }
1420
+ }
1421
+ if (type === 'boolean') {
1422
+ if (!t) return { value: false }
1423
+ if (t === 'true' || t === '1') return { value: true }
1424
+ if (t === 'false' || t === '0') return { value: false }
1425
+ return { error: '布尔默认值仅接受 true / false' }
1426
+ }
1427
+ return { value: String(text == null ? '' : text) }
1428
+ },
1429
+ /** 确定:写入 inputParams 并同步 rootScope(mock 基线),再记一笔历史 */
1430
+ confirmVarDlg() {
1431
+ const dlg = this.varDlg
1432
+ if (!dlg) return
1433
+ const form = dlg.form
1434
+ const key = String(form.key || '').trim()
1435
+ const name = String(form.name || '').trim()
1436
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1437
+ this.varDlgError = '变量名需为字母 / 下划线开头的英文标识(绑定以 $key 引用)'
1438
+ return
1439
+ }
1440
+ if (this.inputParams.some((p, i) => p.key === key && i !== dlg.index)) {
1441
+ this.varDlgError = '已存在同名变量 ' + key
1442
+ return
1443
+ }
1444
+ if (!name) {
1445
+ this.varDlgError = '中文名不能为空'
1446
+ return
1447
+ }
1448
+ const parsed = this.parseDefault(form.type, form.defaultValue)
1449
+ if (parsed.error) {
1450
+ this.varDlgError = parsed.error
1451
+ return
1452
+ }
1453
+ // 容器类型:保存即按 2 空格标准 JSON 格式化回灌(编辑器与存档同形)
1454
+ if (this.isComplexType(form.type)) {
1455
+ const formatted = JSON.stringify(parsed.value, null, 2)
1456
+ form.defaultValue = formatted
1457
+ if (this._dlgJar) this._dlgJar.updateCode(formatted)
1458
+ }
1459
+ const schema = this.schema
1460
+ if (!schema) {
1461
+ this.varDlgError = '当前未载入卡片'
1462
+ return
1463
+ }
1464
+ if (!schema.dictionary || typeof schema.dictionary !== 'object') {
1465
+ schema.dictionary = { dictionaryList: [], inputParams: [] }
1466
+ }
1467
+ if (!Array.isArray(schema.dictionary.inputParams)) schema.dictionary.inputParams = []
1468
+ const params = schema.dictionary.inputParams
1469
+ const root = this.rt.rootScope
1470
+ const mocks = readMocks(schema) || {}
1471
+ const value = cloneJson(parsed.value)
1472
+ if (dlg.index == null) {
1473
+ params.push({ key, name, type: form.type, defaultValue: cloneJson(value), desc: String(form.desc || ''), rule: { type: '', options: {} }, children: [] })
1474
+ // 已有 mock 覆盖时画布继续用 mock,默认值只入 defaultValue 仓
1475
+ if (!hasMockValue(mocks, key)) root[key] = value
1476
+ } else {
1477
+ const param = params[dlg.index]
1478
+ if (!param) return
1479
+ const oldKey = param.key
1480
+ param.key = key
1481
+ param.name = name
1482
+ param.type = form.type
1483
+ param.desc = String(form.desc || '')
1484
+ param.defaultValue = cloneJson(value)
1485
+ if (oldKey !== key) {
1486
+ // 改名时 mock 覆盖跟随迁移,避免旧 key 的 mock 变成孤儿 / 新 key 丢覆盖
1487
+ if (Object.prototype.hasOwnProperty.call(mocks, oldKey)) {
1488
+ mocks[key] = mocks[oldKey]
1489
+ delete mocks[oldKey]
1490
+ }
1491
+ if (root && Object.prototype.hasOwnProperty.call(root, oldKey)) delete root[oldKey]
1492
+ }
1493
+ if (!hasMockValue(mocks, key)) root[key] = value
1494
+ }
1495
+ this.varDlg = null
1496
+ this.onCommit()
1497
+ },
1498
+ askDeleteVar(index) {
1499
+ this.delVarTarget = index
1500
+ },
1501
+ /** 删除:inputParams 移除 + rootScope 清键,再记一笔历史 */
1502
+ /**
1503
+ * 扫描变量引用面:遍历 schema(跳过 dictionary 两仓与 compiled 回声产物),
1504
+ * 按两种引用形态匹配——表达式里的 `$key`、事件脚本里的 `state.key` / `state['key']`;
1505
+ * 分类计数:绑定表达式(itemValue 为字符串)/ 事件脚本(code 键)/ 其它文本。
1506
+ * 仅供删除确认分级(被引用=danger / 未被引用=warn),不做写操作。
1507
+ */
1508
+ scanVarRefs(key) {
1509
+ const refs = { binding: 0, event: 0, other: 0 }
1510
+ const schema = this.schema
1511
+ if (!key || !schema || typeof schema !== 'object') return refs
1512
+ const esc = String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1513
+ const boundary = '(?![A-Za-z0-9_$])'
1514
+ const dollarRe = new RegExp('\\$' + esc + boundary)
1515
+ const stateRe = new RegExp('state\\s*(?:\\.\\s*' + esc + boundary + '|\\[\\s*[\'"]' + esc + '[\'"]\\s*\\])')
1516
+ const hit = (s) => dollarRe.test(s) || stateRe.test(s)
1517
+ const walk = (v) => {
1518
+ if (v == null) return
1519
+ if (Array.isArray(v)) {
1520
+ for (const item of v) walk(item)
1521
+ return
1522
+ }
1523
+ if (typeof v !== 'object') return
1524
+ for (const [k, val] of Object.entries(v)) {
1525
+ if (k === 'dictionary' || k === 'compiled') continue
1526
+ if (typeof val === 'string') {
1527
+ if (!hit(val)) continue
1528
+ if (k === 'itemValue') refs.binding += 1
1529
+ else if (k === 'code') refs.event += 1
1530
+ else refs.other += 1
1531
+ } else {
1532
+ walk(val)
1533
+ }
1534
+ }
1535
+ }
1536
+ walk(schema)
1537
+ return refs
1538
+ },
1539
+ confirmDeleteVar() {
1540
+ const param = this.delVarTarget == null ? null : this.inputParams[this.delVarTarget]
1541
+ this.delVarTarget = null
1542
+ if (!param || !this.schema || !this.schema.dictionary) return
1543
+ const params = this.schema.dictionary.inputParams || []
1544
+ const idx = params.indexOf(param)
1545
+ if (idx >= 0) params.splice(idx, 1)
1546
+ // mock 覆盖一并清除,避免留下孤儿 key 与误报的 m 徽标
1547
+ const mocks = readMocks(this.schema) || {}
1548
+ if (Object.prototype.hasOwnProperty.call(mocks, param.key)) delete mocks[param.key]
1549
+ const root = this.rt.rootScope
1550
+ if (root && Object.prototype.hasOwnProperty.call(root, param.key)) delete root[param.key]
1551
+ this.onCommit()
1552
+ },
1553
+ /** 从 mock 二级视图回到变量列表(编辑器随视图卸载,watcher 负责销毁 CodeJar) */
1554
+ leaveMock() {
1555
+ this.varsMode = 'list'
1556
+ },
1557
+ /** 以当前 dictionary.mocks 播种 mock 编辑器文本(进入视图 / schema 被替换时共用) */
1558
+ seedRawFromMocks() {
1559
+ this.rawError = ''
1560
+ try {
1561
+ // mock 编辑器只编辑覆盖仓 dictionary.mocks(默认值在变量弹窗里改);空表即 {}
1562
+ this.rawText = JSON.stringify(this.mocks, null, 2)
1563
+ } catch (e) {
1564
+ this.rawText = ''
1565
+ this.rawError = '无法序列化当前数据:' + (e && e.message)
1566
+ return
1567
+ }
1568
+ if (this._jar) this._jar.updateCode(this.rawText)
1569
+ },
928
1570
  /** 双击结构树节点:弹出属性面板 */
929
1571
  onInspect(comp) {
930
1572
  this.inspectingComp = comp