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.
Files changed (36) hide show
  1. package/README.md +21 -0
  2. package/dist/preview-editor/assets/index-BQKzMPBq.js +79 -0
  3. package/dist/preview-editor/assets/index-D3zp47Dg.css +1 -0
  4. package/dist/preview-editor/index.html +2 -2
  5. package/dist/server-core/cardFiles.js +9 -4
  6. package/dist/server-core/codeBridge.js +359 -0
  7. package/dist/server-core/index.js +14 -2
  8. package/dist/server-core/sliceUpload.js +97 -0
  9. package/dist/standalone.js +18 -3
  10. package/dist/vendored/App.vue +268 -45
  11. package/dist/vendored/api.js +102 -32
  12. package/dist/vendored/assets/svgIcons/icons.svg +5 -0
  13. package/dist/vendored/components/ApiCallPanel.vue +127 -0
  14. package/dist/vendored/components/ApiPayloadTree.vue +34 -1
  15. package/dist/vendored/components/CardGallery.vue +2 -0
  16. package/dist/vendored/components/CardSelect.vue +6 -0
  17. package/dist/vendored/components/ComponentPalette.vue +45 -38
  18. package/dist/vendored/components/ConfirmDialog.vue +20 -5
  19. package/dist/vendored/components/DebugPanel.vue +168 -127
  20. package/dist/vendored/components/EventEditor.vue +27 -3
  21. package/dist/vendored/components/JsonTree.vue +35 -4
  22. package/dist/vendored/components/LogValue.vue +2 -1
  23. package/dist/vendored/components/PropControl.vue +6 -2
  24. package/dist/vendored/components/StructureTree.vue +53 -2
  25. package/dist/vendored/components/setters/SetCode.vue +111 -3
  26. package/dist/vendored/styles/app.css +507 -132
  27. package/dist/vendored/styles/card-gallery.css +23 -2
  28. package/dist/vendored/styles/card-select.css +15 -0
  29. package/dist/vendored/styles/dropdown.css +3 -0
  30. package/dist/vendored/styles/json-tree.css +1 -1
  31. package/dist/vendored/styles/node-inspector.css +176 -119
  32. package/dist/vendored/styles/setters.css +91 -2
  33. package/dist/vendored/styles/structure-tree.css +158 -15
  34. package/package.json +1 -1
  35. package/dist/preview-editor/assets/index-BYB0hngG.js +0 -72
  36. 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
- if (!confirm(`节点 “${key}” 在 schema 中被引用,删除后可能导致渲染异常。\n\n确定要删除吗?`)) {
487
- return
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
- <div v-else class="log__json"><ApiPayloadTree :value="value" :max-depth="14" /></div>
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: 3px 0; border: 1px solid var(--border);
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.is-active { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
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 ref="editor" class="st-code__editor" :class="{ 'is-disabled': disabled }"></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>