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
@@ -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
- <DebugPanel
319
- v-show="!chromeOff"
320
- :rt="rt"
321
- :preview="preview"
322
- :schema="currentSchema"
323
- :card-id="selectedId"
324
- :reload-token="reloadToken"
325
- :card-count="cardCount"
326
- :selected-node-id="selectedNodeId"
327
- :move-collapse-token="moveCollapseToken"
328
- :active-tab="activeTab"
329
- @schema-changed="onSchemaChanged"
330
- @schema-commit="onSchemaCommit"
331
- @select-node="onSelectNode"
332
- @update:active-tab="activeTab = $event"
333
- @add-node="onAddNode"
334
- @move-node="onMoveNode"
335
- @hover-node="onHoverNode"
336
- @copy-selected="copySelectedNode"
337
- @paste="pasteNode"
338
- @duplicate="duplicateNode"
339
- @delete-selected="deleteSelectedNode"
340
- @reset-import="onRefresh"
341
- :dev-svc-class="devSvcClass"
342
- :dev-svc-title="devSvcTitle"
343
- :dev-svc-label="devSvcLabel"
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
- :danger="confirm.danger"
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
- danger: false,
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
- danger: true,
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, danger, onConfirm }) {
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: !!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
  },