@arronqzy/vue-view 0.1.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 (75) hide show
  1. package/README.md +50 -0
  2. package/package.json +49 -0
  3. package/src/env.d.ts +62 -0
  4. package/src/index.ts +4 -0
  5. package/src/panel/VueViewOnlinePreview.vue +276 -0
  6. package/src/panel/VueViewPanel.vue +871 -0
  7. package/src/panel/components/ConfigHintIcon.vue +34 -0
  8. package/src/panel/components/ElementsLayer.vue +165 -0
  9. package/src/panel/components/MaterialPreview.vue +135 -0
  10. package/src/panel/components/MaterialSidebar.vue +526 -0
  11. package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
  12. package/src/panel/components/MoveableLayer.vue +859 -0
  13. package/src/panel/components/PanelCanvas.vue +630 -0
  14. package/src/panel/components/PanelConfigSidebar.vue +397 -0
  15. package/src/panel/components/PanelRulers.vue +177 -0
  16. package/src/panel/components/SelectLayer.vue +115 -0
  17. package/src/panel/components/ViewElementScopePanel.vue +76 -0
  18. package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
  19. package/src/panel/components/WorkspaceProjectNav.vue +192 -0
  20. package/src/panel/components/WorkspaceStageSplit.vue +258 -0
  21. package/src/panel/components/config/ConfigColorField.vue +52 -0
  22. package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
  23. package/src/panel/components/config/ConfigSection.vue +50 -0
  24. package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
  25. package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
  26. package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
  27. package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
  28. package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
  29. package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
  30. package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
  31. package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
  32. package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
  33. package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
  34. package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
  35. package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
  36. package/src/panel/components/config/shared.ts +74 -0
  37. package/src/panel/components/elementsLayerNodes.ts +830 -0
  38. package/src/panel/components/materialSidebarData.ts +85 -0
  39. package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
  40. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
  41. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
  42. package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
  43. package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
  44. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
  45. package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
  46. package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
  47. package/src/panel/constants/messages.ts +34 -0
  48. package/src/panel/constants/zIndex.ts +6 -0
  49. package/src/panel/hooks/usePanelElements.ts +1075 -0
  50. package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
  51. package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
  52. package/src/panel/lib/panel-ruler-canvas.ts +139 -0
  53. package/src/panel/library/workspace-project-cache.ts +23 -0
  54. package/src/panel/library/workspace-project-db.ts +111 -0
  55. package/src/panel/library/workspace-project-sync.ts +41 -0
  56. package/src/panel/library/workspace-snapshot.ts +30 -0
  57. package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
  58. package/src/panel/scope/view-scope-store.ts +82 -0
  59. package/src/panel/types.ts +127 -0
  60. package/src/panel/utils/chartOptionBuilder.ts +327 -0
  61. package/src/panel/utils/gridPlacement.ts +189 -0
  62. package/src/panel/utils/mappingLayerOps.ts +142 -0
  63. package/src/panel/utils/panelElementDefaults.ts +161 -0
  64. package/src/panel/utils/panelElementNodes.ts +35 -0
  65. package/src/panel/utils/panelStateIO.ts +124 -0
  66. package/src/panel/utils/scope-autocomplete.ts +114 -0
  67. package/src/panel/utils/scope-field-labels.ts +46 -0
  68. package/src/panel/utils/scope-template-chart.ts +92 -0
  69. package/src/panel/utils/scope-template-preview.ts +124 -0
  70. package/src/panel/utils/scope-template-spread.ts +229 -0
  71. package/src/panel/utils/scope-template-warnings.ts +243 -0
  72. package/src/panel/utils/scope-template.ts +97 -0
  73. package/src/panel/utils/updateElementDraft.ts +221 -0
  74. package/src/panel/viewportZoom.ts +26 -0
  75. package/src/tailwind.css +43 -0
@@ -0,0 +1,434 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import { Card, Checkbox, Input, InputNumber, Select } from "ant-design-vue";
4
+ import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
5
+ import { CHART_TYPES } from "../../utils/chartOptionBuilder";
6
+
7
+ const props = defineProps<{
8
+ elements: PanelElement[];
9
+ layers: PanelLayer[];
10
+ normalizedSearch: string;
11
+ hasSearch: boolean;
12
+ updateElement: (
13
+ id: string,
14
+ patch: Partial<PanelElement>,
15
+ options?: { batchId?: string; meta?: Record<string, unknown> }
16
+ ) => void;
17
+ setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
18
+ onExcludeSelectedNode?: (nodeId: string) => void;
19
+ onAdjustNodeZOrder?: (
20
+ nodeId: string,
21
+ action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
22
+ ) => void;
23
+ }>();
24
+
25
+ const expandedNodeCards = ref<Record<string, boolean>>({});
26
+
27
+ const filteredElements = computed(() =>
28
+ props.elements.filter((el) => {
29
+ if (!props.hasSearch) return true;
30
+ const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`.toLowerCase();
31
+ return text.includes(props.normalizedSearch);
32
+ })
33
+ );
34
+
35
+ const noSearchMatch = computed(
36
+ () =>
37
+ props.hasSearch &&
38
+ props.elements.every((el) => {
39
+ const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`.toLowerCase();
40
+ return !text.includes(props.normalizedSearch);
41
+ })
42
+ );
43
+
44
+ function isNodeCardExpanded(id: string) {
45
+ return expandedNodeCards.value[id] ?? true;
46
+ }
47
+
48
+ function setNodeCardExpanded(id: string, open: boolean) {
49
+ expandedNodeCards.value = { ...expandedNodeCards.value, [id]: open };
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <div class="space-y-3">
55
+ <Card size="small" title="批量设置">
56
+ <template #title>
57
+ <span class="text-xs">批量设置({{ elements.length }} 个)</span>
58
+ </template>
59
+ <div class="grid grid-cols-2 gap-2 text-xs">
60
+ <button
61
+ type="button"
62
+ class="rounded border border-gray-200 bg-white px-2 py-1 hover:bg-gray-50"
63
+ @click="elements.forEach((el) => updateElement(el.id, { locked: true }))"
64
+ >
65
+ 全部锁定
66
+ </button>
67
+ <button
68
+ type="button"
69
+ class="rounded border border-gray-200 bg-white px-2 py-1 hover:bg-gray-50"
70
+ @click="elements.forEach((el) => updateElement(el.id, { locked: false }))"
71
+ >
72
+ 全部解锁
73
+ </button>
74
+ <button
75
+ v-if="onAdjustNodeZOrder"
76
+ type="button"
77
+ class="rounded border border-gray-200 bg-white px-2 py-1 hover:bg-gray-50"
78
+ @click="elements.forEach((el) => onAdjustNodeZOrder?.(el.id, 'bringForward'))"
79
+ >
80
+ 全部上移一层
81
+ </button>
82
+ <button
83
+ v-if="onAdjustNodeZOrder"
84
+ type="button"
85
+ class="rounded border border-gray-200 bg-white px-2 py-1 hover:bg-gray-50"
86
+ @click="elements.forEach((el) => onAdjustNodeZOrder?.(el.id, 'sendBackward'))"
87
+ >
88
+ 全部下移一层
89
+ </button>
90
+ <button
91
+ type="button"
92
+ class="rounded border border-gray-200 bg-white px-2 py-1 hover:bg-gray-50"
93
+ @click="elements.forEach((el) => updateElement(el.id, { zIndex: 1 }))"
94
+ >
95
+ 全部 zIndex 设为 1
96
+ </button>
97
+ <button
98
+ type="button"
99
+ class="rounded border border-gray-200 bg-white px-2 py-1 hover:bg-gray-50"
100
+ @click="
101
+ elements.forEach((el) =>
102
+ updateElement(el.id, {
103
+ style: { ...(el.style ?? {}), backgroundColor: '#3b82f6' },
104
+ })
105
+ )
106
+ "
107
+ >
108
+ 全部背景色设为蓝色
109
+ </button>
110
+ </div>
111
+ </Card>
112
+
113
+ <Card
114
+ v-for="el in filteredElements"
115
+ :key="el.id"
116
+ size="small"
117
+ :class="el.locked ? 'border-amber-500/40 bg-amber-500/5' : ''"
118
+ >
119
+ <template #title>
120
+ <div class="flex items-center gap-2">
121
+ <button
122
+ type="button"
123
+ class="inline-flex h-6 w-6 items-center justify-center rounded border border-gray-200 text-[11px] hover:bg-gray-50"
124
+ @click="setNodeCardExpanded(el.id, !isNodeCardExpanded(el.id))"
125
+ >
126
+ {{ isNodeCardExpanded(el.id) ? "▾" : "▸" }}
127
+ </button>
128
+ <span class="min-w-0 flex-1 truncate text-xs">
129
+ {{ el.name?.trim() || el.materialType || "节点" }} · {{ el.id }}
130
+ </span>
131
+ <button
132
+ v-if="onExcludeSelectedNode"
133
+ type="button"
134
+ class="inline-flex h-6 items-center rounded border border-gray-200 px-2 text-[11px] text-gray-500 hover:bg-gray-50"
135
+ @click="onExcludeSelectedNode(el.id)"
136
+ >
137
+ 剔除
138
+ </button>
139
+ </div>
140
+ </template>
141
+ <div v-if="isNodeCardExpanded(el.id)" class="space-y-2 text-xs">
142
+ <label class="flex items-center gap-2">
143
+ <Checkbox
144
+ :checked="el.locked === true"
145
+ @update:checked="(v) => updateElement(el.id, { locked: v === true })"
146
+ />
147
+ <span>锁定节点</span>
148
+ </label>
149
+ <div
150
+ v-if="el.locked"
151
+ class="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700"
152
+ >
153
+ 当前节点已锁定,仅可操作锁定开关。
154
+ </div>
155
+ <fieldset :disabled="el.locked" :class="el.locked ? 'opacity-60' : ''">
156
+ <div class="space-y-2">
157
+ <label class="block space-y-1">
158
+ <div>名称</div>
159
+ <Input
160
+ size="small"
161
+ :value="el.name ?? ''"
162
+ @update:value="(v: string) => updateElement(el.id, { name: v || undefined })"
163
+ />
164
+ </label>
165
+ <div class="grid grid-cols-2 gap-2">
166
+ <label class="block space-y-1">
167
+ <div>zIndex</div>
168
+ <InputNumber
169
+ size="small"
170
+ class="w-full"
171
+ :value="el.zIndex ?? 1"
172
+ @update:value="(v) => updateElement(el.id, { zIndex: Number(v) || 1 })"
173
+ />
174
+ </label>
175
+ <label class="block space-y-1">
176
+ <div>图层</div>
177
+ <Select
178
+ size="small"
179
+ class="w-full"
180
+ :value="el.layerId"
181
+ @update:value="(v) => updateElement(el.id, { layerId: String(v) })"
182
+ >
183
+ <Select.Option v-for="layer in layers" :key="layer.id" :value="layer.id">
184
+ {{ layer.name }}
185
+ </Select.Option>
186
+ </Select>
187
+ </label>
188
+ </div>
189
+ <div class="grid grid-cols-2 gap-2">
190
+ <label class="block space-y-1">
191
+ <div>X</div>
192
+ <InputNumber size="small" class="w-full" :value="el.x" @update:value="(v) => updateElement(el.id, { x: Number(v) || 0 })" />
193
+ </label>
194
+ <label class="block space-y-1">
195
+ <div>Y</div>
196
+ <InputNumber size="small" class="w-full" :value="el.y" @update:value="(v) => updateElement(el.id, { y: Number(v) || 0 })" />
197
+ </label>
198
+ <label class="block space-y-1">
199
+ <div>旋转角度</div>
200
+ <InputNumber size="small" class="w-full" :value="el.rotate ?? 0" @update:value="(v) => updateElement(el.id, { rotate: Number(v) || 0 })" />
201
+ </label>
202
+ <label class="block space-y-1">
203
+ <div>宽</div>
204
+ <InputNumber size="small" class="w-full" :min="1" :value="el.width" @update:value="(v) => updateElement(el.id, { width: Math.max(1, Number(v) || 1) })" />
205
+ </label>
206
+ <label class="block space-y-1">
207
+ <div>高</div>
208
+ <InputNumber size="small" class="w-full" :min="1" :value="el.height" @update:value="(v) => updateElement(el.id, { height: Math.max(1, Number(v) || 1) })" />
209
+ </label>
210
+ </div>
211
+ <div class="grid grid-cols-2 gap-2">
212
+ <label class="block space-y-1">
213
+ <div>背景色</div>
214
+ <Input
215
+ size="small"
216
+ :value="el.style?.backgroundColor ?? ''"
217
+ @update:value="(v: string) => updateElement(el.id, { style: { ...(el.style ?? {}), backgroundColor: v || undefined } })"
218
+ />
219
+ </label>
220
+ <label class="block space-y-1">
221
+ <div>边框色</div>
222
+ <Input
223
+ size="small"
224
+ :value="el.style?.borderColor ?? ''"
225
+ @update:value="(v: string) => updateElement(el.id, { style: { ...(el.style ?? {}), borderColor: v || undefined } })"
226
+ />
227
+ </label>
228
+ </div>
229
+
230
+ <template v-if="CHART_TYPES.has(el.materialType ?? '')">
231
+ <div class="grid grid-cols-2 gap-2">
232
+ <label class="col-span-2 block space-y-1">
233
+ <div>图表标题</div>
234
+ <Input
235
+ size="small"
236
+ :value="el.chart?.title ?? ''"
237
+ @update:value="(v: string) => updateElement(el.id, { chart: { ...(el.chart ?? {}), title: v } })"
238
+ />
239
+ </label>
240
+ <label class="block space-y-1">
241
+ <div>主色</div>
242
+ <Input
243
+ size="small"
244
+ :value="el.chart?.color ?? ''"
245
+ @update:value="(v: string) => updateElement(el.id, { chart: { ...(el.chart ?? {}), color: v || undefined } })"
246
+ />
247
+ </label>
248
+ </div>
249
+ </template>
250
+
251
+ <template v-if="el.materialType === 'text'">
252
+ <div class="grid grid-cols-2 gap-2">
253
+ <label class="col-span-2 block space-y-1">
254
+ <div>文本内容(HTML)</div>
255
+ <Input.TextArea
256
+ :value="el.textHtml ?? ''"
257
+ :rows="3"
258
+ @update:value="(v: string) => updateElement(el.id, { textHtml: v || '<p><br/></p>' })"
259
+ />
260
+ </label>
261
+ <label class="block space-y-1">
262
+ <div>字体大小</div>
263
+ <InputNumber
264
+ size="small"
265
+ class="w-full"
266
+ :min="8"
267
+ :value="el.textFontSize ?? 14"
268
+ @update:value="(v) => updateElement(el.id, { textFontSize: Math.max(8, Number(v) || 14) })"
269
+ />
270
+ </label>
271
+ <label class="block space-y-1">
272
+ <div>文字颜色</div>
273
+ <Input
274
+ size="small"
275
+ :value="el.textColor ?? ''"
276
+ @update:value="(v: string) => updateElement(el.id, { textColor: v || undefined })"
277
+ />
278
+ </label>
279
+ </div>
280
+ </template>
281
+
282
+ <template v-if="el.materialType === 'audio'">
283
+ <div class="grid grid-cols-2 gap-2">
284
+ <label class="col-span-2 block space-y-1">
285
+ <div>音频 URL</div>
286
+ <Input
287
+ size="small"
288
+ :value="el.audioRemoteUrl ?? ''"
289
+ @update:value="(v: string) => updateElement(el.id, { audioRemoteUrl: v || undefined })"
290
+ />
291
+ </label>
292
+ <label class="block space-y-1">
293
+ <div>动效</div>
294
+ <Select
295
+ size="small"
296
+ class="w-full"
297
+ :value="el.audioVisualEffect ?? 'pulse'"
298
+ @update:value="(v) => updateElement(el.id, { audioVisualEffect: v as PanelElement['audioVisualEffect'] })"
299
+ >
300
+ <Select.Option value="none">none</Select.Option>
301
+ <Select.Option value="pulse">pulse</Select.Option>
302
+ <Select.Option value="ripple">ripple</Select.Option>
303
+ </Select>
304
+ </label>
305
+ </div>
306
+ </template>
307
+
308
+ <template v-if="el.materialType === 'video'">
309
+ <label class="block space-y-1">
310
+ <div>视频 URL</div>
311
+ <Input
312
+ size="small"
313
+ :value="el.videoRemoteUrl ?? ''"
314
+ @update:value="(v: string) => updateElement(el.id, { videoRemoteUrl: v || undefined })"
315
+ />
316
+ </label>
317
+ </template>
318
+
319
+ <template v-if="el.materialType === 'grid'">
320
+ <div class="grid grid-cols-2 gap-2 rounded-lg border border-gray-200/60 bg-gray-50/50 p-3">
321
+ <label class="block space-y-1">
322
+ <div>行</div>
323
+ <InputNumber size="small" class="w-full" :min="1" :value="el.gridRows ?? 2" @update:value="(v) => updateElement(el.id, { gridRows: Math.max(1, Number(v) || 2) })" />
324
+ </label>
325
+ <label class="block space-y-1">
326
+ <div>列</div>
327
+ <InputNumber size="small" class="w-full" :min="1" :value="el.gridCols ?? 3" @update:value="(v) => updateElement(el.id, { gridCols: Math.max(1, Number(v) || 3) })" />
328
+ </label>
329
+ </div>
330
+ </template>
331
+
332
+ <template v-if="el.materialType === 'geometry'">
333
+ <div class="grid grid-cols-2 gap-2 rounded-lg border border-gray-200/60 bg-gray-50/50 p-3">
334
+ <label class="block space-y-1">
335
+ <div>形状</div>
336
+ <Select
337
+ size="small"
338
+ class="w-full"
339
+ :value="el.geometryShape ?? 'rect'"
340
+ @update:value="(v) => updateElement(el.id, { geometryShape: v as PanelElement['geometryShape'] })"
341
+ >
342
+ <Select.Option value="rect">矩形</Select.Option>
343
+ <Select.Option value="circle">圆形</Select.Option>
344
+ <Select.Option value="triangle">三角形</Select.Option>
345
+ </Select>
346
+ </label>
347
+ <label class="block space-y-1">
348
+ <div>颜色</div>
349
+ <Input
350
+ size="small"
351
+ :value="el.geometryColor ?? '#3b82f6'"
352
+ @update:value="(v: string) => updateElement(el.id, { geometryColor: v || '#3b82f6' })"
353
+ />
354
+ </label>
355
+ </div>
356
+ </template>
357
+
358
+ <template v-if="el.materialType === 'reference'">
359
+ <div class="grid grid-cols-2 gap-2">
360
+ <label class="block space-y-1">
361
+ <div>引用图层</div>
362
+ <Select
363
+ size="small"
364
+ class="w-full"
365
+ :value="el.refLayerId ?? '__none__'"
366
+ @update:value="(v) => updateElement(el.id, { refLayerId: v === '__none__' ? undefined : String(v) })"
367
+ >
368
+ <Select.Option value="__none__">无</Select.Option>
369
+ <Select.Option
370
+ v-for="layer in layers.filter((l) => l.id !== el.layerId)"
371
+ :key="layer.id"
372
+ :value="layer.id"
373
+ >
374
+ {{ layer.name }}
375
+ </Select.Option>
376
+ </Select>
377
+ </label>
378
+ <label class="block space-y-1">
379
+ <div>拷贝</div>
380
+ <Select
381
+ size="small"
382
+ class="w-full"
383
+ :value="el.refCopyMode ?? 'shallow'"
384
+ @update:value="(v) => setReferenceCopyMode?.(el.id, v as ReferenceCopyMode)"
385
+ >
386
+ <Select.Option value="shallow">shallow</Select.Option>
387
+ <Select.Option value="deep">deep</Select.Option>
388
+ </Select>
389
+ </label>
390
+ </div>
391
+ </template>
392
+
393
+ <template v-if="el.materialType === 'image'">
394
+ <label class="block space-y-1">
395
+ <div>背景图 / URL</div>
396
+ <Input
397
+ size="small"
398
+ :value="el.style?.backgroundImageRemoteUrl ?? el.style?.backgroundImage ?? ''"
399
+ @update:value="(v: string) => updateElement(el.id, {
400
+ style: {
401
+ ...(el.style ?? {}),
402
+ backgroundImage: v.startsWith('url(') ? v : v ? `url('${v}')` : undefined,
403
+ backgroundImageRemoteUrl: v.startsWith('url(') ? undefined : v || undefined,
404
+ },
405
+ })"
406
+ />
407
+ </label>
408
+ <label class="block space-y-1">
409
+ <div>适应方式</div>
410
+ <Select
411
+ size="small"
412
+ class="w-full"
413
+ :value="el.style?.backgroundSize ?? 'cover'"
414
+ @update:value="(v) => updateElement(el.id, { style: { ...(el.style ?? {}), backgroundSize: String(v) } })"
415
+ >
416
+ <Select.Option value="cover">cover</Select.Option>
417
+ <Select.Option value="contain">contain</Select.Option>
418
+ <Select.Option value="100% 100%">fill</Select.Option>
419
+ </Select>
420
+ </label>
421
+ </template>
422
+ </div>
423
+ </fieldset>
424
+ </div>
425
+ </Card>
426
+
427
+ <div
428
+ v-if="noSearchMatch"
429
+ class="rounded border border-gray-200/60 bg-white px-2 py-1.5 text-[11px] text-gray-500"
430
+ >
431
+ 未匹配到可编辑节点,请更换关键词。
432
+ </div>
433
+ </div>
434
+ </template>
@@ -0,0 +1,165 @@
1
+ <script setup lang="ts">
2
+ import { Checkbox, Input, InputNumber, Select } from "ant-design-vue";
3
+ import type { PanelElement, PanelLayer } from "../../types";
4
+ import { PANEL_MESSAGES } from "../../constants/messages";
5
+ import ConfigSection from "./ConfigSection.vue";
6
+
7
+ const props = defineProps<{
8
+ element: PanelElement;
9
+ layers: PanelLayer[];
10
+ isEditable: boolean;
11
+ canToggleNodeLock: boolean;
12
+ nodeZOrderLabel?: string;
13
+ open: boolean;
14
+ forceOpen?: boolean;
15
+ onAdjustNodeZOrder?: (
16
+ nodeId: string,
17
+ action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
18
+ ) => void;
19
+ updateElement: (
20
+ id: string,
21
+ patch: Partial<PanelElement>,
22
+ options?: { batchId?: string; meta?: Record<string, unknown> }
23
+ ) => void;
24
+ }>();
25
+
26
+ const emit = defineEmits<{
27
+ "update:open": [value: boolean];
28
+ }>();
29
+
30
+ function patch(patch: Partial<PanelElement>) {
31
+ props.updateElement(props.element.id, patch);
32
+ }
33
+ </script>
34
+
35
+ <template>
36
+ <ConfigSection
37
+ title="节点信息"
38
+ :open="open"
39
+ :force-open="forceOpen"
40
+ @update:open="emit('update:open', $event)"
41
+ >
42
+ <label class="block space-y-1">
43
+ <div>节点名称</div>
44
+ <Input
45
+ size="small"
46
+ :value="element.name ?? ''"
47
+ :disabled="!isEditable"
48
+ placeholder="自定义节点名称(显示在节点树)"
49
+ @update:value="(v: string) => patch({ name: v || undefined })"
50
+ />
51
+ </label>
52
+ <div class="grid grid-cols-3 gap-2">
53
+ <label class="block space-y-1">
54
+ <div>X</div>
55
+ <InputNumber
56
+ size="small"
57
+ class="w-full"
58
+ :value="element.x"
59
+ :disabled="!isEditable"
60
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ x: n }); }"
61
+ />
62
+ </label>
63
+ <label class="block space-y-1">
64
+ <div>Y</div>
65
+ <InputNumber
66
+ size="small"
67
+ class="w-full"
68
+ :value="element.y"
69
+ :disabled="!isEditable"
70
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ y: n }); }"
71
+ />
72
+ </label>
73
+ </div>
74
+ <div class="grid grid-cols-3 gap-2">
75
+ <label class="block space-y-1">
76
+ <div>旋转角度</div>
77
+ <InputNumber
78
+ size="small"
79
+ class="w-full"
80
+ :value="element.rotate ?? 0"
81
+ :disabled="!isEditable"
82
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ rotate: n }); }"
83
+ />
84
+ </label>
85
+ <label class="block space-y-1">
86
+ <div>宽</div>
87
+ <InputNumber
88
+ size="small"
89
+ class="w-full"
90
+ :min="1"
91
+ :value="element.width"
92
+ :disabled="!isEditable"
93
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ width: Math.max(1, n) }); }"
94
+ />
95
+ </label>
96
+ <label class="block space-y-1">
97
+ <div>高</div>
98
+ <InputNumber
99
+ size="small"
100
+ class="w-full"
101
+ :min="1"
102
+ :value="element.height"
103
+ :disabled="!isEditable"
104
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ height: Math.max(1, n) }); }"
105
+ />
106
+ </label>
107
+ </div>
108
+ <div class="space-y-1.5">
109
+ <div class="text-[11px] text-gray-500">节点层级</div>
110
+ <div class="text-[11px] text-gray-400">当前 zIndex:{{ nodeZOrderLabel ?? "-" }}</div>
111
+ <div v-if="onAdjustNodeZOrder" class="grid grid-cols-2 gap-2">
112
+ <button
113
+ type="button"
114
+ class="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
115
+ :disabled="!isEditable"
116
+ @click="onAdjustNodeZOrder(element.id, 'bringForward')"
117
+ >
118
+ 上移一层
119
+ </button>
120
+ <button
121
+ type="button"
122
+ class="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
123
+ :disabled="!isEditable"
124
+ @click="onAdjustNodeZOrder(element.id, 'sendBackward')"
125
+ >
126
+ 下移一层
127
+ </button>
128
+ <button
129
+ type="button"
130
+ class="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
131
+ :disabled="!isEditable"
132
+ @click="onAdjustNodeZOrder(element.id, 'bringToFront')"
133
+ >
134
+ 置顶
135
+ </button>
136
+ <button
137
+ type="button"
138
+ class="rounded border border-gray-200 bg-white px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
139
+ :disabled="!isEditable"
140
+ @click="onAdjustNodeZOrder(element.id, 'sendToBack')"
141
+ >
142
+ 置底
143
+ </button>
144
+ </div>
145
+ </div>
146
+ <div class="truncate text-gray-400">ID: {{ element.id }}</div>
147
+ <div class="text-gray-400">类型: {{ element.materialType ?? element.id }}</div>
148
+ <div class="rounded-lg border border-gray-200/60 bg-white/80 px-2.5 py-2">
149
+ <label class="flex items-center gap-2">
150
+ <Checkbox
151
+ :checked="element.locked === true"
152
+ :disabled="!canToggleNodeLock"
153
+ @update:checked="(v) => patch({ locked: v === true })"
154
+ />
155
+ <span>锁定节点(禁止层级/位置/大小/旋转)</span>
156
+ </label>
157
+ </div>
158
+ <div
159
+ v-if="!isEditable"
160
+ class="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700"
161
+ >
162
+ {{ element.locked ? PANEL_MESSAGES.nodeConfigLocked : PANEL_MESSAGES.nodeConfigLayerLocked }}
163
+ </div>
164
+ </ConfigSection>
165
+ </template>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { Select } from "ant-design-vue";
3
+ import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
4
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
5
+ import ConfigSection from "./ConfigSection.vue";
6
+
7
+ const props = defineProps<{
8
+ element: PanelElement;
9
+ layers: PanelLayer[];
10
+ isEditable: boolean;
11
+ open: boolean;
12
+ forceOpen?: boolean;
13
+ setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
14
+ updateElement: (
15
+ id: string,
16
+ patch: Partial<PanelElement>,
17
+ options?: { batchId?: string; meta?: Record<string, unknown> }
18
+ ) => void;
19
+ }>();
20
+
21
+ const emit = defineEmits<{
22
+ "update:open": [value: boolean];
23
+ }>();
24
+
25
+ function patch(patch: Partial<PanelElement>) {
26
+ props.updateElement(props.element.id, patch);
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <ConfigSection
32
+ title="引用组件配置"
33
+ :open="open"
34
+ :force-open="forceOpen"
35
+ @update:open="emit('update:open', $event)"
36
+ >
37
+ <ConfigFieldGroup title="引用源">
38
+ <label class="block space-y-1">
39
+ <div>引用图层</div>
40
+ <Select
41
+ size="small"
42
+ class="w-full"
43
+ :value="element.refLayerId ?? '__none__'"
44
+ :disabled="!isEditable"
45
+ @update:value="(v) => patch({ refLayerId: v === '__none__' ? undefined : String(v) })"
46
+ >
47
+ <Select.Option value="__none__">无(不引用)</Select.Option>
48
+ <Select.Option
49
+ v-for="layer in layers.filter((l) => l.id !== element.layerId)"
50
+ :key="layer.id"
51
+ :value="layer.id"
52
+ >
53
+ {{ layer.name }}
54
+ </Select.Option>
55
+ </Select>
56
+ </label>
57
+ </ConfigFieldGroup>
58
+ <ConfigFieldGroup title="拷贝策略">
59
+ <template #hint>
60
+ 浅拷贝会实时同步被引用图层;深拷贝会固定当前快照,不再随源变化。
61
+ </template>
62
+ <label class="block space-y-1">
63
+ <div>拷贝模式</div>
64
+ <Select
65
+ size="small"
66
+ class="w-full"
67
+ :value="element.refCopyMode ?? 'shallow'"
68
+ :disabled="!isEditable"
69
+ @update:value="(v) => setReferenceCopyMode?.(element.id, v as ReferenceCopyMode)"
70
+ >
71
+ <Select.Option value="shallow">浅拷贝(跟随源图层变化)</Select.Option>
72
+ <Select.Option value="deep">深拷贝(冻结当前引用快照)</Select.Option>
73
+ </Select>
74
+ </label>
75
+ </ConfigFieldGroup>
76
+ </ConfigSection>
77
+ </template>