@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,209 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, ref, watch } from "vue";
3
+ import { Input, InputNumber, Select, Textarea } from "ant-design-vue";
4
+ import type { PanelElement } from "../../types";
5
+ import ConfigColorField from "./ConfigColorField.vue";
6
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
7
+ import ConfigSection from "./ConfigSection.vue";
8
+
9
+ const props = defineProps<{
10
+ element: PanelElement;
11
+ isEditable: boolean;
12
+ open: boolean;
13
+ forceOpen?: boolean;
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
+ const geometryDrawPenColor = ref("#111827");
26
+ const geometryDrawPenWidth = ref(3);
27
+ const isGeometryDrawing = ref(false);
28
+ const geometryDrawCanvasRef = ref<HTMLCanvasElement | null>(null);
29
+ const geometryLastPointRef = ref<{ x: number; y: number } | null>(null);
30
+
31
+ function patch(patch: Partial<PanelElement>) {
32
+ if (props.element.materialType !== "geometry") return;
33
+ props.updateElement(props.element.id, patch);
34
+ }
35
+
36
+ function redrawGeometryPadFromElement() {
37
+ const canvas = geometryDrawCanvasRef.value;
38
+ if (!canvas) return;
39
+ const ctx = canvas.getContext("2d");
40
+ if (!ctx) return;
41
+ const width = canvas.width;
42
+ const height = canvas.height;
43
+ ctx.clearRect(0, 0, width, height);
44
+ ctx.fillStyle = "#ffffff";
45
+ ctx.fillRect(0, 0, width, height);
46
+ const sketch = props.element.geometrySketchDataUrl;
47
+ if (!sketch) return;
48
+ const img = new Image();
49
+ img.onload = () => ctx.drawImage(img, 0, 0, width, height);
50
+ img.src = sketch;
51
+ }
52
+
53
+ onMounted(redrawGeometryPadFromElement);
54
+ watch(() => [props.element.id, props.element.geometrySketchDataUrl], redrawGeometryPadFromElement);
55
+
56
+ function onPointerDown(e: PointerEvent) {
57
+ const canvas = geometryDrawCanvasRef.value;
58
+ if (!canvas || !props.isEditable) return;
59
+ const ctx = canvas.getContext("2d");
60
+ if (!ctx) return;
61
+ const rect = canvas.getBoundingClientRect();
62
+ const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
63
+ const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
64
+ geometryLastPointRef.value = { x, y };
65
+ isGeometryDrawing.value = true;
66
+ ctx.lineCap = "round";
67
+ ctx.lineJoin = "round";
68
+ ctx.strokeStyle = geometryDrawPenColor.value;
69
+ ctx.lineWidth = geometryDrawPenWidth.value;
70
+ ctx.beginPath();
71
+ ctx.moveTo(x, y);
72
+ }
73
+
74
+ function onPointerMove(e: PointerEvent) {
75
+ if (!isGeometryDrawing.value) return;
76
+ const canvas = geometryDrawCanvasRef.value;
77
+ const last = geometryLastPointRef.value;
78
+ if (!canvas || !last) return;
79
+ const ctx = canvas.getContext("2d");
80
+ if (!ctx) return;
81
+ const rect = canvas.getBoundingClientRect();
82
+ const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
83
+ const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
84
+ ctx.lineTo(x, y);
85
+ ctx.stroke();
86
+ geometryLastPointRef.value = { x, y };
87
+ }
88
+
89
+ function endDrawing() {
90
+ isGeometryDrawing.value = false;
91
+ geometryLastPointRef.value = null;
92
+ }
93
+
94
+ function applySketch() {
95
+ const canvas = geometryDrawCanvasRef.value;
96
+ if (!canvas) return;
97
+ patch({ geometrySketchDataUrl: canvas.toDataURL("image/png") });
98
+ }
99
+
100
+ function clearSketch() {
101
+ patch({ geometrySketchDataUrl: undefined });
102
+ redrawGeometryPadFromElement();
103
+ }
104
+ </script>
105
+
106
+ <template>
107
+ <ConfigSection
108
+ title="几何配置"
109
+ :open="open"
110
+ :force-open="forceOpen"
111
+ @update:open="emit('update:open', $event)"
112
+ >
113
+ <ConfigFieldGroup title="基础形状">
114
+ <label class="block space-y-1">
115
+ <div>形状</div>
116
+ <Select
117
+ size="small"
118
+ class="w-full"
119
+ :value="element.geometryShape ?? 'rect'"
120
+ :disabled="!isEditable"
121
+ @update:value="(v) => patch({ geometryShape: v as PanelElement['geometryShape'] })"
122
+ >
123
+ <Select.Option value="rect">矩形</Select.Option>
124
+ <Select.Option value="circle">圆形</Select.Option>
125
+ <Select.Option value="triangle">三角形</Select.Option>
126
+ <Select.Option value="diamond">菱形</Select.Option>
127
+ <Select.Option value="hexagon">六边形</Select.Option>
128
+ <Select.Option value="star">星形</Select.Option>
129
+ <Select.Option value="heart">爱心</Select.Option>
130
+ </Select>
131
+ </label>
132
+ <ConfigColorField
133
+ label="几何颜色"
134
+ :value="element.geometryColor ?? '#3b82f6'"
135
+ :disabled="!isEditable"
136
+ @update:value="(v) => patch({ geometryColor: v || '#3b82f6' })"
137
+ />
138
+ </ConfigFieldGroup>
139
+ <ConfigFieldGroup title="高级(Canvas 脚本)">
140
+ <template #hint>
141
+ 可输入 Canvas 绘制逻辑,变量:ctx、width、height、element。脚本异常会被安全忽略。
142
+ </template>
143
+ <Textarea
144
+ :value="element.geometryScript ?? ''"
145
+ :disabled="!isEditable"
146
+ :rows="6"
147
+ spellcheck="false"
148
+ class="font-mono text-[11px]"
149
+ placeholder="// 例: ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fillRect(8,8,width-16,height-16);"
150
+ @update:value="(v: string) => patch({ geometryScript: v || undefined })"
151
+ />
152
+ </ConfigFieldGroup>
153
+ <ConfigFieldGroup title="手绘叠加">
154
+ <div class="flex items-center gap-2">
155
+ <label class="block space-y-1">
156
+ <div class="text-[11px]">画笔颜色</div>
157
+ <input
158
+ v-model="geometryDrawPenColor"
159
+ type="color"
160
+ class="h-7 w-10 cursor-pointer rounded border border-gray-200 p-0.5"
161
+ :disabled="!isEditable"
162
+ />
163
+ </label>
164
+ <label class="block space-y-1">
165
+ <div class="text-[11px]">画笔粗细</div>
166
+ <InputNumber
167
+ size="small"
168
+ class="w-20"
169
+ :min="1"
170
+ :max="24"
171
+ :value="geometryDrawPenWidth"
172
+ :disabled="!isEditable"
173
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) geometryDrawPenWidth = Math.max(1, Math.min(24, n)); }"
174
+ />
175
+ </label>
176
+ </div>
177
+ <div class="rounded border border-gray-200/60 bg-white p-2">
178
+ <canvas
179
+ ref="geometryDrawCanvasRef"
180
+ width="320"
181
+ height="180"
182
+ class="h-[180px] w-full cursor-crosshair rounded border border-gray-200/60"
183
+ @pointerdown="onPointerDown"
184
+ @pointermove="onPointerMove"
185
+ @pointerup="endDrawing"
186
+ @pointerleave="endDrawing"
187
+ />
188
+ </div>
189
+ <div class="flex items-center gap-2">
190
+ <button
191
+ type="button"
192
+ class="rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
193
+ :disabled="!isEditable"
194
+ @click="applySketch"
195
+ >
196
+ 应用手绘到节点
197
+ </button>
198
+ <button
199
+ type="button"
200
+ class="rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50 disabled:opacity-50"
201
+ :disabled="!isEditable"
202
+ @click="clearSketch"
203
+ >
204
+ 清空手绘
205
+ </button>
206
+ </div>
207
+ </ConfigFieldGroup>
208
+ </ConfigSection>
209
+ </template>
@@ -0,0 +1,68 @@
1
+ <script setup lang="ts">
2
+ import { InputNumber } from "ant-design-vue";
3
+ import type { PanelElement } from "../../types";
4
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
5
+ import ConfigSection from "./ConfigSection.vue";
6
+
7
+ const props = defineProps<{
8
+ element: PanelElement;
9
+ isEditable: boolean;
10
+ open: boolean;
11
+ forceOpen?: boolean;
12
+ updateElement: (
13
+ id: string,
14
+ patch: Partial<PanelElement>,
15
+ options?: { batchId?: string; meta?: Record<string, unknown> }
16
+ ) => void;
17
+ }>();
18
+
19
+ const emit = defineEmits<{
20
+ "update:open": [value: boolean];
21
+ }>();
22
+
23
+ function patch(patch: Partial<PanelElement>) {
24
+ props.updateElement(props.element.id, patch);
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <ConfigSection
30
+ v-if="element.parentGridId"
31
+ title="网格子节点占位"
32
+ :open="open"
33
+ :force-open="forceOpen"
34
+ @update:open="emit('update:open', $event)"
35
+ >
36
+ <template #hint>
37
+ 网格子节点可跨越多格,占据更大区域,便于复杂布局。
38
+ </template>
39
+ <ConfigFieldGroup title="跨槽位">
40
+ <div class="grid grid-cols-2 gap-2">
41
+ <label class="block space-y-1">
42
+ <div>跨列(colSpan)</div>
43
+ <InputNumber
44
+ size="small"
45
+ class="w-full"
46
+ :min="1"
47
+ :max="12"
48
+ :value="element.gridColSpan ?? 1"
49
+ :disabled="!isEditable"
50
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ gridColSpan: Math.max(1, Math.min(12, n)) }); }"
51
+ />
52
+ </label>
53
+ <label class="block space-y-1">
54
+ <div>跨行(rowSpan)</div>
55
+ <InputNumber
56
+ size="small"
57
+ class="w-full"
58
+ :min="1"
59
+ :max="12"
60
+ :value="element.gridRowSpan ?? 1"
61
+ :disabled="!isEditable"
62
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ gridRowSpan: Math.max(1, Math.min(12, n)) }); }"
63
+ />
64
+ </label>
65
+ </div>
66
+ </ConfigFieldGroup>
67
+ </ConfigSection>
68
+ </template>
@@ -0,0 +1,103 @@
1
+ <script setup lang="ts">
2
+ import { InputNumber } from "ant-design-vue";
3
+ import type { PanelElement } from "../../types";
4
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
5
+ import ConfigSection from "./ConfigSection.vue";
6
+
7
+ const props = defineProps<{
8
+ element: PanelElement;
9
+ isEditable: boolean;
10
+ open: boolean;
11
+ forceOpen?: boolean;
12
+ updateElement: (
13
+ id: string,
14
+ patch: Partial<PanelElement>,
15
+ options?: { batchId?: string; meta?: Record<string, unknown> }
16
+ ) => void;
17
+ }>();
18
+
19
+ const emit = defineEmits<{
20
+ "update:open": [value: boolean];
21
+ }>();
22
+
23
+ function patch(patch: Partial<PanelElement>) {
24
+ props.updateElement(props.element.id, patch);
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <ConfigSection
30
+ title="网格布局配置"
31
+ :open="open"
32
+ :force-open="forceOpen"
33
+ @update:open="emit('update:open', $event)"
34
+ >
35
+ <template #hint>
36
+ 其他节点拖拽靠近该网格槽位中心时会自动吸附,并在节点树显示为该网格子节点。
37
+ </template>
38
+ <ConfigFieldGroup title="网格参数">
39
+ <div class="grid grid-cols-2 gap-2">
40
+ <label class="block space-y-1">
41
+ <div>行数</div>
42
+ <InputNumber
43
+ size="small"
44
+ class="w-full"
45
+ :min="1"
46
+ :max="12"
47
+ :value="element.gridRows ?? 2"
48
+ :disabled="!isEditable"
49
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ gridRows: Math.max(1, Math.min(12, n)) }); }"
50
+ />
51
+ </label>
52
+ <label class="block space-y-1">
53
+ <div>列数</div>
54
+ <InputNumber
55
+ size="small"
56
+ class="w-full"
57
+ :min="1"
58
+ :max="12"
59
+ :value="element.gridCols ?? 3"
60
+ :disabled="!isEditable"
61
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ gridCols: Math.max(1, Math.min(12, n)) }); }"
62
+ />
63
+ </label>
64
+ <label class="block space-y-1">
65
+ <div>间距(px)</div>
66
+ <InputNumber
67
+ size="small"
68
+ class="w-full"
69
+ :min="0"
70
+ :max="80"
71
+ :value="element.gridGap ?? 8"
72
+ :disabled="!isEditable"
73
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ gridGap: Math.max(0, Math.min(80, n)) }); }"
74
+ />
75
+ </label>
76
+ <label class="block space-y-1">
77
+ <div>内边距(px)</div>
78
+ <InputNumber
79
+ size="small"
80
+ class="w-full"
81
+ :min="0"
82
+ :max="100"
83
+ :value="element.gridPadding ?? 10"
84
+ :disabled="!isEditable"
85
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ gridPadding: Math.max(0, Math.min(100, n)) }); }"
86
+ />
87
+ </label>
88
+ <label class="block space-y-1">
89
+ <div>吸附阈值(px)</div>
90
+ <InputNumber
91
+ size="small"
92
+ class="w-full"
93
+ :min="8"
94
+ :max="120"
95
+ :value="element.gridSnapThreshold ?? 36"
96
+ :disabled="!isEditable"
97
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ gridSnapThreshold: Math.max(8, Math.min(120, n)) }); }"
98
+ />
99
+ </label>
100
+ </div>
101
+ </ConfigFieldGroup>
102
+ </ConfigSection>
103
+ </template>
@@ -0,0 +1,136 @@
1
+ <script setup lang="ts">
2
+ import { ref } from "vue";
3
+ import { Input, Select } from "ant-design-vue";
4
+ import type { PanelElement } from "../../types";
5
+ import {
6
+ PANEL_MESSAGES,
7
+ readFileAsDataUrl,
8
+ uploadFileToRemote,
9
+ } from "./shared";
10
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
11
+ import ConfigSection from "./ConfigSection.vue";
12
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
13
+
14
+ const props = defineProps<{
15
+ element: PanelElement;
16
+ isEditable: boolean;
17
+ open: boolean;
18
+ forceOpen?: boolean;
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
+ const uploadStatus = ref("");
31
+
32
+ function patchStyle(patch: Record<string, string | undefined>) {
33
+ props.updateElement(props.element.id, {
34
+ style: { ...(props.element.style ?? {}), ...patch },
35
+ });
36
+ }
37
+
38
+ async function handleUploadImage(file: File) {
39
+ const base64 = await readFileAsDataUrl(file, PANEL_MESSAGES.readImageFailed);
40
+ patchStyle({ backgroundImage: `url("${base64}")` });
41
+ uploadStatus.value = "已写入 base64";
42
+ const url = await uploadFileToRemote(file);
43
+ if (url) {
44
+ patchStyle({ backgroundImageRemoteUrl: url });
45
+ uploadStatus.value = "已上传服务器并写入 base64";
46
+ } else {
47
+ uploadStatus.value = "服务器上传失败,仅保留 base64";
48
+ }
49
+ }
50
+
51
+ function onFileChange(e: Event) {
52
+ const input = e.target as HTMLInputElement;
53
+ const file = input.files?.[0];
54
+ input.value = "";
55
+ if (file) void handleUploadImage(file);
56
+ }
57
+ </script>
58
+
59
+ <template>
60
+ <ConfigSection
61
+ title="图片配置"
62
+ :open="open"
63
+ :force-open="forceOpen"
64
+ @update:open="emit('update:open', $event)"
65
+ >
66
+ <ConfigFieldGroup title="图片来源">
67
+ <label class="block space-y-1">
68
+ <div class="flex items-center gap-1">
69
+ <span>图片 URL / CSS</span>
70
+ <ConfigHintIcon label="图片地址">
71
+ 可填写远程 URL 或 url("data:...") 形式;上传后会自动写入 base64。
72
+ </ConfigHintIcon>
73
+ </div>
74
+ <Input
75
+ size="small"
76
+ :value="element.style?.backgroundImageRemoteUrl ?? element.style?.backgroundImage ?? ''"
77
+ :disabled="!isEditable"
78
+ placeholder="https://example.com/image.png"
79
+ @update:value="(v: string) => {
80
+ if (v.startsWith('url(')) {
81
+ patchStyle({ backgroundImage: v, backgroundImageRemoteUrl: undefined });
82
+ } else {
83
+ patchStyle({
84
+ backgroundImage: v ? `url('${v}')` : undefined,
85
+ backgroundImageRemoteUrl: v || undefined,
86
+ });
87
+ }
88
+ }"
89
+ />
90
+ </label>
91
+ <div class="flex items-center gap-2">
92
+ <label
93
+ class="inline-flex cursor-pointer items-center rounded border border-gray-200 px-2 py-1 text-[11px] hover:bg-gray-50"
94
+ :class="{ 'pointer-events-none opacity-50': !isEditable }"
95
+ >
96
+ 上传图片
97
+ <input type="file" accept="image/*" class="hidden" :disabled="!isEditable" @change="onFileChange" />
98
+ </label>
99
+ <span v-if="uploadStatus" class="text-[11px] text-gray-500">{{ uploadStatus }}</span>
100
+ </div>
101
+ </ConfigFieldGroup>
102
+ <ConfigFieldGroup title="显示方式">
103
+ <label class="block space-y-1">
104
+ <div>适应方式(object-fit / background-size)</div>
105
+ <Select
106
+ size="small"
107
+ class="w-full"
108
+ :value="element.style?.backgroundSize ?? 'cover'"
109
+ :disabled="!isEditable"
110
+ @update:value="(v) => patchStyle({ backgroundSize: String(v) })"
111
+ >
112
+ <Select.Option value="cover">cover(裁剪铺满)</Select.Option>
113
+ <Select.Option value="contain">contain(完整显示)</Select.Option>
114
+ <Select.Option value="100% 100%">fill(拉伸填满)</Select.Option>
115
+ <Select.Option value="auto">auto</Select.Option>
116
+ </Select>
117
+ </label>
118
+ <label class="block space-y-1">
119
+ <div>对齐位置</div>
120
+ <Select
121
+ size="small"
122
+ class="w-full"
123
+ :value="element.style?.backgroundPosition ?? 'center'"
124
+ :disabled="!isEditable"
125
+ @update:value="(v) => patchStyle({ backgroundPosition: String(v) })"
126
+ >
127
+ <Select.Option value="center">center</Select.Option>
128
+ <Select.Option value="top">top</Select.Option>
129
+ <Select.Option value="bottom">bottom</Select.Option>
130
+ <Select.Option value="left">left</Select.Option>
131
+ <Select.Option value="right">right</Select.Option>
132
+ </Select>
133
+ </label>
134
+ </ConfigFieldGroup>
135
+ </ConfigSection>
136
+ </template>