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
package/dist/vendored/App.vue
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-grid" /></svg>
|
|
37
37
|
</button>
|
|
38
38
|
<div
|
|
39
|
+
ref="validateBadgeEl"
|
|
39
40
|
class="app__badge"
|
|
40
41
|
:class="'app__badge--' + validateBadge.state"
|
|
41
42
|
:title="badgeTitle"
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-save" /></svg>
|
|
70
71
|
<span>保存</span>
|
|
71
72
|
</button>
|
|
72
|
-
<button class="app__btn app__btn--op" :disabled="!preview" @click="copySchema" title="复制 schema
|
|
73
|
+
<button class="app__btn app__btn--op" :disabled="!preview" @click="copySchema" title="复制 schema(含变量 mock 编辑后的数据)">
|
|
73
74
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-copy" /></svg>
|
|
74
75
|
<span>{{ copiedSchema ? '已复制 ✓' : '复制' }}</span>
|
|
75
76
|
</button>
|
|
@@ -198,29 +199,38 @@
|
|
|
198
199
|
>
|
|
199
200
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-link" /></svg>
|
|
200
201
|
</button>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
202
|
+
<!-- 点遮罩关闭:与事件帮助气泡等轻量浮层同一交互 -->
|
|
203
|
+
<div v-if="devServiceOpen" class="app__dev-svc-mask" @click="devServiceOpen = false"></div>
|
|
204
|
+
<Transition name="pop">
|
|
205
|
+
<div v-if="devServiceOpen" class="app__dev-svc-panel" @click.stop>
|
|
206
|
+
<div class="app__dev-svc-head">
|
|
207
|
+
<span class="app__dev-svc-title">本地开发服务</span>
|
|
208
|
+
<button class="app__dev-svc-close" type="button" title="关闭" @click="devServiceOpen = false"></button>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="app__dev-svc-body">
|
|
211
|
+
<label class="app__dev-svc-row">
|
|
212
|
+
<span>启用</span>
|
|
213
|
+
<Switch v-model="devServiceForm.enabled" />
|
|
214
|
+
</label>
|
|
215
|
+
<label class="app__dev-svc-row app__dev-svc-row--col">
|
|
216
|
+
<span>服务地址</span>
|
|
217
|
+
<input type="text" v-model="devServiceForm.baseUrl" placeholder="http://127.0.0.1:8787" />
|
|
218
|
+
</label>
|
|
219
|
+
<label class="app__dev-svc-row app__dev-svc-row--col">
|
|
220
|
+
<span>访问令牌(可选)</span>
|
|
221
|
+
<input type="text" v-model="devServiceForm.token" placeholder="留空则不校验" />
|
|
222
|
+
</label>
|
|
223
|
+
<p class="app__dev-svc-hint" v-if="apiBaseResolved === null">
|
|
224
|
+
<template v-if="devSvcBlockReason === 'disabled'">已填写服务地址,但「启用」开关未打开。请开启「启用」开关并点击保存,本地开发服务才会生效。</template>
|
|
225
|
+
<template v-else>请填写本机 cardbuddy 服务地址(默认 http://127.0.0.1:8787),打开「启用」开关并保存后,编辑功能才会生效。</template>
|
|
226
|
+
</p>
|
|
227
|
+
<div class="app__dev-svc-actions">
|
|
228
|
+
<button class="ni-btn ni-btn--ghost" type="button" @click="devServiceOpen = false">关闭</button>
|
|
229
|
+
<button class="ni-btn" type="button" @click="saveDevService">保存</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
218
232
|
</div>
|
|
219
|
-
|
|
220
|
-
<template v-if="devSvcBlockReason === 'disabled'">已填写服务地址,但「启用」开关未打开。请开启「启用」开关并点击保存,本地开发服务才会生效。</template>
|
|
221
|
-
<template v-else>请填写本机 cardbuddy 服务地址(默认 http://127.0.0.1:8787),打开「启用」开关并保存后,编辑功能才会生效。</template>
|
|
222
|
-
</p>
|
|
223
|
-
</div>
|
|
233
|
+
</Transition>
|
|
224
234
|
</div>
|
|
225
235
|
</div>
|
|
226
236
|
<!-- revision 依赖锚点:setData 自增后驱动本组件重渲染 -->
|
|
@@ -253,9 +263,11 @@
|
|
|
253
263
|
<div class="phone__frame" :style="frameStyle">
|
|
254
264
|
<!-- 画布层:直接按实际列宽/列高布局(width/height 100%),不再使用 transform 缩放 -->
|
|
255
265
|
<div class="phone__scaler">
|
|
256
|
-
<!--
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
<!-- 画布工具条:原 device-bar(顶 28px)+ preview-statusbar(底 28px)合并为一条 32px,
|
|
267
|
+
竖向空间还给手机视口。左组=改变画布(背景 / 宽度预设),右组=画布现状与动作(尺寸读数 / 刷新);
|
|
268
|
+
预览列窄于 300px 时收起读数与文案只留图标(188 预设下仍单行可用)。 -->
|
|
269
|
+
<div class="canvas-bar" :class="{ 'canvas-bar--tight': previewWidth < 300 }" v-show="!chromeOff">
|
|
270
|
+
<!-- 画布背景:触发器带当前底色小样,不展开下拉也可见当前模式 -->
|
|
259
271
|
<Dropdown
|
|
260
272
|
borderless
|
|
261
273
|
size="xs"
|
|
@@ -263,19 +275,47 @@
|
|
|
263
275
|
:value="canvasBg"
|
|
264
276
|
:options="bgOptions.map((b) => ({ key: b.key, value: b.label + '背景' }))"
|
|
265
277
|
@change="chooseBg"
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
278
|
+
>
|
|
279
|
+
<template #trigger>
|
|
280
|
+
<span class="canvas-bar__chip" :title="'画布背景:' + currentBg.label">
|
|
281
|
+
<i class="canvas-bar__swatch" :class="'canvas-bar__swatch--' + canvasBg"></i>
|
|
282
|
+
<span class="canvas-bar__txt">{{ currentBg.label }}背景</span>
|
|
283
|
+
<svg class="canvas-bar__chev" viewBox="0 0 16 16" aria-hidden="true"><use href="#icon-chevron" /></svg>
|
|
284
|
+
</span>
|
|
285
|
+
</template>
|
|
286
|
+
</Dropdown>
|
|
287
|
+
<!-- 宽度预设:触发器直接显示当前宽度,省去独立的「宽度」标签 -->
|
|
270
288
|
<Dropdown
|
|
271
289
|
borderless
|
|
272
290
|
size="xs"
|
|
273
|
-
placement="bottom-
|
|
291
|
+
placement="bottom-start"
|
|
274
292
|
:value="canvasW"
|
|
275
|
-
:label="canvasW + ' px'"
|
|
276
293
|
:options="presetSizes"
|
|
277
294
|
@change="setPresetWidth"
|
|
278
|
-
|
|
295
|
+
>
|
|
296
|
+
<template #trigger>
|
|
297
|
+
<span class="canvas-bar__chip" title="画布宽度预设(高度随列高自适应)">
|
|
298
|
+
<svg class="canvas-bar__icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="8 7 4 12 8 17" /><polyline points="16 7 20 12 16 17" /><line x1="4" y1="12" x2="20" y2="12" /></svg>
|
|
299
|
+
<span class="canvas-bar__txt">{{ canvasW }} px</span>
|
|
300
|
+
<svg class="canvas-bar__chev" viewBox="0 0 16 16" aria-hidden="true"><use href="#icon-chevron" /></svg>
|
|
301
|
+
</span>
|
|
302
|
+
</template>
|
|
303
|
+
</Dropdown>
|
|
304
|
+
<span class="canvas-bar__spacer"></span>
|
|
305
|
+
<span class="canvas-bar__size" title="画布实际尺寸(宽 × 高,高度随窗口自适应)">{{ canvasW }} × {{ viewportHeight }}</span>
|
|
306
|
+
<button
|
|
307
|
+
class="canvas-bar__iconbtn"
|
|
308
|
+
:class="{ 'is-loading': refreshing }"
|
|
309
|
+
type="button"
|
|
310
|
+
:disabled="!selectedId || refreshing"
|
|
311
|
+
title="刷新卡片,重新加载"
|
|
312
|
+
@click="refreshCard"
|
|
313
|
+
>
|
|
314
|
+
<svg class="canvas-bar__icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
315
|
+
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
|
|
316
|
+
<path d="M21 3v6h-6" />
|
|
317
|
+
</svg>
|
|
318
|
+
</button>
|
|
279
319
|
</div>
|
|
280
320
|
<!--
|
|
281
321
|
固定视口:弹层(alertCard / alertDialog / toast)相对此容器 absolute inset:0 定位,
|
|
@@ -307,23 +347,6 @@
|
|
|
307
347
|
<PickerOverlay />
|
|
308
348
|
<DatePickerOverlay />
|
|
309
349
|
</div>
|
|
310
|
-
<!-- 预览区底部状态条:显示画布宽高(左)+ 刷新按钮(右),chromeOff 时隐藏 -->
|
|
311
|
-
<div class="preview-statusbar" v-show="!chromeOff">
|
|
312
|
-
<span class="device-bar__info">{{ canvasW }} × {{ viewportHeight }}</span>
|
|
313
|
-
<span class="device-bar__spacer"></span>
|
|
314
|
-
<button
|
|
315
|
-
class="statusbar__refresh"
|
|
316
|
-
type="button"
|
|
317
|
-
:disabled="!selectedId"
|
|
318
|
-
@click="refreshCard"
|
|
319
|
-
title="刷新卡片,重新加载"
|
|
320
|
-
>
|
|
321
|
-
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
322
|
-
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
|
|
323
|
-
<path d="M21 3v6h-6" />
|
|
324
|
-
</svg>
|
|
325
|
-
</button>
|
|
326
|
-
</div>
|
|
327
350
|
</div>
|
|
328
351
|
</div>
|
|
329
352
|
</div>
|
|
@@ -523,7 +546,9 @@ import logoUrl from '@/assets/site-icon.png'
|
|
|
523
546
|
import { apiFetch, resolveApiBase, subscribeCardsChanged, loadDevServiceConfig, saveDevServiceConfig } from '@/api.js'
|
|
524
547
|
// 由 schema 单向派生 data 的共享实现(与 server/core 同源,避免逻辑漂移)。
|
|
525
548
|
// 预览渲染注入的数据绑定、本地基线比较、历史快照还原均使用此函数,使 data 永不读回 data.json 文件。
|
|
526
|
-
import { buildDataFromSchema } from '../server/core/buildDataFromSchema.js'
|
|
549
|
+
import { buildDataFromSchema, readMocks } from '../server/core/buildDataFromSchema.js'
|
|
550
|
+
// 编辑器预览态派生 data 时叠加 dictionary.mocks(设计期覆盖);服务端写盘仍只用 defaultValue。
|
|
551
|
+
const WITH_MOCKS = { includeMocks: true }
|
|
527
552
|
|
|
528
553
|
// 编辑器自身「保存到本地 / 保存至远端」写盘后,cardbuddy 的 watcher 会把这次落盘经 SSE
|
|
529
554
|
// 推回 cards-changed。这是「自己改自己」的回声,并非外部修改,用此窗口吸收,避免误弹外部重载
|
|
@@ -534,8 +559,8 @@ const SELF_SAVE_SUPPRESS_MS = 2500
|
|
|
534
559
|
// 内部 rootScope 可经 setRootScope 整体替换而引用不变。
|
|
535
560
|
const adapter = new Adapter()
|
|
536
561
|
const cardRuntime = createCardRuntime({}, adapter)
|
|
537
|
-
// 开发期暴露 runtime 单例句柄,供 E2E 回归(如 L2-12
|
|
538
|
-
// 直接驱动 rt.setRootScope —— 与 DebugPanel
|
|
562
|
+
// 开发期暴露 runtime 单例句柄,供 E2E 回归(如 L2-12 rootScope 整体替换实时同步画布)
|
|
563
|
+
// 直接驱动 rt.setRootScope —— 与 DebugPanel「变量 → 配置变量 mock」Raw「应用」同源。生产构建不暴露。
|
|
539
564
|
if (typeof window !== 'undefined' && import.meta.env.DEV) window.__rt = cardRuntime
|
|
540
565
|
|
|
541
566
|
export default {
|
|
@@ -594,9 +619,9 @@ export default {
|
|
|
594
619
|
localSchema: null,
|
|
595
620
|
localData: null,
|
|
596
621
|
selectedNodeId: '',
|
|
597
|
-
// 当前激活的调试面板 tab
|
|
622
|
+
// 当前激活的调试面板 tab(变量/组件结构/卡片来源)。
|
|
598
623
|
// 画布 Ctrl/⌘ 点击会自动切到「组件结构」tab 并在节点树聚焦选中,无需预先停在结构树 tab。
|
|
599
|
-
activeTab: '
|
|
624
|
+
activeTab: 'vars',
|
|
600
625
|
// 右侧常驻「接口调用」面板:展开/收起由顶栏按钮与面板内 ✕ 控制,
|
|
601
626
|
// 初始状态持久化到 localStorage(cardbuddy:api-panel-open)
|
|
602
627
|
apiPanelOpen: true,
|
|
@@ -653,12 +678,15 @@ export default {
|
|
|
653
678
|
baseline: null,
|
|
654
679
|
_commitTimer: null, // 编辑落定防抖计时器,避免连续编辑产生过多快照
|
|
655
680
|
viewportHeight: 700, // 手机视口实际高度(高度自适应填充后由 ResizeObserver 实测更新)
|
|
656
|
-
// 画布背景模式:checker(棋盘格) / white(白色) / gray(浅灰)
|
|
681
|
+
// 画布背景模式:checker(棋盘格) / white(白色) / gray(浅灰) / black(近黑,深色卡片对色用)
|
|
657
682
|
canvasBg: 'gray',
|
|
683
|
+
// 刷新按钮进行态:图标自转 + 禁用,避免连点与「点了没反应」
|
|
684
|
+
refreshing: false,
|
|
658
685
|
bgOptions: [
|
|
659
686
|
{ key: 'checker', label: '棋盘' },
|
|
660
687
|
{ key: 'white', label: '白色' },
|
|
661
688
|
{ key: 'gray', label: '浅灰' },
|
|
689
|
+
{ key: 'black', label: '黑色' },
|
|
662
690
|
],
|
|
663
691
|
// —— 预览区宽度可调(拖拽分隔条 / 预设尺寸,画布按实际像素宽度布局,无 transform 缩放)——
|
|
664
692
|
_previewWidthPx: 372, // 预览列宽度(像素,持久化到 localStorage);第一次打开未主动设置时默认 372px
|
|
@@ -806,7 +834,7 @@ export default {
|
|
|
806
834
|
const w = this._previewWidthPx || minW
|
|
807
835
|
return Math.max(minW, Math.min(maxW, w))
|
|
808
836
|
},
|
|
809
|
-
// 画布实际宽度(列宽,整数):
|
|
837
|
+
// 画布实际宽度(列宽,整数):canvas-bar__size 实时显示用,拖拽分隔条时随之变化
|
|
810
838
|
canvasW() {
|
|
811
839
|
return Math.round(this.previewWidth || 375)
|
|
812
840
|
},
|
|
@@ -857,7 +885,7 @@ export default {
|
|
|
857
885
|
window.removeEventListener('keydown', this.onKeydown)
|
|
858
886
|
},
|
|
859
887
|
mounted() {
|
|
860
|
-
// 视口高度随窗口弹性填充,实测高度同步到
|
|
888
|
+
// 视口高度随窗口弹性填充,实测高度同步到 canvas-bar 尺寸读数
|
|
861
889
|
this.viewportObserver = new ResizeObserver((entries) => {
|
|
862
890
|
for (const entry of entries) {
|
|
863
891
|
this.viewportHeight = Math.round(entry.contentRect.height)
|
|
@@ -919,9 +947,10 @@ export default {
|
|
|
919
947
|
this.selectedId = initial
|
|
920
948
|
await this.loadCard(this.selectedId)
|
|
921
949
|
}
|
|
922
|
-
// 数据绑定(rt.rootScope
|
|
950
|
+
// 数据绑定(rt.rootScope)深度监听:任意编辑实时回写 schema 的权威数据位 ——
|
|
951
|
+
// 已配置 mock 的 key 回写 dictionary.mocks[key],其余回写 inputParams[].defaultValue,
|
|
923
952
|
// 保证「保存到本地」基于最新数据绑定;预览也经 rt 响应式实时刷新。
|
|
924
|
-
// ⚠️
|
|
953
|
+
// ⚠️ 此处【不】写历史——历史由「变量 → 配置变量 mock」视图的「应用」按钮(schema-commit)落定,
|
|
925
954
|
// 与节点属性/事件面板一致:实时编辑只更新预览与内存,点「应用」才记一笔撤销记录。
|
|
926
955
|
this.$watch(
|
|
927
956
|
() => this.rt.rootScope,
|
|
@@ -1142,8 +1171,13 @@ export default {
|
|
|
1142
1171
|
* 底部状态条刷新按钮:重新从本地开发服务拉取当前卡片并重建预览。
|
|
1143
1172
|
*/
|
|
1144
1173
|
async refreshCard() {
|
|
1145
|
-
if (!this.selectedId) return
|
|
1146
|
-
|
|
1174
|
+
if (!this.selectedId || this.refreshing) return
|
|
1175
|
+
this.refreshing = true
|
|
1176
|
+
try {
|
|
1177
|
+
await this.loadCard(this.selectedId)
|
|
1178
|
+
} finally {
|
|
1179
|
+
this.refreshing = false
|
|
1180
|
+
}
|
|
1147
1181
|
},
|
|
1148
1182
|
// 切换深/浅主题:写入 <html data-theme> 并持久化,刷新后保持
|
|
1149
1183
|
toggleTheme() {
|
|
@@ -1195,18 +1229,89 @@ export default {
|
|
|
1195
1229
|
// 渲染端点异常不强行判错(避免 dev server 预热期 SSR 冷启动误报)
|
|
1196
1230
|
const renderOk = r ? !!r.renderOk : true
|
|
1197
1231
|
const ok = contractOk && renderOk
|
|
1232
|
+
const next = ok ? 'ok' : 'error'
|
|
1233
|
+
const changed = this.validateBadge.state !== next
|
|
1198
1234
|
this.validateBadge = {
|
|
1199
|
-
state:
|
|
1235
|
+
state: next,
|
|
1200
1236
|
errors: (v.errors || []).length,
|
|
1201
1237
|
warnings: (v.warnings || []).length,
|
|
1202
1238
|
renderOk,
|
|
1203
1239
|
ts: Date.now(),
|
|
1204
1240
|
}
|
|
1241
|
+
if (changed && (next === 'ok' || next === 'error')) this.celebrateValidate(next)
|
|
1205
1242
|
} catch (e) {
|
|
1206
|
-
// 网络/服务不可用:保底不阻断编辑,标记 fail
|
|
1243
|
+
// 网络/服务不可用:保底不阻断编辑,标记 fail(fail 不触发动画)
|
|
1207
1244
|
this.validateBadge = { ...this.validateBadge, state: 'fail' }
|
|
1208
1245
|
}
|
|
1209
1246
|
},
|
|
1247
|
+
/** 状态跳变时,在徽标局部触发庆祝动画:徽标弹跳 + 碎玻璃碎屑沿轮廓放射爆开,碎屑颜色跟随目标状态主色。仅局部,不铺屏。 */
|
|
1248
|
+
celebrateValidate(state = 'ok') {
|
|
1249
|
+
const el = this.$refs.validateBadgeEl
|
|
1250
|
+
if (!el) return
|
|
1251
|
+
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
|
1252
|
+
// 徽标弹跳
|
|
1253
|
+
el.classList.remove('is-celebrating')
|
|
1254
|
+
void el.offsetWidth // 重排以重启动画
|
|
1255
|
+
el.classList.add('is-celebrating')
|
|
1256
|
+
setTimeout(() => el.classList.remove('is-celebrating'), 600)
|
|
1257
|
+
// 碎屑颜色跟随目标状态徽标主色(--ok / --danger / --warn / --text-faint)
|
|
1258
|
+
const r = el.getBoundingClientRect()
|
|
1259
|
+
const colorVar = { ok: '--ok', error: '--danger', pending: '--warn', fail: '--text-faint', nopython: '--text-faint' }[state] || '--ok'
|
|
1260
|
+
const fallback = { ok: '#52c41a', error: '#F5222D', pending: '#FAAD14', fail: '#8c8c8c', nopython: '#8c8c8c' }[state] || '#52c41a'
|
|
1261
|
+
const badgeColor = getComputedStyle(el).getPropertyValue(colorVar).trim() || fallback
|
|
1262
|
+
const box = document.createElement('div')
|
|
1263
|
+
box.className = 'app__confetti'
|
|
1264
|
+
box.style.left = r.left + r.width / 2 + 'px'
|
|
1265
|
+
box.style.top = r.top + r.height / 2 + 'px'
|
|
1266
|
+
// 碎玻璃碎片:从徽标矩形轮廓迸出,沿外法线方向飞散;不规则尖锐多边形 + 尺寸不一
|
|
1267
|
+
const count = 22
|
|
1268
|
+
const clips = [
|
|
1269
|
+
'polygon(50% 0%, 0% 100%, 100% 100%)',
|
|
1270
|
+
'polygon(62% 0%, 100% 40%, 45% 100%, 0% 52%)',
|
|
1271
|
+
'polygon(0% 0%, 100% 18%, 82% 100%, 12% 70%)',
|
|
1272
|
+
'polygon(22% 0%, 100% 8%, 88% 100%, 0% 76%)',
|
|
1273
|
+
'polygon(0% 32%, 100% 0%, 76% 100%, 18% 86%)',
|
|
1274
|
+
'polygon(8% 0%, 100% 22%, 70% 100%, 0% 64%)',
|
|
1275
|
+
'polygon(0% 12%, 100% 0%, 100% 78%, 30% 100%, 12% 55%)',
|
|
1276
|
+
]
|
|
1277
|
+
const halfW = r.width / 2
|
|
1278
|
+
const halfH = r.height / 2
|
|
1279
|
+
for (let i = 0; i < count; i++) {
|
|
1280
|
+
const bit = document.createElement('span')
|
|
1281
|
+
bit.className = 'app__confetti-bit'
|
|
1282
|
+
// 在徽标矩形轮廓上随机取一点作为迸发起点
|
|
1283
|
+
const edge = (Math.random() * 4) | 0
|
|
1284
|
+
let px, py
|
|
1285
|
+
if (edge === 0) { px = (Math.random() * 2 - 1) * halfW; py = -halfH }
|
|
1286
|
+
else if (edge === 1) { px = halfW; py = (Math.random() * 2 - 1) * halfH }
|
|
1287
|
+
else if (edge === 2) { px = (Math.random() * 2 - 1) * halfW; py = halfH }
|
|
1288
|
+
else { px = -halfW; py = (Math.random() * 2 - 1) * halfH }
|
|
1289
|
+
// 沿轮廓点的外法线方向飞出 + 微抖动
|
|
1290
|
+
const len = Math.hypot(px, py) || 1
|
|
1291
|
+
const a = Math.atan2(py / len, px / len) + (Math.random() - 0.5) * 0.5
|
|
1292
|
+
const dist = 16 + Math.random() * 26
|
|
1293
|
+
bit.style.setProperty('--sx', px.toFixed(1) + 'px')
|
|
1294
|
+
bit.style.setProperty('--sy', py.toFixed(1) + 'px')
|
|
1295
|
+
bit.style.setProperty('--tx', (Math.cos(a) * dist).toFixed(1) + 'px')
|
|
1296
|
+
bit.style.setProperty('--ty', (Math.sin(a) * dist).toFixed(1) + 'px')
|
|
1297
|
+
bit.style.setProperty('--rot', (Math.random() * 720 - 360).toFixed(0) + 'deg')
|
|
1298
|
+
bit.style.setProperty('--clip', clips[(Math.random() * clips.length) | 0])
|
|
1299
|
+
bit.style.setProperty('--c', badgeColor)
|
|
1300
|
+
// 每片独立透明度,半透明玻璃感
|
|
1301
|
+
bit.style.setProperty('--o', (0.68 + Math.random() * 0.27).toFixed(2))
|
|
1302
|
+
// 三层尺寸分布:长薄片 / 中等碎块 / 细微粉尘,强化碎裂参差感
|
|
1303
|
+
const roll = Math.random()
|
|
1304
|
+
let w, h
|
|
1305
|
+
if (roll < 0.35) { w = 1.5 + Math.random() * 2; h = 10 + Math.random() * 10 } // 细长薄片
|
|
1306
|
+
else if (roll < 0.75) { w = 4 + Math.random() * 5; h = w * (0.6 + Math.random() * 0.8) } // 中等碎块
|
|
1307
|
+
else { w = 2 + Math.random() * 2.5; h = w } // 细微粉尘
|
|
1308
|
+
bit.style.width = w.toFixed(0) + 'px'
|
|
1309
|
+
bit.style.height = h.toFixed(0) + 'px'
|
|
1310
|
+
box.appendChild(bit)
|
|
1311
|
+
}
|
|
1312
|
+
document.body.appendChild(box)
|
|
1313
|
+
setTimeout(() => box.remove(), 800)
|
|
1314
|
+
},
|
|
1210
1315
|
/** 实时从磁盘读取并载入指定卡片(GET /__card/<id>),不读任何缓存 */
|
|
1211
1316
|
async loadCard(id, opts = {}) {
|
|
1212
1317
|
if (!id) return
|
|
@@ -1231,7 +1336,7 @@ export default {
|
|
|
1231
1336
|
this.rt.clearApiCalls()
|
|
1232
1337
|
// schema 被事件编辑器改后重建时保留当前数据绑定编辑,避免被磁盘 data(由 schema 派生)覆盖。
|
|
1233
1338
|
// 数据绑定改为由 schema 单向派生,不再读回 data.json 文件。
|
|
1234
|
-
if (!opts.keepScope) this.rt.setRootScope(buildDataFromSchema(fresh.schema))
|
|
1339
|
+
if (!opts.keepScope) this.rt.setRootScope(buildDataFromSchema(fresh.schema, WITH_MOCKS))
|
|
1235
1340
|
this.currentCard = { schema: fresh.schema }
|
|
1236
1341
|
this.currentSchema = fresh.schema
|
|
1237
1342
|
// 重置并在载入后拉取导入来源元数据(Phase E):本地新建卡无 .import-meta.json → null,
|
|
@@ -1241,7 +1346,7 @@ export default {
|
|
|
1241
1346
|
// 记录本地真实版本基线(磁盘当前内容),供「保存到本地」按钮判断与本地是否一致。
|
|
1242
1347
|
// localData 由 schema 派生(与渲染注入同源),保证纯数据编辑也能触发脏标记。
|
|
1243
1348
|
this.localSchema = this.cloneSchema(fresh.schema)
|
|
1244
|
-
this.localData = this.cloneData(buildDataFromSchema(fresh.schema))
|
|
1349
|
+
this.localData = this.cloneData(buildDataFromSchema(fresh.schema, WITH_MOCKS))
|
|
1245
1350
|
this.preview = buildPreview(fresh.schema)
|
|
1246
1351
|
// Task #5:把 Property 树模型森林暴露到 window,供浏览器内 AI / skill 直接读取节点可编辑结构
|
|
1247
1352
|
if (typeof window !== 'undefined') window.__cardModel = buildModelView(fresh.schema)
|
|
@@ -1319,7 +1424,7 @@ export default {
|
|
|
1319
1424
|
if (!id || !snapshot) return
|
|
1320
1425
|
const schema = snapshot.schema || snapshot
|
|
1321
1426
|
// 快照只存 schema;数据绑定由其派生。若老快照带 data 则优先沿用(历史兼容)。
|
|
1322
|
-
const data = snapshot.data != null ? snapshot.data : buildDataFromSchema(schema)
|
|
1427
|
+
const data = snapshot.data != null ? snapshot.data : buildDataFromSchema(schema, WITH_MOCKS)
|
|
1323
1428
|
this.currentSchema = this.cloneSchema(schema)
|
|
1324
1429
|
this.rt.setRootScope(this.cloneData(data))
|
|
1325
1430
|
this.preview = buildPreview(this.currentSchema)
|
|
@@ -1335,7 +1440,7 @@ export default {
|
|
|
1335
1440
|
commitHistory() {
|
|
1336
1441
|
if (!this.currentSchema || !this.baseline) return
|
|
1337
1442
|
const sameSchema = this.structuralEqual(this.currentSchema, this.baseline.schema)
|
|
1338
|
-
const sameData = this.dataEqual(this.rt.rootScope, buildDataFromSchema(this.baseline.schema))
|
|
1443
|
+
const sameData = this.dataEqual(this.rt.rootScope, buildDataFromSchema(this.baseline.schema, WITH_MOCKS))
|
|
1339
1444
|
if (sameSchema && sameData) return
|
|
1340
1445
|
const previous = { schema: this.baseline.schema }
|
|
1341
1446
|
this.past.push(previous)
|
|
@@ -1376,7 +1481,7 @@ export default {
|
|
|
1376
1481
|
// 取末尾最多 30 个作为可回退的上一版本(最新在末尾;仅 schema,data 由 schema 派生)
|
|
1377
1482
|
this.past = snaps.slice(-30).map((s) => ({
|
|
1378
1483
|
schema: s.schema,
|
|
1379
|
-
data: s.data != null ? s.data : buildDataFromSchema(s.schema),
|
|
1484
|
+
data: s.data != null ? s.data : buildDataFromSchema(s.schema, WITH_MOCKS),
|
|
1380
1485
|
}))
|
|
1381
1486
|
} catch (e) {
|
|
1382
1487
|
console.warn('[加载编辑历史失败]', e)
|
|
@@ -1441,10 +1546,11 @@ export default {
|
|
|
1441
1546
|
this.commitHistory()
|
|
1442
1547
|
},
|
|
1443
1548
|
/**
|
|
1444
|
-
* 把数据绑定(rt.rootScope
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1447
|
-
*
|
|
1549
|
+
* 把数据绑定(rt.rootScope)的编辑实时同步回 schema 的权威数据位:
|
|
1550
|
+
* key 已在 dictionary.mocks 中 → 回写 mocks[key](设计期覆盖仓);
|
|
1551
|
+
* 否则回写 schema.dictionary.inputParams[i].defaultValue(随卡发布的缺省仓)。
|
|
1552
|
+
* 这样「本地保存」时 schema.json 携带最新两仓数据,而 data.json 仍只由 defaultValue 派生,
|
|
1553
|
+
* mock 不会泄漏进发布数据;两仓也不会互相覆盖。
|
|
1448
1554
|
*/
|
|
1449
1555
|
syncDataToSchema() {
|
|
1450
1556
|
const schema = this.currentSchema
|
|
@@ -1452,13 +1558,14 @@ export default {
|
|
|
1452
1558
|
const root = this.rt && this.rt.rootScope
|
|
1453
1559
|
if (!root || typeof root !== 'object') return
|
|
1454
1560
|
const params = schema.dictionary.inputParams
|
|
1561
|
+
const mocks = readMocks(schema)
|
|
1455
1562
|
for (const key of Object.keys(root)) {
|
|
1456
1563
|
const param = params.find((p) => p && (p.key === key || p.name === key))
|
|
1457
1564
|
if (!param) continue
|
|
1458
1565
|
// 深拷贝,避免数据绑定与 schema 共享同一引用(setRootScope 重建后会失联)
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1566
|
+
const value = JSON.parse(JSON.stringify(root[key], (k, v) => (k === '_schemaNode' ? undefined : v)))
|
|
1567
|
+
if (mocks && Object.prototype.hasOwnProperty.call(mocks, key)) mocks[key] = value
|
|
1568
|
+
else param.defaultValue = value
|
|
1462
1569
|
}
|
|
1463
1570
|
},
|
|
1464
1571
|
/** 结构树选中节点 */
|
|
@@ -2263,7 +2370,10 @@ export default {
|
|
|
2263
2370
|
// 写盘成功后,当前显示版本即成为本地真实版本:刷新本地基线,使顶栏
|
|
2264
2371
|
// 「保存到本地」按钮在「当前版本 == 本地版本」时自动置灰。
|
|
2265
2372
|
this.localSchema = this.cloneSchema(this.currentSchema)
|
|
2266
|
-
|
|
2373
|
+
// 基线数据必须与载入 / 历史快照同源(含 mocks):rt.rootScope 是含 mock 覆盖的,
|
|
2374
|
+
// 若此处漏传 WITH_MOCKS,只要存在 mock 数据,保存后 rootScope 与 localData 永远
|
|
2375
|
+
// 不等 → historyDirty 恒真,「保存到本地」按钮保存成功也保持高亮。
|
|
2376
|
+
this.localData = this.cloneData(buildDataFromSchema(this.currentSchema, WITH_MOCKS))
|
|
2267
2377
|
this.savedCard = true
|
|
2268
2378
|
// 本地保存后磁盘已更新,刷新整卡校验徽标(端点读磁盘)
|
|
2269
2379
|
this.refreshValidateBadge()
|