@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,650 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from "vue";
3
+ import { Checkbox, Collapse, Input, InputNumber, Select, Textarea } from "ant-design-vue";
4
+ import type { PanelChartConfig, PanelElement } from "../../types";
5
+ import {
6
+ buildChartOption,
7
+ CHART_TYPES,
8
+ getChartLabelsDisplayText,
9
+ getChartValuesDisplayText,
10
+ } from "../../utils/chartOptionBuilder";
11
+ import { mergeOptionPatch } from "./shared";
12
+ import ConfigColorField from "./ConfigColorField.vue";
13
+ import ConfigFieldGroup from "./ConfigFieldGroup.vue";
14
+ import ConfigHintIcon from "../ConfigHintIcon.vue";
15
+ import ConfigSection from "./ConfigSection.vue";
16
+
17
+ type ChartType =
18
+ | "bar"
19
+ | "line"
20
+ | "pie"
21
+ | "area"
22
+ | "scatter"
23
+ | "radar"
24
+ | "gauge"
25
+ | "funnel"
26
+ | "";
27
+
28
+ const props = defineProps<{
29
+ element: PanelElement;
30
+ isEditable: boolean;
31
+ basicOpen: boolean;
32
+ advancedOpen: boolean;
33
+ forceOpen?: boolean;
34
+ updateElement: (
35
+ id: string,
36
+ patch: Partial<PanelElement>,
37
+ options?: { batchId?: string; meta?: Record<string, unknown> }
38
+ ) => void;
39
+ }>();
40
+
41
+ const emit = defineEmits<{
42
+ "update:basicOpen": [value: boolean];
43
+ "update:advancedOpen": [value: boolean];
44
+ }>();
45
+
46
+ const isAdvancedOptionMode = ref(false);
47
+ const optionJsonText = ref("{}");
48
+ const optionJsonError = ref<string | null>(null);
49
+
50
+ const selectedChartType = computed(
51
+ () => (props.element.materialType ?? "") as ChartType
52
+ );
53
+
54
+ const chartOption = computed(
55
+ () => (props.element.chart?.option ?? {}) as Record<string, any>
56
+ );
57
+
58
+ watch(
59
+ () => props.element.id,
60
+ () => {
61
+ if (CHART_TYPES.has(props.element.materialType ?? "")) {
62
+ optionJsonText.value = JSON.stringify(buildChartOption(props.element), null, 2);
63
+ } else {
64
+ optionJsonText.value = "{}";
65
+ }
66
+ optionJsonError.value = null;
67
+ },
68
+ { immediate: true }
69
+ );
70
+
71
+ function updateChart(patch: Partial<PanelChartConfig>) {
72
+ props.updateElement(props.element.id, {
73
+ chart: { ...(props.element.chart ?? {}), ...patch },
74
+ });
75
+ }
76
+
77
+ function updateOptionForm(patch: Record<string, unknown>) {
78
+ updateChart({
79
+ option: mergeOptionPatch(chartOption.value, patch),
80
+ });
81
+ }
82
+
83
+ function gradientCss(direction: string | undefined, from: string, to: string) {
84
+ const map: Record<string, string> = {
85
+ "to-bottom": "to bottom",
86
+ "to-bottom-right": "to bottom right",
87
+ "to-top-right": "to top right",
88
+ "to-right": "to right",
89
+ };
90
+ return `linear-gradient(${map[direction ?? "to-right"] ?? "to right"}, ${from}, ${to})`;
91
+ }
92
+
93
+ function hasDataZoomType(type: string) {
94
+ const zoom = chartOption.value.dataZoom;
95
+ return Array.isArray(zoom) && zoom.some((z) => z?.type === type);
96
+ }
97
+
98
+ function toggleDataZoom(type: "inside" | "slider", checked: boolean) {
99
+ const prev = Array.isArray(chartOption.value.dataZoom)
100
+ ? [...chartOption.value.dataZoom]
101
+ : [];
102
+ const next = checked
103
+ ? [...prev.filter((z) => z?.type !== type), { type }]
104
+ : prev.filter((z) => z?.type !== type);
105
+ updateOptionForm({ dataZoom: next });
106
+ }
107
+
108
+ function onOptionJsonChange(v: string) {
109
+ optionJsonText.value = v;
110
+ try {
111
+ const parsed = JSON.parse(v) as Record<string, unknown>;
112
+ updateChart({ option: parsed });
113
+ optionJsonError.value = null;
114
+ } catch {
115
+ optionJsonError.value = "JSON 格式错误,修正后会自动应用";
116
+ }
117
+ }
118
+ </script>
119
+
120
+ <template>
121
+ <ConfigSection
122
+ title="图表配置 / 基础"
123
+ :open="basicOpen"
124
+ :force-open="forceOpen"
125
+ @update:open="emit('update:basicOpen', $event)"
126
+ >
127
+ <ConfigFieldGroup title="基础显示">
128
+ <label class="block space-y-1.5">
129
+ <div>标题</div>
130
+ <Input
131
+ size="small"
132
+ :value="element.chart?.title ?? ''"
133
+ :disabled="!isEditable"
134
+ @update:value="(v: string) => updateChart({ title: v })"
135
+ />
136
+ </label>
137
+ <ConfigColorField
138
+ label="主色"
139
+ :value="element.chart?.color ?? '#3b82f6'"
140
+ :disabled="!isEditable"
141
+ @update:value="(v) => updateChart({ color: v || '#3b82f6' })"
142
+ />
143
+ <label class="flex items-center gap-2">
144
+ <Checkbox
145
+ :checked="element.chart?.colorMode === 'gradient'"
146
+ :disabled="!isEditable"
147
+ @update:checked="(v) => updateChart({ colorMode: v ? 'gradient' : 'solid' })"
148
+ />
149
+ <span>主色使用渐变</span>
150
+ </label>
151
+ <div v-if="element.chart?.colorMode === 'gradient'" class="grid grid-cols-2 gap-2">
152
+ <ConfigColorField
153
+ label="渐变起始色"
154
+ :value="element.chart?.gradientFrom ?? element.chart?.color ?? '#3b82f6'"
155
+ :disabled="!isEditable"
156
+ @update:value="(v) => updateChart({ gradientFrom: v || '#3b82f6' })"
157
+ />
158
+ <ConfigColorField
159
+ label="渐变结束色"
160
+ :value="element.chart?.gradientTo ?? '#22d3ee'"
161
+ :disabled="!isEditable"
162
+ @update:value="(v) => updateChart({ gradientTo: v || '#22d3ee' })"
163
+ />
164
+ <label class="col-span-2 block space-y-1">
165
+ <div>渐变方向</div>
166
+ <Select
167
+ size="small"
168
+ class="w-full"
169
+ :value="element.chart?.gradientDirection ?? 'to-right'"
170
+ :disabled="!isEditable"
171
+ @update:value="(v) => updateChart({ gradientDirection: v as PanelChartConfig['gradientDirection'] })"
172
+ >
173
+ <Select.Option value="to-right">左 → 右</Select.Option>
174
+ <Select.Option value="to-bottom">上 → 下</Select.Option>
175
+ <Select.Option value="to-bottom-right">左上 → 右下</Select.Option>
176
+ <Select.Option value="to-top-right">左下 → 右上</Select.Option>
177
+ </Select>
178
+ </label>
179
+ <div class="col-span-2 space-y-1">
180
+ <div class="text-[11px] text-gray-500">渐变预览</div>
181
+ <div
182
+ class="h-6 rounded border border-gray-200/60"
183
+ :style="{
184
+ backgroundImage: gradientCss(
185
+ element.chart?.gradientDirection,
186
+ element.chart?.gradientFrom ?? element.chart?.color ?? '#3b82f6',
187
+ element.chart?.gradientTo ?? '#22d3ee'
188
+ ),
189
+ }"
190
+ />
191
+ </div>
192
+ </div>
193
+ <label class="block space-y-1.5">
194
+ <div>显示模式</div>
195
+ <Select
196
+ size="small"
197
+ class="w-full"
198
+ :value="element.chart?.renderMode ?? 'canvas'"
199
+ :disabled="!isEditable"
200
+ @update:value="(v) => updateChart({ renderMode: v as 'canvas' | 'svg' })"
201
+ >
202
+ <Select.Option value="canvas">Canvas</Select.Option>
203
+ <Select.Option value="svg">SVG</Select.Option>
204
+ </Select>
205
+ </label>
206
+ </ConfigFieldGroup>
207
+
208
+ <ConfigFieldGroup title="提示框 Tooltip">
209
+ <div class="grid grid-cols-2 gap-2">
210
+ <label class="flex items-center gap-2">
211
+ <Checkbox
212
+ :checked="element.chart?.tooltipShow ?? true"
213
+ :disabled="!isEditable"
214
+ @update:checked="(v) => updateChart({ tooltipShow: v === true })"
215
+ />
216
+ <span>显示 Tooltip</span>
217
+ </label>
218
+ <label class="block space-y-1">
219
+ <div>Tooltip 触发方式</div>
220
+ <Select
221
+ size="small"
222
+ class="w-full"
223
+ :value="element.chart?.tooltipTrigger ?? 'axis'"
224
+ :disabled="!isEditable"
225
+ @update:value="(v) => updateChart({ tooltipTrigger: v as 'axis' | 'item' })"
226
+ >
227
+ <Select.Option value="axis">axis</Select.Option>
228
+ <Select.Option value="item">item</Select.Option>
229
+ </Select>
230
+ </label>
231
+ </div>
232
+ <div class="grid grid-cols-2 gap-2">
233
+ <ConfigColorField
234
+ label="Tooltip 背景色"
235
+ :value="element.chart?.tooltipBackgroundColor ?? '#0f172a'"
236
+ :disabled="!isEditable"
237
+ @update:value="(v) => updateChart({ tooltipBackgroundColor: v || '#0f172a' })"
238
+ />
239
+ <ConfigColorField
240
+ label="Tooltip 文字色"
241
+ :value="element.chart?.tooltipTextColor ?? '#f8fafc'"
242
+ :disabled="!isEditable"
243
+ @update:value="(v) => updateChart({ tooltipTextColor: v || '#f8fafc' })"
244
+ />
245
+ </div>
246
+ <label class="block space-y-1">
247
+ <div class="flex items-center gap-1">
248
+ <span>Tooltip Formatter(可选)</span>
249
+ <ConfigHintIcon label="Tooltip Formatter" content-class="max-w-[360px]">
250
+ <div class="font-medium">可用占位符</div>
251
+ <div>{"{a}=系列名, {b}=类目名, {c}=数值, {d}=百分比(饼图)"}</div>
252
+ </ConfigHintIcon>
253
+ </div>
254
+ <Input
255
+ size="small"
256
+ :value="element.chart?.tooltipFormatter ?? ''"
257
+ :disabled="!isEditable"
258
+ placeholder="例如:{b}: {c}"
259
+ @update:value="(v: string) => updateChart({ tooltipFormatter: v || undefined })"
260
+ />
261
+ </label>
262
+ </ConfigFieldGroup>
263
+
264
+ <ConfigFieldGroup title="数据">
265
+ <label class="block space-y-1">
266
+ <div>类目(逗号分隔)</div>
267
+ <Input
268
+ size="small"
269
+ :value="getChartLabelsDisplayText(element.chart)"
270
+ :disabled="!isEditable"
271
+ @update:value="(v: string) => updateChart({ labelsText: v })"
272
+ />
273
+ </label>
274
+ <label class="block space-y-1">
275
+ <div>数值(逗号分隔)</div>
276
+ <Input
277
+ size="small"
278
+ :value="getChartValuesDisplayText(element.chart)"
279
+ :disabled="!isEditable"
280
+ @update:value="(v: string) => updateChart({ valuesText: v })"
281
+ />
282
+ </label>
283
+ </ConfigFieldGroup>
284
+
285
+ <ConfigFieldGroup
286
+ v-if="['bar', 'line', 'area', 'scatter'].includes(selectedChartType)"
287
+ title="坐标轴"
288
+ >
289
+ <div class="grid grid-cols-2 gap-2">
290
+ <label class="block">
291
+ <div class="mb-1">X 轴名称</div>
292
+ <Input
293
+ size="small"
294
+ :value="element.chart?.xAxisName ?? ''"
295
+ :disabled="!isEditable"
296
+ @update:value="(v: string) => updateChart({ xAxisName: v })"
297
+ />
298
+ </label>
299
+ <label class="block">
300
+ <div class="mb-1">Y 轴名称</div>
301
+ <Input
302
+ size="small"
303
+ :value="element.chart?.yAxisName ?? ''"
304
+ :disabled="!isEditable"
305
+ @update:value="(v: string) => updateChart({ yAxisName: v })"
306
+ />
307
+ </label>
308
+ </div>
309
+ <div class="grid grid-cols-2 gap-2">
310
+ <label class="flex items-center gap-2">
311
+ <Checkbox
312
+ :checked="element.chart?.xAxisTickShow ?? true"
313
+ :disabled="!isEditable"
314
+ @update:checked="(v) => updateChart({ xAxisTickShow: v === true })"
315
+ />
316
+ <span>X 轴刻度线</span>
317
+ </label>
318
+ <label class="flex items-center gap-2">
319
+ <Checkbox
320
+ :checked="element.chart?.yAxisTickShow ?? true"
321
+ :disabled="!isEditable"
322
+ @update:checked="(v) => updateChart({ yAxisTickShow: v === true })"
323
+ />
324
+ <span>Y 轴刻度线</span>
325
+ </label>
326
+ </div>
327
+ <div class="grid grid-cols-2 gap-2">
328
+ <ConfigColorField
329
+ label="X 轴刻度线颜色"
330
+ :value="element.chart?.xAxisTickColor ?? '#94a3b8'"
331
+ :disabled="!isEditable"
332
+ @update:value="(v) => updateChart({ xAxisTickColor: v || '#94a3b8' })"
333
+ />
334
+ <ConfigColorField
335
+ label="Y 轴刻度线颜色"
336
+ :value="element.chart?.yAxisTickColor ?? '#94a3b8'"
337
+ :disabled="!isEditable"
338
+ @update:value="(v) => updateChart({ yAxisTickColor: v || '#94a3b8' })"
339
+ />
340
+ </div>
341
+ <div class="grid grid-cols-2 gap-2">
342
+ <label class="flex items-center gap-2">
343
+ <Checkbox
344
+ :checked="element.chart?.xAxisSplitLineShow ?? false"
345
+ :disabled="!isEditable"
346
+ @update:checked="(v) => updateChart({ xAxisSplitLineShow: v === true })"
347
+ />
348
+ <span>X 轴分割线</span>
349
+ </label>
350
+ <label class="flex items-center gap-2">
351
+ <Checkbox
352
+ :checked="element.chart?.yAxisSplitLineShow ?? true"
353
+ :disabled="!isEditable"
354
+ @update:checked="(v) => updateChart({ yAxisSplitLineShow: v === true })"
355
+ />
356
+ <span>Y 轴分割线</span>
357
+ </label>
358
+ </div>
359
+ <div class="grid grid-cols-2 gap-2">
360
+ <ConfigColorField
361
+ label="X 轴标签颜色"
362
+ :value="element.chart?.xAxisLabelColor ?? '#64748b'"
363
+ :disabled="!isEditable"
364
+ @update:value="(v) => updateChart({ xAxisLabelColor: v || '#64748b' })"
365
+ />
366
+ <ConfigColorField
367
+ label="Y 轴标签颜色"
368
+ :value="element.chart?.yAxisLabelColor ?? '#64748b'"
369
+ :disabled="!isEditable"
370
+ @update:value="(v) => updateChart({ yAxisLabelColor: v || '#64748b' })"
371
+ />
372
+ </div>
373
+ <div class="grid grid-cols-2 gap-2">
374
+ <label class="block">
375
+ <div class="mb-1">X 轴标签字号</div>
376
+ <InputNumber
377
+ size="small"
378
+ class="w-full"
379
+ :min="8"
380
+ :max="48"
381
+ :value="element.chart?.xAxisLabelFontSize ?? 10"
382
+ :disabled="!isEditable"
383
+ @update:value="(v) => {
384
+ const n = Number(v);
385
+ if (!Number.isNaN(n)) updateChart({ xAxisLabelFontSize: Math.max(8, Math.min(48, n)) });
386
+ }"
387
+ />
388
+ </label>
389
+ <label class="block">
390
+ <div class="mb-1">Y 轴标签字号</div>
391
+ <InputNumber
392
+ size="small"
393
+ class="w-full"
394
+ :min="8"
395
+ :max="48"
396
+ :value="element.chart?.yAxisLabelFontSize ?? 10"
397
+ :disabled="!isEditable"
398
+ @update:value="(v) => {
399
+ const n = Number(v);
400
+ if (!Number.isNaN(n)) updateChart({ yAxisLabelFontSize: Math.max(8, Math.min(48, n)) });
401
+ }"
402
+ />
403
+ </label>
404
+ </div>
405
+ </ConfigFieldGroup>
406
+
407
+ <ConfigFieldGroup
408
+ v-if="['bar', 'line', 'area', 'pie'].includes(selectedChartType)"
409
+ title="系列"
410
+ >
411
+ <label v-if="selectedChartType === 'bar'" class="block space-y-1">
412
+ <div>柱宽(px)</div>
413
+ <InputNumber
414
+ size="small"
415
+ class="w-full"
416
+ :min="1"
417
+ :value="element.chart?.barWidth ?? 24"
418
+ :disabled="!isEditable"
419
+ @update:value="(v) => {
420
+ const n = Number(v);
421
+ if (!Number.isNaN(n)) updateChart({ barWidth: Math.max(1, n) });
422
+ }"
423
+ />
424
+ </label>
425
+ <label
426
+ v-if="selectedChartType === 'line' || selectedChartType === 'area'"
427
+ class="flex items-center gap-2"
428
+ >
429
+ <Checkbox
430
+ :checked="element.chart?.smooth ?? true"
431
+ :disabled="!isEditable"
432
+ @update:checked="(v) => updateChart({ smooth: v === true })"
433
+ />
434
+ <span>平滑曲线</span>
435
+ </label>
436
+ <div v-if="selectedChartType === 'pie'" class="grid grid-cols-2 gap-2">
437
+ <label class="block space-y-1">
438
+ <div>内半径(%)</div>
439
+ <InputNumber
440
+ size="small"
441
+ class="w-full"
442
+ :min="0"
443
+ :max="99"
444
+ :value="element.chart?.pieInnerRadius ?? 30"
445
+ :disabled="!isEditable"
446
+ @update:value="(v) => {
447
+ const n = Number(v);
448
+ if (!Number.isNaN(n)) updateChart({ pieInnerRadius: Math.max(0, Math.min(99, n)) });
449
+ }"
450
+ />
451
+ </label>
452
+ <label class="block space-y-1">
453
+ <div>外半径(%)</div>
454
+ <InputNumber
455
+ size="small"
456
+ class="w-full"
457
+ :min="1"
458
+ :max="100"
459
+ :value="element.chart?.pieOuterRadius ?? 65"
460
+ :disabled="!isEditable"
461
+ @update:value="(v) => {
462
+ const n = Number(v);
463
+ if (!Number.isNaN(n)) updateChart({ pieOuterRadius: Math.max(1, Math.min(100, n)) });
464
+ }"
465
+ />
466
+ </label>
467
+ </div>
468
+ </ConfigFieldGroup>
469
+ </ConfigSection>
470
+
471
+ <ConfigSection
472
+ title="图表配置 / 高级"
473
+ :open="advancedOpen"
474
+ :force-open="forceOpen"
475
+ @update:open="emit('update:advancedOpen', $event)"
476
+ >
477
+ <ConfigFieldGroup title="常用项(表单)">
478
+ <Collapse ghost size="small">
479
+ <Collapse.Panel key="layout" header="布局与坐标">
480
+ <div class="grid grid-cols-2 gap-2">
481
+ <label class="block space-y-1">
482
+ <div class="text-[11px]">网格左距(grid.left)</div>
483
+ <InputNumber
484
+ size="small"
485
+ class="w-full"
486
+ :value="Number(chartOption.grid?.left ?? 28)"
487
+ :disabled="!isEditable"
488
+ @update:value="(v) => updateOptionForm({ grid: { left: Number(v) || 0 } })"
489
+ />
490
+ </label>
491
+ <label class="block space-y-1">
492
+ <div class="text-[11px]">网格右距(grid.right)</div>
493
+ <InputNumber
494
+ size="small"
495
+ class="w-full"
496
+ :value="Number(chartOption.grid?.right ?? 10)"
497
+ :disabled="!isEditable"
498
+ @update:value="(v) => updateOptionForm({ grid: { right: Number(v) || 0 } })"
499
+ />
500
+ </label>
501
+ <label class="block space-y-1">
502
+ <div class="text-[11px]">图例位置(legend.top)</div>
503
+ <Select
504
+ size="small"
505
+ class="w-full"
506
+ :value="String(chartOption.legend?.top ?? 'top')"
507
+ :disabled="!isEditable"
508
+ @update:value="(v) => updateOptionForm({ legend: { top: v } })"
509
+ >
510
+ <Select.Option value="top">顶部</Select.Option>
511
+ <Select.Option value="bottom">底部</Select.Option>
512
+ <Select.Option value="left">左侧</Select.Option>
513
+ <Select.Option value="right">右侧</Select.Option>
514
+ </Select>
515
+ </label>
516
+ <label class="block space-y-1">
517
+ <div class="text-[11px]">图例排列(legend.orient)</div>
518
+ <Select
519
+ size="small"
520
+ class="w-full"
521
+ :value="String(chartOption.legend?.orient ?? 'horizontal')"
522
+ :disabled="!isEditable"
523
+ @update:value="(v) => updateOptionForm({ legend: { orient: v } })"
524
+ >
525
+ <Select.Option value="horizontal">横向</Select.Option>
526
+ <Select.Option value="vertical">纵向</Select.Option>
527
+ </Select>
528
+ </label>
529
+ </div>
530
+ </Collapse.Panel>
531
+ <Collapse.Panel key="highfreq" header="高频项">
532
+ <div class="space-y-2">
533
+ <label class="flex items-center gap-2">
534
+ <Checkbox
535
+ :checked="Boolean(chartOption.legend?.show ?? true)"
536
+ :disabled="!isEditable"
537
+ @update:checked="(v) => updateOptionForm({ legend: { show: v === true } })"
538
+ />
539
+ <span>显示图例</span>
540
+ </label>
541
+ <label class="flex items-center gap-2">
542
+ <Checkbox
543
+ :checked="Boolean(chartOption.grid?.containLabel ?? false)"
544
+ :disabled="!isEditable"
545
+ @update:checked="(v) => updateOptionForm({ grid: { containLabel: v === true } })"
546
+ />
547
+ <span>网格包含标签</span>
548
+ </label>
549
+ <label class="flex items-center gap-2">
550
+ <Checkbox
551
+ :checked="hasDataZoomType('inside')"
552
+ :disabled="!isEditable"
553
+ @update:checked="(v) => toggleDataZoom('inside', v === true)"
554
+ />
555
+ <span>内置缩放</span>
556
+ </label>
557
+ <label class="flex items-center gap-2">
558
+ <Checkbox
559
+ :checked="hasDataZoomType('slider')"
560
+ :disabled="!isEditable"
561
+ @update:checked="(v) => toggleDataZoom('slider', v === true)"
562
+ />
563
+ <span>滑块缩放</span>
564
+ </label>
565
+ <label class="flex items-center gap-2">
566
+ <Checkbox
567
+ :checked="Boolean(chartOption.animation ?? false)"
568
+ :disabled="!isEditable"
569
+ @update:checked="(v) => updateOptionForm({ animation: v === true })"
570
+ />
571
+ <span>开启动画</span>
572
+ </label>
573
+ <label class="block space-y-1">
574
+ <div class="text-[11px]">动画时长(ms)</div>
575
+ <InputNumber
576
+ size="small"
577
+ class="w-full"
578
+ :min="0"
579
+ :value="Number(chartOption.animationDuration ?? 300)"
580
+ :disabled="!isEditable"
581
+ @update:value="(v) => updateOptionForm({ animationDuration: Math.max(0, Number(v) || 0) })"
582
+ />
583
+ </label>
584
+ </div>
585
+ </Collapse.Panel>
586
+ <Collapse.Panel key="axisPointer" header="轴指示器与对齐">
587
+ <div class="space-y-2">
588
+ <label class="flex items-center gap-2">
589
+ <Checkbox
590
+ :checked="Boolean(chartOption.axisPointer?.show ?? false)"
591
+ :disabled="!isEditable"
592
+ @update:checked="(v) => updateOptionForm({ axisPointer: { show: v === true } })"
593
+ />
594
+ <span>显示轴指示器</span>
595
+ </label>
596
+ <label class="block space-y-1">
597
+ <div class="text-[11px]">轴指示器类型</div>
598
+ <Select
599
+ size="small"
600
+ class="w-full"
601
+ :value="String(chartOption.axisPointer?.type ?? 'line')"
602
+ :disabled="!isEditable"
603
+ @update:value="(v) => updateOptionForm({ axisPointer: { type: v } })"
604
+ >
605
+ <Select.Option value="line">线</Select.Option>
606
+ <Select.Option value="shadow">阴影</Select.Option>
607
+ <Select.Option value="cross">十字</Select.Option>
608
+ </Select>
609
+ </label>
610
+ </div>
611
+ </Collapse.Panel>
612
+ </Collapse>
613
+ </ConfigFieldGroup>
614
+
615
+ <ConfigFieldGroup title="JSON 高级模式">
616
+ <label class="flex items-center gap-2">
617
+ <Checkbox
618
+ v-model:checked="isAdvancedOptionMode"
619
+ :disabled="!isEditable"
620
+ />
621
+ <span>开启高级模式(直接编辑图表 JSON 配置)</span>
622
+ <ConfigHintIcon label="图表 JSON 高级模式">
623
+ 基础配置会先生成图表配置,高级模式会在此基础上覆盖(深度合并)。
624
+ </ConfigHintIcon>
625
+ </label>
626
+ <template v-if="isAdvancedOptionMode">
627
+ <Textarea
628
+ :value="optionJsonText"
629
+ :disabled="!isEditable"
630
+ :rows="8"
631
+ spellcheck="false"
632
+ class="font-mono text-[11px]"
633
+ @update:value="onOptionJsonChange"
634
+ />
635
+ <div
636
+ v-if="optionJsonError"
637
+ class="rounded border border-red-400/40 bg-red-50 px-2 py-1.5 text-[11px] text-red-700"
638
+ >
639
+ {{ optionJsonError }}
640
+ </div>
641
+ <div
642
+ v-else
643
+ class="rounded border border-emerald-500/30 bg-emerald-500/10 px-2 py-1.5 text-[11px] text-emerald-700"
644
+ >
645
+ JSON 有效,已实时应用到当前图表。
646
+ </div>
647
+ </template>
648
+ </ConfigFieldGroup>
649
+ </ConfigSection>
650
+ </template>