@arronqzy/vue-view 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-view",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Vue 3 visual view editor for Abuilder (Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,12 +23,12 @@
23
23
  "selecto": "^1.26.3",
24
24
  "uuid": "^13.0.0",
25
25
  "vue": "^3.5.13",
26
- "@arronqzy/blueprint-dsl": "1.0.5",
27
- "@arronqzy/vue-blueprint": "0.1.3",
26
+ "@arronqzy/blueprint-dsl": "1.0.6",
27
+ "@arronqzy/rx-store": "1.0.4",
28
+ "@arronqzy/vue-blueprint": "0.1.4",
28
29
  "@arronqzy/view-table": "0.1.3",
29
- "@arronqzy/i18n": "0.1.3",
30
30
  "@arronqzy/vue-rx-store": "0.1.2",
31
- "@arronqzy/rx-store": "1.0.4"
31
+ "@arronqzy/i18n": "0.1.5"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "vue": "^3.4.0"
@@ -31,6 +31,7 @@ import {
31
31
  parsePanelLayers,
32
32
  resolvePreviewLayerElements,
33
33
  } from "./utils/panelStateIO";
34
+ import { applyPreviewSceneFill, readOutputScale } from "./utils/outputScale";
34
35
 
35
36
  const { t, locale } = useI18n();
36
37
 
@@ -49,6 +50,8 @@ const blueprintLibraryItems = ref<BlueprintLibraryListItem[]>([]);
49
50
  const layoutRevision = ref(0);
50
51
  const layoutReady = ref(false);
51
52
  const sceneRef = ref<HTMLDivElement | null>(null);
53
+ const outputScale = ref(readOutputScale());
54
+ const fillScale = ref({ scaleX: 1, scaleY: 1 });
52
55
 
53
56
  const layers = computed(() => (panelState.value ? parsePanelLayers(panelState.value) : []));
54
57
  const activeLayerId = computed(() =>
@@ -146,24 +149,30 @@ const scopedLayerElements = computed(() => {
146
149
  const sceneBounds = computed(() => computePanelSceneBounds(scopedLayerElements.value));
147
150
 
148
151
  const displayElements = computed(() =>
149
- scopedLayerElements.value.map((el) => ({
150
- ...el,
151
- x: el.x - sceneBounds.value.minX,
152
- y: el.y - sceneBounds.value.minY,
153
- }))
152
+ scopedLayerElements.value.map((el) => {
153
+ const x = el.x - sceneBounds.value.minX;
154
+ const y = el.y - sceneBounds.value.minY;
155
+ if (outputScale.value) return { ...el, x, y };
156
+ return {
157
+ ...el,
158
+ x: x * fillScale.value.scaleX,
159
+ y: y * fillScale.value.scaleY,
160
+ width: el.width * fillScale.value.scaleX,
161
+ height: el.height * fillScale.value.scaleY,
162
+ };
163
+ })
154
164
  );
155
165
 
156
166
  function applySceneFit() {
157
- const scene = sceneRef.value;
158
- if (!scene) return;
159
- const vw = window.innerWidth || 1;
160
- const vh = window.innerHeight || 1;
161
- const sw = Math.max(1, sceneBounds.value.width);
162
- const sh = Math.max(1, sceneBounds.value.height);
163
- const scaleX = vw / sw;
164
- const scaleY = vh / sh;
165
- if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return;
166
- scene.style.transform = `scale(${scaleX}, ${scaleY})`;
167
+ const enabled = readOutputScale();
168
+ outputScale.value = enabled;
169
+ const fill = applyPreviewSceneFill(
170
+ sceneRef.value,
171
+ sceneBounds.value.width,
172
+ sceneBounds.value.height,
173
+ enabled
174
+ );
175
+ fillScale.value = { scaleX: fill.scaleX, scaleY: fill.scaleY };
167
176
  layoutRevision.value += 1;
168
177
  notifyPreviewLayoutChanged();
169
178
  }
@@ -258,9 +267,10 @@ function noopSelect() {}
258
267
  id="preview-scene"
259
268
  class="relative shrink-0 origin-top-left"
260
269
  :style="{
261
- width: `${sceneBounds.width}px`,
262
- height: `${sceneBounds.height}px`,
270
+ width: `${outputScale ? sceneBounds.width : sceneBounds.width * fillScale.scaleX}px`,
271
+ height: `${outputScale ? sceneBounds.height : sceneBounds.height * fillScale.scaleY}px`,
263
272
  transformOrigin: 'left top',
273
+ transform: outputScale ? `scale(${fillScale.scaleX}, ${fillScale.scaleY})` : 'none',
264
274
  }"
265
275
  >
266
276
  <ElementsLayer
@@ -863,6 +863,9 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
863
863
  :on-exclude-selected-node="excludeSelectedNode"
864
864
  :on-adjust-node-z-order="adjustNodeZOrder"
865
865
  :selected-blueprint-node="selectedBlueprintNode"
866
+ :blueprint-graph-nodes="blueprintGraph.document.nodes"
867
+ :blueprint-graph-edges="blueprintGraph.document.edges"
868
+ :blueprint-trace-entries="blueprintDebugSession.entries.value"
866
869
  :allow-false-signal-propagation="blueprintGraph.document.allowFalseSignalPropagation"
867
870
  :blueprint-library-options="blueprintLibraryOptions"
868
871
  :blueprint-node-options="blueprintNodeOptions"
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useI18n } from "@arronqzy/i18n/vue";
3
- import { computed, onMounted, ref, watch } from "vue";
3
+ import { computed, onMounted, provide, ref, watch } from "vue";
4
4
  import { Empty, Input } from "ant-design-vue";
5
5
  import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../types";
6
6
  import { CHART_TYPES } from "../utils/chartOptionBuilder";
@@ -61,8 +61,8 @@ const sidebarScrollRef = ref<HTMLElement | null>(null);
61
61
 
62
62
  const expandedSections = ref<Record<string, boolean>>({
63
63
  nodeInfo: true,
64
- styleBackground: true,
65
- styleBorder: true,
64
+ styleBackground: false,
65
+ styleBorder: false,
66
66
  chartBasic: true,
67
67
  chartAdvanced: false,
68
68
  textConfig: true,
@@ -123,6 +123,7 @@ const scopeWarnings = computed(() => {
123
123
  const materialType = computed(() => props.selectedElement?.materialType ?? "");
124
124
 
125
125
  const forceOpenSections = computed(() => hasSearch.value);
126
+ provide("configHasSearch", hasSearch);
126
127
 
127
128
  function isSectionExpanded(key: string, defaultValue = true) {
128
129
  return expandedSections.value[key] ?? defaultValue;
@@ -259,23 +260,6 @@ watch(
259
260
  @update:open="(v) => setSectionExpanded('nodeInfo', v)"
260
261
  />
261
262
 
262
- <PanelConfigStyleSections
263
- v-if="
264
- shouldShowSection('styleBackground', t('panel.config.sectionStyleBackground'), [t('panel.config.backgroundColor'), 'background']) ||
265
- shouldShowSection('styleBorder', t('panel.config.sectionStyleBorder'), [t('panel.config.searchKwBorder'), 'border'])
266
- "
267
- :element="selectedElement"
268
- :is-editable="isNodeEditable"
269
- :show-background="shouldShowSection('styleBackground', t('panel.config.sectionStyleBackground'), [t('panel.config.backgroundColor'), 'background'])"
270
- :show-border="shouldShowSection('styleBorder', t('panel.config.sectionStyleBorder'), [t('panel.config.searchKwBorder'), 'border'])"
271
- :background-open="isSectionExpanded('styleBackground')"
272
- :border-open="isSectionExpanded('styleBorder')"
273
- :force-open="forceOpenSections"
274
- :update-element="updateElement"
275
- @update:background-open="(v) => setSectionExpanded('styleBackground', v)"
276
- @update:border-open="(v) => setSectionExpanded('styleBorder', v)"
277
- />
278
-
279
263
  <PanelConfigChartSection
280
264
  v-if="isChartElement && shouldShowSection('chartBasic', t('panel.config.sectionChartBasic'), [t('panel.material.charts'), 'tooltip'])"
281
265
  :element="selectedElement"
@@ -381,6 +365,23 @@ watch(
381
365
  @update:open="(v) => setSectionExpanded('reference', v)"
382
366
  />
383
367
 
368
+ <PanelConfigStyleSections
369
+ v-if="
370
+ shouldShowSection('styleBackground', t('panel.config.sectionStyleBackground'), [t('panel.config.backgroundColor'), 'background']) ||
371
+ shouldShowSection('styleBorder', t('panel.config.sectionStyleBorder'), [t('panel.config.searchKwBorder'), 'border'])
372
+ "
373
+ :element="selectedElement"
374
+ :is-editable="isNodeEditable"
375
+ :show-background="shouldShowSection('styleBackground', t('panel.config.sectionStyleBackground'), [t('panel.config.backgroundColor'), 'background'])"
376
+ :show-border="shouldShowSection('styleBorder', t('panel.config.sectionStyleBorder'), [t('panel.config.searchKwBorder'), 'border'])"
377
+ :background-open="isSectionExpanded('styleBackground')"
378
+ :border-open="isSectionExpanded('styleBorder')"
379
+ :force-open="forceOpenSections"
380
+ :update-element="updateElement"
381
+ @update:background-open="(v) => setSectionExpanded('styleBackground', v)"
382
+ @update:border-open="(v) => setSectionExpanded('styleBorder', v)"
383
+ />
384
+
384
385
  <div
385
386
  v-if="!isChartElement && materialType && !['text', 'table', 'audio', 'video', 'image', 'geometry', 'grid', 'reference'].includes(materialType)"
386
387
  class="text-xs leading-6 text-gray-500"
@@ -5,6 +5,7 @@ import { Empty } from "ant-design-vue";
5
5
  import {
6
6
  BlueprintExecutionLogPanel,
7
7
  BlueprintNodeConfigSidebar,
8
+ type BlueprintGraphEdge,
8
9
  type BlueprintGraphNode,
9
10
  type ExecutionLogSettings,
10
11
  type ExecutionTraceEntry,
@@ -56,6 +57,9 @@ export type WorkspaceConfigSidebarProps = Omit<
56
57
  configFocus: WorkspaceConfigFocus;
57
58
  executionLog?: BlueprintExecutionLogViewProps;
58
59
  selectedBlueprintNode: BlueprintGraphNode | null;
60
+ blueprintGraphNodes?: BlueprintGraphNode[];
61
+ blueprintGraphEdges?: BlueprintGraphEdge[];
62
+ blueprintTraceEntries?: ExecutionTraceEntry[];
59
63
  allowFalseSignalPropagation?: boolean;
60
64
  onUpdateAllowFalseSignalPropagation?: (value: boolean) => void;
61
65
  onUpdateBlueprintNode: (
@@ -90,6 +94,9 @@ const props = withDefaults(
90
94
  {
91
95
  allowFalseSignalPropagation: false,
92
96
  blueprintLibraryOptions: () => [],
97
+ blueprintGraphNodes: () => [],
98
+ blueprintGraphEdges: () => [],
99
+ blueprintTraceEntries: () => [],
93
100
  blueprintNodeOptions: () => [],
94
101
  selectedElements: () => [],
95
102
  }
@@ -143,6 +150,9 @@ const viewElementOptions = computed(() =>
143
150
  <BlueprintNodeConfigSidebar
144
151
  v-else
145
152
  :node="selectedBlueprintNode"
153
+ :graph-nodes="blueprintGraphNodes"
154
+ :graph-edges="blueprintGraphEdges"
155
+ :trace-entries="blueprintTraceEntries.length ? blueprintTraceEntries : executionLog?.entries ?? []"
146
156
  :allow-false-signal-propagation="allowFalseSignalPropagation"
147
157
  :on-update-allow-false-signal-propagation="onUpdateAllowFalseSignalPropagation"
148
158
  :view-element-options="viewElementOptions"
@@ -1,20 +1,49 @@
1
1
  <script setup lang="ts">
2
+ import { computed, inject, ref, type ComputedRef } from "vue";
2
3
  import ConfigHintIcon from "../ConfigHintIcon.vue";
3
4
 
4
- defineProps<{
5
- title: string;
6
- hint?: boolean;
7
- }>();
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ title: string;
8
+ hint?: boolean;
9
+ collapsible?: boolean;
10
+ defaultOpen?: boolean;
11
+ }>(),
12
+ { defaultOpen: true, collapsible: false }
13
+ );
14
+
15
+ const hasSearch = inject<ComputedRef<boolean>>(
16
+ "configHasSearch",
17
+ computed(() => false)
18
+ );
19
+
20
+ const open = ref(props.defaultOpen);
21
+ const isOpen = computed(() => !props.collapsible || hasSearch.value || open.value);
22
+
23
+ function toggle() {
24
+ if (!props.collapsible || hasSearch.value) return;
25
+ open.value = !open.value;
26
+ }
8
27
  </script>
9
28
 
10
29
  <template>
11
30
  <div class="space-y-2.5 rounded-lg border border-gray-200/80 bg-white/80 p-2.5">
12
- <div class="flex items-center gap-1">
31
+ <div class="flex items-center gap-1.5">
32
+ <button
33
+ v-if="collapsible"
34
+ type="button"
35
+ class="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-gray-100"
36
+ @click="toggle"
37
+ >
38
+ {{ isOpen ? "▾" : "▸" }}
39
+ </button>
13
40
  <div class="text-[11px] font-semibold text-gray-500">{{ title }}</div>
14
41
  <ConfigHintIcon v-if="$slots.hint" :label="title">
15
42
  <slot name="hint" />
16
43
  </ConfigHintIcon>
17
44
  </div>
18
- <slot />
45
+ <div v-show="isOpen" class="space-y-2.5">
46
+ <slot />
47
+ </div>
19
48
  </div>
20
49
  </template>
@@ -168,7 +168,11 @@ function onPosterFileChange(e: Event) {
168
168
  :src="element.audioSrc || element.audioRemoteUrl || ''"
169
169
  />
170
170
  </ConfigFieldGroup>
171
- <ConfigFieldGroup :title="t('panel.config.groupDisplayStyle')">
171
+ <ConfigFieldGroup
172
+ :title="t('panel.config.groupDisplayStyle')"
173
+ collapsible
174
+ :default-open="false"
175
+ >
172
176
  <label class="flex items-center gap-2">
173
177
  <Checkbox
174
178
  :checked="element.mediaAutoPauseOnEdit !== false"
@@ -143,6 +143,100 @@ function onOptionJsonChange(v: string) {
143
143
  :disabled="!isEditable"
144
144
  @update:value="(v) => updateChart({ color: v || '#3b82f6' })"
145
145
  />
146
+ </ConfigFieldGroup>
147
+ <ConfigFieldGroup :title="t('panel.config.groupData')">
148
+ <label class="block space-y-1">
149
+ <div>{{ t("panel.config.labelsCsv") }}</div>
150
+ <Input
151
+ size="small"
152
+ :value="getChartLabelsDisplayText(element.chart)"
153
+ :disabled="!isEditable"
154
+ @update:value="(v: string) => updateChart({ labelsText: v })"
155
+ />
156
+ </label>
157
+ <label class="block space-y-1">
158
+ <div>{{ t("panel.config.valuesCsv") }}</div>
159
+ <Input
160
+ size="small"
161
+ :value="getChartValuesDisplayText(element.chart)"
162
+ :disabled="!isEditable"
163
+ @update:value="(v: string) => updateChart({ valuesText: v })"
164
+ />
165
+ </label>
166
+ </ConfigFieldGroup>
167
+
168
+ <ConfigFieldGroup
169
+ v-if="['bar', 'line', 'area', 'pie'].includes(selectedChartType)"
170
+ :title="t('panel.config.groupSeries')"
171
+ >
172
+ <label v-if="selectedChartType === 'bar'" class="block space-y-1">
173
+ <div>{{ t("panel.config.barWidthPx") }}</div>
174
+ <InputNumber
175
+ size="small"
176
+ class="w-full"
177
+ :min="1"
178
+ :placeholder="t('panel.config.barWidthAuto')"
179
+ :value="element.chart?.barWidth ?? undefined"
180
+ :disabled="!isEditable"
181
+ @update:value="(v) => {
182
+ if (v == null || v === '') {
183
+ updateChart({ barWidth: undefined });
184
+ return;
185
+ }
186
+ const n = Number(v);
187
+ if (!Number.isNaN(n) && n > 0) updateChart({ barWidth: Math.max(1, n) });
188
+ }"
189
+ />
190
+ </label>
191
+ <label
192
+ v-if="selectedChartType === 'line' || selectedChartType === 'area'"
193
+ class="flex items-center gap-2"
194
+ >
195
+ <Checkbox
196
+ :checked="element.chart?.smooth ?? true"
197
+ :disabled="!isEditable"
198
+ @update:checked="(v) => updateChart({ smooth: v === true })"
199
+ />
200
+ <span>{{ t("panel.config.smooth") }}</span>
201
+ </label>
202
+ <div v-if="selectedChartType === 'pie'" class="grid grid-cols-2 gap-2">
203
+ <label class="block space-y-1">
204
+ <div>{{ t("panel.config.pieInnerRadiusPct") }}</div>
205
+ <InputNumber
206
+ size="small"
207
+ class="w-full"
208
+ :min="0"
209
+ :max="99"
210
+ :value="element.chart?.pieInnerRadius ?? 30"
211
+ :disabled="!isEditable"
212
+ @update:value="(v) => {
213
+ const n = Number(v);
214
+ if (!Number.isNaN(n)) updateChart({ pieInnerRadius: Math.max(0, Math.min(99, n)) });
215
+ }"
216
+ />
217
+ </label>
218
+ <label class="block space-y-1">
219
+ <div>{{ t("panel.config.pieOuterRadiusPct") }}</div>
220
+ <InputNumber
221
+ size="small"
222
+ class="w-full"
223
+ :min="1"
224
+ :max="100"
225
+ :value="element.chart?.pieOuterRadius ?? 65"
226
+ :disabled="!isEditable"
227
+ @update:value="(v) => {
228
+ const n = Number(v);
229
+ if (!Number.isNaN(n)) updateChart({ pieOuterRadius: Math.max(1, Math.min(100, n)) });
230
+ }"
231
+ />
232
+ </label>
233
+ </div>
234
+ </ConfigFieldGroup>
235
+ <ConfigFieldGroup
236
+ :title="t('panel.config.groupChartDisplayMore')"
237
+ collapsible
238
+ :default-open="false"
239
+ >
146
240
  <label class="flex items-center gap-2">
147
241
  <Checkbox
148
242
  :checked="element.chart?.colorMode === 'gradient'"
@@ -208,7 +302,11 @@ function onOptionJsonChange(v: string) {
208
302
  </label>
209
303
  </ConfigFieldGroup>
210
304
 
211
- <ConfigFieldGroup :title="t('panel.config.groupTooltip')">
305
+ <ConfigFieldGroup
306
+ :title="t('panel.config.groupTooltip')"
307
+ collapsible
308
+ :default-open="false"
309
+ >
212
310
  <div class="grid grid-cols-2 gap-2">
213
311
  <label class="flex items-center gap-2">
214
312
  <Checkbox
@@ -264,30 +362,11 @@ function onOptionJsonChange(v: string) {
264
362
  </label>
265
363
  </ConfigFieldGroup>
266
364
 
267
- <ConfigFieldGroup :title="t('panel.config.groupData')">
268
- <label class="block space-y-1">
269
- <div>{{ t("panel.config.labelsCsv") }}</div>
270
- <Input
271
- size="small"
272
- :value="getChartLabelsDisplayText(element.chart)"
273
- :disabled="!isEditable"
274
- @update:value="(v: string) => updateChart({ labelsText: v })"
275
- />
276
- </label>
277
- <label class="block space-y-1">
278
- <div>{{ t("panel.config.valuesCsv") }}</div>
279
- <Input
280
- size="small"
281
- :value="getChartValuesDisplayText(element.chart)"
282
- :disabled="!isEditable"
283
- @update:value="(v: string) => updateChart({ valuesText: v })"
284
- />
285
- </label>
286
- </ConfigFieldGroup>
287
-
288
365
  <ConfigFieldGroup
289
366
  v-if="['bar', 'line', 'area', 'scatter'].includes(selectedChartType)"
290
367
  :title="t('panel.config.groupAxes')"
368
+ collapsible
369
+ :default-open="false"
291
370
  >
292
371
  <div class="grid grid-cols-2 gap-2">
293
372
  <label class="block">
@@ -407,68 +486,6 @@ function onOptionJsonChange(v: string) {
407
486
  </div>
408
487
  </ConfigFieldGroup>
409
488
 
410
- <ConfigFieldGroup
411
- v-if="['bar', 'line', 'area', 'pie'].includes(selectedChartType)"
412
- :title="t('panel.config.groupSeries')"
413
- >
414
- <label v-if="selectedChartType === 'bar'" class="block space-y-1">
415
- <div>{{ t("panel.config.barWidthPx") }}</div>
416
- <InputNumber
417
- size="small"
418
- class="w-full"
419
- :min="1"
420
- :value="element.chart?.barWidth ?? 24"
421
- :disabled="!isEditable"
422
- @update:value="(v) => {
423
- const n = Number(v);
424
- if (!Number.isNaN(n)) updateChart({ barWidth: Math.max(1, n) });
425
- }"
426
- />
427
- </label>
428
- <label
429
- v-if="selectedChartType === 'line' || selectedChartType === 'area'"
430
- class="flex items-center gap-2"
431
- >
432
- <Checkbox
433
- :checked="element.chart?.smooth ?? true"
434
- :disabled="!isEditable"
435
- @update:checked="(v) => updateChart({ smooth: v === true })"
436
- />
437
- <span>{{ t("panel.config.smooth") }}</span>
438
- </label>
439
- <div v-if="selectedChartType === 'pie'" class="grid grid-cols-2 gap-2">
440
- <label class="block space-y-1">
441
- <div>{{ t("panel.config.pieInnerRadiusPct") }}</div>
442
- <InputNumber
443
- size="small"
444
- class="w-full"
445
- :min="0"
446
- :max="99"
447
- :value="element.chart?.pieInnerRadius ?? 30"
448
- :disabled="!isEditable"
449
- @update:value="(v) => {
450
- const n = Number(v);
451
- if (!Number.isNaN(n)) updateChart({ pieInnerRadius: Math.max(0, Math.min(99, n)) });
452
- }"
453
- />
454
- </label>
455
- <label class="block space-y-1">
456
- <div>{{ t("panel.config.pieOuterRadiusPct") }}</div>
457
- <InputNumber
458
- size="small"
459
- class="w-full"
460
- :min="1"
461
- :max="100"
462
- :value="element.chart?.pieOuterRadius ?? 65"
463
- :disabled="!isEditable"
464
- @update:value="(v) => {
465
- const n = Number(v);
466
- if (!Number.isNaN(n)) updateChart({ pieOuterRadius: Math.max(1, Math.min(100, n)) });
467
- }"
468
- />
469
- </label>
470
- </div>
471
- </ConfigFieldGroup>
472
489
  </ConfigSection>
473
490
 
474
491
  <ConfigSection
@@ -138,7 +138,11 @@ function clearSketch() {
138
138
  @update:value="(v) => patch({ geometryColor: v || '#3b82f6' })"
139
139
  />
140
140
  </ConfigFieldGroup>
141
- <ConfigFieldGroup :title="t('panel.config.groupCanvasScript')">
141
+ <ConfigFieldGroup
142
+ :title="t('panel.config.groupCanvasScript')"
143
+ collapsible
144
+ :default-open="false"
145
+ >
142
146
  <template #hint>
143
147
  {{ t("panel.config.canvasScriptHint") }}
144
148
  </template>
@@ -152,7 +156,11 @@ function clearSketch() {
152
156
  @update:value="(v: string) => patch({ geometryScript: v || undefined })"
153
157
  />
154
158
  </ConfigFieldGroup>
155
- <ConfigFieldGroup :title="t('panel.config.groupSketchOverlay')">
159
+ <ConfigFieldGroup
160
+ :title="t('panel.config.groupSketchOverlay')"
161
+ collapsible
162
+ :default-open="false"
163
+ >
156
164
  <div class="flex items-center gap-2">
157
165
  <label class="block space-y-1">
158
166
  <div class="text-[11px]">{{ t("panel.config.penColor") }}</div>
@@ -100,7 +100,11 @@ function onFileChange(e: Event) {
100
100
  <span v-if="uploadStatus" class="text-[11px] text-gray-500">{{ uploadStatus }}</span>
101
101
  </div>
102
102
  </ConfigFieldGroup>
103
- <ConfigFieldGroup :title="t('panel.config.groupDisplayMode')">
103
+ <ConfigFieldGroup
104
+ :title="t('panel.config.groupDisplayMode')"
105
+ collapsible
106
+ :default-open="false"
107
+ >
104
108
  <label class="block space-y-1">
105
109
  <div>{{ t("panel.config.objectFitFull") }}</div>
106
110
  <Select
@@ -4,6 +4,7 @@ import { Checkbox, Input, InputNumber, Select } from "ant-design-vue";
4
4
  import type { PanelElement, PanelLayer } from "../../types";
5
5
  import { getPanelMessages } from "../../constants/messages";
6
6
  import ConfigSection from "./ConfigSection.vue";
7
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
7
8
 
8
9
  const { t, locale } = useI18n();
9
10
  const msgs = () => getPanelMessages(t);
@@ -52,7 +53,7 @@ function patch(patch: Partial<PanelElement>) {
52
53
  @update:value="(v: string) => patch({ name: v || undefined })"
53
54
  />
54
55
  </label>
55
- <div class="grid grid-cols-3 gap-2">
56
+ <div class="grid grid-cols-2 gap-2">
56
57
  <label class="block space-y-1">
57
58
  <div>X</div>
58
59
  <InputNumber
@@ -74,17 +75,7 @@ function patch(patch: Partial<PanelElement>) {
74
75
  />
75
76
  </label>
76
77
  </div>
77
- <div class="grid grid-cols-3 gap-2">
78
- <label class="block space-y-1">
79
- <div>{{ t("panel.config.rotate") }}</div>
80
- <InputNumber
81
- size="small"
82
- class="w-full"
83
- :value="element.rotate ?? 0"
84
- :disabled="!isEditable"
85
- @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ rotate: n }); }"
86
- />
87
- </label>
78
+ <div class="grid grid-cols-2 gap-2">
88
79
  <label class="block space-y-1">
89
80
  <div>{{ t("panel.config.width") }}</div>
90
81
  <InputNumber
@@ -108,6 +99,23 @@ function patch(patch: Partial<PanelElement>) {
108
99
  />
109
100
  </label>
110
101
  </div>
102
+ <ConfigFieldGroup
103
+ :title="t('panel.config.groupNodeMore')"
104
+ collapsible
105
+ :default-open="false"
106
+ >
107
+ <div class="grid grid-cols-2 gap-2">
108
+ <label class="block space-y-1">
109
+ <div>{{ t("panel.config.rotate") }}</div>
110
+ <InputNumber
111
+ size="small"
112
+ class="w-full"
113
+ :value="element.rotate ?? 0"
114
+ :disabled="!isEditable"
115
+ @update:value="(v) => { const n = Number(v); if (!Number.isNaN(n)) patch({ rotate: n }); }"
116
+ />
117
+ </label>
118
+ </div>
111
119
  <div class="space-y-1.5">
112
120
  <div class="text-[11px] text-gray-500">{{ t("panel.config.nodeZOrder") }}</div>
113
121
  <div class="text-[11px] text-gray-400">{{ t("panel.config.currentZIndex", { value: nodeZOrderLabel ?? "-" }) }}</div>
@@ -148,6 +156,7 @@ function patch(patch: Partial<PanelElement>) {
148
156
  </div>
149
157
  <div class="truncate text-gray-400">ID: {{ element.id }}</div>
150
158
  <div class="text-gray-400">{{ t("panel.config.type") }}: {{ element.materialType ?? element.id }}</div>
159
+ </ConfigFieldGroup>
151
160
  <div class="rounded-lg border border-gray-200/60 bg-white/80 px-2.5 py-2">
152
161
  <label class="flex items-center gap-2">
153
162
  <Checkbox
@@ -57,7 +57,11 @@ function patch(patch: Partial<PanelElement>) {
57
57
  </Select>
58
58
  </label>
59
59
  </ConfigFieldGroup>
60
- <ConfigFieldGroup :title="t('panel.config.groupCopyStrategy')">
60
+ <ConfigFieldGroup
61
+ :title="t('panel.config.groupCopyStrategy')"
62
+ collapsible
63
+ :default-open="false"
64
+ >
61
65
  <template #hint>
62
66
  {{ t("panel.config.copyStrategyHint") }}
63
67
  </template>
@@ -76,6 +76,12 @@ function onFileChange(e: Event) {
76
76
  :disabled="!isEditable"
77
77
  @update:value="(v) => patchStyle({ backgroundColor: v || undefined })"
78
78
  />
79
+ </ConfigFieldGroup>
80
+ <ConfigFieldGroup
81
+ :title="t('panel.config.groupBgLayout')"
82
+ collapsible
83
+ :default-open="false"
84
+ >
79
85
  <label class="block space-y-1">
80
86
  <div>{{ t("panel.scope.fieldStyleBackgroundImage") }}</div>
81
87
  <Input
@@ -102,8 +108,6 @@ function onFileChange(e: Event) {
102
108
  </label>
103
109
  <span v-if="uploadStatus" class="text-[11px] text-gray-500">{{ uploadStatus }}</span>
104
110
  </div>
105
- </ConfigFieldGroup>
106
- <ConfigFieldGroup :title="t('panel.config.groupBgLayout')">
107
111
  <div class="grid grid-cols-2 gap-2">
108
112
  <label class="block space-y-1">
109
113
  <div>{{ t("panel.scope.fieldStyleBackgroundSize") }}</div>
@@ -184,7 +184,11 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
184
184
  @update:value="(v) => patch({ textColor: v || undefined })"
185
185
  />
186
186
  </ConfigFieldGroup>
187
- <ConfigFieldGroup :title="t('panel.config.groupInputAbility')">
187
+ <ConfigFieldGroup
188
+ :title="t('panel.config.groupInputAbility')"
189
+ collapsible
190
+ :default-open="false"
191
+ >
188
192
  <label class="flex items-center gap-2">
189
193
  <Checkbox
190
194
  :checked="element.textAllowInput ?? true"
@@ -84,6 +84,11 @@ function deepMerge<T extends Record<string, any>>(base: T, patch?: Record<string
84
84
  return next as T;
85
85
  }
86
86
 
87
+ function resolveBarWidth(chart?: PanelChartConfig): number | undefined {
88
+ const width = chart?.barWidth;
89
+ return typeof width === "number" && Number.isFinite(width) && width > 0 ? width : undefined;
90
+ }
91
+
87
92
  export function buildChartOption(element: PanelElement): EChartsOption {
88
93
  const chartType = (element.materialType ?? "") as ChartType;
89
94
  const labels = resolveChartLabels(element);
@@ -315,7 +320,8 @@ export function buildChartOption(element: PanelElement): EChartsOption {
315
320
  chartType === "line" || chartType === "area"
316
321
  ? (element.chart?.smooth ?? true)
317
322
  : undefined,
318
- barWidth: chartType === "bar" ? (element.chart?.barWidth ?? 24) : undefined,
323
+ barWidth:
324
+ chartType === "bar" ? resolveBarWidth(element.chart) : undefined,
319
325
  areaStyle: chartType === "area" ? { opacity: 0.25 } : undefined,
320
326
  itemStyle: { color: chartColor as any },
321
327
  lineStyle: { color: chartColor as any },
@@ -0,0 +1,56 @@
1
+ /** 产物预览是否缩放内部内容(字体等)。默认 false:块仍按视口撑满,内容保持原尺寸以免失真。 */
2
+ export const OUTPUT_SCALE_STORAGE_KEY = "panel:outputScale";
3
+
4
+ export function readOutputScale(): boolean {
5
+ try {
6
+ return localStorage.getItem(OUTPUT_SCALE_STORAGE_KEY) === "1";
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ export type PreviewFillScale = {
13
+ vw: number;
14
+ vh: number;
15
+ scaleX: number;
16
+ scaleY: number;
17
+ };
18
+
19
+ export function getPreviewFillScale(
20
+ sceneWidth: number,
21
+ sceneHeight: number
22
+ ): PreviewFillScale {
23
+ const vw = window.innerWidth || 1;
24
+ const vh = window.innerHeight || 1;
25
+ const sw = Math.max(1, sceneWidth);
26
+ const sh = Math.max(1, sceneHeight);
27
+ return {
28
+ vw,
29
+ vh,
30
+ scaleX: vw / sw,
31
+ scaleY: vh / sh,
32
+ };
33
+ }
34
+
35
+ export function applyPreviewSceneFill(
36
+ scene: HTMLElement | null,
37
+ sceneWidth: number,
38
+ sceneHeight: number,
39
+ outputScale: boolean
40
+ ): PreviewFillScale {
41
+ const fill = getPreviewFillScale(sceneWidth, sceneHeight);
42
+ if (!Number.isFinite(fill.scaleX) || !Number.isFinite(fill.scaleY)) return fill;
43
+ if (!scene) return fill;
44
+ const root = scene.parentElement;
45
+ if (root) root.style.overflow = "hidden";
46
+ if (outputScale) {
47
+ scene.style.width = `${Math.max(1, sceneWidth)}px`;
48
+ scene.style.height = `${Math.max(1, sceneHeight)}px`;
49
+ scene.style.transform = `translate(0px,0px) scale(${fill.scaleX},${fill.scaleY})`;
50
+ } else {
51
+ scene.style.width = `${fill.vw}px`;
52
+ scene.style.height = `${fill.vh}px`;
53
+ scene.style.transform = "none";
54
+ }
55
+ return fill;
56
+ }
@@ -130,7 +130,6 @@ export function getDefaultChartConfig(
130
130
  return {
131
131
  title: t("panel.defaults.bar"),
132
132
  ...common,
133
- barWidth: 24,
134
133
  };
135
134
  }
136
135
  if (materialType === "line") {