@arronqzy/vue-view 0.1.4 → 0.1.7

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 (41) hide show
  1. package/package.json +6 -5
  2. package/src/panel/VueViewOnlinePreview.vue +55 -19
  3. package/src/panel/VueViewPanel.vue +123 -66
  4. package/src/panel/components/BusyOverlay.vue +24 -0
  5. package/src/panel/components/ConfigHintIcon.vue +10 -10
  6. package/src/panel/components/ElementsLayer.vue +51 -8
  7. package/src/panel/components/MaterialPreview.vue +8 -0
  8. package/src/panel/components/PanelConfigSidebar.vue +32 -20
  9. package/src/panel/components/WorkspaceConfigSidebar.vue +11 -0
  10. package/src/panel/components/WorkspaceProjectNav.vue +36 -8
  11. package/src/panel/components/config/ConfigColorField.vue +33 -9
  12. package/src/panel/components/config/ConfigFieldGroup.vue +35 -6
  13. package/src/panel/components/config/PanelConfigAudioSection.vue +37 -19
  14. package/src/panel/components/config/PanelConfigChartSection.vue +101 -84
  15. package/src/panel/components/config/PanelConfigGeometrySection.vue +10 -2
  16. package/src/panel/components/config/PanelConfigImageSection.vue +18 -10
  17. package/src/panel/components/config/PanelConfigNodeInfo.vue +21 -12
  18. package/src/panel/components/config/PanelConfigReferenceSection.vue +5 -1
  19. package/src/panel/components/config/PanelConfigScene3dSection.vue +505 -0
  20. package/src/panel/components/config/PanelConfigStyleSections.vue +19 -11
  21. package/src/panel/components/config/PanelConfigTextSection.vue +25 -7
  22. package/src/panel/components/config/PanelConfigVideoSection.vue +13 -9
  23. package/src/panel/components/config/shared.ts +16 -21
  24. package/src/panel/components/elementsLayerNodes.ts +64 -59
  25. package/src/panel/components/materialSidebarData.ts +2 -0
  26. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.vue +1 -1
  27. package/src/panel/components/table/PanelConfigTableSection.vue +39 -25
  28. package/src/panel/components/table/TableNodeContent.vue +36 -15
  29. package/src/panel/hooks/usePanelElements.ts +13 -1
  30. package/src/panel/hooks/useWorkspaceProjects.ts +128 -57
  31. package/src/panel/library/workspace-project-cache.ts +7 -2
  32. package/src/panel/library/workspace-project-db.ts +29 -11
  33. package/src/panel/library/workspace-snapshot.ts +21 -4
  34. package/src/panel/scope/view-scope-store.ts +8 -0
  35. package/src/panel/types.ts +4 -2
  36. package/src/panel/utils/async-work.ts +153 -0
  37. package/src/panel/utils/chartOptionBuilder.ts +7 -1
  38. package/src/panel/utils/gridPlacement.ts +29 -0
  39. package/src/panel/utils/outputScale.ts +56 -0
  40. package/src/panel/utils/panelElementDefaults.ts +37 -2
  41. package/src/panel/utils/scope-autocomplete.ts +0 -114
@@ -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";
@@ -20,6 +20,7 @@ import PanelConfigTextSection from "./config/PanelConfigTextSection.vue";
20
20
  import PanelConfigAudioSection from "./config/PanelConfigAudioSection.vue";
21
21
  import PanelConfigVideoSection from "./config/PanelConfigVideoSection.vue";
22
22
  import PanelConfigGeometrySection from "./config/PanelConfigGeometrySection.vue";
23
+ import PanelConfigScene3dSection from "./config/PanelConfigScene3dSection.vue";
23
24
  import PanelConfigGridSection from "./config/PanelConfigGridSection.vue";
24
25
  import PanelConfigGridChildSpan from "./config/PanelConfigGridChildSpan.vue";
25
26
  import PanelConfigReferenceSection from "./config/PanelConfigReferenceSection.vue";
@@ -61,8 +62,8 @@ const sidebarScrollRef = ref<HTMLElement | null>(null);
61
62
 
62
63
  const expandedSections = ref<Record<string, boolean>>({
63
64
  nodeInfo: true,
64
- styleBackground: true,
65
- styleBorder: true,
65
+ styleBackground: false,
66
+ styleBorder: false,
66
67
  chartBasic: true,
67
68
  chartAdvanced: false,
68
69
  textConfig: true,
@@ -123,6 +124,7 @@ const scopeWarnings = computed(() => {
123
124
  const materialType = computed(() => props.selectedElement?.materialType ?? "");
124
125
 
125
126
  const forceOpenSections = computed(() => hasSearch.value);
127
+ provide("configHasSearch", hasSearch);
126
128
 
127
129
  function isSectionExpanded(key: string, defaultValue = true) {
128
130
  return expandedSections.value[key] ?? defaultValue;
@@ -259,23 +261,6 @@ watch(
259
261
  @update:open="(v) => setSectionExpanded('nodeInfo', v)"
260
262
  />
261
263
 
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
264
  <PanelConfigChartSection
280
265
  v-if="isChartElement && shouldShowSection('chartBasic', t('panel.config.sectionChartBasic'), [t('panel.material.charts'), 'tooltip'])"
281
266
  :element="selectedElement"
@@ -349,6 +334,16 @@ watch(
349
334
  @update:open="(v) => setSectionExpanded('geometryConfig', v)"
350
335
  />
351
336
 
337
+ <PanelConfigScene3dSection
338
+ v-if="materialType === 'scene3d' && shouldShowSection('scene3dConfig', t('panel.config.sectionScene3d'), [t('panel.material.scene3d'), '3d', 'glb', t('panel.config.groupScene3dCamera')])"
339
+ :element="selectedElement"
340
+ :is-editable="isNodeEditable"
341
+ :open="isSectionExpanded('scene3dConfig')"
342
+ :force-open="forceOpenSections"
343
+ :update-element="updateElement"
344
+ @update:open="(v) => setSectionExpanded('scene3dConfig', v)"
345
+ />
346
+
352
347
  <PanelConfigGridSection
353
348
  v-if="materialType === 'grid' && shouldShowSection('gridConfig', t('panel.config.sectionGrid'), [t('panel.config.searchKwGrid'), t('panel.config.rowsShort'), t('panel.config.colsShort'), t('panel.config.searchKwGap')])"
354
349
  :element="selectedElement"
@@ -381,6 +376,23 @@ watch(
381
376
  @update:open="(v) => setSectionExpanded('reference', v)"
382
377
  />
383
378
 
379
+ <PanelConfigStyleSections
380
+ v-if="
381
+ shouldShowSection('styleBackground', t('panel.config.sectionStyleBackground'), [t('panel.config.backgroundColor'), 'background']) ||
382
+ shouldShowSection('styleBorder', t('panel.config.sectionStyleBorder'), [t('panel.config.searchKwBorder'), 'border'])
383
+ "
384
+ :element="selectedElement"
385
+ :is-editable="isNodeEditable"
386
+ :show-background="shouldShowSection('styleBackground', t('panel.config.sectionStyleBackground'), [t('panel.config.backgroundColor'), 'background'])"
387
+ :show-border="shouldShowSection('styleBorder', t('panel.config.sectionStyleBorder'), [t('panel.config.searchKwBorder'), 'border'])"
388
+ :background-open="isSectionExpanded('styleBackground')"
389
+ :border-open="isSectionExpanded('styleBorder')"
390
+ :force-open="forceOpenSections"
391
+ :update-element="updateElement"
392
+ @update:background-open="(v) => setSectionExpanded('styleBackground', v)"
393
+ @update:border-open="(v) => setSectionExpanded('styleBorder', v)"
394
+ />
395
+
384
396
  <div
385
397
  v-if="!isChartElement && materialType && !['text', 'table', 'audio', 'video', 'image', 'geometry', 'grid', 'reference'].includes(materialType)"
386
398
  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: (
@@ -76,6 +80,7 @@ export type WorkspaceConfigSidebarProps = Omit<
76
80
  | "jsonConfig"
77
81
  | "logicConfig"
78
82
  | "clockConfig"
83
+ | "eventConfig"
79
84
  >
80
85
  >
81
86
  ) => void;
@@ -90,6 +95,9 @@ const props = withDefaults(
90
95
  {
91
96
  allowFalseSignalPropagation: false,
92
97
  blueprintLibraryOptions: () => [],
98
+ blueprintGraphNodes: () => [],
99
+ blueprintGraphEdges: () => [],
100
+ blueprintTraceEntries: () => [],
93
101
  blueprintNodeOptions: () => [],
94
102
  selectedElements: () => [],
95
103
  }
@@ -143,6 +151,9 @@ const viewElementOptions = computed(() =>
143
151
  <BlueprintNodeConfigSidebar
144
152
  v-else
145
153
  :node="selectedBlueprintNode"
154
+ :graph-nodes="blueprintGraphNodes"
155
+ :graph-edges="blueprintGraphEdges"
156
+ :trace-entries="blueprintTraceEntries.length ? blueprintTraceEntries : executionLog?.entries ?? []"
146
157
  :allow-false-signal-propagation="allowFalseSignalPropagation"
147
158
  :on-update-allow-false-signal-propagation="onUpdateAllowFalseSignalPropagation"
148
159
  :view-element-options="viewElementOptions"
@@ -19,6 +19,7 @@ const props = defineProps<{
19
19
  activeProjectId: string | null;
20
20
  activeProjectName: string | null;
21
21
  dirty: boolean;
22
+ previewingProjectIds?: string[];
22
23
  }>();
23
24
 
24
25
  const emit = defineEmits<{
@@ -76,8 +77,10 @@ const projectMenu = computed(() =>
76
77
  default: () =>
77
78
  props.projects.length === 0
78
79
  ? [h(Menu.Item, { key: "empty", disabled: true }, () => t("panel.workspace.noSavedWorkspaces"))]
79
- : props.projects.map((project) =>
80
- h(
80
+ : props.projects.map((project) => {
81
+ const isCurrent = props.activeProjectId === project.id;
82
+ const isPreviewing = (props.previewingProjectIds ?? []).includes(project.id);
83
+ return h(
81
84
  "div",
82
85
  { key: project.id, class: "px-1 py-0.5" },
83
86
  h("div", { class: "flex items-center gap-1" }, [
@@ -86,16 +89,41 @@ const projectMenu = computed(() =>
86
89
  {
87
90
  type: "button",
88
91
  class: [
89
- "min-w-0 flex-1 rounded px-2 py-1.5 text-left text-xs hover:bg-accent",
90
- props.activeProjectId === project.id
91
- ? "bg-accent/60 font-medium"
92
+ "min-w-0 flex-1 rounded px-2 py-1.5 text-left text-xs hover:bg-gray-100",
93
+ isCurrent
94
+ ? "bg-blue-50 font-medium text-blue-700 ring-1 ring-inset ring-blue-300"
95
+ : "",
96
+ !isCurrent && isPreviewing
97
+ ? "bg-sky-50 ring-1 ring-inset ring-sky-300"
92
98
  : "",
93
99
  ].join(" "),
94
100
  onClick: () =>
95
101
  void runAction(() => emit("openProject", project.id)),
96
102
  },
97
103
  [
98
- h("div", { class: "truncate" }, project.name),
104
+ h("div", { class: "flex items-center gap-1" }, [
105
+ h("div", { class: "min-w-0 flex-1 truncate" }, project.name),
106
+ isCurrent
107
+ ? h(
108
+ "span",
109
+ {
110
+ class:
111
+ "shrink-0 rounded bg-blue-600 px-1 py-px text-[9px] leading-none text-white",
112
+ },
113
+ t("panel.workspace.currentBadge")
114
+ )
115
+ : null,
116
+ isPreviewing
117
+ ? h(
118
+ "span",
119
+ {
120
+ class:
121
+ "shrink-0 rounded bg-sky-600 px-1 py-px text-[9px] leading-none text-white",
122
+ },
123
+ t("panel.workspace.previewingBadge")
124
+ )
125
+ : null,
126
+ ]),
99
127
  h(
100
128
  "div",
101
129
  { class: "truncate text-[10px] text-muted-foreground" },
@@ -132,8 +160,8 @@ const projectMenu = computed(() =>
132
160
  () => t("common.delete")
133
161
  ),
134
162
  ])
135
- )
136
- ),
163
+ );
164
+ }),
137
165
  }
138
166
  )
139
167
  );
@@ -1,23 +1,35 @@
1
1
  <script setup lang="ts">
2
2
  import { useI18n } from "@arronqzy/i18n/vue";
3
3
  import { computed } from "vue";
4
- import { Input } from "ant-design-vue";
4
+ import { Input, Switch } from "ant-design-vue";
5
+ import { isCssTransparent } from "@arronqzy/view-table";
5
6
 
6
- const { t, locale } = useI18n();
7
- const props = defineProps<{
8
- label: string;
9
- value: string;
10
- disabled?: boolean;
11
- }>();
7
+ const { t } = useI18n();
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ label: string;
11
+ value: string;
12
+ disabled?: boolean;
13
+ allowTransparent?: boolean;
14
+ fallbackHex?: string;
15
+ }>(),
16
+ { allowTransparent: false, fallbackHex: "#ffffff" }
17
+ );
12
18
 
13
19
  const emit = defineEmits<{
14
20
  "update:value": [value: string];
15
21
  }>();
16
22
 
23
+ const transparent = computed(
24
+ () => props.allowTransparent && isCssTransparent(props.value)
25
+ );
26
+
17
27
  const pickerValue = computed(() =>
18
28
  /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(props.value || "")
19
29
  ? props.value
20
- : "#000000"
30
+ : props.fallbackHex.startsWith("#")
31
+ ? props.fallbackHex
32
+ : "#000000"
21
33
  );
22
34
 
23
35
  function onTextChange(v: string) {
@@ -28,12 +40,23 @@ function onColorInput(e: Event) {
28
40
  const target = e.target as HTMLInputElement;
29
41
  emit("update:value", target.value);
30
42
  }
43
+
44
+ function onTransparentChange(v: unknown) {
45
+ emit("update:value", v === true ? "transparent" : props.fallbackHex);
46
+ }
31
47
  </script>
32
48
 
33
49
  <template>
34
50
  <label class="block space-y-1.5">
35
51
  <div class="text-[11px] text-gray-500">{{ label }}</div>
36
- <div class="flex items-center gap-2">
52
+ <div v-if="allowTransparent" class="flex items-center gap-2">
53
+ <Switch size="small" :checked="transparent" :disabled="disabled" @update:checked="onTransparentChange" />
54
+ <span class="text-[11px] text-gray-600">{{ t("panel.config.tableBgTransparent") }}</span>
55
+ </div>
56
+ <div v-if="transparent" class="text-[10px] text-gray-500">
57
+ {{ t("panel.config.tableBgTransparentHint") }}
58
+ </div>
59
+ <div v-else class="flex items-center gap-2">
37
60
  <Input
38
61
  size="small"
39
62
  :value="value"
@@ -52,3 +75,4 @@ function onColorInput(e: Event) {
52
75
  </div>
53
76
  </label>
54
77
  </template>
78
+
@@ -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>
@@ -44,22 +44,30 @@ function patch(patch: Partial<PanelElement>) {
44
44
  }
45
45
 
46
46
  async function handleUploadAudioFile(file: File) {
47
- const base64 = await readFileAsDataUrl(file, msgs().readAudioFailed);
48
- patch({ audioSrc: base64 });
49
- audioStatus.value = msgs().audioLocalSaved;
50
- const url = await uploadFileToRemote(file);
51
- if (url) {
52
- patch({ audioRemoteUrl: url });
53
- audioStatus.value = msgs().audioRemoteUploaded;
54
- } else {
55
- audioStatus.value = msgs().audioServerUploadFailed;
47
+ try {
48
+ const base64 = await readFileAsDataUrl(file, msgs().readAudioFailed, "audio");
49
+ patch({ audioSrc: base64 });
50
+ audioStatus.value = msgs().audioLocalSaved;
51
+ const url = await uploadFileToRemote(file);
52
+ if (url) {
53
+ patch({ audioRemoteUrl: url });
54
+ audioStatus.value = msgs().audioRemoteUploaded;
55
+ } else {
56
+ audioStatus.value = msgs().audioServerUploadFailed;
57
+ }
58
+ } catch (error) {
59
+ audioStatus.value = error instanceof Error ? error.message : msgs().readAudioFailed;
56
60
  }
57
61
  }
58
62
 
59
63
  async function handleUploadAudioPoster(file: File) {
60
- const base64 = await readFileAsDataUrl(file, msgs().readImageFailed);
61
- patch({ audioPosterImage: base64 });
62
- audioStatus.value = msgs().audioPosterSet;
64
+ try {
65
+ const base64 = await readFileAsDataUrl(file, msgs().readImageFailed);
66
+ patch({ audioPosterImage: base64 });
67
+ audioStatus.value = msgs().audioPosterSet;
68
+ } catch (error) {
69
+ audioStatus.value = error instanceof Error ? error.message : msgs().readImageFailed;
70
+ }
63
71
  }
64
72
 
65
73
  function stopRecordingAudio() {
@@ -85,12 +93,18 @@ async function startRecordingAudio() {
85
93
  const blob = new Blob(audioChunksRef.value, {
86
94
  type: recorder.mimeType || "audio/webm",
87
95
  });
88
- const dataUrl = await readFileAsDataUrl(
89
- new File([blob], "recording.webm", { type: blob.type }),
90
- msgs().readRecordAudioFailed
91
- );
92
- patch({ audioSrc: dataUrl });
93
- audioStatus.value = msgs().audioRecordSaved;
96
+ try {
97
+ const dataUrl = await readFileAsDataUrl(
98
+ new File([blob], "recording.webm", { type: blob.type }),
99
+ msgs().readRecordAudioFailed,
100
+ "audio"
101
+ );
102
+ patch({ audioSrc: dataUrl });
103
+ audioStatus.value = msgs().audioRecordSaved;
104
+ } catch (error) {
105
+ audioStatus.value =
106
+ error instanceof Error ? error.message : msgs().readRecordAudioFailed;
107
+ }
94
108
  recordStreamRef.value?.getTracks().forEach((track) => track.stop());
95
109
  recordStreamRef.value = null;
96
110
  recorderRef.value = null;
@@ -168,7 +182,11 @@ function onPosterFileChange(e: Event) {
168
182
  :src="element.audioSrc || element.audioRemoteUrl || ''"
169
183
  />
170
184
  </ConfigFieldGroup>
171
- <ConfigFieldGroup :title="t('panel.config.groupDisplayStyle')">
185
+ <ConfigFieldGroup
186
+ :title="t('panel.config.groupDisplayStyle')"
187
+ collapsible
188
+ :default-open="false"
189
+ >
172
190
  <label class="flex items-center gap-2">
173
191
  <Checkbox
174
192
  :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>