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,243 @@
1
+ <!--
2
+ 卡片网格预览弹窗:点击顶栏「网格预览」图标按钮打开。
3
+ 展示本地全部卡片(GET /__cards 的 cardList)的缩略图墙,每格含:
4
+ - 预览图:复用本地静态服务 /__card-image/<id>,读取卡片目录下的同名预览图
5
+ (src/cards/<id>/<id>.png),不触发实时无头截图
6
+ - 标题:card.name
7
+ - 卡 id:card.id
8
+ 缩略图按可见性懒加载(IntersectionObserver),避免一次打开对 70+ 张图同时发起请求。
9
+ 点击任一张 → emit select(id)(父级 onSelect 载入工作台并持久化)+ close 关闭弹窗。
10
+ 搜索框按 id / 名称过滤;Esc 或点遮罩关闭;当前选中卡高亮。
11
+ -->
12
+ <template>
13
+ <transition name="cg-fade">
14
+ <div v-if="visible" class="cg-mask" @click.self="close">
15
+ <div class="cg-modal" role="dialog" aria-modal="true" aria-label="卡片网格预览">
16
+ <header class="cg-modal__head">
17
+ <div class="cg-modal__title">
18
+ <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-grid" /></svg>
19
+ <span>卡片网格预览</span>
20
+ <span class="cg-modal__count">{{ cards.length }} 张本地卡片</span>
21
+ </div>
22
+ <div class="cg-modal__search">
23
+ <svg class="cg-modal__search-icon" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><use href="#icon-search" /></svg>
24
+ <input
25
+ ref="search"
26
+ v-model="query"
27
+ class="cg-modal__input"
28
+ type="text"
29
+ placeholder="搜索卡片 ID / 名称…"
30
+ />
31
+ </div>
32
+ <button
33
+ class="cg-modal__sort"
34
+ :class="{ 'cg-modal__sort--active': sortByTime }"
35
+ type="button"
36
+ :title="sortByTime ? '已按修改时间排序(新→旧),点击取消' : '按修改时间排序(新→旧)'"
37
+ :aria-pressed="sortByTime"
38
+ @click="toggleSort"
39
+ >按修改时间排序</button>
40
+ <button class="cg-modal__close" type="button" title="关闭" @click="close" aria-label="关闭">
41
+ <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"><line x1="6" y1="6" x2="18" y2="18" /><line x1="18" y1="6" x2="6" y2="18" /></svg>
42
+ </button>
43
+ </header>
44
+
45
+ <div ref="grid" class="cg-grid">
46
+ <button
47
+ v-for="card in filtered"
48
+ :key="card.id"
49
+ :data-id="card.id"
50
+ type="button"
51
+ class="cg-tile"
52
+ :class="{
53
+ 'cg-tile--active': card.id === selectedId,
54
+ 'cg-tile--failed': failed.has(card.id),
55
+ }"
56
+ @click="choose(card)"
57
+ >
58
+ <div class="cg-tile__thumb">
59
+ <img
60
+ v-if="visibleIds.has(card.id) && !failed.has(card.id)"
61
+ class="cg-tile__img"
62
+ :src="thumbUrl(card.id)"
63
+ :alt="card.name"
64
+ loading="lazy"
65
+ @error="onImgError(card.id)"
66
+ />
67
+ <div v-else-if="!visibleIds.has(card.id)" class="cg-tile__skeleton">预览加载中…</div>
68
+ <div v-else class="cg-tile__fallback">
69
+ <svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><use href="#icon-glyph-Image" /></svg>
70
+ <span>预览不可用</span>
71
+ </div>
72
+ </div>
73
+ <div class="cg-tile__meta">
74
+ <span class="cg-tile__name" :title="card.name">{{ card.name }}</span>
75
+ <span class="cg-tile__id" :title="card.id">{{ card.id }}</span>
76
+ </div>
77
+ </button>
78
+ <p v-if="!filtered.length" class="cg-empty">无匹配卡片</p>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </transition>
83
+ </template>
84
+
85
+ <script>
86
+ import { resolveApiBase } from '@/api.js'
87
+
88
+ export default {
89
+ name: 'CardGallery',
90
+ props: {
91
+ /** 是否显示弹窗 */
92
+ visible: { type: Boolean, default: false },
93
+ /** 卡片列表(与顶栏 csel-combo 同源:GET /__cards 的 cardList) */
94
+ cards: { type: Array, default: () => [] },
95
+ /** 当前选中卡片 id,用于高亮 */
96
+ selectedId: { type: String, default: '' },
97
+ },
98
+ emits: ['select', 'close'],
99
+ data() {
100
+ return {
101
+ query: '',
102
+ // 排序开关:false=按名称(默认),true=按修改时间新→旧;初始值从 localStorage 读取
103
+ sortByTime: this.loadSortByTime(),
104
+ // 已滚动进入视口、应当发起缩略图请求的卡片 id 集合(IntersectionObserver 填充)
105
+ visibleIds: new Set(),
106
+ // 缩略图加载失败的卡片 id 集合(回退占位)
107
+ failed: new Set(),
108
+ observer: null,
109
+ // 截图端点 base(弹窗打开时按当前 apiBase 解析一次)
110
+ base: '',
111
+ }
112
+ },
113
+ created() {
114
+ // 仅确保 data 阶段读取的持久化值已落盘(无记录时建立基线)
115
+ this.saveSortByTime()
116
+ },
117
+ computed: {
118
+ filtered() {
119
+ const q = this.query.trim().toLowerCase()
120
+ let arr = this.cards
121
+ if (q) {
122
+ arr = arr.filter(
123
+ (c) =>
124
+ c.id.toLowerCase().includes(q) ||
125
+ String(c.name || '').toLowerCase().includes(q)
126
+ )
127
+ }
128
+ // 排序:时间模式按修改时间新→旧;否则按名称(默认)
129
+ const sorted = this.sortByTime
130
+ ? arr.slice().sort((a, b) => (b.mtime || 0) - (a.mtime || 0))
131
+ : arr.slice().sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
132
+ return sorted
133
+ },
134
+ },
135
+ watch: {
136
+ visible(v) {
137
+ if (v) {
138
+ this.query = ''
139
+ this.failed = new Set()
140
+ this.$nextTick(() => {
141
+ this.setupObserver()
142
+ this.focusSearch()
143
+ })
144
+ window.addEventListener('keydown', this.onKey)
145
+ } else {
146
+ this.teardownObserver()
147
+ window.removeEventListener('keydown', this.onKey)
148
+ }
149
+ },
150
+ // 搜索过滤改变列表 DOM → 重新观察新出现的卡片格子,保证懒加载继续生效
151
+ query() {
152
+ if (this.visible) this.$nextTick(this.setupObserver)
153
+ },
154
+ // 切换排序方式时立即持久化,避免每次刷新都要手动再点一次
155
+ sortByTime(v) {
156
+ this.saveSortByTime()
157
+ },
158
+ },
159
+ beforeUnmount() {
160
+ this.teardownObserver()
161
+ window.removeEventListener('keydown', this.onKey)
162
+ },
163
+ methods: {
164
+ /** 缩略图地址:复用本地静态服务 /__card-image/<id>,直接读取卡片目录下的同名预览图
165
+ * (src/cards/<id>/<id>.png),不触发实时无头截图。 */
166
+ thumbUrl(id) {
167
+ const base = this.base || resolveApiBase() || ''
168
+ return base + '/__card-image/' + encodeURIComponent(id)
169
+ },
170
+ /** 建立滚动容器内缩略图懒加载观察器:仅对进入视口(含 200px 预载边距)的格子发起截图请求。 */
171
+ setupObserver() {
172
+ this.base = resolveApiBase() || ''
173
+ const grid = this.$refs.grid
174
+ if (!grid) return
175
+ this.teardownObserver()
176
+ this.observer = new IntersectionObserver(
177
+ (entries) => {
178
+ for (const e of entries) {
179
+ if (!e.isIntersecting) continue
180
+ const id = e.target.getAttribute('data-id')
181
+ if (id && !this.visibleIds.has(id)) {
182
+ const next = new Set(this.visibleIds)
183
+ next.add(id)
184
+ this.visibleIds = next
185
+ }
186
+ this.observer.unobserve(e.target)
187
+ }
188
+ },
189
+ { root: grid, rootMargin: '200px' }
190
+ )
191
+ grid.querySelectorAll('.cg-tile').forEach((el) => this.observer.observe(el))
192
+ },
193
+ teardownObserver() {
194
+ if (this.observer) {
195
+ this.observer.disconnect()
196
+ this.observer = null
197
+ }
198
+ },
199
+ onImgError(id) {
200
+ const next = new Set(this.failed)
201
+ next.add(id)
202
+ this.failed = next
203
+ },
204
+ /** 读取持久化的排序方式(localStorage)。无记录或损坏时回退默认 false。 */
205
+ loadSortByTime() {
206
+ try {
207
+ return localStorage.getItem('cardbuddy:cardgallery-sort-by-time') === '1'
208
+ } catch (e) {
209
+ return false
210
+ }
211
+ },
212
+ /** 持久化排序方式到 localStorage:'1'=按修改时间,'0'=按名称。 */
213
+ saveSortByTime() {
214
+ try {
215
+ localStorage.setItem(
216
+ 'cardbuddy:cardgallery-sort-by-time',
217
+ this.sortByTime ? '1' : '0'
218
+ )
219
+ } catch (e) {
220
+ /* localStorage 不可用时忽略 */
221
+ }
222
+ },
223
+ /** 切换「按修改时间排序」:激活时列表新→旧;再次点击取消,回到按名称默认排序。 */
224
+ toggleSort() {
225
+ this.sortByTime = !this.sortByTime
226
+ },
227
+ onKey(e) {
228
+ if (e.key === 'Escape') this.close()
229
+ },
230
+ focusSearch() {
231
+ if (this.$refs.search) this.$refs.search.focus()
232
+ },
233
+ choose(card) {
234
+ if (!card) return
235
+ this.$emit('select', card.id)
236
+ this.close()
237
+ },
238
+ close() {
239
+ this.$emit('close')
240
+ },
241
+ },
242
+ }
243
+ </script>
@@ -0,0 +1,72 @@
1
+ <!--
2
+ 自包含卡片预览组件(供独立截图/视觉比对复用,无需编辑器和 dev server)。
3
+
4
+ 与编辑器 App.vue 共用同一套渲染内核(RenderTree / Adapter / createCardRuntime /
5
+ buildPreview),但只保留「卡片本身」:
6
+ - provide 下发的 adapter/cardRuntime/customComponents 与编辑器预览一致;
7
+ - 挂载 [data-card-ready] 供无头浏览器等待首帧落定;
8
+ - adapter 使用 apiBase:'',让 /slices 等站内资源保持同源相对路径,
9
+ 不依赖 8787 本地开发服务。
10
+ -->
11
+ <template>
12
+ <div class="card-preview">
13
+ <div
14
+ class="phone__screen phone__screen--bg-white"
15
+ :style="screenStyle"
16
+ :data-card-ready="ready ? '1' : null"
17
+ >
18
+ <RenderTree
19
+ v-if="preview"
20
+ :node="preview.view"
21
+ :scope="runtime.state"
22
+ />
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script>
28
+ import RenderTree from './RenderTree.vue'
29
+ import Adapter from '../runtime/adapter.js'
30
+ import { createCardRuntime } from '../runtime/card-runtime.js'
31
+ import { buildPreview } from '../runtime/toPreview.js'
32
+ import { installJsApiMock } from '../runtime/jsapi-mock.js'
33
+
34
+ export default {
35
+ name: 'CardPreview',
36
+ components: { RenderTree },
37
+ props: {
38
+ schema: { type: Object, required: true },
39
+ data: { type: Object, default: () => ({}) },
40
+ width: { type: [String, Number], default: 375 },
41
+ },
42
+ provide() {
43
+ return {
44
+ adapter: this.adapter,
45
+ cardRuntime: this.runtime,
46
+ customComponents: {},
47
+ }
48
+ },
49
+ data() {
50
+ const adapter = new Adapter({ apiBase: '' })
51
+ const runtime = createCardRuntime(this.data || {}, adapter)
52
+ installJsApiMock(runtime)
53
+ return {
54
+ adapter,
55
+ runtime,
56
+ preview: buildPreview(this.schema),
57
+ ready: false,
58
+ }
59
+ },
60
+ computed: {
61
+ screenStyle() {
62
+ const width = Number(this.width) || (this.preview && this.preview.view && this.preview.view.width) || 375
63
+ return { width: width + 'px' }
64
+ },
65
+ },
66
+ mounted() {
67
+ this.$nextTick(() => {
68
+ this.ready = true
69
+ })
70
+ },
71
+ }
72
+ </script>
@@ -0,0 +1,200 @@
1
+ <template>
2
+ <div ref="root" class="csel" :class="{ 'csel--open': open }">
3
+ <button
4
+ type="button"
5
+ class="csel__trigger"
6
+ :class="{ 'csel__trigger--active': open }"
7
+ @click="toggle"
8
+ >
9
+ <span class="csel__current">
10
+ <span class="csel__id">{{ current.id }}</span>
11
+ <span class="csel__name">{{ current.name }}</span>
12
+ </span>
13
+ <svg class="csel__chev" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><use href="#icon-chevron" /></svg>
14
+ </button>
15
+
16
+ <transition name="csel-fade">
17
+ <div v-if="open" class="csel__pop">
18
+ <div class="csel__search">
19
+ <svg class="csel__search-icon" viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><use href="#icon-search" /></svg>
20
+ <input
21
+ ref="search"
22
+ v-model="query"
23
+ class="csel__input"
24
+ type="text"
25
+ placeholder="搜索卡片 ID / 名称…"
26
+ @keydown.down.prevent="move(1)"
27
+ @keydown.up.prevent="move(-1)"
28
+ @keydown.enter.prevent="choose(filtered[highlighted])"
29
+ @keydown.esc.prevent="close"
30
+ />
31
+ </div>
32
+ <ul ref="list" class="csel__list">
33
+ <li
34
+ v-for="(opt, i) in filtered"
35
+ :key="opt.id"
36
+ :class="[
37
+ 'csel__item',
38
+ {
39
+ 'csel__item--active': i === highlighted,
40
+ 'csel__item--selected': opt.id === value,
41
+ },
42
+ ]"
43
+ @mousemove="highlighted = i"
44
+ @click="choose(opt)"
45
+ >
46
+ <span class="csel__item-id">{{ opt.id }}</span>
47
+ <span class="csel__item-name">{{ opt.name }}</span>
48
+ <svg v-if="opt.id === value" class="csel__check" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><use href="#icon-check" /></svg>
49
+ </li>
50
+ <li v-if="!filtered.length" class="csel__empty">无匹配卡片</li>
51
+ </ul>
52
+ <div class="csel__foot">
53
+ <button
54
+ class="csel__sort"
55
+ :class="{ 'csel__sort--active': sortByTime }"
56
+ type="button"
57
+ :title="sortByTime ? '已按修改时间排序(新→旧),点击取消' : '按修改时间排序(新→旧)'"
58
+ :aria-pressed="sortByTime"
59
+ @click="toggleSort"
60
+ >按修改时间排序</button>
61
+ <span class="csel__count">{{ filtered.length }} / {{ options.length }} 张卡片</span>
62
+ </div>
63
+ </div>
64
+ </transition>
65
+ </div>
66
+ </template>
67
+
68
+ <script>
69
+ export default {
70
+ name: 'CardSelect',
71
+ // 必须声明 emits:否则父组件的 @change 会作为 onChange 透传到根 div,
72
+ // 内部搜索框 <input> 的原生 change 事件冒泡上来会再次触发父级 onSelect,
73
+ // 把 selectedId 覆盖成 Event 对象(表现为搜索后选卡片不生效)。
74
+ emits: ['input', 'change'],
75
+ props: {
76
+ value: { type: String, default: '' },
77
+ options: { type: Array, default: () => [] },
78
+ },
79
+ data() {
80
+ return {
81
+ open: false,
82
+ query: '',
83
+ highlighted: 0,
84
+ // 排序开关:false=按名称(默认),true=按修改时间新→旧
85
+ // 初始值从 localStorage 读取,保证刷新后仍保持上次选择
86
+ sortByTime: this.loadSortByTime(),
87
+ }
88
+ },
89
+ created() {
90
+ // 仅确保 data 阶段读取的持久化值已落盘(无记录时建立基线)
91
+ this.saveSortByTime()
92
+ },
93
+ computed: {
94
+ current() {
95
+ return (
96
+ this.options.find((o) => o.id === this.value) || {
97
+ id: this.value,
98
+ name: this.value,
99
+ }
100
+ )
101
+ },
102
+ filtered() {
103
+ const q = this.query.trim().toLowerCase()
104
+ let arr = this.options
105
+ if (q) {
106
+ arr = this.options.filter(
107
+ (o) =>
108
+ o.id.toLowerCase().includes(q) ||
109
+ String(o.name || '').toLowerCase().includes(q)
110
+ )
111
+ }
112
+ // 排序:时间模式按修改时间新→旧;否则按名称(默认)
113
+ const sorted = this.sortByTime
114
+ ? arr.slice().sort((a, b) => (b.mtime || 0) - (a.mtime || 0))
115
+ : arr.slice().sort((a, b) =>
116
+ String(a.name || '').localeCompare(String(b.name || ''))
117
+ )
118
+ return sorted
119
+ },
120
+ },
121
+ watch: {
122
+ open(v) {
123
+ if (v) {
124
+ this.query = ''
125
+ this.highlighted = Math.max(0, this.filtered.findIndex((o) => o.id === this.value))
126
+ this.$nextTick(() => {
127
+ if (this.$refs.search) this.$refs.search.focus()
128
+ this.scrollToActive()
129
+ })
130
+ document.addEventListener('click', this.onDocClick, true)
131
+ } else {
132
+ document.removeEventListener('click', this.onDocClick, true)
133
+ }
134
+ },
135
+ // 切换排序方式时立即持久化,避免每次刷新都要手动再点一次
136
+ sortByTime(v) {
137
+ this.saveSortByTime()
138
+ },
139
+ },
140
+ beforeUnmount() {
141
+ document.removeEventListener('click', this.onDocClick, true)
142
+ },
143
+ methods: {
144
+ toggle() {
145
+ this.open = !this.open
146
+ },
147
+ /** 切换「按修改时间排序」:激活时列表新→旧;再次点击取消,回到按名称默认排序。
148
+ * 切换后把高亮重新指回当前选中卡片,避免高亮错位到别的行。 */
149
+ toggleSort() {
150
+ this.sortByTime = !this.sortByTime
151
+ this.highlighted = Math.max(0, this.filtered.findIndex((o) => o.id === this.value))
152
+ },
153
+ /** 读取持久化的排序方式(localStorage)。无记录或损坏时回退默认 false。 */
154
+ loadSortByTime() {
155
+ try {
156
+ return localStorage.getItem('cardbuddy:cardselect-sort-by-time') === '1'
157
+ } catch (e) {
158
+ return false
159
+ }
160
+ },
161
+ /** 持久化排序方式到 localStorage:'1'=按修改时间,'0'=按名称。 */
162
+ saveSortByTime() {
163
+ try {
164
+ localStorage.setItem(
165
+ 'cardbuddy:cardselect-sort-by-time',
166
+ this.sortByTime ? '1' : '0'
167
+ )
168
+ } catch (e) {
169
+ /* localStorage 不可用时忽略 */
170
+ }
171
+ },
172
+ close() {
173
+ this.open = false
174
+ },
175
+ onDocClick(e) {
176
+ if (this.$refs.root && !this.$refs.root.contains(e.target)) this.close()
177
+ },
178
+ move(delta) {
179
+ if (!this.filtered.length) return
180
+ const n = this.filtered.length
181
+ this.highlighted = (this.highlighted + delta + n) % n
182
+ this.$nextTick(this.scrollToActive)
183
+ },
184
+ scrollToActive() {
185
+ const list = this.$refs.list
186
+ if (!list) return
187
+ const el = list.children[this.highlighted]
188
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' })
189
+ },
190
+ choose(opt) {
191
+ if (!opt) return
192
+ this.close()
193
+ if (opt.id !== this.value) {
194
+ this.$emit('input', opt.id)
195
+ this.$emit('change', opt.id)
196
+ }
197
+ },
198
+ },
199
+ }
200
+ </script>
@@ -0,0 +1,131 @@
1
+ <!--
2
+ 多选组(官方 module 1980 / 503)。
3
+ 结构对齐在线编辑器渲染产物:
4
+ ce_check_group > ce_check_list(flex row/column) > ce_check_item
5
+ > ce_check_icon(svg) + ce_check_text(span)
6
+ 颜色/尺寸/间距由 schema 属性驱动(value / dataList / selectColor /
7
+ unSelectColor / buttonStyle / direction / itemPadding / itemSpacing /
8
+ textSpacing / fontSize / iconSize)。
9
+ -->
10
+ <template>
11
+ <div class="ce_check_group" :style="rootStyle">
12
+ <div class="ce_check_list" :style="{ flexDirection: direction === 'vertical' ? 'column' : 'row' }">
13
+ <div
14
+ v-for="(item, idx) in dataList"
15
+ :key="item.key"
16
+ class="ce_check_item"
17
+ :style="itemStyle(idx)"
18
+ @click="toggle(item)"
19
+ >
20
+ <div
21
+ v-if="buttonStyle !== 'Text'"
22
+ class="ce_check_icon"
23
+ :style="{ width: iconSize + 'px', height: iconSize + 'px', marginRight: textSpacing + 'px' }"
24
+ >
25
+ <svg class="ce_svgcheckstyle" :viewBox="'0 0 1024 1024'">
26
+ <path
27
+ v-for="(d, i) in checkPaths(item)"
28
+ :key="i"
29
+ :d="d"
30
+ :fill="isChecked(item) ? selectColor : unSelectColor"
31
+ />
32
+ </svg>
33
+ </div>
34
+ <span
35
+ class="ce_check_text"
36
+ :title="item.desc"
37
+ :style="{ color: isChecked(item) ? selectColor : unSelectColor, fontSize: fontSize + 'px' }"
38
+ >{{ item.desc }}</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </template>
43
+
44
+ <script>
45
+ import base from './base.js'
46
+ import { CHECK_PATHS } from './icons.js'
47
+
48
+ export default {
49
+ name: 'CeCheckGroup',
50
+ mixins: [base],
51
+ props: {
52
+ styleProps: { type: Object, default: () => ({}) },
53
+ value: { type: [Array, String, Number], default: () => [] },
54
+ dataList: { type: Array, default: () => [] },
55
+ selectColor: { type: String, default: '#0082ff' },
56
+ unSelectColor: { type: String, default: '#333333' },
57
+ buttonStyle: { type: String, default: 'Circle' },
58
+ maxSelectCount: { type: [String, Number], default: '0' },
59
+ direction: { type: String, default: 'horizontal' },
60
+ itemPadding: { type: [String, Number], default: '4 10 4 10' },
61
+ itemSpacing: { type: [String, Number], default: '4' },
62
+ textSpacing: { type: [String, Number], default: '3' },
63
+ fontSize: { type: [String, Number], default: '13' },
64
+ iconSize: { type: [String, Number], default: '14' },
65
+ },
66
+ emits: ['change', 'selectChange'],
67
+ computed: {
68
+ rootStyle() {
69
+ return { overflow: 'hidden', boxSizing: 'border-box' }
70
+ },
71
+ checkedSet() {
72
+ const v = this.value
73
+ if (Array.isArray(v)) return new Set(v.map(String))
74
+ if (v === '' || v == null) return new Set()
75
+ return new Set([String(v)])
76
+ },
77
+ limit() {
78
+ const n = Number(this.maxSelectCount)
79
+ return Number.isFinite(n) && n > 0 ? n : Infinity
80
+ },
81
+ },
82
+ methods: {
83
+ isChecked(item) {
84
+ return this.checkedSet.has(String(item.key))
85
+ },
86
+ /** 选中态图标路径:Circle 共用一个符号;SquareEmpty / Square 有选中/未选中两套 */
87
+ checkPaths(item) {
88
+ const checked = this.isChecked(item)
89
+ switch (this.buttonStyle) {
90
+ case 'SquareEmpty':
91
+ return CHECK_PATHS[checked ? 'CheckGroupSquareEmptyCheck' : 'CheckGroupSquareEmpty']
92
+ case 'Square':
93
+ return CHECK_PATHS[checked ? 'CheckGroupSquareCheck' : 'CheckGroupSquare']
94
+ case 'Text':
95
+ return []
96
+ case 'Circle':
97
+ default:
98
+ return CHECK_PATHS.CheckGroupCircle
99
+ }
100
+ },
101
+ itemStyle(idx) {
102
+ const pad = String(this.itemPadding).split(/\s+/)
103
+ const spacing = Number(this.itemSpacing) || 0
104
+ const isLast = idx === this.dataList.length - 1
105
+ const marginRight = isLast ? 0 : spacing
106
+ const marginBottom = idx === this.dataList.length - 1 ? 0 : spacing
107
+ const toPx = (a, i) => this.adapter.doSize(a[i] !== undefined ? a[i] : a[0] || 0)
108
+ const padding =
109
+ pad.length >= 4
110
+ ? `${toPx(pad, 0)} ${toPx(pad, 1)} ${toPx(pad, 2)} ${toPx(pad, 3)}`
111
+ : this.adapter.doSize(this.itemPadding)
112
+ return this.direction === 'vertical'
113
+ ? { padding, marginBottom }
114
+ : { padding, marginRight }
115
+ },
116
+ toggle(item) {
117
+ const key = String(item.key)
118
+ const set = new Set(this.checkedSet)
119
+ if (set.has(key)) {
120
+ set.delete(key)
121
+ } else {
122
+ if (set.size >= this.limit) set.clear()
123
+ set.add(key)
124
+ }
125
+ const next = [...set]
126
+ this.$emit('change', next)
127
+ this.$emit('selectChange', { value: next, item })
128
+ },
129
+ },
130
+ }
131
+ </script>
@@ -0,0 +1,25 @@
1
+ <!-- 官方 module 2097 / 539(updateGap 是 IE 兼容分支,省略) -->
2
+ <template>
3
+ <div ref="column" class="ce_column" :style="styleObj" @click="$emit('click', $event)">
4
+ <slot />
5
+ </div>
6
+ </template>
7
+
8
+ <script>
9
+ import StyleParser from '../runtime/StyleParser.js'
10
+ import base from './base.js'
11
+
12
+ export default {
13
+ name: 'CeColumn',
14
+ emits: ['click'],
15
+ mixins: [base],
16
+ props: {
17
+ styleProps: { type: Object, default: () => ({}) },
18
+ },
19
+ computed: {
20
+ styleObj() {
21
+ return new StyleParser(this, this.styleProps).parse()
22
+ },
23
+ },
24
+ }
25
+ </script>