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
package/dist/vendored/App.vue
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
右侧调试面板。
|
|
4
4
|
-->
|
|
5
5
|
<template>
|
|
6
|
-
<div class="app" :class="{ 'app--chrome-off': chromeOff }" :style="{ '--preview-width': previewWidth + 'px' }">
|
|
6
|
+
<div class="app" :class="{ 'app--chrome-off': chromeOff }" :style="{ '--preview-width': previewWidth + 'px', '--api-panel-width': apiPanelWidth + 'px' }">
|
|
7
7
|
<header class="app__bar" v-show="!chromeOff">
|
|
8
8
|
<strong class="app__logo">
|
|
9
9
|
<img class="app__logo-icon" :src="logoUrl" alt="" />
|
|
@@ -74,23 +74,23 @@
|
|
|
74
74
|
<span>{{ copiedSchema ? '已复制 ✓' : '复制' }}</span>
|
|
75
75
|
</button>
|
|
76
76
|
</div>
|
|
77
|
-
<!-- 远端:合并「远端保存 / 远端预览 / 全量同步 / 选中卡片同步」为一个分组,组名「远端」,云图标 -->
|
|
77
|
+
<!-- 远端:合并「远端保存 / 远端预览 / 切图上传 / 全量同步 / 选中卡片同步」为一个分组,组名「远端」,云图标 -->
|
|
78
78
|
<Dropdown
|
|
79
79
|
class="app__remote-menu"
|
|
80
80
|
size="md"
|
|
81
81
|
placement="bottom-end"
|
|
82
|
-
:disabled="savingRemote || syncingRemote"
|
|
83
|
-
:title="savingRemote ? '正在保存至远端…' : (syncingRemote ? '同步中…' : '远端操作:保存到 FinMall / 真机预览 / 全量或选中卡片同步')"
|
|
82
|
+
:disabled="savingRemote || syncingRemote || uploadingSlices"
|
|
83
|
+
:title="savingRemote ? '正在保存至远端…' : (syncingRemote ? '同步中…' : (uploadingSlices ? '正在上传切图…' : '远端操作:保存到 FinMall / 真机预览 / 切图上传 / 全量或选中卡片同步'))"
|
|
84
84
|
data-testid="remote-group"
|
|
85
85
|
>
|
|
86
86
|
<template #trigger="{ open }">
|
|
87
87
|
<button
|
|
88
88
|
type="button"
|
|
89
89
|
class="app__btn app__btn--remote"
|
|
90
|
-
:class="{ 'app__btn--warn': savedRemote && verifiedRemote === false, 'app__remote-trigger--active': open, 'is-processing': savingRemote || syncingRemote }"
|
|
91
|
-
:disabled="savingRemote || syncingRemote"
|
|
90
|
+
:class="{ 'app__btn--warn': savedRemote && verifiedRemote === false, 'app__remote-trigger--active': open, 'is-processing': savingRemote || syncingRemote || uploadingSlices }"
|
|
91
|
+
:disabled="savingRemote || syncingRemote || uploadingSlices"
|
|
92
92
|
>
|
|
93
|
-
<svg v-if="savingRemote || syncingRemote" class="app__icon app__icon--spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-loading" /></svg>
|
|
93
|
+
<svg v-if="savingRemote || syncingRemote || uploadingSlices" class="app__icon app__icon--spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-loading" /></svg>
|
|
94
94
|
<svg v-else-if="savedRemote && verifiedRemote !== false" 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-cloud-check" /></svg>
|
|
95
95
|
<svg v-else-if="savedRemote && verifiedRemote === false" 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-cloud-alert" /></svg>
|
|
96
96
|
<svg v-else 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-cloud" /></svg>
|
|
@@ -115,8 +115,30 @@
|
|
|
115
115
|
<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-refresh" /></svg>
|
|
116
116
|
<span>全量同步</span>
|
|
117
117
|
</li>
|
|
118
|
+
<!-- 切图上传:仅点击时执行,把「当前卡片引用到的」src/slices 切图上传到 FinMall 图片库
|
|
119
|
+
(CC0000.do),并把该卡 schema/data 里的 /slices/ 引用改写为返回的 @filestore/ 线上地址。
|
|
120
|
+
没有任何自动上传路径;共享切图若已上传过(sha256 命中 src/slices/manifest.json)直接复用地址。
|
|
121
|
+
不要求 hasCardSource:本地自建卡同样需要先把切图传上去,才能推版本 / 出真机预览。 -->
|
|
122
|
+
<li class="app__remote-item app__remote-item--upload" :class="{ 'is-disabled': !preview || !selectedId || uploadingSlices }" :title="uploadingSlices ? '正在上传切图…' : '切图上传:把当前卡片引用的本地切图(src/slices)上传到 FinMall 图片库,并把卡片里的 /slices/ 引用替换为线上地址(@filestore/…);本地切图文件保留'" data-testid="slice-upload" role="menuitem" @click="chooseRsave('slices'); close()">
|
|
123
|
+
<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-image-upload" /></svg>
|
|
124
|
+
<span>切图上传</span>
|
|
125
|
+
</li>
|
|
118
126
|
</ul></template>
|
|
119
127
|
</Dropdown>
|
|
128
|
+
<!-- 接口调用面板开关:右侧常驻面板的展开/收起,状态持久化 -->
|
|
129
|
+
<button
|
|
130
|
+
class="app__btn app__btn--icon"
|
|
131
|
+
:class="{ 'app__btn--active': apiPanelOpen }"
|
|
132
|
+
type="button"
|
|
133
|
+
:title="apiPanelOpen ? '收起接口调用面板' : '打开接口调用面板'"
|
|
134
|
+
:aria-pressed="apiPanelOpen"
|
|
135
|
+
@click="toggleApiPanel"
|
|
136
|
+
>
|
|
137
|
+
<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">
|
|
138
|
+
<rect x="3" y="4" width="18" height="16" rx="2" />
|
|
139
|
+
<line x1="15" y1="4" x2="15" y2="20" />
|
|
140
|
+
</svg>
|
|
141
|
+
</button>
|
|
120
142
|
<!-- 主题切换:深色(当前)/ 浅色,复用同名 CSS 变量整体换肤,状态持久化 -->
|
|
121
143
|
<button
|
|
122
144
|
class="theme-toggle"
|
|
@@ -306,7 +328,9 @@
|
|
|
306
328
|
</div>
|
|
307
329
|
</div>
|
|
308
330
|
|
|
309
|
-
|
|
331
|
+
|
|
332
|
+
<!-- 左分隔条:预览列 | 工作区 接缝。底面板 / 状态栏的左缘即此缝(随预览宽度联动),
|
|
333
|
+
故定位在 .app__stage 通到底部,全程可拖;右分隔条才收进两栏容器内。 -->
|
|
310
334
|
<div
|
|
311
335
|
v-show="!chromeOff"
|
|
312
336
|
class="splitter"
|
|
@@ -315,33 +339,61 @@
|
|
|
315
339
|
title="拖拽调整预览区域宽度"
|
|
316
340
|
></div>
|
|
317
341
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
342
|
+
<!-- 中右工作区:上为「调试面板 + 接口调用面板」两栏,下为贯穿两栏的共同状态栏。
|
|
343
|
+
状态栏与底部弹出面板(运行态 / 事件 / 绑定错误)均由 DebugPanel 内 Teleport 注入
|
|
344
|
+
#app-workside-bar / #app-workside-panels,宽度贯穿中右两栏;Teleport 目标须在 DOM 中
|
|
345
|
+
先于面板挂载,故两个空容器写在前面,以 flex order 视觉置底。 -->
|
|
346
|
+
<div class="workside" v-show="!chromeOff">
|
|
347
|
+
<div id="app-workside-bar" class="workside__bar"></div>
|
|
348
|
+
<div id="app-workside-panels" class="workside__panels"></div>
|
|
349
|
+
<div class="workside__cols">
|
|
350
|
+
<!-- 拖拽分隔条:置于两栏容器内(定位上下文),只贯穿列区高度,不穿过底部弹出面板与状态栏 -->
|
|
351
|
+
<!-- 中右分隔条:调试面板 | 接口调用面板 接缝,拖动调整右栏宽度。
|
|
352
|
+
约束:中栏最小宽度 ≥ 屏宽 1/3,右栏最小宽度 ≥ 320px(见 startApiDrag) -->
|
|
353
|
+
<div
|
|
354
|
+
v-show="!chromeOff && apiPanelOpen"
|
|
355
|
+
class="splitter splitter--api"
|
|
356
|
+
:class="{ 'is-dragging': apiDragging }"
|
|
357
|
+
@mousedown.prevent="startApiDrag"
|
|
358
|
+
title="拖拽调整接口调用面板宽度"
|
|
359
|
+
></div>
|
|
360
|
+
<DebugPanel
|
|
361
|
+
:chrome-off="chromeOff"
|
|
362
|
+
:rt="rt"
|
|
363
|
+
:preview="preview"
|
|
364
|
+
:schema="currentSchema"
|
|
365
|
+
:card-id="selectedId"
|
|
366
|
+
:reload-token="reloadToken"
|
|
367
|
+
:card-count="cardCount"
|
|
368
|
+
:selected-node-id="selectedNodeId"
|
|
369
|
+
:move-collapse-token="moveCollapseToken"
|
|
370
|
+
:active-tab="activeTab"
|
|
371
|
+
@schema-changed="onSchemaChanged"
|
|
372
|
+
@schema-commit="onSchemaCommit"
|
|
373
|
+
@select-node="onSelectNode"
|
|
374
|
+
@update:active-tab="activeTab = $event"
|
|
375
|
+
@add-node="onAddNode"
|
|
376
|
+
@move-node="onMoveNode"
|
|
377
|
+
@hover-node="onHoverNode"
|
|
378
|
+
@copy-selected="copySelectedNode"
|
|
379
|
+
@paste="pasteNode"
|
|
380
|
+
@duplicate="duplicateNode"
|
|
381
|
+
@delete-selected="deleteSelectedNode"
|
|
382
|
+
@reset-import="onRefresh"
|
|
383
|
+
:dev-svc-class="devSvcClass"
|
|
384
|
+
:dev-svc-title="devSvcTitle"
|
|
385
|
+
:dev-svc-label="devSvcLabel"
|
|
386
|
+
/>
|
|
387
|
+
|
|
388
|
+
<!-- 右侧常驻「接口调用」面板:与预览列 / 调试面板呈左中右布局,
|
|
389
|
+
展开/收起由顶栏按钮与面板内 ✕ 控制,初始状态持久化 -->
|
|
390
|
+
<ApiCallPanel
|
|
391
|
+
v-if="apiPanelOpen"
|
|
392
|
+
:rt="rt"
|
|
393
|
+
@close="apiPanelOpen = false"
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
345
397
|
</main>
|
|
346
398
|
<!-- 远端预览二维码弹层:点击顶栏「远端预览」按钮打开,手机(FinMall App)扫码真机查看 -->
|
|
347
399
|
<RemotePreview
|
|
@@ -413,7 +465,7 @@
|
|
|
413
465
|
:message="confirm.message"
|
|
414
466
|
:confirm-text="confirm.confirmText"
|
|
415
467
|
:cancel-text="confirm.cancelText"
|
|
416
|
-
:
|
|
468
|
+
:severity="confirm.severity"
|
|
417
469
|
@confirm="onConfirmDialog"
|
|
418
470
|
@cancel="onCancelDialog"
|
|
419
471
|
/>
|
|
@@ -444,6 +496,7 @@ import {
|
|
|
444
496
|
} from '@/runtime/clipboard-node.js'
|
|
445
497
|
import RenderTree from '@/components/RenderTree.vue'
|
|
446
498
|
import DebugPanel from '@/components/DebugPanel.vue'
|
|
499
|
+
import ApiCallPanel from '@/components/ApiCallPanel.vue'
|
|
447
500
|
import CardSelect from '@/components/CardSelect.vue'
|
|
448
501
|
import AlertCardLayer from '@/components/AlertCardLayer.vue'
|
|
449
502
|
import DeviceOverlay from '@/components/DeviceOverlay.vue'
|
|
@@ -482,7 +535,7 @@ if (typeof window !== 'undefined' && import.meta.env.DEV) window.__rt = cardRunt
|
|
|
482
535
|
|
|
483
536
|
export default {
|
|
484
537
|
name: 'App',
|
|
485
|
-
components: { RenderTree, DebugPanel, CardSelect, AlertCardLayer, DeviceOverlay, PickerOverlay, DatePickerOverlay, SelectionOverlay, HoverGuides, RemotePreview, AppToast, ConfirmDialog, CardGallery, ComponentPalette, Switch },
|
|
538
|
+
components: { RenderTree, DebugPanel, ApiCallPanel, CardSelect, AlertCardLayer, DeviceOverlay, PickerOverlay, DatePickerOverlay, SelectionOverlay, HoverGuides, RemotePreview, AppToast, ConfirmDialog, CardGallery, ComponentPalette, Switch },
|
|
486
539
|
provide() {
|
|
487
540
|
return {
|
|
488
541
|
adapter,
|
|
@@ -536,9 +589,12 @@ export default {
|
|
|
536
589
|
localSchema: null,
|
|
537
590
|
localData: null,
|
|
538
591
|
selectedNodeId: '',
|
|
539
|
-
// 当前激活的调试面板 tab
|
|
592
|
+
// 当前激活的调试面板 tab(绑定数据/组件结构/卡片来源)。
|
|
540
593
|
// 画布 Ctrl/⌘ 点击会自动切到「组件结构」tab 并在节点树聚焦选中,无需预先停在结构树 tab。
|
|
541
594
|
activeTab: 'scope',
|
|
595
|
+
// 右侧常驻「接口调用」面板:展开/收起由顶栏按钮与面板内 ✕ 控制,
|
|
596
|
+
// 初始状态持久化到 localStorage(cardbuddy:api-panel-open)
|
|
597
|
+
apiPanelOpen: true,
|
|
542
598
|
// 组件结构树「添加」面板:addNodePanelOpen 控制弹层显隐;addNodeTarget 记录
|
|
543
599
|
// 插入锚点与模式({ node, mode },来自结构树 add-node 事件):
|
|
544
600
|
// node = 右键/选中 viewNode(null 或 Root → 视作根);mode 决定落位规则:
|
|
@@ -565,20 +621,22 @@ export default {
|
|
|
565
621
|
remotePreviewOpen: false, // 「远端预览」二维码弹层开关
|
|
566
622
|
syncTarget: null, // 'full' | 'selected' | null —— 标记上一次/正在进行的同步模式,用于按钮反馈
|
|
567
623
|
syncTimer: null,
|
|
624
|
+
uploadingSlices: false, // 「切图上传」进行中(远端分组,仅用户点击触发,无自动上传)
|
|
568
625
|
// 登录凭据弹窗(远程同步缺凭据时弹出,填写账号密码写入 .local/credentials.json)
|
|
569
626
|
loginModal: false,
|
|
570
627
|
loginName: '',
|
|
571
628
|
loginPassword: '',
|
|
572
629
|
loginSubmitting: false,
|
|
573
630
|
loginError: '',
|
|
574
|
-
// 二次确认弹窗(版本推送 / 当前同步 / 全量同步 等写/覆盖类操作前弹出,确认后才执行 onConfirm
|
|
631
|
+
// 二次确认弹窗(版本推送 / 当前同步 / 全量同步 等写/覆盖类操作前弹出,确认后才执行 onConfirm)。
|
|
632
|
+
// severity 三档:primary 常规 / warn 警告(橙黄+警告三角,覆盖类操作:版本推送、全量同步)/ danger 危险(红+禁止,仅明确负面结果如删除,暂无内置操作使用)
|
|
575
633
|
confirm: {
|
|
576
634
|
show: false,
|
|
577
635
|
title: '',
|
|
578
636
|
message: '',
|
|
579
637
|
confirmText: '继续',
|
|
580
638
|
cancelText: '取消',
|
|
581
|
-
|
|
639
|
+
severity: 'primary',
|
|
582
640
|
onConfirm: null,
|
|
583
641
|
},
|
|
584
642
|
// 编辑历史(撤销/重做):每个快照为 { schema, data },data 即 rt.rootScope(数据绑定),
|
|
@@ -599,6 +657,7 @@ export default {
|
|
|
599
657
|
],
|
|
600
658
|
// —— 预览区宽度可调(拖拽分隔条 / 预设尺寸,画布按实际像素宽度布局,无 transform 缩放)——
|
|
601
659
|
_previewWidthPx: 372, // 预览列宽度(像素,持久化到 localStorage);第一次打开未主动设置时默认 372px
|
|
660
|
+
_apiPanelWidthPx: 360, // 右侧「接口调用」面板宽度(像素,持久化到 localStorage);默认 360px
|
|
602
661
|
presetSizes: [ // 预设尺寸下拉选项(仅宽度;高度随列高动态一致)
|
|
603
662
|
{ key: 375, value: '375' },
|
|
604
663
|
{ key: 430, value: '430' },
|
|
@@ -607,6 +666,7 @@ export default {
|
|
|
607
666
|
stageWidth: typeof window !== 'undefined' ? window.innerWidth : 1280, // 舞台实际宽度(ResizeObserver 实测后更新)
|
|
608
667
|
stageHeight: typeof window !== 'undefined' ? window.innerHeight - 52 : 748, // 舞台实际高度(contain 缩放的“高度上限”基准)
|
|
609
668
|
dragging: false, // 是否正在拖拽分隔条
|
|
669
|
+
apiDragging: false, // 是否正在拖拽「接口调用」面板分隔条
|
|
610
670
|
_stageObserver: null,
|
|
611
671
|
_dragMove: null,
|
|
612
672
|
_dragUp: null,
|
|
@@ -745,6 +805,15 @@ export default {
|
|
|
745
805
|
canvasW() {
|
|
746
806
|
return Math.round(this.previewWidth || 375)
|
|
747
807
|
},
|
|
808
|
+
// 右侧「接口调用」面板宽度(像素):限制在 [320, 舞台宽度 - 预览列宽 - 中栏最小宽(屏宽 1/3)],
|
|
809
|
+
// 保证中栏(调试面板)最小宽度不小于三分之一屏幕宽度、右栏不小于 320px
|
|
810
|
+
apiPanelWidth() {
|
|
811
|
+
const minW = 320 // 右栏最小宽度
|
|
812
|
+
const midMin = Math.floor((typeof window !== 'undefined' ? window.innerWidth : 1280) / 3)
|
|
813
|
+
const maxW = Math.max(minW, Math.floor((this.stageWidth || 0) - this.previewWidth - midMin))
|
|
814
|
+
const w = this._apiPanelWidthPx || 360
|
|
815
|
+
return Math.max(minW, Math.min(maxW, w))
|
|
816
|
+
},
|
|
748
817
|
// 缩放外框占满整列(宽高交由 CSS flex 填满;画布不再做 transform 缩放,直接按实际尺寸布局)
|
|
749
818
|
frameStyle() {
|
|
750
819
|
return {}
|
|
@@ -763,6 +832,14 @@ export default {
|
|
|
763
832
|
theme(val) {
|
|
764
833
|
document.documentElement.dataset.theme = val
|
|
765
834
|
},
|
|
835
|
+
// 「接口调用」面板展开/收起状态变化(顶栏按钮 toggleApiPanel 或面板内 ✕ 关闭)时统一持久化
|
|
836
|
+
apiPanelOpen(val) {
|
|
837
|
+
try {
|
|
838
|
+
localStorage.setItem('cardbuddy:api-panel-open', val ? '1' : '0')
|
|
839
|
+
} catch (e) {
|
|
840
|
+
/* localStorage 不可用时忽略 */
|
|
841
|
+
}
|
|
842
|
+
},
|
|
766
843
|
},
|
|
767
844
|
beforeUnmount() {
|
|
768
845
|
if (this.viewportObserver) this.viewportObserver.disconnect()
|
|
@@ -784,6 +861,7 @@ export default {
|
|
|
784
861
|
if (this.$refs.phoneViewport) this.viewportObserver.observe(this.$refs.phoneViewport)
|
|
785
862
|
// 预览区宽度可调:实测舞台尺寸,驱动 previewWidth/frameStyle 自适应窗口变化
|
|
786
863
|
this.loadPreviewWidth()
|
|
864
|
+
this.loadApiPanelWidth()
|
|
787
865
|
this._stageObserver = new ResizeObserver((entries) => {
|
|
788
866
|
for (const entry of entries) {
|
|
789
867
|
this.stageWidth = Math.round(entry.contentRect.width)
|
|
@@ -809,6 +887,13 @@ export default {
|
|
|
809
887
|
/* localStorage 不可用时忽略 */
|
|
810
888
|
}
|
|
811
889
|
document.documentElement.dataset.theme = this.theme
|
|
890
|
+
// 右侧「接口调用」面板:恢复本地持久化的展开/收起状态(默认展开)
|
|
891
|
+
try {
|
|
892
|
+
const v = localStorage.getItem('cardbuddy:api-panel-open')
|
|
893
|
+
if (v === '0') this.apiPanelOpen = false
|
|
894
|
+
} catch (e) {
|
|
895
|
+
/* localStorage 不可用时忽略 */
|
|
896
|
+
}
|
|
812
897
|
// 实时拉取卡片列表(不再依赖启动快照),再载入首张卡
|
|
813
898
|
await this.fetchCardList()
|
|
814
899
|
// 深链:若 URL 带 ?card=<id> 且命中列表,则预选该卡(供「远端预览」二维码扫码直达)
|
|
@@ -956,6 +1041,50 @@ export default {
|
|
|
956
1041
|
this._previewWidthPx = Math.max(minW, Math.min(maxW, w))
|
|
957
1042
|
this.savePreviewWidth()
|
|
958
1043
|
},
|
|
1044
|
+
// —— 接口调用面板宽度调节 ——
|
|
1045
|
+
// 拖拽分隔条:拖动改变右栏宽度(向左拖加宽、向右拖收窄),松手持久化。
|
|
1046
|
+
// 约束:右栏 [320px, 舞台宽 - 预览列宽 - 中栏最小宽(屏宽 1/3)],与 apiPanelWidth computed 一致
|
|
1047
|
+
startApiDrag() {
|
|
1048
|
+
if (this.chromeOff || !this.apiPanelOpen) return
|
|
1049
|
+
this.apiDragging = true
|
|
1050
|
+
document.body.style.cursor = 'col-resize'
|
|
1051
|
+
document.body.style.userSelect = 'none'
|
|
1052
|
+
const stage = this.$refs.stageEl
|
|
1053
|
+
this._apiDragMove = (ev) => {
|
|
1054
|
+
const rect = stage.getBoundingClientRect()
|
|
1055
|
+
const minW = 320 // 右栏最小宽度
|
|
1056
|
+
const midMin = Math.floor(window.innerWidth / 3) // 中栏最小宽度:屏宽 1/3
|
|
1057
|
+
const maxW = Math.max(minW, Math.floor(rect.width - this.previewWidth - midMin))
|
|
1058
|
+
const w = Math.max(minW, Math.min(maxW, rect.right - ev.clientX))
|
|
1059
|
+
this._apiPanelWidthPx = w
|
|
1060
|
+
}
|
|
1061
|
+
this._apiDragUp = () => {
|
|
1062
|
+
this.apiDragging = false
|
|
1063
|
+
document.body.style.cursor = ''
|
|
1064
|
+
document.body.style.userSelect = ''
|
|
1065
|
+
document.removeEventListener('mousemove', this._apiDragMove)
|
|
1066
|
+
document.removeEventListener('mouseup', this._apiDragUp)
|
|
1067
|
+
this.saveApiPanelWidth()
|
|
1068
|
+
}
|
|
1069
|
+
document.addEventListener('mousemove', this._apiDragMove)
|
|
1070
|
+
document.addEventListener('mouseup', this._apiDragUp)
|
|
1071
|
+
},
|
|
1072
|
+
// 读取上次保存的接口调用面板宽度(像素);首次打开(无记录)时保持默认 360px
|
|
1073
|
+
loadApiPanelWidth() {
|
|
1074
|
+
try {
|
|
1075
|
+
const v = localStorage.getItem('cardbuddy:api-panel-width')
|
|
1076
|
+
if (v != null) {
|
|
1077
|
+
const px = parseInt(v, 10)
|
|
1078
|
+
if (!Number.isNaN(px) && px >= 320) this._apiPanelWidthPx = px
|
|
1079
|
+
}
|
|
1080
|
+
} catch (e) { /* localStorage 不可用时忽略 */ }
|
|
1081
|
+
},
|
|
1082
|
+
// 持久化接口调用面板宽度(像素,取夹取后的实际值)到 localStorage
|
|
1083
|
+
saveApiPanelWidth() {
|
|
1084
|
+
try {
|
|
1085
|
+
localStorage.setItem('cardbuddy:api-panel-width', String(Math.round(this.apiPanelWidth)))
|
|
1086
|
+
} catch (e) { /* 忽略 */ }
|
|
1087
|
+
},
|
|
959
1088
|
/** 背景下拉:选中某项 */
|
|
960
1089
|
chooseBg(key) {
|
|
961
1090
|
this.canvasBg = key
|
|
@@ -1020,6 +1149,10 @@ export default {
|
|
|
1020
1149
|
/* localStorage 不可用时忽略 */
|
|
1021
1150
|
}
|
|
1022
1151
|
},
|
|
1152
|
+
/** 展开/收起右侧常驻「接口调用」面板(默认展开);持久化由 apiPanelOpen 监听器统一处理 */
|
|
1153
|
+
toggleApiPanel() {
|
|
1154
|
+
this.apiPanelOpen = !this.apiPanelOpen
|
|
1155
|
+
},
|
|
1023
1156
|
/**
|
|
1024
1157
|
* 整卡校验徽标(Q3:仅 ok/error,不展开 bindingErrors 明细)。
|
|
1025
1158
|
* 调 /__validate(契约硬校验)与 /__render-status(结构合法 + 渲染不抛错)两个既有端点,
|
|
@@ -2010,6 +2143,7 @@ export default {
|
|
|
2010
2143
|
title: '全量同步',
|
|
2011
2144
|
message: '即将从远端 FinMall 拉取完整卡片列表,并逐张覆盖本地同名卡片。本地独有卡片会保留,但被覆盖卡片的未保存改动将丢失,且无法撤销。是否继续?',
|
|
2012
2145
|
confirmText: '继续同步',
|
|
2146
|
+
severity: 'warn',
|
|
2013
2147
|
onConfirm: () => this.syncRemote(),
|
|
2014
2148
|
})
|
|
2015
2149
|
return
|
|
@@ -2022,7 +2156,7 @@ export default {
|
|
|
2022
2156
|
onConfirm: () => this.syncSelected(),
|
|
2023
2157
|
})
|
|
2024
2158
|
},
|
|
2025
|
-
/**
|
|
2159
|
+
/** 「远端」分组菜单项:版本推送 / 在线预览 / 切图上传(写覆盖类操作先二次确认) */
|
|
2026
2160
|
chooseRsave(target) {
|
|
2027
2161
|
if (target === 'save') {
|
|
2028
2162
|
if (!this.preview || !this.selectedId || this.savingRemote || !this.hasCardSource) return
|
|
@@ -2030,23 +2164,38 @@ export default {
|
|
|
2030
2164
|
title: '版本推送',
|
|
2031
2165
|
message: `即将把本地卡片「${this.selectedId}」的当前版本推送并覆盖到远端 FinMall。线上版本会被立即更新,且该操作不可撤销。是否继续?`,
|
|
2032
2166
|
confirmText: '继续推送',
|
|
2033
|
-
|
|
2167
|
+
severity: 'warn',
|
|
2034
2168
|
onConfirm: () => this.saveToRemote(),
|
|
2035
2169
|
})
|
|
2036
2170
|
return
|
|
2037
2171
|
}
|
|
2172
|
+
if (target === 'slices') {
|
|
2173
|
+
if (!this.preview || !this.selectedId || this.uploadingSlices) return
|
|
2174
|
+
this.requestConfirm({
|
|
2175
|
+
title: '切图上传',
|
|
2176
|
+
message:
|
|
2177
|
+
`即将把当前卡片「${this.selectedId}」引用的本地切图(src/slices/)上传到 FinMall 图片库,` +
|
|
2178
|
+
`并把卡片里的 /slices/ 引用替换为线上地址(@filestore/…)。本地切图文件保留不动;` +
|
|
2179
|
+
`内容未变的切图会直接复用已有线上地址,不重复上传。` +
|
|
2180
|
+
(this.historyDirty ? '\n当前卡片有未保存的改动,会先保存到本地再执行,避免被随后的重载丢弃。' : '') +
|
|
2181
|
+
'\n是否继续?',
|
|
2182
|
+
confirmText: '继续上传',
|
|
2183
|
+
onConfirm: () => this.uploadCardSlices(),
|
|
2184
|
+
})
|
|
2185
|
+
return
|
|
2186
|
+
}
|
|
2038
2187
|
if (!this.preview || !this.selectedId || !this.hasCardSource) return
|
|
2039
2188
|
this.remotePreviewOpen = true
|
|
2040
2189
|
},
|
|
2041
2190
|
/** 请求二次确认:统一在写/覆盖类操作前弹出,确认后执行 onConfirm 回调 */
|
|
2042
|
-
requestConfirm({ title, message, confirmText, cancelText,
|
|
2191
|
+
requestConfirm({ title, message, confirmText, cancelText, severity, onConfirm }) {
|
|
2043
2192
|
this.confirm = {
|
|
2044
2193
|
show: true,
|
|
2045
2194
|
title,
|
|
2046
2195
|
message,
|
|
2047
2196
|
confirmText: confirmText || '继续',
|
|
2048
2197
|
cancelText: cancelText || '取消',
|
|
2049
|
-
danger:
|
|
2198
|
+
severity: ['primary', 'warn', 'danger'].includes(severity) ? severity : 'primary',
|
|
2050
2199
|
onConfirm,
|
|
2051
2200
|
}
|
|
2052
2201
|
},
|
|
@@ -2217,6 +2366,81 @@ export default {
|
|
|
2217
2366
|
this.savingRemote = false
|
|
2218
2367
|
}
|
|
2219
2368
|
},
|
|
2369
|
+
/**
|
|
2370
|
+
* 把「当前卡片」引用的本地切图上传到 FinMall 图片库(CC0000.do),并把卡片里的
|
|
2371
|
+
* `/slices/<name>` 引用改写为返回的线上地址(`@filestore/...`)。
|
|
2372
|
+
* 经 dev-server 的 /__slice-upload 端点触发(端点内部复用 finmall-card-sync 的 uploadSlices,
|
|
2373
|
+
* 与「同步」「版本推送」共用同一后台 FinMall 会话)。纯前端无法直接调 FinMall:
|
|
2374
|
+
* 跨域 + 需 token(token 只在页面上下文内读取,不回传 Node),故必须经此中转。
|
|
2375
|
+
*
|
|
2376
|
+
* 纪律:
|
|
2377
|
+
* - **只在用户点击「远端 → 切图上传」时执行**,保存 / 推送 / 同步都不会顺带上传切图;
|
|
2378
|
+
* - 上传与改写范围都只限当前卡;共享切图若已被别的卡上传过(sha256 命中
|
|
2379
|
+
* src/slices/manifest.json),直接复用线上地址,不重复上传;
|
|
2380
|
+
* - 本地 src/slices 真源保留不动(映射清单记录 本地文件 ↔ 线上地址)。
|
|
2381
|
+
*/
|
|
2382
|
+
async uploadCardSlices() {
|
|
2383
|
+
if (!this.preview || !this.selectedId || this.uploadingSlices) return
|
|
2384
|
+
const id = this.selectedId
|
|
2385
|
+
this.uploadingSlices = true
|
|
2386
|
+
try {
|
|
2387
|
+
// 端点改写的是磁盘上的 schema.json;若编辑器里有未保存改动,先落盘再上传,
|
|
2388
|
+
// 否则随后从磁盘重载会把未保存编辑静默丢掉。
|
|
2389
|
+
if (this.historyDirty) {
|
|
2390
|
+
const saved = await this.saveCard()
|
|
2391
|
+
if (!saved) throw new Error('本地保存失败,已中止切图上传')
|
|
2392
|
+
}
|
|
2393
|
+
// 标记本窗口即将落盘(端点会改写 src/cards/<id>/schema.json),吸收 watcher 经 SSE 推回的
|
|
2394
|
+
// cards-changed 回声,避免误弹「卡片已在编辑器外被修改」提示。
|
|
2395
|
+
this.selfSaveId = id
|
|
2396
|
+
this.selfSaveAt = Date.now()
|
|
2397
|
+
const res = await apiFetch('/__slice-upload', {
|
|
2398
|
+
method: 'POST',
|
|
2399
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2400
|
+
body: JSON.stringify({ cardId: id }),
|
|
2401
|
+
})
|
|
2402
|
+
if (!res.ok) {
|
|
2403
|
+
const err = await res.json().catch(() => ({}))
|
|
2404
|
+
throw new Error(err.message || err.error || 'HTTP ' + res.status)
|
|
2405
|
+
}
|
|
2406
|
+
const r = await res.json()
|
|
2407
|
+
if (r.needCredentials) {
|
|
2408
|
+
// 凭据缺失:弹出登录表单,由用户填写账号密码后再点一次「切图上传」
|
|
2409
|
+
this.openLoginModal()
|
|
2410
|
+
return
|
|
2411
|
+
}
|
|
2412
|
+
// 上传可能耗时数秒(逐张传),落盘发生在响应前一刻:再刷一次自保存时间戳,
|
|
2413
|
+
// 让 2.5s 抑制窗口覆盖到这次回声(否则慢上传会漏抑制)。
|
|
2414
|
+
this.selfSaveAt = Date.now()
|
|
2415
|
+
if (!r.success) {
|
|
2416
|
+
const detail = (r.failed || []).map((f) => `${f.name}: ${f.error}`).join('; ')
|
|
2417
|
+
throw new Error(detail || '切图上传失败')
|
|
2418
|
+
}
|
|
2419
|
+
// 磁盘上的引用已被改写为线上地址,重新载入让预览即时生效。
|
|
2420
|
+
await this.onRefresh()
|
|
2421
|
+
const replaced = (r.rewritten || []).reduce((n, item) => n + item.count, 0)
|
|
2422
|
+
if (!r.referenced) {
|
|
2423
|
+
showAppToast('当前卡片没有引用本地切图,无需上传', 'ok')
|
|
2424
|
+
} else {
|
|
2425
|
+
showAppToast(
|
|
2426
|
+
`切图上传完成:新上传 ${r.uploaded.length} 张、复用 ${r.skipped.length} 张,替换引用 ${replaced} 处`,
|
|
2427
|
+
'ok'
|
|
2428
|
+
)
|
|
2429
|
+
}
|
|
2430
|
+
if (r.dangling && r.dangling.length) {
|
|
2431
|
+
showAppToast(`有 ${r.dangling.length} 处引用的切图在 src/slices 下不存在:${r.dangling.join('、')}`, 'err')
|
|
2432
|
+
}
|
|
2433
|
+
} catch (e) {
|
|
2434
|
+
console.error('[切图上传失败]', e)
|
|
2435
|
+
// 远端操作接口报错:仅弹出错误提示,不写入事件/绑定错误记录(避免污染事件面板)
|
|
2436
|
+
showAppToast('切图上传失败:' + (e && e.message), 'err')
|
|
2437
|
+
// 未落盘:清除自保存标记,避免误吞后续真正的外部改动
|
|
2438
|
+
this.selfSaveId = ''
|
|
2439
|
+
this.selfSaveAt = 0
|
|
2440
|
+
} finally {
|
|
2441
|
+
this.uploadingSlices = false
|
|
2442
|
+
}
|
|
2443
|
+
},
|
|
2220
2444
|
/** 实时拉取卡片列表(GET /__cards),替换下拉框选项,不缓存 */
|
|
2221
2445
|
async fetchCardList() {
|
|
2222
2446
|
try {
|
|
@@ -2309,7 +2533,6 @@ export default {
|
|
|
2309
2533
|
message: `卡片「${name}」的 schema / data 在编辑器外发生了变更(例如 IDE 或其他工具覆盖了文件)。\n当前编辑器存在未保存的改动,是否放弃本地未保存改动并重新载入磁盘版本?`,
|
|
2310
2534
|
confirmText: '重新载入',
|
|
2311
2535
|
cancelText: '保留我的改动',
|
|
2312
|
-
danger: false,
|
|
2313
2536
|
onConfirm: () => this.loadCard(id),
|
|
2314
2537
|
})
|
|
2315
2538
|
},
|