cardbuddy 0.6.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 (167) hide show
  1. package/README.md +211 -0
  2. package/dist/cli.js +76 -0
  3. package/dist/config.js +63 -0
  4. package/dist/core.js +2 -0
  5. package/dist/preview-editor/assets/apple-touch-icon-BQcES9QK.png +0 -0
  6. package/dist/preview-editor/assets/default-image-wzZYQ86K.png +0 -0
  7. package/dist/preview-editor/assets/favicon-16-Cg6Nrosk.png +0 -0
  8. package/dist/preview-editor/assets/favicon-32-B_1744sp.png +0 -0
  9. package/dist/preview-editor/assets/index-BYB0hngG.js +72 -0
  10. package/dist/preview-editor/assets/index-CpBZEQDy.css +1 -0
  11. package/dist/preview-editor/assets/site-icon-CFMif8z4.png +0 -0
  12. package/dist/preview-editor/assets/svg-symbols-B2exBrAr.svg +118 -0
  13. package/dist/preview-editor/index.html +17 -0
  14. package/dist/server-core/_vendor/finmall-env.mjs +96 -0
  15. package/dist/server-core/assetProxy.js +154 -0
  16. package/dist/server-core/bbox.js +153 -0
  17. package/dist/server-core/browser.js +108 -0
  18. package/dist/server-core/buildDataFromSchema.js +50 -0
  19. package/dist/server-core/cardFiles.js +188 -0
  20. package/dist/server-core/cardHistory.js +156 -0
  21. package/dist/server-core/cardImage.js +85 -0
  22. package/dist/server-core/cardImportMeta.js +139 -0
  23. package/dist/server-core/cardListCache.js +68 -0
  24. package/dist/server-core/cardOpenFolder.js +135 -0
  25. package/dist/server-core/cardSave.js +127 -0
  26. package/dist/server-core/cardSidecar.js +72 -0
  27. package/dist/server-core/cardSync.js +259 -0
  28. package/dist/server-core/cardValidate.js +134 -0
  29. package/dist/server-core/index.js +82 -0
  30. package/dist/server-core/logger.js +241 -0
  31. package/dist/server-core/nodeModels.js +52 -0
  32. package/dist/server-core/renderStatus.js +207 -0
  33. package/dist/server-core/screenshot.js +123 -0
  34. package/dist/server-core/sliceStatic.js +44 -0
  35. package/dist/standalone.js +447 -0
  36. package/dist/vendored/App.vue +2330 -0
  37. package/dist/vendored/api.js +142 -0
  38. package/dist/vendored/assets/apple-touch-icon.png +0 -0
  39. package/dist/vendored/assets/default-image.png +0 -0
  40. package/dist/vendored/assets/favicon-16.png +0 -0
  41. package/dist/vendored/assets/favicon-32.png +0 -0
  42. package/dist/vendored/assets/logo.png +0 -0
  43. package/dist/vendored/assets/site-icon.png +0 -0
  44. package/dist/vendored/assets/svg-symbols.svg +118 -0
  45. package/dist/vendored/assets/svgIcons/icons.svg +54 -0
  46. package/dist/vendored/assets/svgIcons/index.js +39 -0
  47. package/dist/vendored/components/AbsoluteLayout.vue +42 -0
  48. package/dist/vendored/components/AlertCardInstance.vue +58 -0
  49. package/dist/vendored/components/AlertCardLayer.vue +59 -0
  50. package/dist/vendored/components/ApiPayloadTree.vue +168 -0
  51. package/dist/vendored/components/AppToast.vue +37 -0
  52. package/dist/vendored/components/Button.vue +37 -0
  53. package/dist/vendored/components/Canvas.vue +65 -0
  54. package/dist/vendored/components/CardGallery.vue +243 -0
  55. package/dist/vendored/components/CardPreview.vue +72 -0
  56. package/dist/vendored/components/CardSelect.vue +200 -0
  57. package/dist/vendored/components/CheckGroup.vue +131 -0
  58. package/dist/vendored/components/Column.vue +25 -0
  59. package/dist/vendored/components/ComponentPalette.vue +294 -0
  60. package/dist/vendored/components/ConfirmDialog.vue +39 -0
  61. package/dist/vendored/components/CustomPlaceholder.vue +33 -0
  62. package/dist/vendored/components/DatePickerOverlay.vue +345 -0
  63. package/dist/vendored/components/DebugPanel.vue +806 -0
  64. package/dist/vendored/components/DeviceOverlay.vue +70 -0
  65. package/dist/vendored/components/Divider.vue +22 -0
  66. package/dist/vendored/components/Dropdown.vue +191 -0
  67. package/dist/vendored/components/EventEditor.vue +247 -0
  68. package/dist/vendored/components/GridLayout.vue +67 -0
  69. package/dist/vendored/components/HoverGuides.vue +122 -0
  70. package/dist/vendored/components/Icon.vue +34 -0
  71. package/dist/vendored/components/Image.vue +34 -0
  72. package/dist/vendored/components/Input.vue +90 -0
  73. package/dist/vendored/components/JsonTree.vue +516 -0
  74. package/dist/vendored/components/ListLayout.vue +68 -0
  75. package/dist/vendored/components/LogValue.vue +51 -0
  76. package/dist/vendored/components/Map.vue +110 -0
  77. package/dist/vendored/components/MultiColumn.vue +49 -0
  78. package/dist/vendored/components/MultiText.vue +58 -0
  79. package/dist/vendored/components/NodeInspector.vue +295 -0
  80. package/dist/vendored/components/PickerOverlay.vue +131 -0
  81. package/dist/vendored/components/ProgressSlider.vue +167 -0
  82. package/dist/vendored/components/PropControl.vue +241 -0
  83. package/dist/vendored/components/PropTree.vue +128 -0
  84. package/dist/vendored/components/RadioButton.vue +74 -0
  85. package/dist/vendored/components/RemotePreview.vue +171 -0
  86. package/dist/vendored/components/RenderTree.vue +437 -0
  87. package/dist/vendored/components/Root.vue +30 -0
  88. package/dist/vendored/components/Row.vue +25 -0
  89. package/dist/vendored/components/SelectionOverlay.vue +105 -0
  90. package/dist/vendored/components/StructureTree.vue +949 -0
  91. package/dist/vendored/components/SvgCode.vue +19 -0
  92. package/dist/vendored/components/SvgSymbols.vue +30 -0
  93. package/dist/vendored/components/Switch.vue +38 -0
  94. package/dist/vendored/components/Text.vue +32 -0
  95. package/dist/vendored/components/Video.vue +70 -0
  96. package/dist/vendored/components/ViewContainer.vue +52 -0
  97. package/dist/vendored/components/base.js +12 -0
  98. package/dist/vendored/components/icons.js +47 -0
  99. package/dist/vendored/components/index.js +58 -0
  100. package/dist/vendored/components/setters/SetBgPosition.vue +64 -0
  101. package/dist/vendored/components/setters/SetBgSize.vue +90 -0
  102. package/dist/vendored/components/setters/SetCode.vue +90 -0
  103. package/dist/vendored/components/setters/SetColor.vue +244 -0
  104. package/dist/vendored/components/setters/SetCustomSelect.vue +58 -0
  105. package/dist/vendored/components/setters/SetExpr.vue +107 -0
  106. package/dist/vendored/components/setters/SetGradient.vue +125 -0
  107. package/dist/vendored/components/setters/SetGrid9.vue +68 -0
  108. package/dist/vendored/components/setters/SetIconSelect.vue +67 -0
  109. package/dist/vendored/components/setters/SetIfForm.vue +61 -0
  110. package/dist/vendored/components/setters/SetImage.vue +43 -0
  111. package/dist/vendored/components/setters/SetList.vue +139 -0
  112. package/dist/vendored/components/setters/SetNumber.vue +118 -0
  113. package/dist/vendored/components/setters/SetNumberStyle.vue +113 -0
  114. package/dist/vendored/components/setters/SetPadding.vue +77 -0
  115. package/dist/vendored/components/setters/SetSegment.vue +160 -0
  116. package/dist/vendored/components/setters/SetSelect.vue +51 -0
  117. package/dist/vendored/components/setters/SetSwitch.vue +49 -0
  118. package/dist/vendored/components/setters/SetText.vue +52 -0
  119. package/dist/vendored/components/setters/props.js +23 -0
  120. package/dist/vendored/fidelity/baseline-messageOverview.json +87 -0
  121. package/dist/vendored/fidelity/calibration.json +29 -0
  122. package/dist/vendored/fidelity/fidelity-messageOverview-after.html +114 -0
  123. package/dist/vendored/fidelity/fidelity-messageOverview-after.json +272 -0
  124. package/dist/vendored/fidelity/fidelity-messageOverview.html +185 -0
  125. package/dist/vendored/fidelity/fidelity-messageOverview.json +336 -0
  126. package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +262 -0
  127. package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +502 -0
  128. package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +201 -0
  129. package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +417 -0
  130. package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +125 -0
  131. package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +338 -0
  132. package/dist/vendored/index.html +16 -0
  133. package/dist/vendored/main.js +36 -0
  134. package/dist/vendored/meta/action-meta.json +141 -0
  135. package/dist/vendored/meta/component-meta.json +2714 -0
  136. package/dist/vendored/meta/directive-meta.json +40 -0
  137. package/dist/vendored/meta/style-meta.json +1086 -0
  138. package/dist/vendored/runtime/StyleParser.js +179 -0
  139. package/dist/vendored/runtime/adapter.js +166 -0
  140. package/dist/vendored/runtime/alert-card.js +125 -0
  141. package/dist/vendored/runtime/app-toast.js +33 -0
  142. package/dist/vendored/runtime/assets.js +16 -0
  143. package/dist/vendored/runtime/canvas-context.js +141 -0
  144. package/dist/vendored/runtime/card-runtime.js +289 -0
  145. package/dist/vendored/runtime/clipboard-node.js +83 -0
  146. package/dist/vendored/runtime/clipboard.js +29 -0
  147. package/dist/vendored/runtime/evaluate.js +152 -0
  148. package/dist/vendored/runtime/event-model.js +300 -0
  149. package/dist/vendored/runtime/hoverGuide.js +31 -0
  150. package/dist/vendored/runtime/jsapi-mock.js +256 -0
  151. package/dist/vendored/runtime/node-model.js +217 -0
  152. package/dist/vendored/runtime/property.js +364 -0
  153. package/dist/vendored/runtime/selectionOverlay.js +53 -0
  154. package/dist/vendored/runtime/toPreview.js +648 -0
  155. package/dist/vendored/standalone-main.js +36 -0
  156. package/dist/vendored/standalone.html +12 -0
  157. package/dist/vendored/styles/app.css +2565 -0
  158. package/dist/vendored/styles/card-gallery.css +234 -0
  159. package/dist/vendored/styles/card-select.css +281 -0
  160. package/dist/vendored/styles/components.css +383 -0
  161. package/dist/vendored/styles/dropdown.css +337 -0
  162. package/dist/vendored/styles/json-tree.css +308 -0
  163. package/dist/vendored/styles/node-inspector.css +1003 -0
  164. package/dist/vendored/styles/setters.css +962 -0
  165. package/dist/vendored/styles/standalone.css +27 -0
  166. package/dist/vendored/styles/structure-tree.css +320 -0
  167. package/package.json +42 -0
@@ -0,0 +1,110 @@
1
+ <!--
2
+ 地图(官方 module 1983 / 506)。
3
+ 在线端用 Leaflet 渲染瓦片;预览端不内嵌 Leaflet(体积大且与本地资源代理不兼容),
4
+ 改用「ce_map_view 占位 + 背景网格 + 标记点」还原在线结构(ce_map > ce_map_view + ce_map_placeholder),
5
+ 视觉表现与在线一致:一个地图容器 + 中心标记点。
6
+ -->
7
+ <template>
8
+ <div class="ce_map" :style="rootStyle">
9
+ <div class="ce_map_view">
10
+ <svg class="ce_map_grid" viewBox="0 0 100 100" preserveAspectRatio="none">
11
+ <defs>
12
+ <pattern id="ceMapGrid" width="10" height="10" patternUnits="userSpaceOnUse">
13
+ <path d="M10 0 L0 0 0 10" fill="none" stroke="#dfe3ea" stroke-width="0.4" />
14
+ </pattern>
15
+ </defs>
16
+ <rect width="100" height="100" fill="#eef1f6" />
17
+ <rect width="100" height="100" fill="url(#ceMapGrid)" />
18
+ <!-- 模拟几条道路 -->
19
+ <path d="M0 60 Q40 50 100 65" fill="none" stroke="#cdd4df" stroke-width="2.2" />
20
+ <path d="M30 0 L45 100" fill="none" stroke="#cdd4df" stroke-width="2.2" />
21
+ </svg>
22
+ <div class="ce_map_center">
23
+ <div class="ce_map_pin">
24
+ <svg viewBox="0 0 24 24" width="26" height="26">
25
+ <path
26
+ d="M12 2C7.6 2 4 5.6 4 10c0 5.4 7.1 11.6 7.4 11.9.6.6 1.6.6 2.2 0C13.9 21.6 21 15.4 21 10c0-4.4-3.6-8-9-8z"
27
+ fill="#ff4d4f"
28
+ />
29
+ <circle cx="12" cy="10" r="3" fill="#fff" />
30
+ </svg>
31
+ </div>
32
+ <div v-if="markerName" class="ce_map_markname">{{ doI18n(markerName) }}</div>
33
+ </div>
34
+ </div>
35
+ <div class="ce_map_placeholder" />
36
+ </div>
37
+ </template>
38
+
39
+ <script>
40
+ import base from './base.js'
41
+ import StyleParser from '../runtime/StyleParser.js'
42
+
43
+ export default {
44
+ name: 'CeMap',
45
+ mixins: [base],
46
+ props: {
47
+ styleProps: { type: Object, default: () => ({}) },
48
+ centerLat: { type: [String, Number], default: '' },
49
+ centerLng: { type: [String, Number], default: '' },
50
+ zoom: { type: [String, Number], default: '' },
51
+ markerLat: { type: [String, Number], default: '' },
52
+ markerLng: { type: [String, Number], default: '' },
53
+ markerName: { type: String, default: '' },
54
+ },
55
+ emits: ['click'],
56
+ computed: {
57
+ // 沿用官方统一做法:用 StyleParser 解析样式面板设置(rect.height / border /
58
+ // borderRadius 等),让高度、边框等样式真正生效;地图占位仍需裁掉溢出的网格与标记点。
59
+ rootStyle() {
60
+ const out = new StyleParser(this, this.styleProps).parse()
61
+ out.overflow = 'hidden'
62
+ return out
63
+ },
64
+ },
65
+ }
66
+ </script>
67
+
68
+ <style scoped>
69
+ .ce_map_view {
70
+ width: 100%;
71
+ height: 100%;
72
+ min-height: 160px;
73
+ position: relative;
74
+ background-repeat: no-repeat;
75
+ background-position: center center;
76
+ background-size: 100% 100%;
77
+ }
78
+ .ce_map_grid {
79
+ position: absolute;
80
+ inset: 0;
81
+ width: 100%;
82
+ height: 100%;
83
+ }
84
+ .ce_map_center {
85
+ position: absolute;
86
+ left: 50%;
87
+ top: 50%;
88
+ transform: translate(-50%, -100%);
89
+ display: flex;
90
+ flex-direction: column;
91
+ align-items: center;
92
+ pointer-events: none;
93
+ }
94
+ .ce_map_pin {
95
+ filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25));
96
+ }
97
+ .ce_map_markname {
98
+ margin-top: 2px;
99
+ padding: 2px 6px;
100
+ font-size: 12px;
101
+ line-height: 1.3;
102
+ color: #333;
103
+ background: rgba(255, 255, 255, 0.9);
104
+ border-radius: 4px;
105
+ white-space: nowrap;
106
+ max-width: 160px;
107
+ overflow: hidden;
108
+ text-overflow: ellipsis;
109
+ }
110
+ </style>
@@ -0,0 +1,49 @@
1
+ <!--
2
+ 多列布局容器(官方组件 MultiColumn)。
3
+
4
+ 预览运行时此前未注册该组件,导致画布显示「未知组件」占位(问题 #4)。
5
+ 本组件对齐 Row/Column 的容器范式:用 StyleParser 解析样式(rect / flex / padding …),
6
+ 渲染默认插槽内容;当 `column` 属性 > 1 时切换为 CSS Grid 多列布局(列数 = column)。
7
+
8
+ 说明:当前测试卡的 MultiColumn 实例(comp_3)为「空容器」(无插槽子节点、无 column 属性),
9
+ 此处仍提供完整的多列渲染能力,便于后续接入真实带子节点的多列布局卡片。
10
+ -->
11
+ <template>
12
+ <div ref="mc" class="ce_multicolumn" :style="styleObj" @click="$emit('click', $event)">
13
+ <slot />
14
+ </div>
15
+ </template>
16
+
17
+ <script>
18
+ import StyleParser from '../runtime/StyleParser.js'
19
+ import base from './base.js'
20
+
21
+ export default {
22
+ name: 'CeMultiColumn',
23
+ emits: ['click'],
24
+ mixins: [base],
25
+ props: {
26
+ styleProps: { type: Object, default: () => ({}) },
27
+ column: { type: [Number, String], default: 2 },
28
+ },
29
+ computed: {
30
+ styleObj() {
31
+ const s = new StyleParser(this, this.styleProps).parse()
32
+ const cols = parseInt(this.column, 10)
33
+ if (cols && cols > 1) {
34
+ // 多列:用 grid 把子节点按 column 列均分;flex gap 同时充当 grid 行列间距。
35
+ s.display = 'grid'
36
+ s.gridTemplateColumns = `repeat(${cols}, 1fr)`
37
+ }
38
+ return s
39
+ },
40
+ },
41
+ }
42
+ </script>
43
+
44
+ <style scoped>
45
+ .ce_multicolumn {
46
+ box-sizing: border-box;
47
+ min-height: 24px;
48
+ }
49
+ </style>
@@ -0,0 +1,58 @@
1
+ <!--
2
+ 官方 module 1981 / 504。
3
+ 一行里多段不同字体的文本;点击时用 event.__do__ 做冒泡去重,
4
+ 子段先命中,父段再收到同一个 event 就不重复 emit 了。
5
+ -->
6
+ <template>
7
+ <span class="ce_multitext" :style="styleObj" @click="doClick($event)">
8
+ <span class="ce_multitext_item">
9
+ <span
10
+ v-for="(item, index) in textList"
11
+ :key="index"
12
+ :style="parseFont(item)"
13
+ @click="doClick($event, item, index)"
14
+ >{{ doI18n(item.value) }}</span
15
+ >
16
+ </span>
17
+ </span>
18
+ </template>
19
+
20
+ <script>
21
+ import StyleParser from '../runtime/StyleParser.js'
22
+ import base from './base.js'
23
+
24
+ export default {
25
+ name: 'CeMultiText',
26
+ emits: ['click'],
27
+ mixins: [base],
28
+ props: {
29
+ styleProps: { type: Object, default: () => ({}) },
30
+ textList: { type: Array, default: () => [] },
31
+ },
32
+ computed: {
33
+ styleObj() {
34
+ const parser = new StyleParser(this, this.styleProps)
35
+ const out = parser.parse()
36
+ parser.parseFont(this.styleProps.font || {}, out)
37
+ return out
38
+ },
39
+ },
40
+ methods: {
41
+ doClick(e, item, index) {
42
+ if (e.__do__) return
43
+ e.__do__ = true
44
+ this.$emit('click', {
45
+ data: { index, tag: item ? item.tag : '', text: item ? item.value : '' },
46
+ })
47
+ },
48
+ parseFont(item) {
49
+ const out = {}
50
+ new StyleParser(this, {}).parseFont(item || {}, out)
51
+ if (item && item.backgroundColor) {
52
+ out.backgroundColor = this.adapter.doColor(item.backgroundColor)
53
+ }
54
+ return out
55
+ },
56
+ },
57
+ }
58
+ </script>
@@ -0,0 +1,295 @@
1
+ <!--
2
+ 节点属性面板(meta 驱动,可编辑写回)。
3
+
4
+ 数据源:预览树节点 comp._schemaNode(与 currentSchema 同引用)。构建 NodeEditModel 后:
5
+ - 组件属性 / 指令:表单树 == 实例树,直接 mutate Property.itemValue,整树导出(applyToSchema);
6
+ - 样式:表单树(styleForm, 完整 meta) 与实例树(style, 精简) 分离,改动经 applyStyleChange 定点并入;
7
+ - 编辑即时(防抖)重建预览(emit apply),「应用」把改动暂存进内存工作副本(currentSchema),落盘由顶栏「保存到本地」负责;不再 emit write-back。
8
+
9
+ 面板布局对齐线上编辑器右侧「属性 / 事件」:
10
+ 属性 tab:基本信息 + 组件属性 + 样式 + 指令(分组可折叠,叶子用类型化控件);
11
+ 事件 tab:事件动作列表,每条 = 触发事件 + 动作类型 + 动作参数。
12
+ 参数由 meta/action-meta.json 声明(Property meta 形态),
13
+ 因此和属性页签共用 Property / PropTree / PropControl 同一套渲染与回写机制,
14
+ customAction 的脚本走 itemType=Code 的 SetCode 设置器(CodeJar + Prism)。
15
+ 导出纪律见 runtime/event-model.js 文件头:必填/原有/改过的参数才写回 schema。
16
+ -->
17
+ <template>
18
+ <div class="ni-overlay">
19
+ <div class="ni-panel">
20
+ <div class="ni-head">
21
+ <span class="ni-title">
22
+ 节点属性 · {{ compName }}
23
+ <em class="ni-cls">{{ schemaNode && schemaNode.className }}</em>
24
+ </span>
25
+ <button class="ni-close" type="button" @click="onClose"></button>
26
+ </div>
27
+
28
+ <div v-if="!model" class="ni-body">
29
+ <p class="ni-empty">无法定位该节点的原始 schema(_schemaNode 缺失)</p>
30
+ </div>
31
+
32
+ <div v-else class="ni-body">
33
+ <!-- ============ 属性 ============ -->
34
+ <div class="ni-scroll">
35
+ <!-- 基本信息 -->
36
+ <section class="ni-sec">
37
+ <div class="ni-sec-head" @click="toggleSec('basic')">
38
+ <span class="ni-caret" :class="{ 'is-open': secOpen.basic }">▸</span>基本信息
39
+ </div>
40
+ <div v-show="secOpen.basic" class="ni-sec-body">
41
+ <div class="ni-field-row">
42
+ <label class="ni-field-label">组件类型</label>
43
+ <span class="ni-static">{{ compName }}</span>
44
+ </div>
45
+ <div class="ni-field-row" v-if="hasField('componentId')">
46
+ <label class="ni-field-label">实例 ID</label>
47
+ <input class="ni-input" :value="schemaNode.componentId" @input="onMetaInput('componentId', $event.target.value)" />
48
+ </div>
49
+ <div class="ni-field-row" v-if="hasField('name')">
50
+ <label class="ni-field-label">节点名</label>
51
+ <input class="ni-input" :value="schemaNode.name" @input="onMetaInput('name', $event.target.value)" />
52
+ </div>
53
+ </div>
54
+ </section>
55
+
56
+ <!-- 组件属性 -->
57
+ <section class="ni-sec" v-show="model.properties.length">
58
+ <div class="ni-sec-head" @click="toggleSec('props')">
59
+ <span class="ni-caret" :class="{ 'is-open': secOpen.props }">▸</span>
60
+ 组件属性
61
+ </div>
62
+ <div v-show="secOpen.props" class="ni-sec-body">
63
+ <prop-tree
64
+ :properties="model.properties"
65
+ mode="property"
66
+ :plain="true"
67
+ @leaf-change="onPropChange"
68
+ @leaf-sync="onStyleSync"
69
+ />
70
+ </div>
71
+ </section>
72
+
73
+ <!-- 样式 -->
74
+ <section class="ni-sec">
75
+ <div class="ni-sec-head" @click="toggleSec('style')">
76
+ <span class="ni-caret" :class="{ 'is-open': secOpen.style }">▸</span>
77
+ 样式
78
+ </div>
79
+ <div v-show="secOpen.style" class="ni-sec-body">
80
+ <prop-tree
81
+ :properties="model.styleForm"
82
+ mode="style"
83
+ :watches="styleWatches"
84
+ :plain="true"
85
+ @leaf-change="onStyleChange"
86
+ @leaf-sync="onStyleSync"
87
+ />
88
+ </div>
89
+ </section>
90
+
91
+ <!-- 指令 -->
92
+ <section class="ni-sec" v-if="model.hasDirectives">
93
+ <div class="ni-sec-head" @click="toggleSec('directive')">
94
+ <span class="ni-caret" :class="{ 'is-open': secOpen.directive }">▸</span>
95
+ 指令
96
+ </div>
97
+ <div v-show="secOpen.directive" class="ni-sec-body">
98
+ <p v-if="!model.directives.length" class="ni-empty">无指令</p>
99
+ <prop-tree
100
+ v-else
101
+ :properties="model.directives"
102
+ mode="directive"
103
+ :b-exchange="false"
104
+ :plain="true"
105
+ @leaf-change="onPropChange"
106
+ @leaf-sync="onStyleSync"
107
+ />
108
+ </div>
109
+ </section>
110
+ </div>
111
+
112
+ </div>
113
+
114
+ <div class="ni-foot">
115
+ <span class="ni-tip">编辑即时预览(不记历史);「取消」丢弃改动并还原,「应用」写入历史并关闭,落盘由顶栏「保存到本地」负责</span>
116
+ <div class="ni-foot-btns">
117
+ <button class="ni-btn ni-btn--ghost" type="button" @click="onCancel">取消</button>
118
+ <button class="ni-btn" type="button" @click="onApplyWrite">应用</button>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </template>
124
+
125
+ <script>
126
+ import { reactive } from 'vue'
127
+ import { NodeEditModel } from '@/runtime/node-model.js'
128
+
129
+ export default {
130
+ name: 'NodeInspector',
131
+ props: {
132
+ comp: { type: Object, required: true },
133
+ compNames: { type: Object, default: () => ({}) },
134
+ rt: { type: Object, required: true },
135
+ },
136
+ emits: ['apply', 'commit', 'close'],
137
+ data() {
138
+ return {
139
+ model: null,
140
+ secOpen: { basic: true, props: true, style: true, directive: true },
141
+ _t: null,
142
+ _snapshot: null,
143
+ _dirty: false,
144
+ _written: false,
145
+ }
146
+ },
147
+ computed: {
148
+ schemaNode() {
149
+ return (this.comp && this.comp._schemaNode) || null
150
+ },
151
+ compName() {
152
+ const cn = this.schemaNode && this.schemaNode.className
153
+ if (!cn) return '节点'
154
+ return (this.compNames && this.compNames[cn]) || cn
155
+ },
156
+ /** 样式编辑联动快照:把所有样式字段 itemKey→itemValue 摊平,供 itemWatch 使用 */
157
+ styleWatches() {
158
+ const w = {}
159
+ const walk = (props) => {
160
+ (props || []).forEach((p) => {
161
+ if (p.itemKey != null) w[p.itemKey] = p.itemValue
162
+ if (p.itemChildren && p.itemChildren.properties) walk(p.itemChildren.properties)
163
+ })
164
+ }
165
+ if (this.model) walk(this.model.styleForm)
166
+ return w
167
+ },
168
+ },
169
+ watch: {
170
+ comp: {
171
+ immediate: true,
172
+ handler() {
173
+ this.buildModel()
174
+ },
175
+ },
176
+ },
177
+ methods: {
178
+ buildModel() {
179
+ const sn = this.schemaNode
180
+ this.model = sn ? reactive(new NodeEditModel(sn)) : null
181
+ this._snapshot = sn ? this.snapshotNode(sn) : null
182
+ this._dirty = false
183
+ this._written = false
184
+ },
185
+ snapshotNode(node) {
186
+ const has = (k) => Object.prototype.hasOwnProperty.call(node, k)
187
+ const clone = (v) => v === undefined ? undefined : JSON.parse(JSON.stringify(v))
188
+ return {
189
+ hasProperties: has('properties'), properties: clone(node.properties),
190
+ hasStyle: has('style'), style: clone(node.style),
191
+ hasDirectives: has('directives'), directives: clone(node.directives),
192
+ hasComponentId: has('componentId'), componentId: clone(node.componentId),
193
+ hasName: has('name'), name: clone(node.name),
194
+ }
195
+ },
196
+ restoreSnapshot() {
197
+ const sn = this.schemaNode
198
+ if (!sn || !this._snapshot) return
199
+ const s = this._snapshot
200
+ const setOrDel = (had, key, val) => { if (had) sn[key] = JSON.parse(JSON.stringify(val)); else delete sn[key] }
201
+ setOrDel(s.hasProperties, 'properties', s.properties)
202
+ setOrDel(s.hasStyle, 'style', s.style)
203
+ setOrDel(s.hasDirectives, 'directives', s.directives)
204
+ setOrDel(s.hasComponentId, 'componentId', s.componentId)
205
+ setOrDel(s.hasName, 'name', s.name)
206
+ },
207
+ toggleSec(k) {
208
+ this.secOpen[k] = !this.secOpen[k]
209
+ this.secOpen = Object.assign({}, this.secOpen)
210
+ },
211
+ hasField(name) {
212
+ return this.schemaNode && Object.prototype.hasOwnProperty.call(this.schemaNode, name)
213
+ },
214
+ onMetaInput(field, v) {
215
+ if (!this.schemaNode) return
216
+ this.schemaNode[field] = v
217
+ this._dirty = true
218
+ this.scheduleApply()
219
+ },
220
+ /** 组件属性 / 指令:整树导出即可 */
221
+ onPropChange() {
222
+ this.model.refreshVisible()
223
+ this._dirty = true
224
+ this.scheduleApply()
225
+ },
226
+ /** 样式:定点并入实例树 */
227
+ onStyleChange(chain) {
228
+ if (!chain || !chain.length) return
229
+ this.model.applyStyleChange(chain[0], chain.slice(1))
230
+ this.model.refreshVisible()
231
+ this._dirty = true
232
+ this.scheduleApply()
233
+ },
234
+ /** 常用样式(CustomSelect)批量写入:sync map 形如 { 'style.font.color': '#333' } */
235
+ onStyleSync(map) {
236
+ if (!map) return
237
+ for (const path in map) {
238
+ const parts = String(path).split('.')
239
+ if (parts.length < 3 || parts[0] !== 'style') continue
240
+ const groupKey = parts[1]
241
+ const fieldKey = parts[2]
242
+ const group = this.model.styleForm.find((p) => p.itemKey === groupKey)
243
+ if (!group) continue
244
+ const child = group.itemChildren.properties.find((c) => c.itemKey === fieldKey)
245
+ if (!child) continue
246
+ child.itemValue = map[path]
247
+ this.model.applyStyleChange(group, [child])
248
+ }
249
+ this.model.refreshVisible()
250
+ this.scheduleApply()
251
+ },
252
+ scheduleApply() {
253
+ if (this._t) clearTimeout(this._t)
254
+ this._t = setTimeout(() => {
255
+ this.applyAll()
256
+ this.$emit('apply')
257
+ }, 250)
258
+ },
259
+ applyAll() {
260
+ if (this.model) this.model.applyToSchema()
261
+ },
262
+ /** 将改动暂存到内存工作副本(currentSchema,与预览同引用),并重建预览;
263
+ * 不写盘——落盘由顶栏「保存到本地」统一负责。_written=true 表示已提交到内存,
264
+ * 「取消」不再还原。 */
265
+ onApplyWrite() {
266
+ if (this._t) {
267
+ clearTimeout(this._t)
268
+ this._t = null
269
+ }
270
+ this.applyAll()
271
+ this._written = true
272
+ this.$emit('apply')
273
+ this.$emit('commit')
274
+ this.$emit('close')
275
+ },
276
+
277
+ /** 丢弃本次编辑(还原到打开时快照)+ 关闭面板。
278
+ * 已本地保存(_written)的改动视为已提交,不再还原。 */
279
+ discardAndClose() {
280
+ if (this._t) { clearTimeout(this._t); this._t = null }
281
+ if (this._dirty && !this._written) {
282
+ this.restoreSnapshot()
283
+ this.$emit('apply')
284
+ }
285
+ this.$emit('close')
286
+ },
287
+ onClose() {
288
+ this.discardAndClose()
289
+ },
290
+ onCancel() {
291
+ this.discardAndClose()
292
+ },
293
+ },
294
+ }
295
+ </script>
@@ -0,0 +1,131 @@
1
+ <!--
2
+ showPicker 模拟展现层:底部弹出级联选择器。
3
+ 相对固定视口 phone-viewport 定位(absolute inset:0),不随卡片内容滚动;
4
+ 层级同 DeviceOverlay(z-index 1100),盖住弹窗卡片。
5
+
6
+ 交互对齐 docs「showPicker - 选择器」:
7
+ - mode='single' 单列;mode='multi' 多列(一级驱动二级 children 级联)。
8
+ - 点击某列项即选中(中间高亮带提示当前项);点「确定」回传 result,点「取消」不回传。
9
+ - result 数组每项 { key, value },value 取节点 text。
10
+ -->
11
+ <template>
12
+ <div class="device-ui">
13
+ <transition name="picker-fade">
14
+ <div v-if="picker" class="pk-mask" @click.self="onCancel">
15
+ <div class="pk-sheet">
16
+ <div class="pk-bar">
17
+ <button type="button" class="pk-bar__btn pk-bar__btn--cancel" @click="onCancel">取消</button>
18
+ <div v-if="picker.title" class="pk-bar__title">{{ picker.title }}</div>
19
+ <button type="button" class="pk-bar__btn pk-bar__btn--ok" @click="onConfirm">确定</button>
20
+ </div>
21
+ <div class="pk-cols" :class="'pk-cols--' + columns.length">
22
+ <div v-for="(col, ci) in columns" :key="ci" class="pk-col">
23
+ <div class="pk-col__band"></div>
24
+ <div class="pk-col__list">
25
+ <div
26
+ v-for="(item, ii) in col.items"
27
+ :key="item.key"
28
+ class="pk-item"
29
+ :class="{ 'pk-item--on': sel[ci] === ii }"
30
+ @click="select(ci, ii)"
31
+ >{{ item.text }}</div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </transition>
38
+ </div>
39
+ </template>
40
+
41
+ <script>
42
+ import { deviceUI, dismissPicker } from '@/runtime/jsapi-mock.js'
43
+
44
+ export default {
45
+ name: 'PickerOverlay',
46
+ data() {
47
+ return {
48
+ sel: [], // 每列选中索引(列数由 mode 决定)
49
+ }
50
+ },
51
+ computed: {
52
+ picker() {
53
+ return deviceUI.picker
54
+ },
55
+ // 列定义:single -> 单列(顶级项,忽略 children);multi -> 两列(级联,第二列为第一列选中项的 children)
56
+ columns() {
57
+ if (!this.picker) return []
58
+ const ds = this.picker.dataSource || []
59
+ if (this.picker.mode === 'multi') {
60
+ const col1 = ds.map((n) => ({ key: String(n.key), text: n.text }))
61
+ const sel1 = this.sel[0] || 0
62
+ const children = (ds[sel1] && ds[sel1].children) || []
63
+ const col2 = children.map((n) => ({ key: String(n.key), text: n.text }))
64
+ return [{ items: col1, level: 0 }, { items: col2, level: 1 }]
65
+ }
66
+ return [{ items: ds.map((n) => ({ key: String(n.key), text: n.text })), level: 0 }]
67
+ },
68
+ },
69
+ watch: {
70
+ // picker 切换(含入参变化)时按 currentValue 初始化选中项
71
+ picker: {
72
+ immediate: true,
73
+ handler(p) {
74
+ if (!p) {
75
+ this.sel = []
76
+ return
77
+ }
78
+ const ds = p.dataSource || []
79
+ const cv = p.currentValue || []
80
+ if (p.mode === 'multi') {
81
+ const i1 = ds.findIndex((n) => String(n.key) === String(cv[0]))
82
+ const idx1 = i1 >= 0 ? i1 : 0
83
+ const children = (ds[idx1] && ds[idx1].children) || []
84
+ const i2 = children.findIndex((n) => String(n.key) === String(cv[1]))
85
+ this.sel = [idx1, i2 >= 0 ? i2 : children.length ? 0 : -1]
86
+ } else {
87
+ const i1 = ds.findIndex((n) => String(n.key) === String(cv[0]))
88
+ this.sel = [i1 >= 0 ? i1 : 0]
89
+ }
90
+ },
91
+ },
92
+ },
93
+ methods: {
94
+ select(ci, ii) {
95
+ const next = this.sel.slice()
96
+ next[ci] = ii
97
+ // 切换第一列时重置第二列选中(级联)
98
+ if (ci === 0 && this.picker.mode === 'multi') {
99
+ const ds = this.picker.dataSource || []
100
+ const children = (ds[ii] && ds[ii].children) || []
101
+ next[1] = children.length ? 0 : -1
102
+ }
103
+ this.sel = next
104
+ },
105
+ buildResult() {
106
+ const p = this.picker
107
+ if (!p) return []
108
+ const ds = p.dataSource || []
109
+ // 回传对象携带 text(卡片回调读 result[0].text)与 value(文档出参规范),两者取节点 text。
110
+ const wrap = (n) => ({ key: String(n.key), text: n.text, value: n.text })
111
+ if (p.mode === 'multi') {
112
+ const n1 = ds[this.sel[0]] || ds[0]
113
+ const children = (n1 && n1.children) || []
114
+ const n2 = children[this.sel[1]] || children[0]
115
+ const arr = []
116
+ if (n1) arr.push(wrap(n1))
117
+ if (n2) arr.push(wrap(n2))
118
+ return arr
119
+ }
120
+ const n1 = ds[this.sel[0]] || ds[0]
121
+ return n1 ? [wrap(n1)] : []
122
+ },
123
+ onConfirm() {
124
+ dismissPicker(true, this.buildResult())
125
+ },
126
+ onCancel() {
127
+ dismissPicker(false)
128
+ },
129
+ },
130
+ }
131
+ </script>