@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.
- package/README.md +50 -0
- package/package.json +49 -0
- package/src/env.d.ts +62 -0
- package/src/index.ts +4 -0
- package/src/panel/VueViewOnlinePreview.vue +276 -0
- package/src/panel/VueViewPanel.vue +871 -0
- package/src/panel/components/ConfigHintIcon.vue +34 -0
- package/src/panel/components/ElementsLayer.vue +165 -0
- package/src/panel/components/MaterialPreview.vue +135 -0
- package/src/panel/components/MaterialSidebar.vue +526 -0
- package/src/panel/components/MaterialSidebarTreeNode.vue +305 -0
- package/src/panel/components/MoveableLayer.vue +859 -0
- package/src/panel/components/PanelCanvas.vue +630 -0
- package/src/panel/components/PanelConfigSidebar.vue +397 -0
- package/src/panel/components/PanelRulers.vue +177 -0
- package/src/panel/components/SelectLayer.vue +115 -0
- package/src/panel/components/ViewElementScopePanel.vue +76 -0
- package/src/panel/components/WorkspaceConfigSidebar.vue +147 -0
- package/src/panel/components/WorkspaceProjectNav.vue +192 -0
- package/src/panel/components/WorkspaceStageSplit.vue +258 -0
- package/src/panel/components/config/ConfigColorField.vue +52 -0
- package/src/panel/components/config/ConfigFieldGroup.vue +20 -0
- package/src/panel/components/config/ConfigSection.vue +50 -0
- package/src/panel/components/config/PanelConfigAudioSection.vue +256 -0
- package/src/panel/components/config/PanelConfigChartSection.vue +650 -0
- package/src/panel/components/config/PanelConfigGeometrySection.vue +209 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.vue +68 -0
- package/src/panel/components/config/PanelConfigGridSection.vue +103 -0
- package/src/panel/components/config/PanelConfigImageSection.vue +136 -0
- package/src/panel/components/config/PanelConfigMultiSelect.vue +434 -0
- package/src/panel/components/config/PanelConfigNodeInfo.vue +165 -0
- package/src/panel/components/config/PanelConfigReferenceSection.vue +77 -0
- package/src/panel/components/config/PanelConfigStyleSections.vue +208 -0
- package/src/panel/components/config/PanelConfigTextSection.vue +195 -0
- package/src/panel/components/config/PanelConfigVideoSection.vue +107 -0
- package/src/panel/components/config/shared.ts +74 -0
- package/src/panel/components/elementsLayerNodes.ts +830 -0
- package/src/panel/components/materialSidebarData.ts +85 -0
- package/src/panel/components/scope-config/ScopeConfigProvider.vue +153 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +234 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.vue +192 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewPanel.vue +42 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.vue +20 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.vue +63 -0
- package/src/panel/components/scope-config/scopeConfigContext.ts +17 -0
- package/src/panel/components/scope-config/useScopeConfig.ts +11 -0
- package/src/panel/constants/messages.ts +34 -0
- package/src/panel/constants/zIndex.ts +6 -0
- package/src/panel/hooks/usePanelElements.ts +1075 -0
- package/src/panel/hooks/useRafThrottledScroll.ts +25 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +240 -0
- package/src/panel/lib/panel-ruler-canvas.ts +139 -0
- package/src/panel/library/workspace-project-cache.ts +23 -0
- package/src/panel/library/workspace-project-db.ts +111 -0
- package/src/panel/library/workspace-project-sync.ts +41 -0
- package/src/panel/library/workspace-snapshot.ts +30 -0
- package/src/panel/parseOnlinePreviewSearchParams.ts +13 -0
- package/src/panel/scope/view-scope-store.ts +82 -0
- package/src/panel/types.ts +127 -0
- package/src/panel/utils/chartOptionBuilder.ts +327 -0
- package/src/panel/utils/gridPlacement.ts +189 -0
- package/src/panel/utils/mappingLayerOps.ts +142 -0
- package/src/panel/utils/panelElementDefaults.ts +161 -0
- package/src/panel/utils/panelElementNodes.ts +35 -0
- package/src/panel/utils/panelStateIO.ts +124 -0
- package/src/panel/utils/scope-autocomplete.ts +114 -0
- package/src/panel/utils/scope-field-labels.ts +46 -0
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +124 -0
- package/src/panel/utils/scope-template-spread.ts +229 -0
- package/src/panel/utils/scope-template-warnings.ts +243 -0
- package/src/panel/utils/scope-template.ts +97 -0
- package/src/panel/utils/updateElementDraft.ts +221 -0
- package/src/panel/viewportZoom.ts +26 -0
- 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>
|