cardbuddy 0.6.0 → 0.7.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/dist/preview-editor/assets/{index-BYB0hngG.js → index-BlscexDE.js} +18 -11
- package/dist/preview-editor/assets/index-dlbSPb5W.css +1 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/server-core/cardFiles.js +9 -4
- package/dist/server-core/index.js +6 -0
- package/dist/server-core/sliceUpload.js +97 -0
- package/dist/vendored/App.vue +263 -44
- package/dist/vendored/assets/svgIcons/icons.svg +5 -0
- package/dist/vendored/components/ApiCallPanel.vue +115 -0
- package/dist/vendored/components/CardGallery.vue +2 -0
- package/dist/vendored/components/CardSelect.vue +6 -0
- package/dist/vendored/components/ConfirmDialog.vue +20 -5
- package/dist/vendored/components/DebugPanel.vue +22 -88
- package/dist/vendored/components/JsonTree.vue +35 -4
- package/dist/vendored/components/StructureTree.vue +53 -2
- package/dist/vendored/styles/app.css +142 -51
- package/dist/vendored/styles/card-gallery.css +20 -0
- package/dist/vendored/styles/card-select.css +15 -0
- package/dist/vendored/styles/json-tree.css +1 -1
- package/dist/vendored/styles/structure-tree.css +135 -5
- package/package.json +1 -1
- 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"
|
|
@@ -315,33 +337,59 @@
|
|
|
315
337
|
title="拖拽调整预览区域宽度"
|
|
316
338
|
></div>
|
|
317
339
|
|
|
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
|
-
|
|
340
|
+
<!-- 拖拽分隔条:位于调试面板与右侧「接口调用」面板之间,拖动调整右栏宽度。
|
|
341
|
+
约束:中栏(调试面板)最小宽度 ≥ 屏宽 1/3,右栏最小宽度 ≥ 320px(见 startApiDrag) -->
|
|
342
|
+
<div
|
|
343
|
+
v-show="!chromeOff && apiPanelOpen"
|
|
344
|
+
class="splitter splitter--api"
|
|
345
|
+
:class="{ 'is-dragging': apiDragging }"
|
|
346
|
+
@mousedown.prevent="startApiDrag"
|
|
347
|
+
title="拖拽调整接口调用面板宽度"
|
|
348
|
+
></div>
|
|
349
|
+
|
|
350
|
+
<!-- 中右工作区:上为「调试面板 + 接口调用面板」两栏,下为贯穿两栏的共同状态栏。
|
|
351
|
+
状态栏由 DebugPanel 内 Teleport 注入 #app-workside-bar;Teleport 目标须在 DOM 中
|
|
352
|
+
先于面板挂载,故 .workside__bar 写在前面,以 flex order 视觉置底。 -->
|
|
353
|
+
<div class="workside" v-show="!chromeOff">
|
|
354
|
+
<div id="app-workside-bar" class="workside__bar"></div>
|
|
355
|
+
<div class="workside__cols">
|
|
356
|
+
<DebugPanel
|
|
357
|
+
:chrome-off="chromeOff"
|
|
358
|
+
:rt="rt"
|
|
359
|
+
:preview="preview"
|
|
360
|
+
:schema="currentSchema"
|
|
361
|
+
:card-id="selectedId"
|
|
362
|
+
:reload-token="reloadToken"
|
|
363
|
+
:card-count="cardCount"
|
|
364
|
+
:selected-node-id="selectedNodeId"
|
|
365
|
+
:move-collapse-token="moveCollapseToken"
|
|
366
|
+
:active-tab="activeTab"
|
|
367
|
+
@schema-changed="onSchemaChanged"
|
|
368
|
+
@schema-commit="onSchemaCommit"
|
|
369
|
+
@select-node="onSelectNode"
|
|
370
|
+
@update:active-tab="activeTab = $event"
|
|
371
|
+
@add-node="onAddNode"
|
|
372
|
+
@move-node="onMoveNode"
|
|
373
|
+
@hover-node="onHoverNode"
|
|
374
|
+
@copy-selected="copySelectedNode"
|
|
375
|
+
@paste="pasteNode"
|
|
376
|
+
@duplicate="duplicateNode"
|
|
377
|
+
@delete-selected="deleteSelectedNode"
|
|
378
|
+
@reset-import="onRefresh"
|
|
379
|
+
:dev-svc-class="devSvcClass"
|
|
380
|
+
:dev-svc-title="devSvcTitle"
|
|
381
|
+
:dev-svc-label="devSvcLabel"
|
|
382
|
+
/>
|
|
383
|
+
|
|
384
|
+
<!-- 右侧常驻「接口调用」面板:与预览列 / 调试面板呈左中右布局,
|
|
385
|
+
展开/收起由顶栏按钮与面板内 ✕ 控制,初始状态持久化 -->
|
|
386
|
+
<ApiCallPanel
|
|
387
|
+
v-if="apiPanelOpen"
|
|
388
|
+
:rt="rt"
|
|
389
|
+
@close="apiPanelOpen = false"
|
|
390
|
+
/>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
345
393
|
</main>
|
|
346
394
|
<!-- 远端预览二维码弹层:点击顶栏「远端预览」按钮打开,手机(FinMall App)扫码真机查看 -->
|
|
347
395
|
<RemotePreview
|
|
@@ -413,7 +461,7 @@
|
|
|
413
461
|
:message="confirm.message"
|
|
414
462
|
:confirm-text="confirm.confirmText"
|
|
415
463
|
:cancel-text="confirm.cancelText"
|
|
416
|
-
:
|
|
464
|
+
:severity="confirm.severity"
|
|
417
465
|
@confirm="onConfirmDialog"
|
|
418
466
|
@cancel="onCancelDialog"
|
|
419
467
|
/>
|
|
@@ -444,6 +492,7 @@ import {
|
|
|
444
492
|
} from '@/runtime/clipboard-node.js'
|
|
445
493
|
import RenderTree from '@/components/RenderTree.vue'
|
|
446
494
|
import DebugPanel from '@/components/DebugPanel.vue'
|
|
495
|
+
import ApiCallPanel from '@/components/ApiCallPanel.vue'
|
|
447
496
|
import CardSelect from '@/components/CardSelect.vue'
|
|
448
497
|
import AlertCardLayer from '@/components/AlertCardLayer.vue'
|
|
449
498
|
import DeviceOverlay from '@/components/DeviceOverlay.vue'
|
|
@@ -482,7 +531,7 @@ if (typeof window !== 'undefined' && import.meta.env.DEV) window.__rt = cardRunt
|
|
|
482
531
|
|
|
483
532
|
export default {
|
|
484
533
|
name: 'App',
|
|
485
|
-
components: { RenderTree, DebugPanel, CardSelect, AlertCardLayer, DeviceOverlay, PickerOverlay, DatePickerOverlay, SelectionOverlay, HoverGuides, RemotePreview, AppToast, ConfirmDialog, CardGallery, ComponentPalette, Switch },
|
|
534
|
+
components: { RenderTree, DebugPanel, ApiCallPanel, CardSelect, AlertCardLayer, DeviceOverlay, PickerOverlay, DatePickerOverlay, SelectionOverlay, HoverGuides, RemotePreview, AppToast, ConfirmDialog, CardGallery, ComponentPalette, Switch },
|
|
486
535
|
provide() {
|
|
487
536
|
return {
|
|
488
537
|
adapter,
|
|
@@ -536,9 +585,12 @@ export default {
|
|
|
536
585
|
localSchema: null,
|
|
537
586
|
localData: null,
|
|
538
587
|
selectedNodeId: '',
|
|
539
|
-
// 当前激活的调试面板 tab
|
|
588
|
+
// 当前激活的调试面板 tab(绑定数据/组件结构/卡片来源)。
|
|
540
589
|
// 画布 Ctrl/⌘ 点击会自动切到「组件结构」tab 并在节点树聚焦选中,无需预先停在结构树 tab。
|
|
541
590
|
activeTab: 'scope',
|
|
591
|
+
// 右侧常驻「接口调用」面板:展开/收起由顶栏按钮与面板内 ✕ 控制,
|
|
592
|
+
// 初始状态持久化到 localStorage(cardbuddy:api-panel-open)
|
|
593
|
+
apiPanelOpen: true,
|
|
542
594
|
// 组件结构树「添加」面板:addNodePanelOpen 控制弹层显隐;addNodeTarget 记录
|
|
543
595
|
// 插入锚点与模式({ node, mode },来自结构树 add-node 事件):
|
|
544
596
|
// node = 右键/选中 viewNode(null 或 Root → 视作根);mode 决定落位规则:
|
|
@@ -565,20 +617,22 @@ export default {
|
|
|
565
617
|
remotePreviewOpen: false, // 「远端预览」二维码弹层开关
|
|
566
618
|
syncTarget: null, // 'full' | 'selected' | null —— 标记上一次/正在进行的同步模式,用于按钮反馈
|
|
567
619
|
syncTimer: null,
|
|
620
|
+
uploadingSlices: false, // 「切图上传」进行中(远端分组,仅用户点击触发,无自动上传)
|
|
568
621
|
// 登录凭据弹窗(远程同步缺凭据时弹出,填写账号密码写入 .local/credentials.json)
|
|
569
622
|
loginModal: false,
|
|
570
623
|
loginName: '',
|
|
571
624
|
loginPassword: '',
|
|
572
625
|
loginSubmitting: false,
|
|
573
626
|
loginError: '',
|
|
574
|
-
// 二次确认弹窗(版本推送 / 当前同步 / 全量同步 等写/覆盖类操作前弹出,确认后才执行 onConfirm
|
|
627
|
+
// 二次确认弹窗(版本推送 / 当前同步 / 全量同步 等写/覆盖类操作前弹出,确认后才执行 onConfirm)。
|
|
628
|
+
// severity 三档:primary 常规 / warn 警告(橙黄+警告三角,覆盖类操作:版本推送、全量同步)/ danger 危险(红+禁止,仅明确负面结果如删除,暂无内置操作使用)
|
|
575
629
|
confirm: {
|
|
576
630
|
show: false,
|
|
577
631
|
title: '',
|
|
578
632
|
message: '',
|
|
579
633
|
confirmText: '继续',
|
|
580
634
|
cancelText: '取消',
|
|
581
|
-
|
|
635
|
+
severity: 'primary',
|
|
582
636
|
onConfirm: null,
|
|
583
637
|
},
|
|
584
638
|
// 编辑历史(撤销/重做):每个快照为 { schema, data },data 即 rt.rootScope(数据绑定),
|
|
@@ -599,6 +653,7 @@ export default {
|
|
|
599
653
|
],
|
|
600
654
|
// —— 预览区宽度可调(拖拽分隔条 / 预设尺寸,画布按实际像素宽度布局,无 transform 缩放)——
|
|
601
655
|
_previewWidthPx: 372, // 预览列宽度(像素,持久化到 localStorage);第一次打开未主动设置时默认 372px
|
|
656
|
+
_apiPanelWidthPx: 360, // 右侧「接口调用」面板宽度(像素,持久化到 localStorage);默认 360px
|
|
602
657
|
presetSizes: [ // 预设尺寸下拉选项(仅宽度;高度随列高动态一致)
|
|
603
658
|
{ key: 375, value: '375' },
|
|
604
659
|
{ key: 430, value: '430' },
|
|
@@ -607,6 +662,7 @@ export default {
|
|
|
607
662
|
stageWidth: typeof window !== 'undefined' ? window.innerWidth : 1280, // 舞台实际宽度(ResizeObserver 实测后更新)
|
|
608
663
|
stageHeight: typeof window !== 'undefined' ? window.innerHeight - 52 : 748, // 舞台实际高度(contain 缩放的“高度上限”基准)
|
|
609
664
|
dragging: false, // 是否正在拖拽分隔条
|
|
665
|
+
apiDragging: false, // 是否正在拖拽「接口调用」面板分隔条
|
|
610
666
|
_stageObserver: null,
|
|
611
667
|
_dragMove: null,
|
|
612
668
|
_dragUp: null,
|
|
@@ -745,6 +801,15 @@ export default {
|
|
|
745
801
|
canvasW() {
|
|
746
802
|
return Math.round(this.previewWidth || 375)
|
|
747
803
|
},
|
|
804
|
+
// 右侧「接口调用」面板宽度(像素):限制在 [320, 舞台宽度 - 预览列宽 - 中栏最小宽(屏宽 1/3)],
|
|
805
|
+
// 保证中栏(调试面板)最小宽度不小于三分之一屏幕宽度、右栏不小于 320px
|
|
806
|
+
apiPanelWidth() {
|
|
807
|
+
const minW = 320 // 右栏最小宽度
|
|
808
|
+
const midMin = Math.floor((typeof window !== 'undefined' ? window.innerWidth : 1280) / 3)
|
|
809
|
+
const maxW = Math.max(minW, Math.floor((this.stageWidth || 0) - this.previewWidth - midMin))
|
|
810
|
+
const w = this._apiPanelWidthPx || 360
|
|
811
|
+
return Math.max(minW, Math.min(maxW, w))
|
|
812
|
+
},
|
|
748
813
|
// 缩放外框占满整列(宽高交由 CSS flex 填满;画布不再做 transform 缩放,直接按实际尺寸布局)
|
|
749
814
|
frameStyle() {
|
|
750
815
|
return {}
|
|
@@ -763,6 +828,14 @@ export default {
|
|
|
763
828
|
theme(val) {
|
|
764
829
|
document.documentElement.dataset.theme = val
|
|
765
830
|
},
|
|
831
|
+
// 「接口调用」面板展开/收起状态变化(顶栏按钮 toggleApiPanel 或面板内 ✕ 关闭)时统一持久化
|
|
832
|
+
apiPanelOpen(val) {
|
|
833
|
+
try {
|
|
834
|
+
localStorage.setItem('cardbuddy:api-panel-open', val ? '1' : '0')
|
|
835
|
+
} catch (e) {
|
|
836
|
+
/* localStorage 不可用时忽略 */
|
|
837
|
+
}
|
|
838
|
+
},
|
|
766
839
|
},
|
|
767
840
|
beforeUnmount() {
|
|
768
841
|
if (this.viewportObserver) this.viewportObserver.disconnect()
|
|
@@ -784,6 +857,7 @@ export default {
|
|
|
784
857
|
if (this.$refs.phoneViewport) this.viewportObserver.observe(this.$refs.phoneViewport)
|
|
785
858
|
// 预览区宽度可调:实测舞台尺寸,驱动 previewWidth/frameStyle 自适应窗口变化
|
|
786
859
|
this.loadPreviewWidth()
|
|
860
|
+
this.loadApiPanelWidth()
|
|
787
861
|
this._stageObserver = new ResizeObserver((entries) => {
|
|
788
862
|
for (const entry of entries) {
|
|
789
863
|
this.stageWidth = Math.round(entry.contentRect.width)
|
|
@@ -809,6 +883,13 @@ export default {
|
|
|
809
883
|
/* localStorage 不可用时忽略 */
|
|
810
884
|
}
|
|
811
885
|
document.documentElement.dataset.theme = this.theme
|
|
886
|
+
// 右侧「接口调用」面板:恢复本地持久化的展开/收起状态(默认展开)
|
|
887
|
+
try {
|
|
888
|
+
const v = localStorage.getItem('cardbuddy:api-panel-open')
|
|
889
|
+
if (v === '0') this.apiPanelOpen = false
|
|
890
|
+
} catch (e) {
|
|
891
|
+
/* localStorage 不可用时忽略 */
|
|
892
|
+
}
|
|
812
893
|
// 实时拉取卡片列表(不再依赖启动快照),再载入首张卡
|
|
813
894
|
await this.fetchCardList()
|
|
814
895
|
// 深链:若 URL 带 ?card=<id> 且命中列表,则预选该卡(供「远端预览」二维码扫码直达)
|
|
@@ -956,6 +1037,50 @@ export default {
|
|
|
956
1037
|
this._previewWidthPx = Math.max(minW, Math.min(maxW, w))
|
|
957
1038
|
this.savePreviewWidth()
|
|
958
1039
|
},
|
|
1040
|
+
// —— 接口调用面板宽度调节 ——
|
|
1041
|
+
// 拖拽分隔条:拖动改变右栏宽度(向左拖加宽、向右拖收窄),松手持久化。
|
|
1042
|
+
// 约束:右栏 [320px, 舞台宽 - 预览列宽 - 中栏最小宽(屏宽 1/3)],与 apiPanelWidth computed 一致
|
|
1043
|
+
startApiDrag() {
|
|
1044
|
+
if (this.chromeOff || !this.apiPanelOpen) return
|
|
1045
|
+
this.apiDragging = true
|
|
1046
|
+
document.body.style.cursor = 'col-resize'
|
|
1047
|
+
document.body.style.userSelect = 'none'
|
|
1048
|
+
const stage = this.$refs.stageEl
|
|
1049
|
+
this._apiDragMove = (ev) => {
|
|
1050
|
+
const rect = stage.getBoundingClientRect()
|
|
1051
|
+
const minW = 320 // 右栏最小宽度
|
|
1052
|
+
const midMin = Math.floor(window.innerWidth / 3) // 中栏最小宽度:屏宽 1/3
|
|
1053
|
+
const maxW = Math.max(minW, Math.floor(rect.width - this.previewWidth - midMin))
|
|
1054
|
+
const w = Math.max(minW, Math.min(maxW, rect.right - ev.clientX))
|
|
1055
|
+
this._apiPanelWidthPx = w
|
|
1056
|
+
}
|
|
1057
|
+
this._apiDragUp = () => {
|
|
1058
|
+
this.apiDragging = false
|
|
1059
|
+
document.body.style.cursor = ''
|
|
1060
|
+
document.body.style.userSelect = ''
|
|
1061
|
+
document.removeEventListener('mousemove', this._apiDragMove)
|
|
1062
|
+
document.removeEventListener('mouseup', this._apiDragUp)
|
|
1063
|
+
this.saveApiPanelWidth()
|
|
1064
|
+
}
|
|
1065
|
+
document.addEventListener('mousemove', this._apiDragMove)
|
|
1066
|
+
document.addEventListener('mouseup', this._apiDragUp)
|
|
1067
|
+
},
|
|
1068
|
+
// 读取上次保存的接口调用面板宽度(像素);首次打开(无记录)时保持默认 360px
|
|
1069
|
+
loadApiPanelWidth() {
|
|
1070
|
+
try {
|
|
1071
|
+
const v = localStorage.getItem('cardbuddy:api-panel-width')
|
|
1072
|
+
if (v != null) {
|
|
1073
|
+
const px = parseInt(v, 10)
|
|
1074
|
+
if (!Number.isNaN(px) && px >= 320) this._apiPanelWidthPx = px
|
|
1075
|
+
}
|
|
1076
|
+
} catch (e) { /* localStorage 不可用时忽略 */ }
|
|
1077
|
+
},
|
|
1078
|
+
// 持久化接口调用面板宽度(像素,取夹取后的实际值)到 localStorage
|
|
1079
|
+
saveApiPanelWidth() {
|
|
1080
|
+
try {
|
|
1081
|
+
localStorage.setItem('cardbuddy:api-panel-width', String(Math.round(this.apiPanelWidth)))
|
|
1082
|
+
} catch (e) { /* 忽略 */ }
|
|
1083
|
+
},
|
|
959
1084
|
/** 背景下拉:选中某项 */
|
|
960
1085
|
chooseBg(key) {
|
|
961
1086
|
this.canvasBg = key
|
|
@@ -1020,6 +1145,10 @@ export default {
|
|
|
1020
1145
|
/* localStorage 不可用时忽略 */
|
|
1021
1146
|
}
|
|
1022
1147
|
},
|
|
1148
|
+
/** 展开/收起右侧常驻「接口调用」面板(默认展开);持久化由 apiPanelOpen 监听器统一处理 */
|
|
1149
|
+
toggleApiPanel() {
|
|
1150
|
+
this.apiPanelOpen = !this.apiPanelOpen
|
|
1151
|
+
},
|
|
1023
1152
|
/**
|
|
1024
1153
|
* 整卡校验徽标(Q3:仅 ok/error,不展开 bindingErrors 明细)。
|
|
1025
1154
|
* 调 /__validate(契约硬校验)与 /__render-status(结构合法 + 渲染不抛错)两个既有端点,
|
|
@@ -2010,6 +2139,7 @@ export default {
|
|
|
2010
2139
|
title: '全量同步',
|
|
2011
2140
|
message: '即将从远端 FinMall 拉取完整卡片列表,并逐张覆盖本地同名卡片。本地独有卡片会保留,但被覆盖卡片的未保存改动将丢失,且无法撤销。是否继续?',
|
|
2012
2141
|
confirmText: '继续同步',
|
|
2142
|
+
severity: 'warn',
|
|
2013
2143
|
onConfirm: () => this.syncRemote(),
|
|
2014
2144
|
})
|
|
2015
2145
|
return
|
|
@@ -2022,7 +2152,7 @@ export default {
|
|
|
2022
2152
|
onConfirm: () => this.syncSelected(),
|
|
2023
2153
|
})
|
|
2024
2154
|
},
|
|
2025
|
-
/**
|
|
2155
|
+
/** 「远端」分组菜单项:版本推送 / 在线预览 / 切图上传(写覆盖类操作先二次确认) */
|
|
2026
2156
|
chooseRsave(target) {
|
|
2027
2157
|
if (target === 'save') {
|
|
2028
2158
|
if (!this.preview || !this.selectedId || this.savingRemote || !this.hasCardSource) return
|
|
@@ -2030,23 +2160,38 @@ export default {
|
|
|
2030
2160
|
title: '版本推送',
|
|
2031
2161
|
message: `即将把本地卡片「${this.selectedId}」的当前版本推送并覆盖到远端 FinMall。线上版本会被立即更新,且该操作不可撤销。是否继续?`,
|
|
2032
2162
|
confirmText: '继续推送',
|
|
2033
|
-
|
|
2163
|
+
severity: 'warn',
|
|
2034
2164
|
onConfirm: () => this.saveToRemote(),
|
|
2035
2165
|
})
|
|
2036
2166
|
return
|
|
2037
2167
|
}
|
|
2168
|
+
if (target === 'slices') {
|
|
2169
|
+
if (!this.preview || !this.selectedId || this.uploadingSlices) return
|
|
2170
|
+
this.requestConfirm({
|
|
2171
|
+
title: '切图上传',
|
|
2172
|
+
message:
|
|
2173
|
+
`即将把当前卡片「${this.selectedId}」引用的本地切图(src/slices/)上传到 FinMall 图片库,` +
|
|
2174
|
+
`并把卡片里的 /slices/ 引用替换为线上地址(@filestore/…)。本地切图文件保留不动;` +
|
|
2175
|
+
`内容未变的切图会直接复用已有线上地址,不重复上传。` +
|
|
2176
|
+
(this.historyDirty ? '\n当前卡片有未保存的改动,会先保存到本地再执行,避免被随后的重载丢弃。' : '') +
|
|
2177
|
+
'\n是否继续?',
|
|
2178
|
+
confirmText: '继续上传',
|
|
2179
|
+
onConfirm: () => this.uploadCardSlices(),
|
|
2180
|
+
})
|
|
2181
|
+
return
|
|
2182
|
+
}
|
|
2038
2183
|
if (!this.preview || !this.selectedId || !this.hasCardSource) return
|
|
2039
2184
|
this.remotePreviewOpen = true
|
|
2040
2185
|
},
|
|
2041
2186
|
/** 请求二次确认:统一在写/覆盖类操作前弹出,确认后执行 onConfirm 回调 */
|
|
2042
|
-
requestConfirm({ title, message, confirmText, cancelText,
|
|
2187
|
+
requestConfirm({ title, message, confirmText, cancelText, severity, onConfirm }) {
|
|
2043
2188
|
this.confirm = {
|
|
2044
2189
|
show: true,
|
|
2045
2190
|
title,
|
|
2046
2191
|
message,
|
|
2047
2192
|
confirmText: confirmText || '继续',
|
|
2048
2193
|
cancelText: cancelText || '取消',
|
|
2049
|
-
danger:
|
|
2194
|
+
severity: ['primary', 'warn', 'danger'].includes(severity) ? severity : 'primary',
|
|
2050
2195
|
onConfirm,
|
|
2051
2196
|
}
|
|
2052
2197
|
},
|
|
@@ -2217,6 +2362,81 @@ export default {
|
|
|
2217
2362
|
this.savingRemote = false
|
|
2218
2363
|
}
|
|
2219
2364
|
},
|
|
2365
|
+
/**
|
|
2366
|
+
* 把「当前卡片」引用的本地切图上传到 FinMall 图片库(CC0000.do),并把卡片里的
|
|
2367
|
+
* `/slices/<name>` 引用改写为返回的线上地址(`@filestore/...`)。
|
|
2368
|
+
* 经 dev-server 的 /__slice-upload 端点触发(端点内部复用 finmall-card-sync 的 uploadSlices,
|
|
2369
|
+
* 与「同步」「版本推送」共用同一后台 FinMall 会话)。纯前端无法直接调 FinMall:
|
|
2370
|
+
* 跨域 + 需 token(token 只在页面上下文内读取,不回传 Node),故必须经此中转。
|
|
2371
|
+
*
|
|
2372
|
+
* 纪律:
|
|
2373
|
+
* - **只在用户点击「远端 → 切图上传」时执行**,保存 / 推送 / 同步都不会顺带上传切图;
|
|
2374
|
+
* - 上传与改写范围都只限当前卡;共享切图若已被别的卡上传过(sha256 命中
|
|
2375
|
+
* src/slices/manifest.json),直接复用线上地址,不重复上传;
|
|
2376
|
+
* - 本地 src/slices 真源保留不动(映射清单记录 本地文件 ↔ 线上地址)。
|
|
2377
|
+
*/
|
|
2378
|
+
async uploadCardSlices() {
|
|
2379
|
+
if (!this.preview || !this.selectedId || this.uploadingSlices) return
|
|
2380
|
+
const id = this.selectedId
|
|
2381
|
+
this.uploadingSlices = true
|
|
2382
|
+
try {
|
|
2383
|
+
// 端点改写的是磁盘上的 schema.json;若编辑器里有未保存改动,先落盘再上传,
|
|
2384
|
+
// 否则随后从磁盘重载会把未保存编辑静默丢掉。
|
|
2385
|
+
if (this.historyDirty) {
|
|
2386
|
+
const saved = await this.saveCard()
|
|
2387
|
+
if (!saved) throw new Error('本地保存失败,已中止切图上传')
|
|
2388
|
+
}
|
|
2389
|
+
// 标记本窗口即将落盘(端点会改写 src/cards/<id>/schema.json),吸收 watcher 经 SSE 推回的
|
|
2390
|
+
// cards-changed 回声,避免误弹「卡片已在编辑器外被修改」提示。
|
|
2391
|
+
this.selfSaveId = id
|
|
2392
|
+
this.selfSaveAt = Date.now()
|
|
2393
|
+
const res = await apiFetch('/__slice-upload', {
|
|
2394
|
+
method: 'POST',
|
|
2395
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2396
|
+
body: JSON.stringify({ cardId: id }),
|
|
2397
|
+
})
|
|
2398
|
+
if (!res.ok) {
|
|
2399
|
+
const err = await res.json().catch(() => ({}))
|
|
2400
|
+
throw new Error(err.message || err.error || 'HTTP ' + res.status)
|
|
2401
|
+
}
|
|
2402
|
+
const r = await res.json()
|
|
2403
|
+
if (r.needCredentials) {
|
|
2404
|
+
// 凭据缺失:弹出登录表单,由用户填写账号密码后再点一次「切图上传」
|
|
2405
|
+
this.openLoginModal()
|
|
2406
|
+
return
|
|
2407
|
+
}
|
|
2408
|
+
// 上传可能耗时数秒(逐张传),落盘发生在响应前一刻:再刷一次自保存时间戳,
|
|
2409
|
+
// 让 2.5s 抑制窗口覆盖到这次回声(否则慢上传会漏抑制)。
|
|
2410
|
+
this.selfSaveAt = Date.now()
|
|
2411
|
+
if (!r.success) {
|
|
2412
|
+
const detail = (r.failed || []).map((f) => `${f.name}: ${f.error}`).join('; ')
|
|
2413
|
+
throw new Error(detail || '切图上传失败')
|
|
2414
|
+
}
|
|
2415
|
+
// 磁盘上的引用已被改写为线上地址,重新载入让预览即时生效。
|
|
2416
|
+
await this.onRefresh()
|
|
2417
|
+
const replaced = (r.rewritten || []).reduce((n, item) => n + item.count, 0)
|
|
2418
|
+
if (!r.referenced) {
|
|
2419
|
+
showAppToast('当前卡片没有引用本地切图,无需上传', 'ok')
|
|
2420
|
+
} else {
|
|
2421
|
+
showAppToast(
|
|
2422
|
+
`切图上传完成:新上传 ${r.uploaded.length} 张、复用 ${r.skipped.length} 张,替换引用 ${replaced} 处`,
|
|
2423
|
+
'ok'
|
|
2424
|
+
)
|
|
2425
|
+
}
|
|
2426
|
+
if (r.dangling && r.dangling.length) {
|
|
2427
|
+
showAppToast(`有 ${r.dangling.length} 处引用的切图在 src/slices 下不存在:${r.dangling.join('、')}`, 'err')
|
|
2428
|
+
}
|
|
2429
|
+
} catch (e) {
|
|
2430
|
+
console.error('[切图上传失败]', e)
|
|
2431
|
+
// 远端操作接口报错:仅弹出错误提示,不写入事件/绑定错误记录(避免污染事件面板)
|
|
2432
|
+
showAppToast('切图上传失败:' + (e && e.message), 'err')
|
|
2433
|
+
// 未落盘:清除自保存标记,避免误吞后续真正的外部改动
|
|
2434
|
+
this.selfSaveId = ''
|
|
2435
|
+
this.selfSaveAt = 0
|
|
2436
|
+
} finally {
|
|
2437
|
+
this.uploadingSlices = false
|
|
2438
|
+
}
|
|
2439
|
+
},
|
|
2220
2440
|
/** 实时拉取卡片列表(GET /__cards),替换下拉框选项,不缓存 */
|
|
2221
2441
|
async fetchCardList() {
|
|
2222
2442
|
try {
|
|
@@ -2309,7 +2529,6 @@ export default {
|
|
|
2309
2529
|
message: `卡片「${name}」的 schema / data 在编辑器外发生了变更(例如 IDE 或其他工具覆盖了文件)。\n当前编辑器存在未保存的改动,是否放弃本地未保存改动并重新载入磁盘版本?`,
|
|
2310
2530
|
confirmText: '重新载入',
|
|
2311
2531
|
cancelText: '保留我的改动',
|
|
2312
|
-
danger: false,
|
|
2313
2532
|
onConfirm: () => this.loadCard(id),
|
|
2314
2533
|
})
|
|
2315
2534
|
},
|
|
@@ -12,11 +12,16 @@
|
|
|
12
12
|
<symbol id="icon-cloud-alert" viewBox="0 0 24 24"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path><line x1="12" y1="13" x2="12" y2="16"></line><line x1="12" y1="18" x2="12.01" y2="18"></line></symbol>
|
|
13
13
|
<symbol id="icon-cloud" viewBox="0 0 24 24"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></symbol>
|
|
14
14
|
<symbol id="icon-upload-cloud" viewBox="0 0 24 24"><path d="M12 13v8"></path><path d="M8 17l4-4 4 4"></path><path d="M20 16.5A4.5 4.5 0 0 0 17.5 8h-1.26A7 7 0 1 0 4 14.9"></path></symbol>
|
|
15
|
+
<!-- 切图上传(image-upload):图片框 + 框内上箭头,语义「把本地切图传到远端图床」,
|
|
16
|
+
与 icon-upload-cloud(版本推送)区分开:一个传素材,一个传卡片版本。 -->
|
|
17
|
+
<symbol id="icon-image-upload" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><path d="M12 19v-7"></path><path d="M9 15l3-3 3 3"></path></symbol>
|
|
15
18
|
<symbol id="icon-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><path d="M14 14h3v3M21 14v7h-7v-3"></path></symbol>
|
|
16
19
|
<symbol id="icon-sync-box" viewBox="0 0 24 24"><rect x="3" y="5" width="14" height="14" rx="2" ry="2"></rect><path d="M21 7v10a2 2 0 0 1-2 2H7"></path></symbol>
|
|
17
20
|
<symbol id="icon-locate" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7"></circle><line x1="12" y1="1" x2="12" y2="5"></line><line x1="12" y1="19" x2="12" y2="23"></line><line x1="1" y1="12" x2="5" y2="12"></line><line x1="19" y1="12" x2="23" y2="12"></line></symbol>
|
|
18
21
|
<symbol id="icon-gear" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></symbol>
|
|
19
22
|
<symbol id="icon-no-entry" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><line x1="6" y1="6" x2="18" y2="18"></line></symbol>
|
|
23
|
+
<!-- 警告三角(alert-triangle):确认框 warn 级标题图标(警告操作,橙黄档);danger 级用 icon-no-entry -->
|
|
24
|
+
<symbol id="icon-alert-triangle" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></symbol>
|
|
20
25
|
<symbol id="icon-copy-sm" viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></symbol>
|
|
21
26
|
<symbol id="icon-trash" viewBox="0 0 24 24"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></symbol>
|
|
22
27
|
<!-- 连接(link):本地开发服务「连接前后端」语义图标,呼应主题【连接】 -->
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
接口调用面板:右侧常驻独立面板(由 App 顶栏按钮 / 面板内 ✕ 控制展开收起)。
|
|
3
|
+
展示 ynet.callJSAPI / postMessage 的调用流水,内容原属调试面板的「接口调用」tab,
|
|
4
|
+
拆出后调试面板聚焦「绑定数据 / 组件结构 / 卡片来源」,接口流水可常驻可视。
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<aside class="apipanel">
|
|
8
|
+
<div class="apipanel__head">
|
|
9
|
+
<span class="apipanel__title">
|
|
10
|
+
API调用
|
|
11
|
+
<span v-if="apiCalls.length" class="debug__count">{{ apiCalls.length }}</span>
|
|
12
|
+
</span>
|
|
13
|
+
<span class="apipanel__spacer"></span>
|
|
14
|
+
<button
|
|
15
|
+
class="apipanel__btn"
|
|
16
|
+
type="button"
|
|
17
|
+
:disabled="!apiCalls.length"
|
|
18
|
+
title="清空日志"
|
|
19
|
+
@click="clearApiLog"
|
|
20
|
+
>
|
|
21
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
22
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-trash" /></svg>
|
|
23
|
+
</button>
|
|
24
|
+
<button
|
|
25
|
+
class="apipanel__btn"
|
|
26
|
+
type="button"
|
|
27
|
+
title="收起接口调用面板"
|
|
28
|
+
@click="$emit('close')"
|
|
29
|
+
>
|
|
30
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
31
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="5" x2="19" y2="19" /><line x1="19" y1="5" x2="5" y2="19" /></svg>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="apipanel__body">
|
|
35
|
+
<p v-if="!apiCalls.length" class="debug__empty">暂无接口调用</p>
|
|
36
|
+
<ul class="debug__list">
|
|
37
|
+
<li v-for="(c, i) in apiCalls" :key="i" class="debug__item debug__item--api">
|
|
38
|
+
<div class="debug__api-row">
|
|
39
|
+
<span class="debug__kind">{{ c.api }}</span>
|
|
40
|
+
<button
|
|
41
|
+
class="debug__copy-ic"
|
|
42
|
+
type="button"
|
|
43
|
+
:title="copiedIndex === i ? '已复制 ✓' : '复制数据(不含方法名)'"
|
|
44
|
+
@click="copyApiPayload(c, i)"
|
|
45
|
+
>
|
|
46
|
+
<svg v-if="copiedIndex !== i" width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
47
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-copy-sm" /></svg>
|
|
48
|
+
<span v-else class="debug__copy-ok">✓</span>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
<!-- 返回数据:结构化折叠树(Chrome 控制台风格) -->
|
|
52
|
+
<ApiPayloadTree :value="c.payload" />
|
|
53
|
+
</li>
|
|
54
|
+
</ul>
|
|
55
|
+
</div>
|
|
56
|
+
</aside>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script>
|
|
60
|
+
import ApiPayloadTree from './ApiPayloadTree.vue'
|
|
61
|
+
import { copyText } from '@/runtime/clipboard.js'
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
name: 'ApiCallPanel',
|
|
65
|
+
components: { ApiPayloadTree },
|
|
66
|
+
props: {
|
|
67
|
+
rt: { type: Object, required: true },
|
|
68
|
+
},
|
|
69
|
+
emits: ['close'],
|
|
70
|
+
data() {
|
|
71
|
+
return {
|
|
72
|
+
// 刚复制过数据的条目下标(用于图标变 ✓ 反馈),null=无
|
|
73
|
+
copiedIndex: null,
|
|
74
|
+
apiCopyTimer: null,
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
computed: {
|
|
78
|
+
// 依赖 apiRev,使接口调用流水在 pushApiCall / clearApiCalls 后实时刷新
|
|
79
|
+
// (apiCalls 本身是非响应式数组,模板直接读它不会触发重渲染)
|
|
80
|
+
//
|
|
81
|
+
// ⚠️ 必须 slice() 返回**新数组**,不能直接 return 原数组引用:
|
|
82
|
+
// Vue 3.4+ 引入了「computed 值稳定性」——重算后若新值与旧值 Object.is 相等,
|
|
83
|
+
// 就不再通知下游 effect。原数组是同一个引用,即使 apiRev 自增触发了重算,
|
|
84
|
+
// 渲染 effect 也收不到通知,界面纹丝不动。
|
|
85
|
+
apiCalls() {
|
|
86
|
+
void this.rt.apiRev
|
|
87
|
+
return this.rt.apiCalls.slice()
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
methods: {
|
|
91
|
+
/** 清空接口调用流水:调用运行时 clearApiCalls(清空数组 + 自增 apiRev),
|
|
92
|
+
* 因 apiCalls computed 返回 slice() 新引用,触发实时重渲染 */
|
|
93
|
+
clearApiLog() {
|
|
94
|
+
this.rt.clearApiCalls()
|
|
95
|
+
},
|
|
96
|
+
/** 复制某条接口调用的「返回数据」(即 payload 全文),不含方法名(api) */
|
|
97
|
+
async copyApiPayload(c, i) {
|
|
98
|
+
let text = ''
|
|
99
|
+
try {
|
|
100
|
+
text = JSON.stringify(c.payload, null, 2)
|
|
101
|
+
} catch {
|
|
102
|
+
text = String(c.payload)
|
|
103
|
+
}
|
|
104
|
+
const ok = await copyText(text)
|
|
105
|
+
if (ok) {
|
|
106
|
+
this.copiedIndex = i
|
|
107
|
+
if (this.apiCopyTimer) clearTimeout(this.apiCopyTimer)
|
|
108
|
+
this.apiCopyTimer = setTimeout(() => {
|
|
109
|
+
if (this.copiedIndex === i) this.copiedIndex = null
|
|
110
|
+
}, 1500)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
@click="choose(card)"
|
|
57
57
|
>
|
|
58
58
|
<div class="cg-tile__thumb">
|
|
59
|
+
<!-- 导入标记:仅含 .import-meta.json 侧车的卡(经远端同步导入)显示,悬浮于缩略图右上角 -->
|
|
60
|
+
<span v-if="card.imported" class="cg-tile__tag" title="导入卡片(含 .import-meta.json 来源信息)">导入</span>
|
|
59
61
|
<img
|
|
60
62
|
v-if="visibleIds.has(card.id) && !failed.has(card.id)"
|
|
61
63
|
class="cg-tile__img"
|