@arronqzy/vue-view 0.1.17 → 0.1.19
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 +4 -4
- package/src/panel/VueViewPanel.vue +26 -2
- package/src/panel/components/ElementsLayer.vue +3 -0
- package/src/panel/components/MoveableLayer.vue +170 -132
- package/src/panel/components/config/PanelConfigAudioSection.vue +4 -2
- package/src/panel/components/config/PanelConfigChartSection.vue +6 -2
- package/src/panel/components/config/PanelConfigImageSection.vue +8 -14
- package/src/panel/components/config/PanelConfigMultiSelect.vue +20 -7
- package/src/panel/components/config/PanelConfigScene3dSection.vue +68 -1
- package/src/panel/components/config/PanelConfigStyleSections.vue +8 -5
- package/src/panel/components/config/PanelConfigTextSection.vue +15 -0
- package/src/panel/components/config/PanelConfigVideoSection.vue +4 -2
- package/src/panel/components/elementsLayerNodes.ts +2 -1
- package/src/panel/hooks/usePanelElements.ts +20 -11
- package/src/panel/utils/background-image-style.ts +39 -0
- package/src/panel/utils/canvas-interaction-busy.ts +26 -0
- package/src/panel/utils/materializePanelElements.ts +96 -0
- package/src/panel/utils/panelStateIO.ts +22 -2
- package/src/panel/utils/scope-template-chart.ts +12 -0
- package/src/panel/utils/scope-template.ts +29 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arronqzy/vue-view",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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/
|
|
27
|
-
"@arronqzy/
|
|
26
|
+
"@arronqzy/i18n": "0.1.13",
|
|
27
|
+
"@arronqzy/blueprint-dsl": "1.0.15",
|
|
28
28
|
"@arronqzy/view-scene3d": "0.1.5",
|
|
29
29
|
"@arronqzy/view-table": "0.1.8",
|
|
30
30
|
"@arronqzy/rx-store": "1.0.8",
|
|
31
|
-
"@arronqzy/vue-blueprint": "0.1.
|
|
31
|
+
"@arronqzy/vue-blueprint": "0.1.14",
|
|
32
32
|
"@arronqzy/vue-rx-store": "0.1.6"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
@@ -61,9 +61,12 @@ import WorkspaceConfigSidebar from "./components/WorkspaceConfigSidebar.vue";
|
|
|
61
61
|
import type { WorkspaceConfigFocus } from "./components/WorkspaceConfigSidebar.vue";
|
|
62
62
|
import {
|
|
63
63
|
clearViewElementScopes,
|
|
64
|
+
getViewElementScope,
|
|
64
65
|
setViewElementScopes,
|
|
65
66
|
useViewElementScope,
|
|
67
|
+
useViewScopeStoreVersion,
|
|
66
68
|
} from "./scope/view-scope-store";
|
|
69
|
+
import { resolvePanelElementScope } from "./utils/scope-template";
|
|
67
70
|
import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
|
|
68
71
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
69
72
|
import { getPanelMessages } from "./constants/messages";
|
|
@@ -191,6 +194,13 @@ const hasUnlockedSelection = computed(() =>
|
|
|
191
194
|
const viewElementScope = useViewElementScope(
|
|
192
195
|
computed(() => selectedElement.value?.id ?? null)
|
|
193
196
|
);
|
|
197
|
+
const scopeStoreVersion = useViewScopeStoreVersion();
|
|
198
|
+
const scopedCanvasElements = computed(() => {
|
|
199
|
+
void scopeStoreVersion.value;
|
|
200
|
+
return elements.value.map((el) =>
|
|
201
|
+
resolvePanelElementScope(el, getViewElementScope(el.id))
|
|
202
|
+
);
|
|
203
|
+
});
|
|
194
204
|
|
|
195
205
|
function excludeSelectedNode(nodeId: string) {
|
|
196
206
|
const nextIds = selectedIds.value.filter((id) => id !== nodeId);
|
|
@@ -409,11 +419,25 @@ const workspaceProjects = useWorkspaceProjects({
|
|
|
409
419
|
nameSpace: () => props.nameSpace,
|
|
410
420
|
});
|
|
411
421
|
|
|
422
|
+
const blueprintLifecycleToken = computed(() => {
|
|
423
|
+
const doc = blueprintGraph.value.document;
|
|
424
|
+
const nodes = doc.nodes;
|
|
425
|
+
const edges = doc.edges;
|
|
426
|
+
let sig = `${nodes.length}:${edges.length}`;
|
|
427
|
+
for (const node of nodes) {
|
|
428
|
+
sig += `|${node.id}:${resolveRunnableNodeType(node)}:${node.lifecyclePhase ?? ""}`;
|
|
429
|
+
}
|
|
430
|
+
for (const edge of edges) {
|
|
431
|
+
sig += `>${edge.id}:${edge.source}:${edge.target}`;
|
|
432
|
+
}
|
|
433
|
+
return sig;
|
|
434
|
+
});
|
|
435
|
+
|
|
412
436
|
const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
|
|
413
437
|
graph: blueprintGraph,
|
|
414
438
|
active: blueprintOpen,
|
|
415
439
|
bootKey: computed(() => workspaceProjects.activeProjectId.value ?? undefined),
|
|
416
|
-
onUpdated: computed(() => `${activeLayerId.value}|${
|
|
440
|
+
onUpdated: computed(() => `${activeLayerId.value}|${blueprintLifecycleToken.value}`),
|
|
417
441
|
resolveLibraryBlueprint,
|
|
418
442
|
libraryNameById: blueprintLibraryNameById,
|
|
419
443
|
rootLibraryBlueprintId: activeBlueprintLibraryId,
|
|
@@ -817,7 +841,7 @@ onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
|
817
841
|
@scroll-change="onViewportScrollChange"
|
|
818
842
|
>
|
|
819
843
|
<ElementsLayer
|
|
820
|
-
:elements="
|
|
844
|
+
:elements="scopedCanvasElements"
|
|
821
845
|
:all-elements="allElements"
|
|
822
846
|
:selected-ids="selectedIds"
|
|
823
847
|
:update-element="updateElement"
|
|
@@ -185,6 +185,7 @@ function hasViewEvent(el: PanelElement) {
|
|
|
185
185
|
<div
|
|
186
186
|
v-for="el in sortedElements"
|
|
187
187
|
:key="el.id"
|
|
188
|
+
v-memo="[el, selectedIds.includes(el.id), previewMode, layerLocked, hasViewEvent(el), previewLayoutKey]"
|
|
188
189
|
:class="[
|
|
189
190
|
'absolute select-none',
|
|
190
191
|
previewMode ? '' : 'rv-selectable',
|
|
@@ -202,6 +203,8 @@ function hasViewEvent(el: PanelElement) {
|
|
|
202
203
|
transformOrigin: 'center center',
|
|
203
204
|
boxSizing: 'border-box',
|
|
204
205
|
overflow: el.materialType === 'viewport' ? 'hidden' : undefined,
|
|
206
|
+
contentVisibility: 'auto',
|
|
207
|
+
containIntrinsicSize: `${Math.max(1, el.width)}px ${Math.max(1, el.height)}px`,
|
|
205
208
|
...getNodeVisualStyle(el),
|
|
206
209
|
}"
|
|
207
210
|
@mousedown="onSelect(el.id, $event)"
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import Moveable from "moveable";
|
|
12
12
|
import type { PanelElement } from "../types";
|
|
13
13
|
import { uniformViewportZoom } from "../viewportZoom";
|
|
14
|
-
import {
|
|
14
|
+
import { setCanvasInteractionBusy } from "../utils/canvas-interaction-busy";
|
|
15
15
|
|
|
16
16
|
const { t, locale } = useI18n();
|
|
17
17
|
|
|
@@ -444,6 +444,7 @@ function createMoveable() {
|
|
|
444
444
|
const point = readClientPoint(e);
|
|
445
445
|
e.datas.__startClientX = point?.x ?? null;
|
|
446
446
|
e.datas.__startClientY = point?.y ?? null;
|
|
447
|
+
setCanvasInteractionBusy(true);
|
|
447
448
|
});
|
|
448
449
|
|
|
449
450
|
moveable.on("drag", (e: any) => {
|
|
@@ -479,6 +480,7 @@ function createMoveable() {
|
|
|
479
480
|
ev.datas.__startClientX = point?.x ?? null;
|
|
480
481
|
ev.datas.__startClientY = point?.y ?? null;
|
|
481
482
|
});
|
|
483
|
+
setCanvasInteractionBusy(true);
|
|
482
484
|
});
|
|
483
485
|
|
|
484
486
|
moveable.on("dragGroup", (e: any) => {
|
|
@@ -513,6 +515,7 @@ function createMoveable() {
|
|
|
513
515
|
e.datas.__startY = data.y;
|
|
514
516
|
e.datas.__startClientX = e.inputEvent?.clientX ?? null;
|
|
515
517
|
e.datas.__startClientY = e.inputEvent?.clientY ?? null;
|
|
518
|
+
setCanvasInteractionBusy(true);
|
|
516
519
|
});
|
|
517
520
|
|
|
518
521
|
moveable.on("resize", (e: any) => {
|
|
@@ -526,7 +529,6 @@ function createMoveable() {
|
|
|
526
529
|
const ty = toCanvasDeltaY(e.drag.beforeTranslate?.[1] ?? 0);
|
|
527
530
|
e.target.style.left = `${sx + tx}px`;
|
|
528
531
|
e.target.style.top = `${sy + ty}px`;
|
|
529
|
-
notifyPreviewLayoutChanged();
|
|
530
532
|
});
|
|
531
533
|
|
|
532
534
|
moveable.on("resizeGroupStart", (e: any) => {
|
|
@@ -543,6 +545,7 @@ function createMoveable() {
|
|
|
543
545
|
ev.datas.__startClientX = input?.clientX ?? null;
|
|
544
546
|
ev.datas.__startClientY = input?.clientY ?? null;
|
|
545
547
|
});
|
|
548
|
+
setCanvasInteractionBusy(true);
|
|
546
549
|
});
|
|
547
550
|
|
|
548
551
|
moveable.on("resizeGroup", (e: any) => {
|
|
@@ -557,7 +560,6 @@ function createMoveable() {
|
|
|
557
560
|
ev.target.style.left = `${sx + tx}px`;
|
|
558
561
|
ev.target.style.top = `${sy + ty}px`;
|
|
559
562
|
});
|
|
560
|
-
notifyPreviewLayoutChanged();
|
|
561
563
|
});
|
|
562
564
|
|
|
563
565
|
moveable.on("rotateStart", (e: any) => {
|
|
@@ -566,6 +568,7 @@ function createMoveable() {
|
|
|
566
568
|
const data = props.elementsById.get(id);
|
|
567
569
|
if (!data) return;
|
|
568
570
|
e.set(data.rotate ?? 0);
|
|
571
|
+
setCanvasInteractionBusy(true);
|
|
569
572
|
});
|
|
570
573
|
|
|
571
574
|
moveable.on("rotate", (e: any) => {
|
|
@@ -573,6 +576,17 @@ function createMoveable() {
|
|
|
573
576
|
e.target.style.transform = `rotate(${e.beforeRotate}deg)`;
|
|
574
577
|
});
|
|
575
578
|
|
|
579
|
+
moveable.on("rotateGroupStart", (e: any) => {
|
|
580
|
+
e.events.forEach((ev: any) => {
|
|
581
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
582
|
+
if (!id) return;
|
|
583
|
+
const data = props.elementsById.get(id);
|
|
584
|
+
if (!data) return;
|
|
585
|
+
ev.set(data.rotate ?? 0);
|
|
586
|
+
});
|
|
587
|
+
setCanvasInteractionBusy(true);
|
|
588
|
+
});
|
|
589
|
+
|
|
576
590
|
moveable.on("rotateGroup", (e: any) => {
|
|
577
591
|
e.events.forEach((ev: any) => {
|
|
578
592
|
if (!ev?.target?.style) return;
|
|
@@ -581,175 +595,199 @@ function createMoveable() {
|
|
|
581
595
|
});
|
|
582
596
|
|
|
583
597
|
moveable.on("dragEnd", (e: any) => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const data = props.elementsById.get(id);
|
|
587
|
-
if (!data) return;
|
|
588
|
-
const sx = e.datas.__startX ?? data.x;
|
|
589
|
-
const sy = e.datas.__startY ?? data.y;
|
|
590
|
-
const point = readClientPoint(e);
|
|
591
|
-
const hasClient =
|
|
592
|
-
typeof e.datas.__startClientX === "number" &&
|
|
593
|
-
typeof e.datas.__startClientY === "number" &&
|
|
594
|
-
!!point;
|
|
595
|
-
const tx = hasClient
|
|
596
|
-
? toCanvasDeltaX(point.x - e.datas.__startClientX)
|
|
597
|
-
: toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
598
|
-
const ty = hasClient
|
|
599
|
-
? toCanvasDeltaY(point.y - e.datas.__startClientY)
|
|
600
|
-
: toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
601
|
-
const nextX = sx + tx;
|
|
602
|
-
const nextY = sy + ty;
|
|
603
|
-
const size = readTargetCanvasSize(e.target as HTMLElement | null, {
|
|
604
|
-
width: data.width,
|
|
605
|
-
height: data.height,
|
|
606
|
-
});
|
|
607
|
-
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
608
|
-
props.updateElement(id, { x: nextX, y: nextY, ...patch });
|
|
609
|
-
if (data.materialType === "grid") {
|
|
610
|
-
const dx = nextX - data.x;
|
|
611
|
-
const dy = nextY - data.y;
|
|
612
|
-
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
613
|
-
const children = getGridDirectChildren(id);
|
|
614
|
-
const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
615
|
-
children.forEach((child) => {
|
|
616
|
-
props.updateElement(
|
|
617
|
-
child.id,
|
|
618
|
-
{ x: child.x + dx, y: child.y + dy },
|
|
619
|
-
{ batchId, meta: { type: "node.group-drag" } }
|
|
620
|
-
);
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
scheduleViewportSync();
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
moveable.on("dragGroupEnd", (e: any) => {
|
|
628
|
-
const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
629
|
-
const selectedSet = new Set(
|
|
630
|
-
e.events
|
|
631
|
-
.map((ev: any) => (ev.target ? getId(ev.target) : null))
|
|
632
|
-
.filter((id: string | null): id is string => !!id)
|
|
633
|
-
);
|
|
634
|
-
const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
|
|
635
|
-
e.events.forEach((ev: any) => {
|
|
636
|
-
const id = ev.target ? getId(ev.target) : null;
|
|
598
|
+
try {
|
|
599
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
637
600
|
if (!id) return;
|
|
638
601
|
const data = props.elementsById.get(id);
|
|
639
602
|
if (!data) return;
|
|
640
|
-
const sx =
|
|
641
|
-
const sy =
|
|
642
|
-
const point = readClientPoint(
|
|
603
|
+
const sx = e.datas.__startX ?? data.x;
|
|
604
|
+
const sy = e.datas.__startY ?? data.y;
|
|
605
|
+
const point = readClientPoint(e);
|
|
643
606
|
const hasClient =
|
|
644
|
-
typeof
|
|
645
|
-
typeof
|
|
607
|
+
typeof e.datas.__startClientX === "number" &&
|
|
608
|
+
typeof e.datas.__startClientY === "number" &&
|
|
646
609
|
!!point;
|
|
647
610
|
const tx = hasClient
|
|
648
|
-
? toCanvasDeltaX(point.x -
|
|
649
|
-
: toCanvasDeltaX(
|
|
611
|
+
? toCanvasDeltaX(point.x - e.datas.__startClientX)
|
|
612
|
+
: toCanvasDeltaX(e.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
650
613
|
const ty = hasClient
|
|
651
|
-
? toCanvasDeltaY(point.y -
|
|
652
|
-
: toCanvasDeltaY(
|
|
614
|
+
? toCanvasDeltaY(point.y - e.datas.__startClientY)
|
|
615
|
+
: toCanvasDeltaY(e.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
653
616
|
const nextX = sx + tx;
|
|
654
617
|
const nextY = sy + ty;
|
|
655
|
-
const size = readTargetCanvasSize(
|
|
618
|
+
const size = readTargetCanvasSize(e.target as HTMLElement | null, {
|
|
656
619
|
width: data.width,
|
|
657
620
|
height: data.height,
|
|
658
621
|
});
|
|
659
622
|
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
660
|
-
props.updateElement(
|
|
661
|
-
id,
|
|
662
|
-
{ x: nextX, y: nextY, ...patch },
|
|
663
|
-
{ batchId, meta: { type: "node.group-drag" } }
|
|
664
|
-
);
|
|
623
|
+
props.updateElement(id, { x: nextX, y: nextY, ...patch });
|
|
665
624
|
if (data.materialType === "grid") {
|
|
666
625
|
const dx = nextX - data.x;
|
|
667
626
|
const dy = nextY - data.y;
|
|
668
627
|
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
669
|
-
|
|
628
|
+
const children = getGridDirectChildren(id);
|
|
629
|
+
const batchId = `move-grid-children-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
630
|
+
children.forEach((child) => {
|
|
631
|
+
props.updateElement(
|
|
632
|
+
child.id,
|
|
633
|
+
{ x: child.x + dx, y: child.y + dy },
|
|
634
|
+
{ batchId, meta: { type: "node.group-drag" } }
|
|
635
|
+
);
|
|
636
|
+
});
|
|
670
637
|
}
|
|
671
638
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
639
|
+
scheduleViewportSync();
|
|
640
|
+
} finally {
|
|
641
|
+
setCanvasInteractionBusy(false);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
moveable.on("dragGroupEnd", (e: any) => {
|
|
646
|
+
try {
|
|
647
|
+
const batchId = `move-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
648
|
+
const selectedSet = new Set(
|
|
649
|
+
e.events
|
|
650
|
+
.map((ev: any) => (ev.target ? getId(ev.target) : null))
|
|
651
|
+
.filter((id: string | null): id is string => !!id)
|
|
652
|
+
);
|
|
653
|
+
const gridDeltaMap = new Map<string, { dx: number; dy: number }>();
|
|
654
|
+
e.events.forEach((ev: any) => {
|
|
655
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
656
|
+
if (!id) return;
|
|
657
|
+
const data = props.elementsById.get(id);
|
|
658
|
+
if (!data) return;
|
|
659
|
+
const sx = ev.datas.__startX ?? data.x;
|
|
660
|
+
const sy = ev.datas.__startY ?? data.y;
|
|
661
|
+
const point = readClientPoint(ev) ?? readClientPoint(e);
|
|
662
|
+
const hasClient =
|
|
663
|
+
typeof ev.datas.__startClientX === "number" &&
|
|
664
|
+
typeof ev.datas.__startClientY === "number" &&
|
|
665
|
+
!!point;
|
|
666
|
+
const tx = hasClient
|
|
667
|
+
? toCanvasDeltaX(point.x - ev.datas.__startClientX)
|
|
668
|
+
: toCanvasDeltaX(ev.lastEvent?.beforeTranslate?.[0] ?? 0);
|
|
669
|
+
const ty = hasClient
|
|
670
|
+
? toCanvasDeltaY(point.y - ev.datas.__startClientY)
|
|
671
|
+
: toCanvasDeltaY(ev.lastEvent?.beforeTranslate?.[1] ?? 0);
|
|
672
|
+
const nextX = sx + tx;
|
|
673
|
+
const nextY = sy + ty;
|
|
674
|
+
const size = readTargetCanvasSize(ev.target as HTMLElement | null, {
|
|
675
|
+
width: data.width,
|
|
676
|
+
height: data.height,
|
|
677
|
+
});
|
|
678
|
+
const patch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
677
679
|
props.updateElement(
|
|
678
|
-
|
|
679
|
-
{ x:
|
|
680
|
+
id,
|
|
681
|
+
{ x: nextX, y: nextY, ...patch },
|
|
680
682
|
{ batchId, meta: { type: "node.group-drag" } }
|
|
681
683
|
);
|
|
684
|
+
if (data.materialType === "grid") {
|
|
685
|
+
const dx = nextX - data.x;
|
|
686
|
+
const dy = nextY - data.y;
|
|
687
|
+
if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
|
|
688
|
+
gridDeltaMap.set(id, { dx, dy });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
682
691
|
});
|
|
692
|
+
for (const [gridId, delta] of gridDeltaMap.entries()) {
|
|
693
|
+
const children = getGridDirectChildren(gridId);
|
|
694
|
+
children.forEach((child) => {
|
|
695
|
+
if (selectedSet.has(child.id)) return;
|
|
696
|
+
props.updateElement(
|
|
697
|
+
child.id,
|
|
698
|
+
{ x: child.x + delta.dx, y: child.y + delta.dy },
|
|
699
|
+
{ batchId, meta: { type: "node.group-drag" } }
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
scheduleViewportSync();
|
|
704
|
+
} finally {
|
|
705
|
+
setCanvasInteractionBusy(false);
|
|
683
706
|
}
|
|
684
|
-
scheduleViewportSync();
|
|
685
707
|
});
|
|
686
708
|
|
|
687
709
|
moveable.on("resizeEnd", (e: any) => {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const data = props.elementsById.get(id);
|
|
691
|
-
if (!data) return;
|
|
692
|
-
const width = e.lastEvent?.width ?? data.width;
|
|
693
|
-
const height = e.lastEvent?.height ?? data.height;
|
|
694
|
-
const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
695
|
-
const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
696
|
-
const sx = e.datas.__startX ?? data.x;
|
|
697
|
-
const sy = e.datas.__startY ?? data.y;
|
|
698
|
-
const nextX = sx + tx;
|
|
699
|
-
const nextY = sy + ty;
|
|
700
|
-
const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
|
|
701
|
-
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
702
|
-
props.updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
|
|
703
|
-
scheduleViewportSync();
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
moveable.on("resizeGroupEnd", (e: any) => {
|
|
707
|
-
const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
708
|
-
e.events.forEach((ev: any) => {
|
|
709
|
-
const id = ev.target ? getId(ev.target) : null;
|
|
710
|
+
try {
|
|
711
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
710
712
|
if (!id) return;
|
|
711
713
|
const data = props.elementsById.get(id);
|
|
712
714
|
if (!data) return;
|
|
713
|
-
const width =
|
|
714
|
-
const height =
|
|
715
|
-
const tx = toCanvasDeltaX(
|
|
716
|
-
const ty = toCanvasDeltaY(
|
|
717
|
-
const sx =
|
|
718
|
-
const sy =
|
|
715
|
+
const width = e.lastEvent?.width ?? data.width;
|
|
716
|
+
const height = e.lastEvent?.height ?? data.height;
|
|
717
|
+
const tx = toCanvasDeltaX(e.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
718
|
+
const ty = toCanvasDeltaY(e.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
719
|
+
const sx = e.datas.__startX ?? data.x;
|
|
720
|
+
const sy = e.datas.__startY ?? data.y;
|
|
719
721
|
const nextX = sx + tx;
|
|
720
722
|
const nextY = sy + ty;
|
|
721
|
-
const size = readTargetCanvasSize(
|
|
723
|
+
const size = readTargetCanvasSize(e.target as HTMLElement | null, { width, height });
|
|
722
724
|
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
723
|
-
props.updateElement(
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
});
|
|
729
|
-
scheduleViewportSync();
|
|
725
|
+
props.updateElement(id, { width, height, x: nextX, y: nextY, ...snapPatch });
|
|
726
|
+
scheduleViewportSync();
|
|
727
|
+
} finally {
|
|
728
|
+
setCanvasInteractionBusy(false);
|
|
729
|
+
}
|
|
730
730
|
});
|
|
731
731
|
|
|
732
|
-
moveable.on("
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
732
|
+
moveable.on("resizeGroupEnd", (e: any) => {
|
|
733
|
+
try {
|
|
734
|
+
const batchId = `resize-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
735
|
+
e.events.forEach((ev: any) => {
|
|
736
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
737
|
+
if (!id) return;
|
|
738
|
+
const data = props.elementsById.get(id);
|
|
739
|
+
if (!data) return;
|
|
740
|
+
const width = ev.lastEvent?.width ?? data.width;
|
|
741
|
+
const height = ev.lastEvent?.height ?? data.height;
|
|
742
|
+
const tx = toCanvasDeltaX(ev.lastEvent?.drag?.beforeTranslate?.[0] ?? 0);
|
|
743
|
+
const ty = toCanvasDeltaY(ev.lastEvent?.drag?.beforeTranslate?.[1] ?? 0);
|
|
744
|
+
const sx = ev.datas.__startX ?? data.x;
|
|
745
|
+
const sy = ev.datas.__startY ?? data.y;
|
|
746
|
+
const nextX = sx + tx;
|
|
747
|
+
const nextY = sy + ty;
|
|
748
|
+
const size = readTargetCanvasSize(ev.target as HTMLElement | null, { width, height });
|
|
749
|
+
const snapPatch = getSnapPatch(id, nextX, nextY, size.width, size.height);
|
|
750
|
+
props.updateElement(
|
|
751
|
+
id,
|
|
752
|
+
{ width, height, x: nextX, y: nextY, ...snapPatch },
|
|
753
|
+
{ batchId, meta: { type: "node.group-resize" } }
|
|
754
|
+
);
|
|
755
|
+
});
|
|
756
|
+
scheduleViewportSync();
|
|
757
|
+
} finally {
|
|
758
|
+
setCanvasInteractionBusy(false);
|
|
759
|
+
}
|
|
740
760
|
});
|
|
741
761
|
|
|
742
|
-
moveable.on("
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const id = ev.target ? getId(ev.target) : null;
|
|
762
|
+
moveable.on("rotateEnd", (e: any) => {
|
|
763
|
+
try {
|
|
764
|
+
const id = resolveSingleEventId(e.target as HTMLElement | null);
|
|
746
765
|
if (!id) return;
|
|
747
766
|
const data = props.elementsById.get(id);
|
|
748
767
|
if (!data) return;
|
|
749
|
-
const rotate =
|
|
750
|
-
props.updateElement(id, { rotate }
|
|
751
|
-
|
|
752
|
-
|
|
768
|
+
const rotate = e.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
769
|
+
props.updateElement(id, { rotate });
|
|
770
|
+
scheduleViewportSync();
|
|
771
|
+
} finally {
|
|
772
|
+
setCanvasInteractionBusy(false);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
moveable.on("rotateGroupEnd", (e: any) => {
|
|
777
|
+
try {
|
|
778
|
+
const batchId = `rotate-group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
779
|
+
e.events.forEach((ev: any) => {
|
|
780
|
+
const id = ev.target ? getId(ev.target) : null;
|
|
781
|
+
if (!id) return;
|
|
782
|
+
const data = props.elementsById.get(id);
|
|
783
|
+
if (!data) return;
|
|
784
|
+
const rotate = ev.lastEvent?.beforeRotate ?? data.rotate ?? 0;
|
|
785
|
+
props.updateElement(id, { rotate }, { batchId, meta: { type: "node.group-rotate" } });
|
|
786
|
+
});
|
|
787
|
+
scheduleViewportSync();
|
|
788
|
+
} finally {
|
|
789
|
+
setCanvasInteractionBusy(false);
|
|
790
|
+
}
|
|
753
791
|
});
|
|
754
792
|
|
|
755
793
|
moveableRef.value = moveable;
|
|
@@ -140,18 +140,20 @@ function onPosterFileChange(e: Event) {
|
|
|
140
140
|
@update:open="emit('update:open', $event)"
|
|
141
141
|
>
|
|
142
142
|
<ConfigFieldGroup :title="t('panel.config.groupAudioSource')">
|
|
143
|
-
<label class="block space-y-1">
|
|
143
|
+
<label class="block space-y-1" data-config-field="audioRemoteUrl">
|
|
144
144
|
<div>{{ t("panel.config.audioUrl") }}</div>
|
|
145
145
|
<Input
|
|
146
146
|
size="small"
|
|
147
|
+
class="font-mono text-[11px]"
|
|
147
148
|
:value="element.audioRemoteUrl ?? ''"
|
|
148
149
|
:disabled="!isEditable"
|
|
149
|
-
placeholder="
|
|
150
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
150
151
|
@update:value="(v: string) => patch({
|
|
151
152
|
audioRemoteUrl: v || undefined,
|
|
152
153
|
audioSrc: v || element.audioSrc,
|
|
153
154
|
})"
|
|
154
155
|
/>
|
|
156
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
155
157
|
</label>
|
|
156
158
|
<div class="flex items-center gap-2">
|
|
157
159
|
<label
|
|
@@ -145,23 +145,27 @@ function onOptionJsonChange(v: string) {
|
|
|
145
145
|
/>
|
|
146
146
|
</ConfigFieldGroup>
|
|
147
147
|
<ConfigFieldGroup :title="t('panel.config.groupData')">
|
|
148
|
-
<label class="block space-y-1">
|
|
148
|
+
<label class="block space-y-1" data-config-field="chart.labelsText">
|
|
149
149
|
<div>{{ t("panel.config.labelsCsv") }}</div>
|
|
150
150
|
<Input
|
|
151
151
|
size="small"
|
|
152
152
|
:value="getChartLabelsDisplayText(element.chart)"
|
|
153
153
|
:disabled="!isEditable"
|
|
154
|
+
:placeholder="t('panel.config.labelsCsvPlaceholder')"
|
|
154
155
|
@update:value="(v: string) => updateChart({ labelsText: v })"
|
|
155
156
|
/>
|
|
157
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
|
|
156
158
|
</label>
|
|
157
|
-
<label class="block space-y-1">
|
|
159
|
+
<label class="block space-y-1" data-config-field="chart.valuesText">
|
|
158
160
|
<div>{{ t("panel.config.valuesCsv") }}</div>
|
|
159
161
|
<Input
|
|
160
162
|
size="small"
|
|
161
163
|
:value="getChartValuesDisplayText(element.chart)"
|
|
162
164
|
:disabled="!isEditable"
|
|
165
|
+
:placeholder="t('panel.config.valuesCsvPlaceholder')"
|
|
163
166
|
@update:value="(v: string) => updateChart({ valuesText: v })"
|
|
164
167
|
/>
|
|
168
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
|
|
165
169
|
</label>
|
|
166
170
|
</ConfigFieldGroup>
|
|
167
171
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
3
3
|
import { ref } from "vue";
|
|
4
4
|
import { Input, Select } from "ant-design-vue";
|
|
5
|
-
import type { PanelElement } from "../../types";
|
|
5
|
+
import type { PanelElement, PanelElementStyle } from "../../types";
|
|
6
6
|
import { readFileAsDataUrl, uploadFileToRemote } from "./shared";
|
|
7
|
+
import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
|
|
7
8
|
import { getPanelMessages } from "../../constants/messages";
|
|
8
9
|
import ConfigFieldGroup from "./ConfigFieldGroup.vue";
|
|
9
10
|
import ConfigSection from "./ConfigSection.vue";
|
|
@@ -30,7 +31,7 @@ const emit = defineEmits<{
|
|
|
30
31
|
|
|
31
32
|
const uploadStatus = ref("");
|
|
32
33
|
|
|
33
|
-
function patchStyle(patch: Record<string, string | undefined>) {
|
|
34
|
+
function patchStyle(patch: Partial<PanelElementStyle> | Record<string, string | undefined>) {
|
|
34
35
|
props.updateElement(props.element.id, {
|
|
35
36
|
style: { ...(props.element.style ?? {}), ...patch },
|
|
36
37
|
});
|
|
@@ -69,7 +70,7 @@ function onFileChange(e: Event) {
|
|
|
69
70
|
@update:open="emit('update:open', $event)"
|
|
70
71
|
>
|
|
71
72
|
<ConfigFieldGroup :title="t('panel.config.groupImageSource')">
|
|
72
|
-
<label class="block space-y-1">
|
|
73
|
+
<label class="block space-y-1" data-config-field="style.backgroundImageRemoteUrl">
|
|
73
74
|
<div class="flex items-center gap-1">
|
|
74
75
|
<span>{{ t("panel.config.imageUrlCss") }}</span>
|
|
75
76
|
<ConfigHintIcon :label="t('panel.config.imageUrlHintLabel')">
|
|
@@ -78,20 +79,13 @@ function onFileChange(e: Event) {
|
|
|
78
79
|
</div>
|
|
79
80
|
<Input
|
|
80
81
|
size="small"
|
|
82
|
+
class="font-mono text-[11px]"
|
|
81
83
|
:value="element.style?.backgroundImageRemoteUrl ?? element.style?.backgroundImage ?? ''"
|
|
82
84
|
:disabled="!isEditable"
|
|
83
|
-
placeholder="
|
|
84
|
-
@update:value="(v: string) =>
|
|
85
|
-
if (v.startsWith('url(')) {
|
|
86
|
-
patchStyle({ backgroundImage: v, backgroundImageRemoteUrl: undefined });
|
|
87
|
-
} else {
|
|
88
|
-
patchStyle({
|
|
89
|
-
backgroundImage: v ? `url('${v}')` : undefined,
|
|
90
|
-
backgroundImageRemoteUrl: v || undefined,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}"
|
|
85
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
86
|
+
@update:value="(v: string) => patchStyle(patchBackgroundImageFromInput(String(v ?? '')))"
|
|
94
87
|
/>
|
|
88
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
95
89
|
</label>
|
|
96
90
|
<div class="flex items-center gap-2">
|
|
97
91
|
<label
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
normalizeViewportOverflow,
|
|
10
10
|
type ViewportOverflowMode,
|
|
11
11
|
} from "../../utils/viewportPlacement";
|
|
12
|
+
import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
|
|
12
13
|
|
|
13
14
|
const { t, locale } = useI18n();
|
|
14
15
|
const props = defineProps<{
|
|
@@ -288,12 +289,17 @@ function setNodeCardExpanded(id: string, open: boolean) {
|
|
|
288
289
|
|
|
289
290
|
<template v-if="el.materialType === 'audio'">
|
|
290
291
|
<div class="grid grid-cols-2 gap-2">
|
|
291
|
-
<label class="col-span-2 block space-y-1">
|
|
292
|
+
<label class="col-span-2 block space-y-1" data-config-field="audioRemoteUrl">
|
|
292
293
|
<div>{{ t("panel.config.audioUrl") }}</div>
|
|
293
294
|
<Input
|
|
294
295
|
size="small"
|
|
296
|
+
class="font-mono text-[11px]"
|
|
295
297
|
:value="el.audioRemoteUrl ?? ''"
|
|
296
|
-
|
|
298
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
299
|
+
@update:value="(v: string) => updateElement(el.id, {
|
|
300
|
+
audioRemoteUrl: v || undefined,
|
|
301
|
+
audioSrc: v || el.audioSrc,
|
|
302
|
+
})"
|
|
297
303
|
/>
|
|
298
304
|
</label>
|
|
299
305
|
<label class="block space-y-1">
|
|
@@ -313,12 +319,17 @@ function setNodeCardExpanded(id: string, open: boolean) {
|
|
|
313
319
|
</template>
|
|
314
320
|
|
|
315
321
|
<template v-if="el.materialType === 'video'">
|
|
316
|
-
<label class="block space-y-1">
|
|
322
|
+
<label class="block space-y-1" data-config-field="videoRemoteUrl">
|
|
317
323
|
<div>{{ t("panel.config.videoUrl") }}</div>
|
|
318
324
|
<Input
|
|
319
325
|
size="small"
|
|
326
|
+
class="font-mono text-[11px]"
|
|
320
327
|
:value="el.videoRemoteUrl ?? ''"
|
|
321
|
-
|
|
328
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
329
|
+
@update:value="(v: string) => updateElement(el.id, {
|
|
330
|
+
videoRemoteUrl: v || undefined,
|
|
331
|
+
videoSrc: v || el.videoSrc,
|
|
332
|
+
})"
|
|
322
333
|
/>
|
|
323
334
|
</label>
|
|
324
335
|
</template>
|
|
@@ -422,19 +433,21 @@ function setNodeCardExpanded(id: string, open: boolean) {
|
|
|
422
433
|
</template>
|
|
423
434
|
|
|
424
435
|
<template v-if="el.materialType === 'image'">
|
|
425
|
-
<label class="block space-y-1">
|
|
436
|
+
<label class="block space-y-1" data-config-field="style.backgroundImageRemoteUrl">
|
|
426
437
|
<div>{{ t("panel.config.backgroundImageUrl") }}</div>
|
|
427
438
|
<Input
|
|
428
439
|
size="small"
|
|
440
|
+
class="font-mono text-[11px]"
|
|
429
441
|
:value="el.style?.backgroundImageRemoteUrl ?? el.style?.backgroundImage ?? ''"
|
|
442
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
430
443
|
@update:value="(v: string) => updateElement(el.id, {
|
|
431
444
|
style: {
|
|
432
445
|
...(el.style ?? {}),
|
|
433
|
-
|
|
434
|
-
backgroundImageRemoteUrl: v.startsWith('url(') ? undefined : v || undefined,
|
|
446
|
+
...patchBackgroundImageFromInput(String(v ?? '')),
|
|
435
447
|
},
|
|
436
448
|
})"
|
|
437
449
|
/>
|
|
450
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
438
451
|
</label>
|
|
439
452
|
<label class="block space-y-1">
|
|
440
453
|
<div>{{ t("panel.config.objectFit") }}</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, reactive, useTemplateRef } from "vue";
|
|
2
|
+
import { computed, reactive, ref, useTemplateRef } from "vue";
|
|
3
3
|
import { useI18n } from "@arronqzy/i18n/vue";
|
|
4
4
|
import { Button, Checkbox, Input, Select } from "ant-design-vue";
|
|
5
5
|
import {
|
|
@@ -38,6 +38,7 @@ const props = defineProps<{
|
|
|
38
38
|
const emit = defineEmits<{ "update:open": [value: boolean] }>();
|
|
39
39
|
|
|
40
40
|
const fileInputRef = useTemplateRef<HTMLInputElement>("fileInputRef");
|
|
41
|
+
const modelUrlDraft = ref("");
|
|
41
42
|
const config = computed(() => mergeScene3dConfig(props.element.scene3d));
|
|
42
43
|
type CameraAnimPreset = "orbit" | "pushIn" | "pullBack" | "truckLeft" | "craneUp";
|
|
43
44
|
|
|
@@ -67,6 +68,42 @@ async function handleUploadModels(files: FileList | null) {
|
|
|
67
68
|
patch({ models: nextModels });
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function handleAddModelByUrl() {
|
|
72
|
+
const url = modelUrlDraft.value.trim();
|
|
73
|
+
if (!url) return;
|
|
74
|
+
const fileName = url.split(/[/?#]/).filter(Boolean).pop() || "model";
|
|
75
|
+
patch({
|
|
76
|
+
models: [
|
|
77
|
+
...config.value.models,
|
|
78
|
+
{
|
|
79
|
+
id: randomId("mdl"),
|
|
80
|
+
label: fileName.replace(/\.[^.]+$/, "") || fileName,
|
|
81
|
+
url,
|
|
82
|
+
fileName,
|
|
83
|
+
format: inferModelFormat(fileName),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
modelUrlDraft.value = "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateModelUrl(modelId: string, nextUrl: string) {
|
|
91
|
+
const fileName =
|
|
92
|
+
nextUrl.split(/[/?#]/).filter(Boolean).pop() || "model";
|
|
93
|
+
patch({
|
|
94
|
+
models: config.value.models.map((m) =>
|
|
95
|
+
m.id === modelId
|
|
96
|
+
? {
|
|
97
|
+
...m,
|
|
98
|
+
url: nextUrl,
|
|
99
|
+
fileName,
|
|
100
|
+
format: inferModelFormat(fileName),
|
|
101
|
+
}
|
|
102
|
+
: m
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
70
107
|
function updateModelAnimation(id: string, next: Partial<Scene3dModelAnimationRule>) {
|
|
71
108
|
patch({
|
|
72
109
|
modelAnimations: config.value.modelAnimations.map((rule) =>
|
|
@@ -336,6 +373,27 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
|
|
|
336
373
|
@change="(e) => { void handleUploadModels((e.target as HTMLInputElement).files); (e.target as HTMLInputElement).value = ''; }"
|
|
337
374
|
/>
|
|
338
375
|
</div>
|
|
376
|
+
<label class="block space-y-1" data-config-field="scene3d.modelUrl">
|
|
377
|
+
<div class="text-[10px] text-gray-500">{{ t("panel.config.scene3dModelUrl") }}</div>
|
|
378
|
+
<div class="flex gap-1.5">
|
|
379
|
+
<Input
|
|
380
|
+
size="small"
|
|
381
|
+
class="font-mono text-[11px]"
|
|
382
|
+
:disabled="!isEditable"
|
|
383
|
+
:value="modelUrlDraft"
|
|
384
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
385
|
+
@update:value="(v: string) => (modelUrlDraft = String(v ?? ''))"
|
|
386
|
+
/>
|
|
387
|
+
<Button
|
|
388
|
+
size="small"
|
|
389
|
+
:disabled="!isEditable || !modelUrlDraft.trim()"
|
|
390
|
+
@click="handleAddModelByUrl"
|
|
391
|
+
>
|
|
392
|
+
{{ t("common.add") }}
|
|
393
|
+
</Button>
|
|
394
|
+
</div>
|
|
395
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
396
|
+
</label>
|
|
339
397
|
<div v-if="config.models.length === 0" class="text-[11px] text-gray-500">
|
|
340
398
|
{{ t("panel.config.scene3dNoModels") }}
|
|
341
399
|
</div>
|
|
@@ -346,6 +404,15 @@ function onTagHtmlInput(modelId: string, objectName: string, event: Event) {
|
|
|
346
404
|
:value="model.label"
|
|
347
405
|
@update:value="(v: string) => patch({ models: config.models.map((m) => m.id === model.id ? { ...m, label: v } : m) })"
|
|
348
406
|
/>
|
|
407
|
+
<Input
|
|
408
|
+
size="small"
|
|
409
|
+
class="font-mono text-[10px]"
|
|
410
|
+
data-config-field="scene3d.models.url"
|
|
411
|
+
:disabled="!isEditable"
|
|
412
|
+
:value="model.url"
|
|
413
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
414
|
+
@update:value="(v: string) => updateModelUrl(model.id, String(v ?? ''))"
|
|
415
|
+
/>
|
|
349
416
|
<div v-if="model.objectNames?.length" class="space-y-1">
|
|
350
417
|
<details :open="model.objectNames.length <= 12">
|
|
351
418
|
<summary class="cursor-pointer text-[10px] text-gray-500">
|
|
@@ -4,6 +4,7 @@ import { ref } from "vue";
|
|
|
4
4
|
import { Input, Select } from "ant-design-vue";
|
|
5
5
|
import type { PanelElement, PanelElementStyle } from "../../types";
|
|
6
6
|
import { readFileAsDataUrl, uploadFileToRemote } from "./shared";
|
|
7
|
+
import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
|
|
7
8
|
import { getPanelMessages } from "../../constants/messages";
|
|
8
9
|
import ConfigColorField from "./ConfigColorField.vue";
|
|
9
10
|
import ConfigFieldGroup from "./ConfigFieldGroup.vue";
|
|
@@ -86,15 +87,17 @@ function onFileChange(e: Event) {
|
|
|
86
87
|
collapsible
|
|
87
88
|
:default-open="false"
|
|
88
89
|
>
|
|
89
|
-
<label class="block space-y-1">
|
|
90
|
-
<div>{{ t("panel.
|
|
90
|
+
<label class="block space-y-1" data-config-field="style.backgroundImage">
|
|
91
|
+
<div>{{ t("panel.config.backgroundImage") }}</div>
|
|
91
92
|
<Input
|
|
92
93
|
size="small"
|
|
93
|
-
|
|
94
|
+
class="font-mono text-[11px]"
|
|
95
|
+
:value="element.style?.backgroundImageRemoteUrl ?? element.style?.backgroundImage ?? ''"
|
|
94
96
|
:disabled="!isEditable"
|
|
95
|
-
placeholder=
|
|
96
|
-
@update:value="(v: string) => patchStyle(
|
|
97
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
98
|
+
@update:value="(v: string) => patchStyle(patchBackgroundImageFromInput(String(v ?? '')))"
|
|
97
99
|
/>
|
|
100
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
98
101
|
</label>
|
|
99
102
|
<div class="flex items-center gap-2">
|
|
100
103
|
<label
|
|
@@ -88,6 +88,7 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
|
|
|
88
88
|
</div>
|
|
89
89
|
<div
|
|
90
90
|
ref="textEditorRef"
|
|
91
|
+
data-config-field="textHtml"
|
|
91
92
|
data-panel-user-text=""
|
|
92
93
|
class="min-h-[120px] rounded border border-gray-200 bg-white px-2 py-1.5 leading-6 outline-none"
|
|
93
94
|
:contenteditable="isEditable"
|
|
@@ -104,6 +105,20 @@ watch(() => [props.element.id, props.element.textHtml], syncEditorHtml);
|
|
|
104
105
|
patch({ textHtml: nextHtml || '<p><br/></p>' });
|
|
105
106
|
}"
|
|
106
107
|
/>
|
|
108
|
+
<label class="block space-y-1">
|
|
109
|
+
<div>{{ t("panel.config.textHtmlSource") }}</div>
|
|
110
|
+
<textarea
|
|
111
|
+
data-config-field="textHtml"
|
|
112
|
+
class="w-full rounded border border-gray-200 bg-white px-2 py-1.5 font-mono text-[11px] leading-relaxed outline-none"
|
|
113
|
+
rows="4"
|
|
114
|
+
spellcheck="false"
|
|
115
|
+
:value="element.textHtml ?? ''"
|
|
116
|
+
:disabled="!isEditable"
|
|
117
|
+
:placeholder="t('panel.config.textHtmlSourcePlaceholder')"
|
|
118
|
+
@input="(e) => patch({ textHtml: (e.target as HTMLTextAreaElement).value || '<p><br/></p>' })"
|
|
119
|
+
/>
|
|
120
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.scopeTemplateHint") }}</p>
|
|
121
|
+
</label>
|
|
107
122
|
</ConfigFieldGroup>
|
|
108
123
|
<ConfigFieldGroup :title="t('panel.config.groupTextStyle')">
|
|
109
124
|
<label class="block space-y-1">
|
|
@@ -66,18 +66,20 @@ function onVideoFileChange(e: Event) {
|
|
|
66
66
|
@update:open="emit('update:open', $event)"
|
|
67
67
|
>
|
|
68
68
|
<ConfigFieldGroup :title="t('panel.config.groupVideoSource')">
|
|
69
|
-
<label class="block space-y-1">
|
|
69
|
+
<label class="block space-y-1" data-config-field="videoRemoteUrl">
|
|
70
70
|
<div>{{ t("panel.config.videoUrl") }}</div>
|
|
71
71
|
<Input
|
|
72
72
|
size="small"
|
|
73
|
+
class="font-mono text-[11px]"
|
|
73
74
|
:value="element.videoRemoteUrl ?? ''"
|
|
74
75
|
:disabled="!isEditable"
|
|
75
|
-
placeholder="
|
|
76
|
+
:placeholder="t('panel.config.urlScopePlaceholder')"
|
|
76
77
|
@update:value="(v: string) => patch({
|
|
77
78
|
videoRemoteUrl: v || undefined,
|
|
78
79
|
videoSrc: v || element.videoSrc,
|
|
79
80
|
})"
|
|
80
81
|
/>
|
|
82
|
+
<p class="text-[10px] text-gray-500">{{ t("panel.config.urlScopeHint") }}</p>
|
|
81
83
|
</label>
|
|
82
84
|
<div class="flex items-center gap-2">
|
|
83
85
|
<label
|
|
@@ -16,6 +16,7 @@ import type { PanelElement } from "../types";
|
|
|
16
16
|
import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
|
|
17
17
|
import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
|
|
18
18
|
import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
|
|
19
|
+
import { resolveBackgroundImageStyle } from "../utils/background-image-style";
|
|
19
20
|
import {
|
|
20
21
|
isViewportNode,
|
|
21
22
|
normalizeViewportOverflow,
|
|
@@ -36,7 +37,7 @@ export function getNodeVisualStyle(element: PanelElement) {
|
|
|
36
37
|
const style = element.style ?? {};
|
|
37
38
|
return {
|
|
38
39
|
backgroundColor: style.backgroundColor,
|
|
39
|
-
backgroundImage: style
|
|
40
|
+
backgroundImage: resolveBackgroundImageStyle(style),
|
|
40
41
|
backgroundSize: style.backgroundSize,
|
|
41
42
|
backgroundPosition: style.backgroundPosition,
|
|
42
43
|
borderWidth: style.borderWidth,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, onMounted } from "vue";
|
|
1
|
+
import { computed, onMounted, shallowRef } from "vue";
|
|
2
2
|
import { store, type Node, type State } from "@arronqzy/rx-store";
|
|
3
3
|
import { useStoreRef } from "@arronqzy/vue-rx-store";
|
|
4
4
|
import { useI18nOptional } from "@arronqzy/i18n/vue";
|
|
@@ -47,6 +47,10 @@ import {
|
|
|
47
47
|
applyGridLayoutPatchAcrossMappingFamily,
|
|
48
48
|
applyMappingFamilySyncPatch,
|
|
49
49
|
} from "../utils/updateElementDraft";
|
|
50
|
+
import {
|
|
51
|
+
materializePanelElements,
|
|
52
|
+
stabilizeElementList,
|
|
53
|
+
} from "../utils/materializePanelElements";
|
|
50
54
|
|
|
51
55
|
export type { PanelActionResult, PanelHistoryItem, PanelLayer } from "../types";
|
|
52
56
|
|
|
@@ -126,19 +130,24 @@ export function usePanelElements() {
|
|
|
126
130
|
(stateRef.value.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID
|
|
127
131
|
);
|
|
128
132
|
|
|
133
|
+
const allElementsCache = shallowRef<PanelElement[]>([]);
|
|
134
|
+
const elementsCache = shallowRef<PanelElement[]>([]);
|
|
135
|
+
|
|
129
136
|
const allElements = computed(() => {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
+
const next = materializePanelElements(
|
|
138
|
+
stateRef.value.root.children,
|
|
139
|
+
allElementsCache.value
|
|
140
|
+
);
|
|
141
|
+
allElementsCache.value = next;
|
|
142
|
+
return next;
|
|
137
143
|
});
|
|
138
144
|
|
|
139
|
-
const elements = computed(() =>
|
|
140
|
-
allElements.value.filter((el) => el.layerId === activeLayerId.value)
|
|
141
|
-
|
|
145
|
+
const elements = computed(() => {
|
|
146
|
+
const next = allElements.value.filter((el) => el.layerId === activeLayerId.value);
|
|
147
|
+
const stable = stabilizeElementList(next, elementsCache.value);
|
|
148
|
+
elementsCache.value = stable;
|
|
149
|
+
return stable;
|
|
150
|
+
});
|
|
142
151
|
|
|
143
152
|
const byId = computed(() => {
|
|
144
153
|
const map = new Map<string, PanelElement>();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PanelElementStyle } from "../types";
|
|
2
|
+
|
|
3
|
+
/** Build a CSS `url('...')` value from a plain URL / template string. */
|
|
4
|
+
export function cssUrlFromValue(value: string): string {
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
if (!trimmed) return "";
|
|
7
|
+
if (/^url\(/i.test(trimmed) || /gradient\(/i.test(trimmed)) return trimmed;
|
|
8
|
+
const escaped = trimmed.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
9
|
+
return `url('${escaped}')`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Prefer explicit backgroundImage; fall back to remote URL (supports scope templates after resolve). */
|
|
13
|
+
export function resolveBackgroundImageStyle(
|
|
14
|
+
style?: PanelElementStyle | null
|
|
15
|
+
): string | undefined {
|
|
16
|
+
if (!style) return undefined;
|
|
17
|
+
if (style.backgroundImage?.trim()) return style.backgroundImage;
|
|
18
|
+
if (style.backgroundImageRemoteUrl?.trim()) {
|
|
19
|
+
return cssUrlFromValue(style.backgroundImageRemoteUrl);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Normalize config input: plain URL/template → both css + remote; css/gradient → backgroundImage only. */
|
|
25
|
+
export function patchBackgroundImageFromInput(
|
|
26
|
+
value: string
|
|
27
|
+
): Partial<PanelElementStyle> {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
return { backgroundImage: undefined, backgroundImageRemoteUrl: undefined };
|
|
31
|
+
}
|
|
32
|
+
if (/^url\(/i.test(trimmed) || /gradient\(/i.test(trimmed)) {
|
|
33
|
+
return { backgroundImage: trimmed, backgroundImageRemoteUrl: undefined };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
backgroundImage: cssUrlFromValue(trimmed),
|
|
37
|
+
backgroundImageRemoteUrl: trimmed,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* While Moveable is actively dragging/resizing/rotating, pause expensive
|
|
3
|
+
* canvas-wide work (e.g. ellipsis overflow measurement via MutationObserver).
|
|
4
|
+
*/
|
|
5
|
+
let busyDepth = 0;
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
export function isCanvasInteractionBusy(): boolean {
|
|
9
|
+
return busyDepth > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function setCanvasInteractionBusy(busy: boolean): void {
|
|
13
|
+
const prev = busyDepth > 0;
|
|
14
|
+
if (busy) busyDepth += 1;
|
|
15
|
+
else busyDepth = Math.max(0, busyDepth - 1);
|
|
16
|
+
const next = busyDepth > 0;
|
|
17
|
+
if (prev === next) return;
|
|
18
|
+
listeners.forEach((listener) => listener());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function subscribeCanvasInteractionBusy(listener: () => void): () => void {
|
|
22
|
+
listeners.add(listener);
|
|
23
|
+
return () => {
|
|
24
|
+
listeners.delete(listener);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { PanelElement } from "../types";
|
|
2
|
+
import { isPanelElementNode } from "./panelElementNodes";
|
|
3
|
+
import type { Node } from "@arronqzy/rx-store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Preserve Immer structural sharing: unchanged nodes keep the same props
|
|
7
|
+
* object reference so React.memo / Vue can skip re-rendering them.
|
|
8
|
+
*/
|
|
9
|
+
export function materializePanelElements(
|
|
10
|
+
nodes: Node[] | undefined,
|
|
11
|
+
previous?: readonly PanelElement[] | null
|
|
12
|
+
): PanelElement[] {
|
|
13
|
+
const list = nodes ?? [];
|
|
14
|
+
const prevById = previous?.length
|
|
15
|
+
? new Map(previous.map((el) => [el.id, el] as const))
|
|
16
|
+
: null;
|
|
17
|
+
const next: PanelElement[] = [];
|
|
18
|
+
|
|
19
|
+
for (const n of list) {
|
|
20
|
+
if (!isPanelElementNode(n) || !n.props) continue;
|
|
21
|
+
const props = n.props as PanelElement;
|
|
22
|
+
const normalized: PanelElement =
|
|
23
|
+
typeof props.zIndex === "number" ? props : { ...props, zIndex: 1 };
|
|
24
|
+
|
|
25
|
+
const prev = prevById?.get(normalized.id);
|
|
26
|
+
if (prev && prev === normalized) {
|
|
27
|
+
next.push(prev);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (
|
|
31
|
+
prev &&
|
|
32
|
+
prev.id === normalized.id &&
|
|
33
|
+
prev.x === normalized.x &&
|
|
34
|
+
prev.y === normalized.y &&
|
|
35
|
+
prev.width === normalized.width &&
|
|
36
|
+
prev.height === normalized.height &&
|
|
37
|
+
prev.rotate === normalized.rotate &&
|
|
38
|
+
prev.zIndex === normalized.zIndex &&
|
|
39
|
+
prev.layerId === normalized.layerId &&
|
|
40
|
+
prev.locked === normalized.locked &&
|
|
41
|
+
prev.materialType === normalized.materialType &&
|
|
42
|
+
prev.name === normalized.name &&
|
|
43
|
+
prev.chart === normalized.chart &&
|
|
44
|
+
prev.table === normalized.table &&
|
|
45
|
+
prev.style === normalized.style &&
|
|
46
|
+
prev.textHtml === normalized.textHtml &&
|
|
47
|
+
prev.textAllowInput === normalized.textAllowInput &&
|
|
48
|
+
prev.gridRows === normalized.gridRows &&
|
|
49
|
+
prev.gridCols === normalized.gridCols &&
|
|
50
|
+
prev.gridGap === normalized.gridGap &&
|
|
51
|
+
prev.gridPadding === normalized.gridPadding &&
|
|
52
|
+
prev.parentGridId === normalized.parentGridId &&
|
|
53
|
+
prev.gridSlotIndex === normalized.gridSlotIndex &&
|
|
54
|
+
prev.gridColSpan === normalized.gridColSpan &&
|
|
55
|
+
prev.gridRowSpan === normalized.gridRowSpan &&
|
|
56
|
+
prev.viewportOverflow === normalized.viewportOverflow &&
|
|
57
|
+
prev.scene3d === normalized.scene3d &&
|
|
58
|
+
prev.refLayerId === normalized.refLayerId &&
|
|
59
|
+
prev.refCopyMode === normalized.refCopyMode &&
|
|
60
|
+
prev.refSnapshot === normalized.refSnapshot &&
|
|
61
|
+
prev.audioSrc === normalized.audioSrc &&
|
|
62
|
+
prev.videoSrc === normalized.videoSrc &&
|
|
63
|
+
prev.geometryShape === normalized.geometryShape &&
|
|
64
|
+
prev.geometryColor === normalized.geometryColor &&
|
|
65
|
+
prev.geometrySketchDataUrl === normalized.geometrySketchDataUrl
|
|
66
|
+
) {
|
|
67
|
+
next.push(prev);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
next.push(normalized);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
previous &&
|
|
75
|
+
previous.length === next.length &&
|
|
76
|
+
previous.every((el, i) => el === next[i])
|
|
77
|
+
) {
|
|
78
|
+
return previous as PanelElement[];
|
|
79
|
+
}
|
|
80
|
+
return next;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Keep previous array identity when item references are unchanged (order-sensitive). */
|
|
84
|
+
export function stabilizeElementList(
|
|
85
|
+
next: PanelElement[],
|
|
86
|
+
previous?: readonly PanelElement[] | null
|
|
87
|
+
): PanelElement[] {
|
|
88
|
+
if (
|
|
89
|
+
previous &&
|
|
90
|
+
previous.length === next.length &&
|
|
91
|
+
previous.every((el, i) => el === next[i])
|
|
92
|
+
) {
|
|
93
|
+
return previous as PanelElement[];
|
|
94
|
+
}
|
|
95
|
+
return next;
|
|
96
|
+
}
|
|
@@ -137,6 +137,26 @@ export function computePanelSceneBounds(elements: PanelElement[]): PanelSceneBou
|
|
|
137
137
|
|
|
138
138
|
export const PREVIEW_LAYOUT_EVENT = "arronqzy-preview-layout";
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
let previewLayoutRaf = 0;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Notify chart/preview hosts that layout changed.
|
|
144
|
+
* Default path is rAF-coalesced (safe during Moveable resize).
|
|
145
|
+
* Pass `{ flush: true }` after resize ends so charts catch the final size.
|
|
146
|
+
*/
|
|
147
|
+
export function notifyPreviewLayoutChanged(options?: { flush?: boolean }) {
|
|
148
|
+
if (typeof window === "undefined") return;
|
|
149
|
+
if (options?.flush) {
|
|
150
|
+
if (previewLayoutRaf) {
|
|
151
|
+
window.cancelAnimationFrame(previewLayoutRaf);
|
|
152
|
+
previewLayoutRaf = 0;
|
|
153
|
+
}
|
|
154
|
+
window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (previewLayoutRaf) return;
|
|
158
|
+
previewLayoutRaf = window.requestAnimationFrame(() => {
|
|
159
|
+
previewLayoutRaf = 0;
|
|
160
|
+
window.dispatchEvent(new Event(PREVIEW_LAYOUT_EVENT));
|
|
161
|
+
});
|
|
142
162
|
}
|
|
@@ -90,3 +90,15 @@ export function materializeChartLabelsFromScope(
|
|
|
90
90
|
}
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Prefer valuesText templates; expand [...{scope}] / {scope} into number[]. */
|
|
95
|
+
export function materializeChartValuesFromScope(
|
|
96
|
+
chart: PanelChartConfig | undefined,
|
|
97
|
+
scope: unknown
|
|
98
|
+
): number[] | undefined {
|
|
99
|
+
if (!chart) return undefined;
|
|
100
|
+
if (chart.valuesText !== undefined) {
|
|
101
|
+
return materializeChartValuesText(chart.valuesText, scope);
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PanelElement } from "../types";
|
|
2
2
|
import type { PanelTableConfig } from "@arronqzy/view-table";
|
|
3
3
|
import { isUsableTableRawData } from "@arronqzy/view-table";
|
|
4
|
-
import { materializeChartLabelsFromScope } from "./scope-template-chart";
|
|
4
|
+
import { materializeChartLabelsFromScope, materializeChartValuesFromScope } from "./scope-template-chart";
|
|
5
5
|
import {
|
|
6
6
|
SCOPE_SPREAD_TEMPLATE_RE,
|
|
7
7
|
evaluateSpreadScopeExpression,
|
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
resolveSpreadScopePath,
|
|
12
12
|
} from "./scope-template-spread";
|
|
13
13
|
|
|
14
|
-
const
|
|
14
|
+
const SCOPE_TOKEN_RE = /\{(scope[^}]*)\}/g;
|
|
15
15
|
|
|
16
16
|
export function hasScopeTemplate(value: string): boolean {
|
|
17
|
-
|
|
17
|
+
SCOPE_TOKEN_RE.lastIndex = 0;
|
|
18
|
+
SCOPE_SPREAD_TEMPLATE_RE.lastIndex = 0;
|
|
19
|
+
return SCOPE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function evaluateScopeExpression(
|
|
@@ -31,6 +33,19 @@ export function evaluateScopeExpression(
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function stringifyScopeValue(value: unknown): string {
|
|
37
|
+
if (value === null || value === undefined) return "";
|
|
38
|
+
if (typeof value === "string") return value;
|
|
39
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
} catch {
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
export function evaluateScopeTemplate(
|
|
35
50
|
template: string,
|
|
36
51
|
scope: unknown
|
|
@@ -39,25 +54,16 @@ export function evaluateScopeTemplate(
|
|
|
39
54
|
|
|
40
55
|
const withSpread = template.replace(
|
|
41
56
|
SCOPE_SPREAD_TEMPLATE_RE,
|
|
42
|
-
(_, rawExpr: string) => {
|
|
57
|
+
(_: string, rawExpr: string) => {
|
|
43
58
|
const result = evaluateSpreadScopeExpression(rawExpr, scope);
|
|
44
59
|
return result.ok ? result.value : "";
|
|
45
60
|
}
|
|
46
61
|
);
|
|
47
62
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
53
|
-
return String(value);
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
return JSON.stringify(value);
|
|
57
|
-
} catch {
|
|
58
|
-
return String(value);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
63
|
+
// Only `{scope...}` tokens — keep ECharts / CSS braces like `{b}` intact.
|
|
64
|
+
return withSpread.replace(SCOPE_TOKEN_RE, (_: string, rawExpr: string) =>
|
|
65
|
+
stringifyScopeValue(evaluateScopeExpression(rawExpr, scope))
|
|
66
|
+
);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
function tryParseJson(text: string): unknown | undefined {
|
|
@@ -85,7 +91,7 @@ function materializeTemplateValue(template: string, scope: unknown): unknown {
|
|
|
85
91
|
);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
const singleMatch = /^\{([^}]
|
|
94
|
+
const singleMatch = /^\{(scope[^}]*)\}$/.exec(trimmed);
|
|
89
95
|
if (singleMatch) {
|
|
90
96
|
return evaluateScopeExpression(singleMatch[1]!, scope);
|
|
91
97
|
}
|
|
@@ -149,6 +155,11 @@ export function resolvePanelElementScope(
|
|
|
149
155
|
resolved.chart.labels = labels;
|
|
150
156
|
delete resolved.chart.labelsText;
|
|
151
157
|
}
|
|
158
|
+
const values = materializeChartValuesFromScope(element.chart, scope);
|
|
159
|
+
if (values !== undefined) {
|
|
160
|
+
resolved.chart.values = values;
|
|
161
|
+
delete resolved.chart.valuesText;
|
|
162
|
+
}
|
|
152
163
|
}
|
|
153
164
|
if (resolved.table) {
|
|
154
165
|
const hasDynamicSource =
|