cardbuddy 0.6.0 → 0.8.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 +21 -0
- package/dist/preview-editor/assets/index-BQKzMPBq.js +79 -0
- package/dist/preview-editor/assets/index-D3zp47Dg.css +1 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/server-core/cardFiles.js +9 -4
- package/dist/server-core/codeBridge.js +359 -0
- package/dist/server-core/index.js +14 -2
- package/dist/server-core/sliceUpload.js +97 -0
- package/dist/standalone.js +18 -3
- package/dist/vendored/App.vue +268 -45
- package/dist/vendored/api.js +102 -32
- package/dist/vendored/assets/svgIcons/icons.svg +5 -0
- package/dist/vendored/components/ApiCallPanel.vue +127 -0
- package/dist/vendored/components/ApiPayloadTree.vue +34 -1
- package/dist/vendored/components/CardGallery.vue +2 -0
- package/dist/vendored/components/CardSelect.vue +6 -0
- package/dist/vendored/components/ComponentPalette.vue +45 -38
- package/dist/vendored/components/ConfirmDialog.vue +20 -5
- package/dist/vendored/components/DebugPanel.vue +168 -127
- package/dist/vendored/components/EventEditor.vue +27 -3
- package/dist/vendored/components/JsonTree.vue +35 -4
- package/dist/vendored/components/LogValue.vue +2 -1
- package/dist/vendored/components/PropControl.vue +6 -2
- package/dist/vendored/components/StructureTree.vue +53 -2
- package/dist/vendored/components/setters/SetCode.vue +111 -3
- package/dist/vendored/styles/app.css +507 -132
- package/dist/vendored/styles/card-gallery.css +23 -2
- package/dist/vendored/styles/card-select.css +15 -0
- package/dist/vendored/styles/dropdown.css +3 -0
- package/dist/vendored/styles/json-tree.css +1 -1
- package/dist/vendored/styles/node-inspector.css +176 -119
- package/dist/vendored/styles/setters.css +91 -2
- package/dist/vendored/styles/structure-tree.css +158 -15
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-BYB0hngG.js +0 -72
- package/dist/preview-editor/assets/index-CpBZEQDy.css +0 -1
|
@@ -135,6 +135,17 @@
|
|
|
135
135
|
<button v-if="editable" class="jt__ctxmenu-item jt__ctxmenu-item--del" type="button" @click="ctxDelete">删除节点</button>
|
|
136
136
|
</div>
|
|
137
137
|
<div v-if="ctxMenu.visible" class="jt__ctxmenu-mask" @click="hideContextMenu" @contextmenu.prevent="hideContextMenu"></div>
|
|
138
|
+
|
|
139
|
+
<!-- 删除被引用节点的二次确认:警告级(橙黄+警告三角),分级见 design/深浅主题配色规范手册.md §3.4 -->
|
|
140
|
+
<ConfirmDialog
|
|
141
|
+
:visible="delConfirm.show"
|
|
142
|
+
title="删除被引用节点"
|
|
143
|
+
:message="delConfirm.message"
|
|
144
|
+
confirm-text="删除"
|
|
145
|
+
severity="warn"
|
|
146
|
+
@confirm="onDelConfirm"
|
|
147
|
+
@cancel="onDelCancel"
|
|
148
|
+
/>
|
|
138
149
|
</div>
|
|
139
150
|
</template>
|
|
140
151
|
|
|
@@ -142,6 +153,7 @@
|
|
|
142
153
|
import { reactive } from 'vue'
|
|
143
154
|
import { copyText } from '@/runtime/clipboard.js'
|
|
144
155
|
import { showAppToast } from '@/runtime/app-toast.js'
|
|
156
|
+
import ConfirmDialog from './ConfirmDialog.vue'
|
|
145
157
|
|
|
146
158
|
// 模块级共享状态:驱动「全部展开 / 全部折叠」和「选中节点」。
|
|
147
159
|
// 所有 JsonTree 实例(含递归子实例)共享同一份,避免 provide/inject
|
|
@@ -187,6 +199,7 @@ function getByPath(obj, path) {
|
|
|
187
199
|
|
|
188
200
|
export default {
|
|
189
201
|
name: 'JsonTree',
|
|
202
|
+
components: { ConfirmDialog },
|
|
190
203
|
props: {
|
|
191
204
|
data: { type: [Object, Array], default: () => ({}) },
|
|
192
205
|
depth: { type: Number, default: 0 },
|
|
@@ -208,6 +221,11 @@ export default {
|
|
|
208
221
|
y: 0,
|
|
209
222
|
item: null,
|
|
210
223
|
},
|
|
224
|
+
delConfirm: {
|
|
225
|
+
show: false,
|
|
226
|
+
message: '',
|
|
227
|
+
item: null,
|
|
228
|
+
},
|
|
211
229
|
}
|
|
212
230
|
},
|
|
213
231
|
computed: {
|
|
@@ -479,16 +497,29 @@ export default {
|
|
|
479
497
|
const item = this.ctxMenu.item
|
|
480
498
|
this.hideContextMenu()
|
|
481
499
|
if (!item) return
|
|
482
|
-
//
|
|
500
|
+
// 检查是否被引用:被引用时走警告级二次确认(ConfirmDialog severity=warn),未引用直接删除
|
|
483
501
|
const referenced = this.isNodeReferenced(item.path)
|
|
484
502
|
if (referenced) {
|
|
485
503
|
const key = item.path.join('.')
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
504
|
+
this.delConfirm.message = `节点 “${key}” 在 schema 中被引用,删除后可能导致渲染异常。\n\n确定要删除吗?`
|
|
505
|
+
this.delConfirm.item = item
|
|
506
|
+
this.delConfirm.show = true
|
|
507
|
+
return
|
|
489
508
|
}
|
|
490
509
|
this.deleteNode(item)
|
|
491
510
|
},
|
|
511
|
+
/** 警告级二次确认点「删除」:执行挂起的删除 */
|
|
512
|
+
onDelConfirm() {
|
|
513
|
+
const item = this.delConfirm.item
|
|
514
|
+
this.delConfirm.show = false
|
|
515
|
+
this.delConfirm.item = null
|
|
516
|
+
if (item) this.deleteNode(item)
|
|
517
|
+
},
|
|
518
|
+
/** 警告级二次确认点「取消」/遮罩:丢弃挂起的删除 */
|
|
519
|
+
onDelCancel() {
|
|
520
|
+
this.delConfirm.show = false
|
|
521
|
+
this.delConfirm.item = null
|
|
522
|
+
},
|
|
492
523
|
/** 检测节点是否被 schema 引用(属性绑定 $key 或事件代码 state.key) */
|
|
493
524
|
isNodeReferenced(path) {
|
|
494
525
|
if (!this.schema) return false
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
-->
|
|
8
8
|
<template>
|
|
9
9
|
<span v-if="!complex" class="log__val" :class="'jt__val--' + typeClass">{{ display }}</span>
|
|
10
|
-
|
|
10
|
+
<!-- 日志里的对象/数组参数:根节点默认一行摘要、点击展开,保证日志行密度 -->
|
|
11
|
+
<div v-else class="log__json"><ApiPayloadTree :value="value" :max-depth="14" root-collapsed /></div>
|
|
11
12
|
</template>
|
|
12
13
|
|
|
13
14
|
<script>
|
|
@@ -233,9 +233,13 @@ export default {
|
|
|
233
233
|
.pc-body { line-height: 0; }
|
|
234
234
|
.pc-srcs { display: flex; gap: 4px; line-height: 1.5; }
|
|
235
235
|
.pc-src {
|
|
236
|
-
flex: 1; font-size: 12px; padding:
|
|
236
|
+
flex: 1; height: 24px; font-size: 12px; padding: 0 10px; border: 1px solid var(--border);
|
|
237
237
|
background: transparent; color: var(--text-dim); border-radius: 6px; cursor: pointer;
|
|
238
|
+
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
|
238
239
|
}
|
|
239
|
-
.pc-src
|
|
240
|
+
.pc-src:hover { background: var(--bg-hover); color: var(--text); }
|
|
241
|
+
/* 选中态与分段控件 .st-seg 同语言:浅底 + 同色文字 + 发丝描边,不做实心填充 */
|
|
242
|
+
.pc-src.is-active { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border); }
|
|
243
|
+
.pc-src.is-active:hover { background: var(--accent-soft); color: var(--accent); }
|
|
240
244
|
.pc--readonly .pc-src { opacity: .6; cursor: not-allowed; }
|
|
241
245
|
</style>
|
|
@@ -51,8 +51,6 @@
|
|
|
51
51
|
@click="onAddFromToolbar"
|
|
52
52
|
>+ 添加组件</button>
|
|
53
53
|
</div>
|
|
54
|
-
<p class="st__tip">右键节点可 复制 / 粘贴 / 副本 / 复制节点ID / 删除,右键卡片根还可 复制卡片code(英文名 / 文件夹名);容器节点(含卡片根)还可 添加子节点、非根节点还可 前面/后面插入节点,也可在此 设置属性 / 事件;按住 Ctrl / ⌘ 点击画布元素,可在本树中定位并展开该节点</p>
|
|
55
|
-
|
|
56
54
|
<ul class="st__list" @contextmenu.prevent="closeContextMenu">
|
|
57
55
|
<!-- 根节点 -->
|
|
58
56
|
<li class="st__node st__node--root">
|
|
@@ -152,6 +150,46 @@
|
|
|
152
150
|
<div class="st__ctx-sep"></div>
|
|
153
151
|
<button class="st__ctx-item st__ctx-item--danger" type="button" :disabled="!selectedNodeId || selectedNodeId === '_root'" @click="onCtxDelete">删除</button>
|
|
154
152
|
</div>
|
|
153
|
+
|
|
154
|
+
<!--
|
|
155
|
+
帮助悬浮按钮:固定在面板左下角(不占用固定布局区域),点击弹出使用说明气泡;
|
|
156
|
+
气泡内 ✕ 关闭,再次点击按钮亦关闭。说明文本即原固定展示在面板顶部的 st__tip 内容。
|
|
157
|
+
实现:零高度的 sticky 容器吸附在滚动视口底部(滚动在外层 .debug__body),
|
|
158
|
+
按钮与气泡以绝对定位挂靠其上,内容长短均保持悬浮于面板左下角。
|
|
159
|
+
-->
|
|
160
|
+
<div class="st__help-wrap">
|
|
161
|
+
<button
|
|
162
|
+
class="st__help"
|
|
163
|
+
type="button"
|
|
164
|
+
:class="{ 'st__help--open': helpOpen }"
|
|
165
|
+
title="使用说明"
|
|
166
|
+
:aria-expanded="helpOpen"
|
|
167
|
+
@click.stop="helpOpen = !helpOpen"
|
|
168
|
+
>
|
|
169
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
170
|
+
<circle cx="12" cy="12" r="9.2" />
|
|
171
|
+
<path d="M9.4 9.2a2.6 2.6 0 0 1 5.1.7c0 1.7-2.5 2.2-2.5 3.7" />
|
|
172
|
+
<line x1="12" y1="17" x2="12" y2="17.01" />
|
|
173
|
+
</svg>
|
|
174
|
+
</button>
|
|
175
|
+
<div v-if="helpOpen" class="st__help-pop" @click.stop>
|
|
176
|
+
<div class="st__help-pop-head">
|
|
177
|
+
<span class="st__help-pop-title">快捷键</span>
|
|
178
|
+
<button class="st__help-pop-close" type="button" title="关闭" @click="helpOpen = false">✕</button>
|
|
179
|
+
</div>
|
|
180
|
+
<!-- 快捷键清单:左列按键示意(kbd 芯片),右列一句话说明;与 App onKeydown 保持一致 -->
|
|
181
|
+
<ul class="st__help-pop-body st__keys">
|
|
182
|
+
<li v-for="s in shortcuts" :key="s.desc" class="st__keys-row">
|
|
183
|
+
<span class="st__keys-combo">
|
|
184
|
+
<template v-for="(key, ki) in s.keys" :key="key">
|
|
185
|
+
<kbd class="st__kbd">{{ key }}</kbd><span v-if="ki < s.keys.length - 1" class="st__kbd-plus">+</span>
|
|
186
|
+
</template>
|
|
187
|
+
</span>
|
|
188
|
+
<span class="st__keys-desc">{{ s.desc }}</span>
|
|
189
|
+
</li>
|
|
190
|
+
</ul>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
155
193
|
</div>
|
|
156
194
|
</template>
|
|
157
195
|
|
|
@@ -518,6 +556,19 @@ export default {
|
|
|
518
556
|
locateEnabled: false,
|
|
519
557
|
// 右键上下文菜单状态:visible 显隐、x/y 为光标视口坐标(position:fixed)、nodeId 记录触发节点、node 记录触发节点对象(供「添加节点」定位容器)
|
|
520
558
|
contextMenu: { visible: false, x: 0, y: 0, nodeId: '', node: null },
|
|
559
|
+
// 帮助气泡:左下角悬浮按钮弹出的使用说明,默认收起(不占用固定布局区域)
|
|
560
|
+
helpOpen: false,
|
|
561
|
+
// 快捷键清单(帮助气泡内容,与 App onKeydown 的实现保持一致)
|
|
562
|
+
shortcuts: [
|
|
563
|
+
{ keys: ['⌘/Ctrl', 'C'], desc: '复制选中节点' },
|
|
564
|
+
{ keys: ['⌘/Ctrl', 'V'], desc: '粘贴节点' },
|
|
565
|
+
{ keys: ['⌘/Ctrl', 'D'], desc: '创建节点副本' },
|
|
566
|
+
{ keys: ['⌘/Ctrl', 'Z'], desc: '撤销' },
|
|
567
|
+
{ keys: ['⌘/Ctrl', '⇧', 'Z'], desc: '重做' },
|
|
568
|
+
{ keys: ['Delete'], desc: '删除选中节点' },
|
|
569
|
+
{ keys: ['Esc'], desc: '取消选中' },
|
|
570
|
+
{ keys: ['⌘/Ctrl', '点击'], desc: '点击画布元素,在本树中定位该节点' },
|
|
571
|
+
],
|
|
521
572
|
}
|
|
522
573
|
},
|
|
523
574
|
computed: {
|
|
@@ -12,9 +12,40 @@
|
|
|
12
12
|
-->
|
|
13
13
|
<template>
|
|
14
14
|
<div class="st-code">
|
|
15
|
-
<div
|
|
15
|
+
<div
|
|
16
|
+
ref="editor"
|
|
17
|
+
class="st-code__editor"
|
|
18
|
+
:class="{ 'is-disabled': disabled, 'is-bridged': !!bridge }"
|
|
19
|
+
></div>
|
|
20
|
+
<!-- 右上角语言标签:弱化的只读标识,不参与交互 -->
|
|
21
|
+
<span class="st-code__lang" aria-hidden="true">JS</span>
|
|
16
22
|
<div class="st-code__foot">
|
|
17
23
|
<span class="st-code__hint">{{ lines }} 行 · {{ chars }} 字符</span>
|
|
24
|
+
<!-- VSCode 代码桥:生成临时 js 用外部编辑器编辑,保存实时回写;断开即删临时文件 -->
|
|
25
|
+
<span class="st-code__foot-actions">
|
|
26
|
+
<span v-if="bridge" class="st-code__bridge-tag" :title="bridge.file">VSCode 同步中</span>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
class="st-code__vscode"
|
|
30
|
+
:class="{ 'is-on': !!bridge }"
|
|
31
|
+
:title="
|
|
32
|
+
bridge
|
|
33
|
+
? '断开 VSCode 桥接并删除临时文件'
|
|
34
|
+
: '在 VSCode 中编辑:保存实时同步回编辑区,断开后清除临时文件'
|
|
35
|
+
"
|
|
36
|
+
:disabled="disabled"
|
|
37
|
+
@click="toggleBridge"
|
|
38
|
+
>
|
|
39
|
+
<!-- 图标左侧小字:未桥接时是动作说明;桥接后由状态徽标接替,避免两段文字挤一行 -->
|
|
40
|
+
<span v-if="!bridge" class="st-code__vscode-label">在编辑器中打开</span>
|
|
41
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
|
42
|
+
<path
|
|
43
|
+
fill="currentColor"
|
|
44
|
+
d="M23.15 2.587 18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
</button>
|
|
48
|
+
</span>
|
|
18
49
|
</div>
|
|
19
50
|
</div>
|
|
20
51
|
</template>
|
|
@@ -24,13 +55,20 @@ import { CodeJar } from 'codejar'
|
|
|
24
55
|
import Prism from 'prismjs'
|
|
25
56
|
import 'prismjs/components/prism-javascript'
|
|
26
57
|
import { setterProps, setterEmits } from './props.js'
|
|
58
|
+
import {
|
|
59
|
+
codeBridgeOpen,
|
|
60
|
+
codeBridgeFetch,
|
|
61
|
+
codeBridgeHeartbeat,
|
|
62
|
+
codeBridgeClose,
|
|
63
|
+
subscribeCodeBridge,
|
|
64
|
+
} from '../../api.js'
|
|
27
65
|
|
|
28
66
|
export default {
|
|
29
67
|
name: 'SetCode',
|
|
30
68
|
props: setterProps,
|
|
31
69
|
emits: setterEmits,
|
|
32
70
|
data() {
|
|
33
|
-
return { code: '' }
|
|
71
|
+
return { code: '', bridge: null }
|
|
34
72
|
},
|
|
35
73
|
computed: {
|
|
36
74
|
codeKey() {
|
|
@@ -49,6 +87,8 @@ export default {
|
|
|
49
87
|
},
|
|
50
88
|
beforeUnmount() {
|
|
51
89
|
if (this._t) clearTimeout(this._t)
|
|
90
|
+
// 组件卸载(抽屉关闭 / 切节点 / key 重建)即结束桥接会话,服务端删除临时文件
|
|
91
|
+
this.teardownBridge(true)
|
|
52
92
|
if (this._jar && this._jar.destroy) this._jar.destroy()
|
|
53
93
|
},
|
|
54
94
|
methods: {
|
|
@@ -75,7 +115,7 @@ export default {
|
|
|
75
115
|
this._jar.onUpdate((code) => this.onCode(code))
|
|
76
116
|
},
|
|
77
117
|
onCode(code) {
|
|
78
|
-
if (this.disabled) return
|
|
118
|
+
if (this.disabled || this.bridge) return
|
|
79
119
|
this.code = code
|
|
80
120
|
// 保留 compiled 等未知键,只覆写 codeKey
|
|
81
121
|
const base = this.value && typeof this.value === 'object' ? Object.assign({}, this.value) : {}
|
|
@@ -85,6 +125,74 @@ export default {
|
|
|
85
125
|
if (this._t) clearTimeout(this._t)
|
|
86
126
|
this._t = setTimeout(() => this.$emit('change'), 400)
|
|
87
127
|
},
|
|
128
|
+
/* ---------------- VSCode 代码桥 ---------------- */
|
|
129
|
+
toggleBridge() {
|
|
130
|
+
if (this.bridge) this.teardownBridge(true)
|
|
131
|
+
else this.setupBridge()
|
|
132
|
+
},
|
|
133
|
+
async setupBridge() {
|
|
134
|
+
if (this.disabled || this.bridge) return
|
|
135
|
+
const label = this.itemInfo && this.itemInfo.itemKey ? String(this.itemInfo.itemKey) : ''
|
|
136
|
+
try {
|
|
137
|
+
const info = await codeBridgeOpen(this.code, label)
|
|
138
|
+
this.bridge = { id: info.id, file: info.file }
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error('[VSCode 桥接建立失败]', e)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
// 桥接期间 Web 编辑区只读:外部编辑器为唯一写入源,避免双向冲突
|
|
144
|
+
this.setEditorEditable(false)
|
|
145
|
+
this._hb = setInterval(() => {
|
|
146
|
+
if (!this.bridge) return
|
|
147
|
+
codeBridgeHeartbeat(this.bridge.id).catch(() => {})
|
|
148
|
+
}, 5000)
|
|
149
|
+
this._bridgeUnsub = subscribeCodeBridge((payload) => {
|
|
150
|
+
if (!this.bridge || !payload || payload.id !== this.bridge.id) return
|
|
151
|
+
// code-bridge-closed 带 reason(ttl / client):服务端已清理,本地同步收 UI
|
|
152
|
+
if (payload.reason) return this.teardownBridge(false)
|
|
153
|
+
this.pullBridgeCode()
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
async pullBridgeCode() {
|
|
157
|
+
if (!this.bridge) return
|
|
158
|
+
let r
|
|
159
|
+
try {
|
|
160
|
+
r = await codeBridgeFetch(this.bridge.id)
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return // 会话已失效:等 closed 事件或 TTL 收尾
|
|
163
|
+
}
|
|
164
|
+
const code = String(r.code == null ? '' : r.code)
|
|
165
|
+
if (code === this.code) return
|
|
166
|
+
this.applyExternalCode(code)
|
|
167
|
+
},
|
|
168
|
+
applyExternalCode(code) {
|
|
169
|
+
this.code = code
|
|
170
|
+
// callOnUpdate=false:不触发 onUpdate,切断「回写 → input → 再写临时文件」回声环
|
|
171
|
+
if (this._jar) this._jar.updateCode(code, false)
|
|
172
|
+
const base = this.value && typeof this.value === 'object' ? Object.assign({}, this.value) : {}
|
|
173
|
+
base[this.codeKey] = code
|
|
174
|
+
this.$emit('input', base)
|
|
175
|
+
if (this._t) clearTimeout(this._t)
|
|
176
|
+
this._t = setTimeout(() => this.$emit('change'), 400)
|
|
177
|
+
},
|
|
178
|
+
teardownBridge(notifyServer) {
|
|
179
|
+
const b = this.bridge
|
|
180
|
+
this.bridge = null
|
|
181
|
+
if (this._hb) {
|
|
182
|
+
clearInterval(this._hb)
|
|
183
|
+
this._hb = null
|
|
184
|
+
}
|
|
185
|
+
if (this._bridgeUnsub) {
|
|
186
|
+
this._bridgeUnsub()
|
|
187
|
+
this._bridgeUnsub = null
|
|
188
|
+
}
|
|
189
|
+
this.setEditorEditable(true)
|
|
190
|
+
if (b && notifyServer) codeBridgeClose(b.id).catch(() => {})
|
|
191
|
+
},
|
|
192
|
+
setEditorEditable(on) {
|
|
193
|
+
const el = this.$refs.editor
|
|
194
|
+
if (el) el.setAttribute('contenteditable', on ? 'true' : 'false')
|
|
195
|
+
},
|
|
88
196
|
},
|
|
89
197
|
}
|
|
90
198
|
</script>
|